electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 097ac144d976eb46dff809e1809783dc78ab6d8b
parent 30a7952cbb2e1c59c5eabaaee64d8a4e15a1b0cb
Author: Janus <ysangkok@gmail.com>
Date:   Wed, 11 Jul 2018 17:38:47 +0200

file reorganization with top-level module

Diffstat:
M.gitignore | 3+--
MREADME.rst | 6+++---
Mcontrib/build-osx/make_osx | 4++--
Mcontrib/build-osx/osx.spec | 193+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcontrib/build-wine/build-electrum-git.sh | 4++--
Mcontrib/build-wine/deterministic.spec | 41++++++++++++++++++++---------------------
Mcontrib/make_apk | 2+-
Mcontrib/make_locale | 11+++++------
Delectrum | 480-------------------------------------------------------------------------------
Melectrum-env | 2+-
Aelectrum/__init__.py | 14++++++++++++++
Aelectrum/base_crash_reporter.py | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/base_wizard.py -> electrum/base_wizard.py | 0
Rlib/bitcoin.py -> electrum/bitcoin.py | 0
Rlib/blockchain.py -> electrum/blockchain.py | 0
Rlib/checkpoints.json -> electrum/checkpoints.json | 0
Rlib/checkpoints_testnet.json -> electrum/checkpoints_testnet.json | 0
Rlib/coinchooser.py -> electrum/coinchooser.py | 0
Aelectrum/commands.py | 892+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/constants.py -> electrum/constants.py | 0
Rlib/contacts.py -> electrum/contacts.py | 0
Rlib/crypto.py -> electrum/crypto.py | 0
Rlib/currencies.json -> electrum/currencies.json | 0
Aelectrum/daemon.py | 316+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rlib/dnssec.py -> electrum/dnssec.py | 0
Rlib/ecc.py -> electrum/ecc.py | 0
Rlib/ecc_fast.py -> electrum/ecc_fast.py | 0
Aelectrum/electrum | 2++
Aelectrum/exchange_rate.py | 573+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/__init__.py -> electrum/gui/__init__.py | 0
Aelectrum/gui/kivy/Makefile | 32++++++++++++++++++++++++++++++++
Rgui/kivy/Readme.md -> electrum/gui/kivy/Readme.md | 0
Rgui/kivy/__init__.py -> electrum/gui/kivy/__init__.py | 0
Rgui/kivy/data/background.png -> electrum/gui/kivy/data/background.png | 0
Rgui/kivy/data/fonts/Roboto-Bold.ttf -> electrum/gui/kivy/data/fonts/Roboto-Bold.ttf | 0
Rgui/kivy/data/fonts/Roboto-Condensed.ttf -> electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf | 0
Rgui/kivy/data/fonts/Roboto-Medium.ttf -> electrum/gui/kivy/data/fonts/Roboto-Medium.ttf | 0
Rgui/kivy/data/fonts/Roboto.ttf -> electrum/gui/kivy/data/fonts/Roboto.ttf | 0
Rgui/kivy/data/fonts/tron/License.txt -> electrum/gui/kivy/data/fonts/tron/License.txt | 0
Rgui/kivy/data/fonts/tron/Readme.txt -> electrum/gui/kivy/data/fonts/tron/Readme.txt | 0
Rgui/kivy/data/fonts/tron/Tr2n.ttf -> electrum/gui/kivy/data/fonts/tron/Tr2n.ttf | 0
Rgui/kivy/data/glsl/default.fs -> electrum/gui/kivy/data/glsl/default.fs | 0
Rgui/kivy/data/glsl/default.png -> electrum/gui/kivy/data/glsl/default.png | 0
Rgui/kivy/data/glsl/default.vs -> electrum/gui/kivy/data/glsl/default.vs | 0
Rgui/kivy/data/glsl/header.fs -> electrum/gui/kivy/data/glsl/header.fs | 0
Rgui/kivy/data/glsl/header.vs -> electrum/gui/kivy/data/glsl/header.vs | 0
Rgui/kivy/data/images/defaulttheme-0.png -> electrum/gui/kivy/data/images/defaulttheme-0.png | 0
Rgui/kivy/data/images/defaulttheme.atlas -> electrum/gui/kivy/data/images/defaulttheme.atlas | 0
Rgui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java -> electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java | 0
Rgui/kivy/data/logo/kivy-icon-32.png -> electrum/gui/kivy/data/logo/kivy-icon-32.png | 0
Rgui/kivy/data/style.kv -> electrum/gui/kivy/data/style.kv | 0
Rgui/kivy/i18n.py -> electrum/gui/kivy/i18n.py | 0
Aelectrum/gui/kivy/main.kv | 464+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/main_window.py | 1028+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/nfc_scanner/__init__.py | 44++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/nfc_scanner/scanner_android.py | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/nfc_scanner/scanner_dummy.py | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/theming/light/action_bar.png -> electrum/gui/kivy/theming/light/action_bar.png | 0
Rgui/kivy/theming/light/action_button_group.png -> electrum/gui/kivy/theming/light/action_button_group.png | 0
Rgui/kivy/theming/light/action_group_dark.png -> electrum/gui/kivy/theming/light/action_group_dark.png | 0
Rgui/kivy/theming/light/action_group_light.png -> electrum/gui/kivy/theming/light/action_group_light.png | 0
Rgui/kivy/theming/light/add_contact.png -> electrum/gui/kivy/theming/light/add_contact.png | 0
Rgui/kivy/theming/light/arrow_back.png -> electrum/gui/kivy/theming/light/arrow_back.png | 0
Rgui/kivy/theming/light/bit_logo.png -> electrum/gui/kivy/theming/light/bit_logo.png | 0
Rgui/kivy/theming/light/blue_bg_round_rb.png -> electrum/gui/kivy/theming/light/blue_bg_round_rb.png | 0
Rgui/kivy/theming/light/btn_create_account.png -> electrum/gui/kivy/theming/light/btn_create_account.png | 0
Rgui/kivy/theming/light/btn_create_act_disabled.png -> electrum/gui/kivy/theming/light/btn_create_act_disabled.png | 0
Rgui/kivy/theming/light/btn_nfc.png -> electrum/gui/kivy/theming/light/btn_nfc.png | 0
Rgui/kivy/theming/light/btn_send_address.png -> electrum/gui/kivy/theming/light/btn_send_address.png | 0
Rgui/kivy/theming/light/btn_send_nfc.png -> electrum/gui/kivy/theming/light/btn_send_nfc.png | 0
Rgui/kivy/theming/light/calculator.png -> electrum/gui/kivy/theming/light/calculator.png | 0
Rgui/kivy/theming/light/camera.png -> electrum/gui/kivy/theming/light/camera.png | 0
Rgui/kivy/theming/light/card.png -> electrum/gui/kivy/theming/light/card.png | 0
Rgui/kivy/theming/light/card_bottom.png -> electrum/gui/kivy/theming/light/card_bottom.png | 0
Rgui/kivy/theming/light/card_btn.png -> electrum/gui/kivy/theming/light/card_btn.png | 0
Rgui/kivy/theming/light/card_top.png -> electrum/gui/kivy/theming/light/card_top.png | 0
Rgui/kivy/theming/light/carousel_deselected.png -> electrum/gui/kivy/theming/light/carousel_deselected.png | 0
Rgui/kivy/theming/light/carousel_selected.png -> electrum/gui/kivy/theming/light/carousel_selected.png | 0
Rgui/kivy/theming/light/clock1.png -> electrum/gui/kivy/theming/light/clock1.png | 0
Rgui/kivy/theming/light/clock2.png -> electrum/gui/kivy/theming/light/clock2.png | 0
Rgui/kivy/theming/light/clock3.png -> electrum/gui/kivy/theming/light/clock3.png | 0
Rgui/kivy/theming/light/clock4.png -> electrum/gui/kivy/theming/light/clock4.png | 0
Rgui/kivy/theming/light/clock5.png -> electrum/gui/kivy/theming/light/clock5.png | 0
Rgui/kivy/theming/light/close.png -> electrum/gui/kivy/theming/light/close.png | 0
Rgui/kivy/theming/light/closebutton.png -> electrum/gui/kivy/theming/light/closebutton.png | 0
Rgui/kivy/theming/light/confirmed.png -> electrum/gui/kivy/theming/light/confirmed.png | 0
Rgui/kivy/theming/light/contact.png -> electrum/gui/kivy/theming/light/contact.png | 0
Rgui/kivy/theming/light/contact_overlay.png -> electrum/gui/kivy/theming/light/contact_overlay.png | 0
Rgui/kivy/theming/light/create_act_text.png -> electrum/gui/kivy/theming/light/create_act_text.png | 0
Rgui/kivy/theming/light/create_act_text_active.png -> electrum/gui/kivy/theming/light/create_act_text_active.png | 0
Rgui/kivy/theming/light/dialog.png -> electrum/gui/kivy/theming/light/dialog.png | 0
Rgui/kivy/theming/light/dropdown_background.png -> electrum/gui/kivy/theming/light/dropdown_background.png | 0
Rgui/kivy/theming/light/electrum_icon640.png -> electrum/gui/kivy/theming/light/electrum_icon640.png | 0
Rgui/kivy/theming/light/error.png -> electrum/gui/kivy/theming/light/error.png | 0
Rgui/kivy/theming/light/gear.png -> electrum/gui/kivy/theming/light/gear.png | 0
Rgui/kivy/theming/light/globe.png -> electrum/gui/kivy/theming/light/globe.png | 0
Rgui/kivy/theming/light/icon_border.png -> electrum/gui/kivy/theming/light/icon_border.png | 0
Rgui/kivy/theming/light/important.png -> electrum/gui/kivy/theming/light/important.png | 0
Rgui/kivy/theming/light/info.png -> electrum/gui/kivy/theming/light/info.png | 0
Rgui/kivy/theming/light/lightblue_bg_round_lb.png -> electrum/gui/kivy/theming/light/lightblue_bg_round_lb.png | 0
Rgui/kivy/theming/light/logo.png -> electrum/gui/kivy/theming/light/logo.png | 0
Rgui/kivy/theming/light/logo_atom_dull.png -> electrum/gui/kivy/theming/light/logo_atom_dull.png | 0
Rgui/kivy/theming/light/mail_icon.png -> electrum/gui/kivy/theming/light/mail_icon.png | 0
Rgui/kivy/theming/light/manualentry.png -> electrum/gui/kivy/theming/light/manualentry.png | 0
Rgui/kivy/theming/light/network.png -> electrum/gui/kivy/theming/light/network.png | 0
Rgui/kivy/theming/light/nfc.png -> electrum/gui/kivy/theming/light/nfc.png | 0
Rgui/kivy/theming/light/nfc_clock.png -> electrum/gui/kivy/theming/light/nfc_clock.png | 0
Rgui/kivy/theming/light/nfc_phone.png -> electrum/gui/kivy/theming/light/nfc_phone.png | 0
Rgui/kivy/theming/light/nfc_stage_one.png -> electrum/gui/kivy/theming/light/nfc_stage_one.png | 0
Rgui/kivy/theming/light/overflow_background.png -> electrum/gui/kivy/theming/light/overflow_background.png | 0
Rgui/kivy/theming/light/overflow_btn_dn.png -> electrum/gui/kivy/theming/light/overflow_btn_dn.png | 0
Rgui/kivy/theming/light/paste_icon.png -> electrum/gui/kivy/theming/light/paste_icon.png | 0
Rgui/kivy/theming/light/pen.png -> electrum/gui/kivy/theming/light/pen.png | 0
Rgui/kivy/theming/light/qrcode.png -> electrum/gui/kivy/theming/light/qrcode.png | 0
Rgui/kivy/theming/light/save.png -> electrum/gui/kivy/theming/light/save.png | 0
Rgui/kivy/theming/light/settings.png -> electrum/gui/kivy/theming/light/settings.png | 0
Rgui/kivy/theming/light/shadow.png -> electrum/gui/kivy/theming/light/shadow.png | 0
Rgui/kivy/theming/light/shadow_right.png -> electrum/gui/kivy/theming/light/shadow_right.png | 0
Rgui/kivy/theming/light/share.png -> electrum/gui/kivy/theming/light/share.png | 0
Rgui/kivy/theming/light/star_big_inactive.png -> electrum/gui/kivy/theming/light/star_big_inactive.png | 0
Rgui/kivy/theming/light/stepper_full.png -> electrum/gui/kivy/theming/light/stepper_full.png | 0
Rgui/kivy/theming/light/stepper_left.png -> electrum/gui/kivy/theming/light/stepper_left.png | 0
Rgui/kivy/theming/light/stepper_restore_password.png -> electrum/gui/kivy/theming/light/stepper_restore_password.png | 0
Rgui/kivy/theming/light/stepper_restore_seed.png -> electrum/gui/kivy/theming/light/stepper_restore_seed.png | 0
Rgui/kivy/theming/light/tab.png -> electrum/gui/kivy/theming/light/tab.png | 0
Rgui/kivy/theming/light/tab_btn.png -> electrum/gui/kivy/theming/light/tab_btn.png | 0
Rgui/kivy/theming/light/tab_btn_disabled.png -> electrum/gui/kivy/theming/light/tab_btn_disabled.png | 0
Rgui/kivy/theming/light/tab_btn_pressed.png -> electrum/gui/kivy/theming/light/tab_btn_pressed.png | 0
Rgui/kivy/theming/light/tab_disabled.png -> electrum/gui/kivy/theming/light/tab_disabled.png | 0
Rgui/kivy/theming/light/tab_strip.png -> electrum/gui/kivy/theming/light/tab_strip.png | 0
Rgui/kivy/theming/light/textinput_active.png -> electrum/gui/kivy/theming/light/textinput_active.png | 0
Rgui/kivy/theming/light/unconfirmed.png -> electrum/gui/kivy/theming/light/unconfirmed.png | 0
Rgui/kivy/theming/light/wallet.png -> electrum/gui/kivy/theming/light/wallet.png | 0
Rgui/kivy/theming/light/wallets.png -> electrum/gui/kivy/theming/light/wallets.png | 0
Rgui/kivy/theming/light/white_bg_round_top.png -> electrum/gui/kivy/theming/light/white_bg_round_top.png | 0
Rgui/kivy/tools/bitcoin_intent.xml -> electrum/gui/kivy/tools/bitcoin_intent.xml | 0
Rgui/kivy/tools/blacklist.txt -> electrum/gui/kivy/tools/blacklist.txt | 0
Rgui/kivy/tools/buildozer.spec -> electrum/gui/kivy/tools/buildozer.spec | 0
Rgui/kivy/uix/__init__.py -> electrum/gui/kivy/uix/__init__.py | 0
Rgui/kivy/uix/combobox.py -> electrum/gui/kivy/uix/combobox.py | 0
Aelectrum/gui/kivy/uix/context_menu.py | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/__init__.py | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/addresses.py | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/dialogs/amount_dialog.py -> electrum/gui/kivy/uix/dialogs/amount_dialog.py | 0
Aelectrum/gui/kivy/uix/dialogs/bump_fee_dialog.py | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/dialogs/checkbox_dialog.py -> electrum/gui/kivy/uix/dialogs/checkbox_dialog.py | 0
Rgui/kivy/uix/dialogs/choice_dialog.py -> electrum/gui/kivy/uix/dialogs/choice_dialog.py | 0
Rgui/kivy/uix/dialogs/crash_reporter.py -> electrum/gui/kivy/uix/dialogs/crash_reporter.py | 0
Aelectrum/gui/kivy/uix/dialogs/fee_dialog.py | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/fx_dialog.py | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/installwizard.py | 1038+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/invoices.py | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/label_dialog.py | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/nfc_transaction.py | 33+++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/password_dialog.py | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/dialogs/qr_dialog.py -> electrum/gui/kivy/uix/dialogs/qr_dialog.py | 0
Aelectrum/gui/kivy/uix/dialogs/qr_scanner.py | 44++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/question.py | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/requests.py | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/dialogs/seed_options.py -> electrum/gui/kivy/uix/dialogs/seed_options.py | 0
Aelectrum/gui/kivy/uix/dialogs/settings.py | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/dialogs/tx_dialog.py | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/dialogs/wallets.py -> electrum/gui/kivy/uix/dialogs/wallets.py | 0
Rgui/kivy/uix/drawer.py -> electrum/gui/kivy/uix/drawer.py | 0
Rgui/kivy/uix/gridview.py -> electrum/gui/kivy/uix/gridview.py | 0
Aelectrum/gui/kivy/uix/menus.py | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/qrcodewidget.py -> electrum/gui/kivy/uix/qrcodewidget.py | 0
Aelectrum/gui/kivy/uix/screens.py | 484+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/ui_screens/about.kv -> electrum/gui/kivy/uix/ui_screens/about.kv | 0
Aelectrum/gui/kivy/uix/ui_screens/history.kv | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/ui_screens/invoice.kv -> electrum/gui/kivy/uix/ui_screens/invoice.kv | 0
Rgui/kivy/uix/ui_screens/network.kv -> electrum/gui/kivy/uix/ui_screens/network.kv | 0
Rgui/kivy/uix/ui_screens/proxy.kv -> electrum/gui/kivy/uix/ui_screens/proxy.kv | 0
Aelectrum/gui/kivy/uix/ui_screens/receive.kv | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/kivy/uix/ui_screens/send.kv | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/uix/ui_screens/server.kv -> electrum/gui/kivy/uix/ui_screens/server.kv | 0
Rgui/kivy/uix/ui_screens/status.kv -> electrum/gui/kivy/uix/ui_screens/status.kv | 0
Aelectrum/gui/qt/__init__.py | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/qt/address_dialog.py -> electrum/gui/qt/address_dialog.py | 0
Aelectrum/gui/qt/address_list.py | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/qt/amountedit.py -> electrum/gui/qt/amountedit.py | 0
Aelectrum/gui/qt/completion_text_edit.py | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/qt/console.py -> electrum/gui/qt/console.py | 0
Aelectrum/gui/qt/contact_list.py | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/qt/exception_window.py -> electrum/gui/qt/exception_window.py | 0
Rgui/qt/fee_slider.py -> electrum/gui/qt/fee_slider.py | 0
Rgui/qt/history_list.py -> electrum/gui/qt/history_list.py | 0
Aelectrum/gui/qt/installwizard.py | 644+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/qt/invoice_list.py -> electrum/gui/qt/invoice_list.py | 0
Aelectrum/gui/qt/main_window.py | 3220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/qt/network_dialog.py -> electrum/gui/qt/network_dialog.py | 0
Aelectrum/gui/qt/password_dialog.py | 305++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/qt/paytoedit.py -> electrum/gui/qt/paytoedit.py | 0
Rgui/qt/qrcodewidget.py -> electrum/gui/qt/qrcodewidget.py | 0
Aelectrum/gui/qt/qrtextedit.py | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/qt/qrwindow.py | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/qt/request_list.py | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/qt/seed_dialog.py | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/gui/qt/transaction_dialog.py | 328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/qt/util.py -> electrum/gui/qt/util.py | 0
Rgui/qt/utxo_list.py -> electrum/gui/qt/utxo_list.py | 0
Rgui/stdio.py -> electrum/gui/stdio.py | 0
Aelectrum/gui/text.py | 503+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/i18n.py | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/interface.py | 407+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/jsonrpc.py | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/keystore.py | 798+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/mnemonic.py | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/msqr.py | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/network.py | 1297+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/old_mnemonic.py | 1697+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/paymentrequest.proto | 47+++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/paymentrequest.py | 523+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/paymentrequest_pb2.py | 367+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/pem.py | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plot.py | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugin.py | 566+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/README | 31+++++++++++++++++++++++++++++++
Aelectrum/plugins/__init__.py | 26++++++++++++++++++++++++++
Aelectrum/plugins/audio_modem/__init__.py | 7+++++++
Aelectrum/plugins/audio_modem/qt.py | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/cosigner_pool/__init__.py | 9+++++++++
Aelectrum/plugins/cosigner_pool/qt.py | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/digitalbitbox/__init__.py | 6++++++
Aelectrum/plugins/digitalbitbox/cmdline.py | 14++++++++++++++
Aelectrum/plugins/digitalbitbox/digitalbitbox.py | 767+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/digitalbitbox/qt.py | 43+++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/email_requests/__init__.py | 5+++++
Aelectrum/plugins/email_requests/qt.py | 271+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/greenaddress_instant/__init__.py | 5+++++
Aelectrum/plugins/greenaddress_instant/qt.py | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/hw_wallet/__init__.py | 2++
Aelectrum/plugins/hw_wallet/cmdline.py | 46++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/hw_wallet/plugin.py | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/hw_wallet/qt.py | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/keepkey/__init__.py | 7+++++++
Aelectrum/plugins/keepkey/client.py | 14++++++++++++++
Aelectrum/plugins/keepkey/clientbase.py | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/keepkey/cmdline.py | 14++++++++++++++
Aelectrum/plugins/keepkey/keepkey.py | 438+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/keepkey/qt.py | 586+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/labels/__init__.py | 9+++++++++
Aelectrum/plugins/labels/cmdline.py | 11+++++++++++
Aelectrum/plugins/labels/kivy.py | 14++++++++++++++
Aelectrum/plugins/labels/labels.py | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/labels/qt.py | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/ledger/__init__.py | 7+++++++
Aelectrum/plugins/ledger/auth2fa.py | 358+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/ledger/cmdline.py | 14++++++++++++++
Aelectrum/plugins/ledger/ledger.py | 637+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/ledger/qt.py | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/revealer/DejaVuSansMono-Bold.ttf | 0
Aelectrum/plugins/revealer/LICENSE_DEJAVU.txt | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/revealer/SIL Open Font License.txt | 44++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/revealer/SourceSansPro-Bold.otf | 0
Aelectrum/plugins/revealer/__init__.py | 16++++++++++++++++
Aelectrum/plugins/revealer/qt.py | 724+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/trezor/__init__.py | 8++++++++
Aelectrum/plugins/trezor/client.py | 11+++++++++++
Aelectrum/plugins/trezor/clientbase.py | 265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/trezor/cmdline.py | 14++++++++++++++
Aelectrum/plugins/trezor/qt.py | 613+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/trezor/transport.py | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/trezor/trezor.py | 516+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/trustedcoin/__init__.py | 11+++++++++++
Aelectrum/plugins/trustedcoin/cmdline.py | 45+++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/trustedcoin/kivy.py | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/trustedcoin/qt.py | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/trustedcoin/trustedcoin.py | 672+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/virtualkeyboard/__init__.py | 5+++++
Aelectrum/plugins/virtualkeyboard/qt.py | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/qrscanner.py | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/ripemd.py | 393+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/rsakey.py | 542+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/scripts/bip70.py | 35+++++++++++++++++++++++++++++++++++
Aelectrum/scripts/block_headers.py | 29+++++++++++++++++++++++++++++
Aelectrum/scripts/estimate_fee.py | 7+++++++
Aelectrum/scripts/get_history.py | 18++++++++++++++++++
Aelectrum/scripts/peers.py | 14++++++++++++++
Aelectrum/scripts/servers.py | 10++++++++++
Aelectrum/scripts/txradar.py | 20++++++++++++++++++++
Aelectrum/scripts/util.py | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/scripts/watch_address.py | 36++++++++++++++++++++++++++++++++++++
Aelectrum/segwit_addr.py | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/servers.json | 304+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/servers_regtest.json | 8++++++++
Aelectrum/servers_testnet.json | 31+++++++++++++++++++++++++++++++
Aelectrum/simple_config.py | 552+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/storage.py | 645+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/synchronizer.py | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/__init__.py | 38++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_bitcoin.py | 761+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_commands.py | 33+++++++++++++++++++++++++++++++++
Aelectrum/tests/test_dnssec.py | 41+++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_interface.py | 28++++++++++++++++++++++++++++
Aelectrum/tests/test_mnemonic.py | 42++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_simple_config.py | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_storage_upgrade.py | 301+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_transaction.py | 813+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_util.py | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_wallet.py | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_wallet_vertical.py | 1603+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/transaction.py | 1229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/util.py | 903+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/verifier.py | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/version.py | 18++++++++++++++++++
Aelectrum/wallet.py | 2374+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/websockets.py | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/wordlist/chinese_simplified.txt | 2048+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/wordlist/english.txt | 2048+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/wordlist/japanese.txt | 2048+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/wordlist/portuguese.txt | 1654+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/wordlist/spanish.txt | 2048+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/x509.py | 341+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dgui/kivy/Makefile | 32--------------------------------
Dgui/kivy/main.kv | 464-------------------------------------------------------------------------------
Dgui/kivy/main_window.py | 1028-------------------------------------------------------------------------------
Dgui/kivy/nfc_scanner/__init__.py | 44--------------------------------------------
Dgui/kivy/nfc_scanner/scanner_android.py | 242-------------------------------------------------------------------------------
Dgui/kivy/nfc_scanner/scanner_dummy.py | 52----------------------------------------------------
Dgui/kivy/uix/context_menu.py | 56--------------------------------------------------------
Dgui/kivy/uix/dialogs/__init__.py | 220-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/addresses.py | 180-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/bump_fee_dialog.py | 118-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/fee_dialog.py | 131-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/fx_dialog.py | 111-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/installwizard.py | 1038-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/invoices.py | 169-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/label_dialog.py | 55-------------------------------------------------------
Dgui/kivy/uix/dialogs/nfc_transaction.py | 33---------------------------------
Dgui/kivy/uix/dialogs/password_dialog.py | 142-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/qr_scanner.py | 44--------------------------------------------
Dgui/kivy/uix/dialogs/question.py | 53-----------------------------------------------------
Dgui/kivy/uix/dialogs/requests.py | 157-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/settings.py | 220-------------------------------------------------------------------------------
Dgui/kivy/uix/dialogs/tx_dialog.py | 184-------------------------------------------------------------------------------
Dgui/kivy/uix/menus.py | 95-------------------------------------------------------------------------------
Dgui/kivy/uix/screens.py | 484-------------------------------------------------------------------------------
Dgui/kivy/uix/ui_screens/history.kv | 78------------------------------------------------------------------------------
Dgui/kivy/uix/ui_screens/receive.kv | 142-------------------------------------------------------------------------------
Dgui/kivy/uix/ui_screens/send.kv | 127-------------------------------------------------------------------------------
Dgui/qt/__init__.py | 313-------------------------------------------------------------------------------
Dgui/qt/address_list.py | 195-------------------------------------------------------------------------------
Dgui/qt/completion_text_edit.py | 121-------------------------------------------------------------------------------
Dgui/qt/contact_list.py | 98-------------------------------------------------------------------------------
Dgui/qt/installwizard.py | 643-------------------------------------------------------------------------------
Dgui/qt/main_window.py | 3221-------------------------------------------------------------------------------
Dgui/qt/password_dialog.py | 305------------------------------------------------------------------------------
Dgui/qt/qrtextedit.py | 76----------------------------------------------------------------------------
Dgui/qt/qrwindow.py | 89-------------------------------------------------------------------------------
Dgui/qt/request_list.py | 129-------------------------------------------------------------------------------
Dgui/qt/seed_dialog.py | 211-------------------------------------------------------------------------------
Dgui/qt/transaction_dialog.py | 328-------------------------------------------------------------------------------
Dgui/text.py | 503-------------------------------------------------------------------------------
Dlib/__init__.py | 14--------------
Dlib/base_crash_reporter.py | 127-------------------------------------------------------------------------------
Dlib/commands.py | 892-------------------------------------------------------------------------------
Dlib/daemon.py | 316-------------------------------------------------------------------------------
Dlib/exchange_rate.py | 573-------------------------------------------------------------------------------
Dlib/i18n.py | 81-------------------------------------------------------------------------------
Dlib/interface.py | 407-------------------------------------------------------------------------------
Dlib/jsonrpc.py | 98-------------------------------------------------------------------------------
Dlib/keystore.py | 799-------------------------------------------------------------------------------
Dlib/mnemonic.py | 183-------------------------------------------------------------------------------
Dlib/msqr.py | 94-------------------------------------------------------------------------------
Dlib/network.py | 1297-------------------------------------------------------------------------------
Dlib/old_mnemonic.py | 1697-------------------------------------------------------------------------------
Dlib/paymentrequest.proto | 47-----------------------------------------------
Dlib/paymentrequest.py | 528-------------------------------------------------------------------------------
Dlib/paymentrequest_pb2.py | 367-------------------------------------------------------------------------------
Dlib/pem.py | 191-------------------------------------------------------------------------------
Dlib/plot.py | 63---------------------------------------------------------------
Dlib/plugins.py | 572-------------------------------------------------------------------------------
Dlib/qrscanner.py | 88-------------------------------------------------------------------------------
Dlib/ripemd.py | 393-------------------------------------------------------------------------------
Dlib/rsakey.py | 542-------------------------------------------------------------------------------
Dlib/segwit_addr.py | 122-------------------------------------------------------------------------------
Dlib/servers.json | 304-------------------------------------------------------------------------------
Dlib/servers_regtest.json | 8--------
Dlib/servers_testnet.json | 31-------------------------------
Dlib/simple_config.py | 552-------------------------------------------------------------------------------
Dlib/storage.py | 647-------------------------------------------------------------------------------
Dlib/synchronizer.py | 213-------------------------------------------------------------------------------
Dlib/tests/__init__.py | 38--------------------------------------
Dlib/tests/test_bitcoin.py | 761-------------------------------------------------------------------------------
Dlib/tests/test_commands.py | 33---------------------------------
Dlib/tests/test_dnssec.py | 41-----------------------------------------
Dlib/tests/test_interface.py | 28----------------------------
Dlib/tests/test_mnemonic.py | 42------------------------------------------
Dlib/tests/test_simple_config.py | 149-------------------------------------------------------------------------------
Dlib/tests/test_storage_upgrade.py | 301-------------------------------------------------------------------------------
Dlib/tests/test_transaction.py | 813-------------------------------------------------------------------------------
Dlib/tests/test_util.py | 109-------------------------------------------------------------------------------
Dlib/tests/test_wallet.py | 71-----------------------------------------------------------------------
Dlib/tests/test_wallet_vertical.py | 1604-------------------------------------------------------------------------------
Dlib/transaction.py | 1229-------------------------------------------------------------------------------
Dlib/util.py | 903-------------------------------------------------------------------------------
Dlib/verifier.py | 158-------------------------------------------------------------------------------
Dlib/version.py | 18------------------
Dlib/wallet.py | 2377-------------------------------------------------------------------------------
Dlib/websockets.py | 140-------------------------------------------------------------------------------
Dlib/wordlist/chinese_simplified.txt | 2048-------------------------------------------------------------------------------
Dlib/wordlist/english.txt | 2048-------------------------------------------------------------------------------
Dlib/wordlist/japanese.txt | 2048-------------------------------------------------------------------------------
Dlib/wordlist/portuguese.txt | 1654-------------------------------------------------------------------------------
Dlib/wordlist/spanish.txt | 2048-------------------------------------------------------------------------------
Dlib/x509.py | 341-------------------------------------------------------------------------------
Dplugins/README | 31-------------------------------
Dplugins/__init__.py | 26--------------------------
Dplugins/audio_modem/__init__.py | 7-------
Dplugins/audio_modem/qt.py | 128-------------------------------------------------------------------------------
Dplugins/cosigner_pool/__init__.py | 9---------
Dplugins/cosigner_pool/qt.py | 228-------------------------------------------------------------------------------
Dplugins/digitalbitbox/__init__.py | 6------
Dplugins/digitalbitbox/cmdline.py | 14--------------
Dplugins/digitalbitbox/digitalbitbox.py | 768-------------------------------------------------------------------------------
Dplugins/digitalbitbox/qt.py | 43-------------------------------------------
Dplugins/email_requests/__init__.py | 5-----
Dplugins/email_requests/qt.py | 271-------------------------------------------------------------------------------
Dplugins/greenaddress_instant/__init__.py | 5-----
Dplugins/greenaddress_instant/qt.py | 107-------------------------------------------------------------------------------
Dplugins/hw_wallet/__init__.py | 2--
Dplugins/hw_wallet/cmdline.py | 46----------------------------------------------
Dplugins/hw_wallet/plugin.py | 89-------------------------------------------------------------------------------
Dplugins/hw_wallet/qt.py | 235-------------------------------------------------------------------------------
Dplugins/keepkey/__init__.py | 7-------
Dplugins/keepkey/client.py | 14--------------
Dplugins/keepkey/clientbase.py | 250-------------------------------------------------------------------------------
Dplugins/keepkey/cmdline.py | 14--------------
Dplugins/keepkey/keepkey.py | 438-------------------------------------------------------------------------------
Dplugins/keepkey/qt.py | 586-------------------------------------------------------------------------------
Dplugins/labels/__init__.py | 9---------
Dplugins/labels/cmdline.py | 11-----------
Dplugins/labels/kivy.py | 14--------------
Dplugins/labels/labels.py | 168-------------------------------------------------------------------------------
Dplugins/labels/qt.py | 78------------------------------------------------------------------------------
Dplugins/ledger/__init__.py | 7-------
Dplugins/ledger/auth2fa.py | 358-------------------------------------------------------------------------------
Dplugins/ledger/cmdline.py | 14--------------
Dplugins/ledger/ledger.py | 637-------------------------------------------------------------------------------
Dplugins/ledger/qt.py | 81-------------------------------------------------------------------------------
Dplugins/revealer/DejaVuSansMono-Bold.ttf | 0
Dplugins/revealer/LICENSE_DEJAVU.txt | 99-------------------------------------------------------------------------------
Dplugins/revealer/SIL Open Font License.txt | 44--------------------------------------------
Dplugins/revealer/SourceSansPro-Bold.otf | 0
Dplugins/revealer/__init__.py | 17-----------------
Dplugins/revealer/qt.py | 723-------------------------------------------------------------------------------
Dplugins/trezor/__init__.py | 8--------
Dplugins/trezor/client.py | 11-----------
Dplugins/trezor/clientbase.py | 265-------------------------------------------------------------------------------
Dplugins/trezor/cmdline.py | 14--------------
Dplugins/trezor/qt.py | 613-------------------------------------------------------------------------------
Dplugins/trezor/transport.py | 95-------------------------------------------------------------------------------
Dplugins/trezor/trezor.py | 516-------------------------------------------------------------------------------
Dplugins/trustedcoin/__init__.py | 11-----------
Dplugins/trustedcoin/cmdline.py | 45---------------------------------------------
Dplugins/trustedcoin/kivy.py | 110-------------------------------------------------------------------------------
Dplugins/trustedcoin/qt.py | 313-------------------------------------------------------------------------------
Dplugins/trustedcoin/trustedcoin.py | 676-------------------------------------------------------------------------------
Dplugins/virtualkeyboard/__init__.py | 5-----
Dplugins/virtualkeyboard/qt.py | 61-------------------------------------------------------------
Arun_electrum | 473+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dscripts/bip70 | 35-----------------------------------
Dscripts/block_headers | 29-----------------------------
Dscripts/estimate_fee | 6------
Dscripts/get_history | 18------------------
Dscripts/peers | 14--------------
Dscripts/servers | 9---------
Dscripts/txradar | 19-------------------
Dscripts/util.py | 87-------------------------------------------------------------------------------
Dscripts/watch_address | 36------------------------------------
Msetup.py | 44+++++++++++++++++++++-----------------------
Msnap/snapcraft.yaml | 2+-
Mtox.ini | 2+-
474 files changed, 51372 insertions(+), 51404 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -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 diff --git a/README.rst b/README.rst @@ -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:: @@ -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):: diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx @@ -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..." diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec @@ -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' + } +) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh @@ -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 diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec @@ -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') @@ -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, @@ -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 @@ -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, diff --git a/contrib/make_apk b/contrib/make_apk @@ -1,6 +1,6 @@ #!/bin/bash -pushd ./gui/kivy/ +pushd ./electrum/gui/kivy/ if [[ -n "$1" && "$1" == "release" ]] ; then echo -n Keystore Password: diff --git a/contrib/make_locale b/contrib/make_locale @@ -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) @@ -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]' diff --git a/electrum b/electrum @@ -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) diff --git a/electrum-env b/electrum-env @@ -22,6 +22,6 @@ fi export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH" -./electrum "$@" +./run_electrum "$@" deactivate diff --git a/electrum/__init__.py b/electrum/__init__.py @@ -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 diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py @@ -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 diff --git a/lib/base_wizard.py b/electrum/base_wizard.py diff --git a/lib/bitcoin.py b/electrum/bitcoin.py diff --git a/lib/blockchain.py b/electrum/blockchain.py diff --git a/lib/checkpoints.json b/electrum/checkpoints.json diff --git a/lib/checkpoints_testnet.json b/electrum/checkpoints_testnet.json diff --git a/lib/coinchooser.py b/electrum/coinchooser.py diff --git a/electrum/commands.py b/electrum/commands.py @@ -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 diff --git a/lib/constants.py b/electrum/constants.py diff --git a/lib/contacts.py b/electrum/contacts.py diff --git a/lib/crypto.py b/electrum/crypto.py diff --git a/lib/currencies.json b/electrum/currencies.json diff --git a/electrum/daemon.py b/electrum/daemon.py @@ -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 diff --git a/lib/dnssec.py b/electrum/dnssec.py diff --git a/lib/ecc.py b/electrum/ecc.py diff --git a/lib/ecc_fast.py b/electrum/ecc_fast.py diff --git a/electrum/electrum b/electrum/electrum @@ -0,0 +1 @@ +../run_electrum+ \ No newline at end of file diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py @@ -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) diff --git a/gui/__init__.py b/electrum/gui/__init__.py diff --git a/electrum/gui/kivy/Makefile b/electrum/gui/kivy/Makefile @@ -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 diff --git a/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md diff --git a/gui/kivy/__init__.py b/electrum/gui/kivy/__init__.py diff --git a/gui/kivy/data/background.png b/electrum/gui/kivy/data/background.png Binary files differ. diff --git a/gui/kivy/data/fonts/Roboto-Bold.ttf b/electrum/gui/kivy/data/fonts/Roboto-Bold.ttf Binary files differ. diff --git a/gui/kivy/data/fonts/Roboto-Condensed.ttf b/electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf Binary files differ. diff --git a/gui/kivy/data/fonts/Roboto-Medium.ttf b/electrum/gui/kivy/data/fonts/Roboto-Medium.ttf Binary files differ. diff --git a/gui/kivy/data/fonts/Roboto.ttf b/electrum/gui/kivy/data/fonts/Roboto.ttf Binary files differ. diff --git a/gui/kivy/data/fonts/tron/License.txt b/electrum/gui/kivy/data/fonts/tron/License.txt diff --git a/gui/kivy/data/fonts/tron/Readme.txt b/electrum/gui/kivy/data/fonts/tron/Readme.txt diff --git a/gui/kivy/data/fonts/tron/Tr2n.ttf b/electrum/gui/kivy/data/fonts/tron/Tr2n.ttf Binary files differ. diff --git a/gui/kivy/data/glsl/default.fs b/electrum/gui/kivy/data/glsl/default.fs diff --git a/gui/kivy/data/glsl/default.png b/electrum/gui/kivy/data/glsl/default.png Binary files differ. diff --git a/gui/kivy/data/glsl/default.vs b/electrum/gui/kivy/data/glsl/default.vs diff --git a/gui/kivy/data/glsl/header.fs b/electrum/gui/kivy/data/glsl/header.fs diff --git a/gui/kivy/data/glsl/header.vs b/electrum/gui/kivy/data/glsl/header.vs diff --git a/gui/kivy/data/images/defaulttheme-0.png b/electrum/gui/kivy/data/images/defaulttheme-0.png Binary files differ. diff --git a/gui/kivy/data/images/defaulttheme.atlas b/electrum/gui/kivy/data/images/defaulttheme.atlas 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 diff --git a/gui/kivy/data/logo/kivy-icon-32.png b/electrum/gui/kivy/data/logo/kivy-icon-32.png Binary files differ. diff --git a/gui/kivy/data/style.kv b/electrum/gui/kivy/data/style.kv diff --git a/gui/kivy/i18n.py b/electrum/gui/kivy/i18n.py diff --git a/electrum/gui/kivy/main.kv b/electrum/gui/kivy/main.kv @@ -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 diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -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)) diff --git a/electrum/gui/kivy/nfc_scanner/__init__.py b/electrum/gui/kivy/nfc_scanner/__init__.py @@ -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') diff --git a/electrum/gui/kivy/nfc_scanner/scanner_android.py b/electrum/gui/kivy/nfc_scanner/scanner_android.py @@ -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() diff --git a/electrum/gui/kivy/nfc_scanner/scanner_dummy.py b/electrum/gui/kivy/nfc_scanner/scanner_dummy.py @@ -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') diff --git a/gui/kivy/theming/light/action_bar.png b/electrum/gui/kivy/theming/light/action_bar.png Binary files differ. diff --git a/gui/kivy/theming/light/action_button_group.png b/electrum/gui/kivy/theming/light/action_button_group.png Binary files differ. diff --git a/gui/kivy/theming/light/action_group_dark.png b/electrum/gui/kivy/theming/light/action_group_dark.png Binary files differ. diff --git a/gui/kivy/theming/light/action_group_light.png b/electrum/gui/kivy/theming/light/action_group_light.png Binary files differ. diff --git a/gui/kivy/theming/light/add_contact.png b/electrum/gui/kivy/theming/light/add_contact.png Binary files differ. diff --git a/gui/kivy/theming/light/arrow_back.png b/electrum/gui/kivy/theming/light/arrow_back.png Binary files differ. diff --git a/gui/kivy/theming/light/bit_logo.png b/electrum/gui/kivy/theming/light/bit_logo.png Binary files differ. 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. diff --git a/gui/kivy/theming/light/btn_create_account.png b/electrum/gui/kivy/theming/light/btn_create_account.png Binary files differ. 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. diff --git a/gui/kivy/theming/light/btn_nfc.png b/electrum/gui/kivy/theming/light/btn_nfc.png Binary files differ. diff --git a/gui/kivy/theming/light/btn_send_address.png b/electrum/gui/kivy/theming/light/btn_send_address.png Binary files differ. diff --git a/gui/kivy/theming/light/btn_send_nfc.png b/electrum/gui/kivy/theming/light/btn_send_nfc.png Binary files differ. diff --git a/gui/kivy/theming/light/calculator.png b/electrum/gui/kivy/theming/light/calculator.png Binary files differ. diff --git a/gui/kivy/theming/light/camera.png b/electrum/gui/kivy/theming/light/camera.png Binary files differ. diff --git a/gui/kivy/theming/light/card.png b/electrum/gui/kivy/theming/light/card.png Binary files differ. diff --git a/gui/kivy/theming/light/card_bottom.png b/electrum/gui/kivy/theming/light/card_bottom.png Binary files differ. diff --git a/gui/kivy/theming/light/card_btn.png b/electrum/gui/kivy/theming/light/card_btn.png Binary files differ. diff --git a/gui/kivy/theming/light/card_top.png b/electrum/gui/kivy/theming/light/card_top.png Binary files differ. diff --git a/gui/kivy/theming/light/carousel_deselected.png b/electrum/gui/kivy/theming/light/carousel_deselected.png Binary files differ. diff --git a/gui/kivy/theming/light/carousel_selected.png b/electrum/gui/kivy/theming/light/carousel_selected.png Binary files differ. diff --git a/gui/kivy/theming/light/clock1.png b/electrum/gui/kivy/theming/light/clock1.png Binary files differ. diff --git a/gui/kivy/theming/light/clock2.png b/electrum/gui/kivy/theming/light/clock2.png Binary files differ. diff --git a/gui/kivy/theming/light/clock3.png b/electrum/gui/kivy/theming/light/clock3.png Binary files differ. diff --git a/gui/kivy/theming/light/clock4.png b/electrum/gui/kivy/theming/light/clock4.png Binary files differ. diff --git a/gui/kivy/theming/light/clock5.png b/electrum/gui/kivy/theming/light/clock5.png Binary files differ. diff --git a/gui/kivy/theming/light/close.png b/electrum/gui/kivy/theming/light/close.png Binary files differ. diff --git a/gui/kivy/theming/light/closebutton.png b/electrum/gui/kivy/theming/light/closebutton.png Binary files differ. diff --git a/gui/kivy/theming/light/confirmed.png b/electrum/gui/kivy/theming/light/confirmed.png Binary files differ. diff --git a/gui/kivy/theming/light/contact.png b/electrum/gui/kivy/theming/light/contact.png Binary files differ. diff --git a/gui/kivy/theming/light/contact_overlay.png b/electrum/gui/kivy/theming/light/contact_overlay.png Binary files differ. diff --git a/gui/kivy/theming/light/create_act_text.png b/electrum/gui/kivy/theming/light/create_act_text.png Binary files differ. 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. diff --git a/gui/kivy/theming/light/dialog.png b/electrum/gui/kivy/theming/light/dialog.png Binary files differ. diff --git a/gui/kivy/theming/light/dropdown_background.png b/electrum/gui/kivy/theming/light/dropdown_background.png Binary files differ. diff --git a/gui/kivy/theming/light/electrum_icon640.png b/electrum/gui/kivy/theming/light/electrum_icon640.png Binary files differ. diff --git a/gui/kivy/theming/light/error.png b/electrum/gui/kivy/theming/light/error.png Binary files differ. diff --git a/gui/kivy/theming/light/gear.png b/electrum/gui/kivy/theming/light/gear.png Binary files differ. diff --git a/gui/kivy/theming/light/globe.png b/electrum/gui/kivy/theming/light/globe.png Binary files differ. diff --git a/gui/kivy/theming/light/icon_border.png b/electrum/gui/kivy/theming/light/icon_border.png Binary files differ. diff --git a/gui/kivy/theming/light/important.png b/electrum/gui/kivy/theming/light/important.png Binary files differ. diff --git a/gui/kivy/theming/light/info.png b/electrum/gui/kivy/theming/light/info.png Binary files differ. 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. diff --git a/gui/kivy/theming/light/logo.png b/electrum/gui/kivy/theming/light/logo.png Binary files differ. diff --git a/gui/kivy/theming/light/logo_atom_dull.png b/electrum/gui/kivy/theming/light/logo_atom_dull.png Binary files differ. diff --git a/gui/kivy/theming/light/mail_icon.png b/electrum/gui/kivy/theming/light/mail_icon.png Binary files differ. diff --git a/gui/kivy/theming/light/manualentry.png b/electrum/gui/kivy/theming/light/manualentry.png Binary files differ. diff --git a/gui/kivy/theming/light/network.png b/electrum/gui/kivy/theming/light/network.png Binary files differ. diff --git a/gui/kivy/theming/light/nfc.png b/electrum/gui/kivy/theming/light/nfc.png Binary files differ. diff --git a/gui/kivy/theming/light/nfc_clock.png b/electrum/gui/kivy/theming/light/nfc_clock.png Binary files differ. diff --git a/gui/kivy/theming/light/nfc_phone.png b/electrum/gui/kivy/theming/light/nfc_phone.png Binary files differ. diff --git a/gui/kivy/theming/light/nfc_stage_one.png b/electrum/gui/kivy/theming/light/nfc_stage_one.png Binary files differ. diff --git a/gui/kivy/theming/light/overflow_background.png b/electrum/gui/kivy/theming/light/overflow_background.png Binary files differ. diff --git a/gui/kivy/theming/light/overflow_btn_dn.png b/electrum/gui/kivy/theming/light/overflow_btn_dn.png Binary files differ. diff --git a/gui/kivy/theming/light/paste_icon.png b/electrum/gui/kivy/theming/light/paste_icon.png Binary files differ. diff --git a/gui/kivy/theming/light/pen.png b/electrum/gui/kivy/theming/light/pen.png Binary files differ. diff --git a/gui/kivy/theming/light/qrcode.png b/electrum/gui/kivy/theming/light/qrcode.png Binary files differ. diff --git a/gui/kivy/theming/light/save.png b/electrum/gui/kivy/theming/light/save.png Binary files differ. diff --git a/gui/kivy/theming/light/settings.png b/electrum/gui/kivy/theming/light/settings.png Binary files differ. diff --git a/gui/kivy/theming/light/shadow.png b/electrum/gui/kivy/theming/light/shadow.png Binary files differ. diff --git a/gui/kivy/theming/light/shadow_right.png b/electrum/gui/kivy/theming/light/shadow_right.png Binary files differ. diff --git a/gui/kivy/theming/light/share.png b/electrum/gui/kivy/theming/light/share.png Binary files differ. diff --git a/gui/kivy/theming/light/star_big_inactive.png b/electrum/gui/kivy/theming/light/star_big_inactive.png Binary files differ. diff --git a/gui/kivy/theming/light/stepper_full.png b/electrum/gui/kivy/theming/light/stepper_full.png Binary files differ. diff --git a/gui/kivy/theming/light/stepper_left.png b/electrum/gui/kivy/theming/light/stepper_left.png Binary files differ. diff --git a/gui/kivy/theming/light/stepper_restore_password.png b/electrum/gui/kivy/theming/light/stepper_restore_password.png Binary files differ. diff --git a/gui/kivy/theming/light/stepper_restore_seed.png b/electrum/gui/kivy/theming/light/stepper_restore_seed.png Binary files differ. diff --git a/gui/kivy/theming/light/tab.png b/electrum/gui/kivy/theming/light/tab.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_btn.png b/electrum/gui/kivy/theming/light/tab_btn.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_btn_disabled.png b/electrum/gui/kivy/theming/light/tab_btn_disabled.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_btn_pressed.png b/electrum/gui/kivy/theming/light/tab_btn_pressed.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_disabled.png b/electrum/gui/kivy/theming/light/tab_disabled.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_strip.png b/electrum/gui/kivy/theming/light/tab_strip.png Binary files differ. diff --git a/gui/kivy/theming/light/textinput_active.png b/electrum/gui/kivy/theming/light/textinput_active.png Binary files differ. diff --git a/gui/kivy/theming/light/unconfirmed.png b/electrum/gui/kivy/theming/light/unconfirmed.png Binary files differ. diff --git a/gui/kivy/theming/light/wallet.png b/electrum/gui/kivy/theming/light/wallet.png Binary files differ. diff --git a/gui/kivy/theming/light/wallets.png b/electrum/gui/kivy/theming/light/wallets.png Binary files differ. 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. diff --git a/gui/kivy/tools/bitcoin_intent.xml b/electrum/gui/kivy/tools/bitcoin_intent.xml diff --git a/gui/kivy/tools/blacklist.txt b/electrum/gui/kivy/tools/blacklist.txt diff --git a/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec diff --git a/gui/kivy/uix/__init__.py b/electrum/gui/kivy/uix/__init__.py diff --git a/gui/kivy/uix/combobox.py b/electrum/gui/kivy/uix/combobox.py diff --git a/electrum/gui/kivy/uix/context_menu.py b/electrum/gui/kivy/uix/context_menu.py @@ -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() diff --git a/electrum/gui/kivy/uix/dialogs/__init__.py b/electrum/gui/kivy/uix/dialogs/__init__.py @@ -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 diff --git a/electrum/gui/kivy/uix/dialogs/addresses.py b/electrum/gui/kivy/uix/dialogs/addresses.py @@ -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 diff --git a/gui/kivy/uix/dialogs/amount_dialog.py b/electrum/gui/kivy/uix/dialogs/amount_dialog.py diff --git a/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py b/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py @@ -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() diff --git a/gui/kivy/uix/dialogs/checkbox_dialog.py b/electrum/gui/kivy/uix/dialogs/checkbox_dialog.py diff --git a/gui/kivy/uix/dialogs/choice_dialog.py b/electrum/gui/kivy/uix/dialogs/choice_dialog.py diff --git a/gui/kivy/uix/dialogs/crash_reporter.py b/electrum/gui/kivy/uix/dialogs/crash_reporter.py diff --git a/electrum/gui/kivy/uix/dialogs/fee_dialog.py b/electrum/gui/kivy/uix/dialogs/fee_dialog.py @@ -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() diff --git a/electrum/gui/kivy/uix/dialogs/fx_dialog.py b/electrum/gui/kivy/uix/dialogs/fx_dialog.py @@ -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()) diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py @@ -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() diff --git a/electrum/gui/kivy/uix/dialogs/invoices.py b/electrum/gui/kivy/uix/dialogs/invoices.py @@ -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 diff --git a/electrum/gui/kivy/uix/dialogs/label_dialog.py b/electrum/gui/kivy/uix/dialogs/label_dialog.py @@ -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 diff --git a/electrum/gui/kivy/uix/dialogs/nfc_transaction.py b/electrum/gui/kivy/uix/dialogs/nfc_transaction.py @@ -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 diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py @@ -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 = '' diff --git a/gui/kivy/uix/dialogs/qr_dialog.py b/electrum/gui/kivy/uix/dialogs/qr_dialog.py diff --git a/electrum/gui/kivy/uix/dialogs/qr_scanner.py b/electrum/gui/kivy/uix/dialogs/qr_scanner.py @@ -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) +''') diff --git a/electrum/gui/kivy/uix/dialogs/question.py b/electrum/gui/kivy/uix/dialogs/question.py @@ -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 diff --git a/electrum/gui/kivy/uix/dialogs/requests.py b/electrum/gui/kivy/uix/dialogs/requests.py @@ -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 diff --git a/gui/kivy/uix/dialogs/seed_options.py b/electrum/gui/kivy/uix/dialogs/seed_options.py diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py @@ -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() diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -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) diff --git a/gui/kivy/uix/dialogs/wallets.py b/electrum/gui/kivy/uix/dialogs/wallets.py diff --git a/gui/kivy/uix/drawer.py b/electrum/gui/kivy/uix/drawer.py diff --git a/gui/kivy/uix/gridview.py b/electrum/gui/kivy/uix/gridview.py diff --git a/electrum/gui/kivy/uix/menus.py b/electrum/gui/kivy/uix/menus.py @@ -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) diff --git a/gui/kivy/uix/qrcodewidget.py b/electrum/gui/kivy/uix/qrcodewidget.py diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py @@ -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) diff --git a/gui/kivy/uix/ui_screens/about.kv b/electrum/gui/kivy/uix/ui_screens/about.kv diff --git a/electrum/gui/kivy/uix/ui_screens/history.kv b/electrum/gui/kivy/uix/ui_screens/history.kv @@ -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' diff --git a/gui/kivy/uix/ui_screens/invoice.kv b/electrum/gui/kivy/uix/ui_screens/invoice.kv diff --git a/gui/kivy/uix/ui_screens/network.kv b/electrum/gui/kivy/uix/ui_screens/network.kv diff --git a/gui/kivy/uix/ui_screens/proxy.kv b/electrum/gui/kivy/uix/ui_screens/proxy.kv diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv @@ -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()) diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv @@ -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 + + diff --git a/gui/kivy/uix/ui_screens/server.kv b/electrum/gui/kivy/uix/ui_screens/server.kv diff --git a/gui/kivy/uix/ui_screens/status.kv b/electrum/gui/kivy/uix/ui_screens/status.kv diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py @@ -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() diff --git a/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py @@ -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 diff --git a/gui/qt/amountedit.py b/electrum/gui/qt/amountedit.py diff --git a/electrum/gui/qt/completion_text_edit.py b/electrum/gui/qt/completion_text_edit.py @@ -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_() diff --git a/gui/qt/console.py b/electrum/gui/qt/console.py diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py @@ -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) diff --git a/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py diff --git a/gui/qt/fee_slider.py b/electrum/gui/qt/fee_slider.py diff --git a/gui/qt/history_list.py b/electrum/gui/qt/history_list.py diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py @@ -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) diff --git a/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -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 diff --git a/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py diff --git a/electrum/gui/qt/password_dialog.py b/electrum/gui/qt/password_dialog.py @@ -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() diff --git a/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py diff --git a/gui/qt/qrcodewidget.py b/electrum/gui/qt/qrcodewidget.py diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py @@ -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()) diff --git a/electrum/gui/qt/qrwindow.py b/electrum/gui/qt/qrwindow.py @@ -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) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py @@ -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)) diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py @@ -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))) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py @@ -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) diff --git a/gui/qt/util.py b/electrum/gui/qt/util.py diff --git a/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py diff --git a/gui/stdio.py b/electrum/gui/stdio.py diff --git a/electrum/gui/text.py b/electrum/gui/text.py @@ -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 diff --git a/electrum/i18n.py b/electrum/i18n.py @@ -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') +} diff --git a/electrum/interface.py b/electrum/interface.py @@ -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() diff --git a/electrum/jsonrpc.py b/electrum/jsonrpc.py @@ -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() diff --git a/electrum/keystore.py b/electrum/keystore.py @@ -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 diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py @@ -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 diff --git a/electrum/msqr.py b/electrum/msqr.py @@ -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 diff --git a/electrum/network.py b/electrum/network.py @@ -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) diff --git a/electrum/old_mnemonic.py b/electrum/old_mnemonic.py @@ -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:])) diff --git a/electrum/paymentrequest.proto b/electrum/paymentrequest.proto @@ -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 +} diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py @@ -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())] diff --git a/electrum/paymentrequest_pb2.py b/electrum/paymentrequest_pb2.py @@ -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', + serialized_pb=_b('\n\x14paymentrequest.proto\x12\x08payments\"+\n\x06Output\x12\x11\n\x06\x61mount\x18\x01 \x01(\x04:\x01\x30\x12\x0e\n\x06script\x18\x02 \x02(\x0c\"\xa3\x01\n\x0ePaymentDetails\x12\x15\n\x07network\x18\x01 \x01(\t:\x04main\x12!\n\x07outputs\x18\x02 \x03(\x0b\x32\x10.payments.Output\x12\x0c\n\x04time\x18\x03 \x02(\x04\x12\x0f\n\x07\x65xpires\x18\x04 \x01(\x04\x12\x0c\n\x04memo\x18\x05 \x01(\t\x12\x13\n\x0bpayment_url\x18\x06 \x01(\t\x12\x15\n\rmerchant_data\x18\x07 \x01(\x0c\"\x95\x01\n\x0ePaymentRequest\x12\"\n\x17payment_details_version\x18\x01 \x01(\r:\x01\x31\x12\x16\n\x08pki_type\x18\x02 \x01(\t:\x04none\x12\x10\n\x08pki_data\x18\x03 \x01(\x0c\x12\"\n\x1aserialized_payment_details\x18\x04 \x02(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"\'\n\x10X509Certificates\x12\x13\n\x0b\x63\x65rtificate\x18\x01 \x03(\x0c\"i\n\x07Payment\x12\x15\n\rmerchant_data\x18\x01 \x01(\x0c\x12\x14\n\x0ctransactions\x18\x02 \x03(\x0c\x12#\n\trefund_to\x18\x03 \x03(\x0b\x32\x10.payments.Output\x12\x0c\n\x04memo\x18\x04 \x01(\t\">\n\nPaymentACK\x12\"\n\x07payment\x18\x01 \x02(\x0b\x32\x11.payments.Payment\x12\x0c\n\x04memo\x18\x02 \x01(\tB(\n\x1eorg.bitcoin.protocols.paymentsB\x06Protos') +) +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + + + + +_OUTPUT = _descriptor.Descriptor( + name='Output', + full_name='payments.Output', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='amount', full_name='payments.Output.amount', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='script', full_name='payments.Output.script', index=1, + number=2, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=34, + serialized_end=77, +) + + +_PAYMENTDETAILS = _descriptor.Descriptor( + name='PaymentDetails', + full_name='payments.PaymentDetails', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='network', full_name='payments.PaymentDetails.network', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=True, default_value=_b("main").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='outputs', full_name='payments.PaymentDetails.outputs', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='time', full_name='payments.PaymentDetails.time', index=2, + number=3, type=4, cpp_type=4, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='expires', full_name='payments.PaymentDetails.expires', index=3, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='memo', full_name='payments.PaymentDetails.memo', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='payment_url', full_name='payments.PaymentDetails.payment_url', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='merchant_data', full_name='payments.PaymentDetails.merchant_data', index=6, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=80, + serialized_end=243, +) + + +_PAYMENTREQUEST = _descriptor.Descriptor( + name='PaymentRequest', + full_name='payments.PaymentRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='payment_details_version', full_name='payments.PaymentRequest.payment_details_version', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=True, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='pki_type', full_name='payments.PaymentRequest.pki_type', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=True, default_value=_b("none").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='pki_data', full_name='payments.PaymentRequest.pki_data', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='serialized_payment_details', full_name='payments.PaymentRequest.serialized_payment_details', index=3, + number=4, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='signature', full_name='payments.PaymentRequest.signature', index=4, + number=5, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=246, + serialized_end=395, +) + + +_X509CERTIFICATES = _descriptor.Descriptor( + name='X509Certificates', + full_name='payments.X509Certificates', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='certificate', full_name='payments.X509Certificates.certificate', index=0, + number=1, type=12, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=397, + serialized_end=436, +) + + +_PAYMENT = _descriptor.Descriptor( + name='Payment', + full_name='payments.Payment', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='merchant_data', full_name='payments.Payment.merchant_data', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='transactions', full_name='payments.Payment.transactions', index=1, + number=2, type=12, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='refund_to', full_name='payments.Payment.refund_to', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='memo', full_name='payments.Payment.memo', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=438, + serialized_end=543, +) + + +_PAYMENTACK = _descriptor.Descriptor( + name='PaymentACK', + full_name='payments.PaymentACK', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='payment', full_name='payments.PaymentACK.payment', index=0, + number=1, type=11, cpp_type=10, label=2, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='memo', full_name='payments.PaymentACK.memo', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=545, + serialized_end=607, +) + +_PAYMENTDETAILS.fields_by_name['outputs'].message_type = _OUTPUT +_PAYMENT.fields_by_name['refund_to'].message_type = _OUTPUT +_PAYMENTACK.fields_by_name['payment'].message_type = _PAYMENT +DESCRIPTOR.message_types_by_name['Output'] = _OUTPUT +DESCRIPTOR.message_types_by_name['PaymentDetails'] = _PAYMENTDETAILS +DESCRIPTOR.message_types_by_name['PaymentRequest'] = _PAYMENTREQUEST +DESCRIPTOR.message_types_by_name['X509Certificates'] = _X509CERTIFICATES +DESCRIPTOR.message_types_by_name['Payment'] = _PAYMENT +DESCRIPTOR.message_types_by_name['PaymentACK'] = _PAYMENTACK + +Output = _reflection.GeneratedProtocolMessageType('Output', (_message.Message,), dict( + DESCRIPTOR = _OUTPUT, + __module__ = 'paymentrequest_pb2' + # @@protoc_insertion_point(class_scope:payments.Output) + )) +_sym_db.RegisterMessage(Output) + +PaymentDetails = _reflection.GeneratedProtocolMessageType('PaymentDetails', (_message.Message,), dict( + DESCRIPTOR = _PAYMENTDETAILS, + __module__ = 'paymentrequest_pb2' + # @@protoc_insertion_point(class_scope:payments.PaymentDetails) + )) +_sym_db.RegisterMessage(PaymentDetails) + +PaymentRequest = _reflection.GeneratedProtocolMessageType('PaymentRequest', (_message.Message,), dict( + DESCRIPTOR = _PAYMENTREQUEST, + __module__ = 'paymentrequest_pb2' + # @@protoc_insertion_point(class_scope:payments.PaymentRequest) + )) +_sym_db.RegisterMessage(PaymentRequest) + +X509Certificates = _reflection.GeneratedProtocolMessageType('X509Certificates', (_message.Message,), dict( + DESCRIPTOR = _X509CERTIFICATES, + __module__ = 'paymentrequest_pb2' + # @@protoc_insertion_point(class_scope:payments.X509Certificates) + )) +_sym_db.RegisterMessage(X509Certificates) + +Payment = _reflection.GeneratedProtocolMessageType('Payment', (_message.Message,), dict( + DESCRIPTOR = _PAYMENT, + __module__ = 'paymentrequest_pb2' + # @@protoc_insertion_point(class_scope:payments.Payment) + )) +_sym_db.RegisterMessage(Payment) + +PaymentACK = _reflection.GeneratedProtocolMessageType('PaymentACK', (_message.Message,), dict( + DESCRIPTOR = _PAYMENTACK, + __module__ = 'paymentrequest_pb2' + # @@protoc_insertion_point(class_scope:payments.PaymentACK) + )) +_sym_db.RegisterMessage(PaymentACK) + + +DESCRIPTOR.has_options = True +DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\036org.bitcoin.protocols.paymentsB\006Protos')) +# @@protoc_insertion_point(module_scope) diff --git a/electrum/pem.py b/electrum/pem.py @@ -0,0 +1,191 @@ +#!/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. + + +# This module uses code from TLSLlite +# TLSLite Author: Trevor Perrin) + + +import binascii + +from .x509 import ASN1_Node, bytestr_to_int, decode_OID + + +def a2b_base64(s): + try: + b = bytearray(binascii.a2b_base64(s)) + except Exception as e: + raise SyntaxError("base64 error: %s" % e) + return b + +def b2a_base64(b): + return binascii.b2a_base64(b) + + +def dePem(s, name): + """Decode a PEM string into a bytearray of its payload. + + The input must contain an appropriate PEM prefix and postfix + based on the input name string, e.g. for name="CERTIFICATE": + + -----BEGIN CERTIFICATE----- + MIIBXDCCAUSgAwIBAgIBADANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQDEwRUQUNL + ... + KoZIhvcNAQEFBQADAwA5kw== + -----END CERTIFICATE----- + + The first such PEM block in the input will be found, and its + payload will be base64 decoded and returned. + """ + prefix = "-----BEGIN %s-----" % name + postfix = "-----END %s-----" % name + start = s.find(prefix) + if start == -1: + raise SyntaxError("Missing PEM prefix") + end = s.find(postfix, start+len(prefix)) + if end == -1: + raise SyntaxError("Missing PEM postfix") + s = s[start+len("-----BEGIN %s-----" % name) : end] + retBytes = a2b_base64(s) # May raise SyntaxError + return retBytes + +def dePemList(s, name): + """Decode a sequence of PEM blocks into a list of bytearrays. + + The input must contain any number of PEM blocks, each with the appropriate + PEM prefix and postfix based on the input name string, e.g. for + name="TACK BREAK SIG". Arbitrary text can appear between and before and + after the PEM blocks. For example: + + " Created by TACK.py 0.9.3 Created at 2012-02-01T00:30:10Z -----BEGIN TACK + BREAK SIG----- + ATKhrz5C6JHJW8BF5fLVrnQss6JnWVyEaC0p89LNhKPswvcC9/s6+vWLd9snYTUv + YMEBdw69PUP8JB4AdqA3K6Ap0Fgd9SSTOECeAKOUAym8zcYaXUwpk0+WuPYa7Zmm + SkbOlK4ywqt+amhWbg9txSGUwFO5tWUHT3QrnRlE/e3PeNFXLx5Bckg= -----END TACK + BREAK SIG----- Created by TACK.py 0.9.3 Created at 2012-02-01T00:30:11Z + -----BEGIN TACK BREAK SIG----- + ATKhrz5C6JHJW8BF5fLVrnQss6JnWVyEaC0p89LNhKPswvcC9/s6+vWLd9snYTUv + YMEBdw69PUP8JB4AdqA3K6BVCWfcjN36lx6JwxmZQncS6sww7DecFO/qjSePCxwM + +kdDqX/9/183nmjx6bf0ewhPXkA0nVXsDYZaydN8rJU1GaMlnjcIYxY= -----END TACK + BREAK SIG----- " + + All such PEM blocks will be found, decoded, and return in an ordered list + of bytearrays, which may have zero elements if not PEM blocks are found. + """ + bList = [] + prefix = "-----BEGIN %s-----" % name + postfix = "-----END %s-----" % name + while 1: + start = s.find(prefix) + if start == -1: + return bList + end = s.find(postfix, start+len(prefix)) + if end == -1: + raise SyntaxError("Missing PEM postfix") + s2 = s[start+len(prefix) : end] + retBytes = a2b_base64(s2) # May raise SyntaxError + bList.append(retBytes) + s = s[end+len(postfix) : ] + +def pem(b, name): + """Encode a payload bytearray into a PEM string. + + The input will be base64 encoded, then wrapped in a PEM prefix/postfix + based on the name string, e.g. for name="CERTIFICATE": + + -----BEGIN CERTIFICATE----- + MIIBXDCCAUSgAwIBAgIBADANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQDEwRUQUNL + ... + KoZIhvcNAQEFBQADAwA5kw== + -----END CERTIFICATE----- + """ + s1 = b2a_base64(b)[:-1] # remove terminating \n + s2 = b"" + while s1: + s2 += s1[:64] + b"\n" + s1 = s1[64:] + s = ("-----BEGIN %s-----\n" % name).encode('ascii') + s2 + \ + ("-----END %s-----\n" % name).encode('ascii') + return s + +def pemSniff(inStr, name): + searchStr = "-----BEGIN %s-----" % name + return searchStr in inStr + + +def parse_private_key(s): + """Parse a string containing a PEM-encoded <privateKey>.""" + if pemSniff(s, "PRIVATE KEY"): + bytes = dePem(s, "PRIVATE KEY") + return _parsePKCS8(bytes) + elif pemSniff(s, "RSA PRIVATE KEY"): + bytes = dePem(s, "RSA PRIVATE KEY") + return _parseSSLeay(bytes) + else: + raise SyntaxError("Not a PEM private key file") + + +def _parsePKCS8(_bytes): + s = ASN1_Node(_bytes) + root = s.root() + version_node = s.first_child(root) + version = bytestr_to_int(s.get_value_of_type(version_node, 'INTEGER')) + if version != 0: + raise SyntaxError("Unrecognized PKCS8 version") + rsaOID_node = s.next_node(version_node) + ii = s.first_child(rsaOID_node) + rsaOID = decode_OID(s.get_value_of_type(ii, 'OBJECT IDENTIFIER')) + if rsaOID != '1.2.840.113549.1.1.1': + raise SyntaxError("Unrecognized AlgorithmIdentifier") + privkey_node = s.next_node(rsaOID_node) + value = s.get_value_of_type(privkey_node, 'OCTET STRING') + return _parseASN1PrivateKey(value) + + +def _parseSSLeay(bytes): + return _parseASN1PrivateKey(ASN1_Node(bytes)) + + +def bytesToNumber(s): + return int(binascii.hexlify(s), 16) + + +def _parseASN1PrivateKey(s): + s = ASN1_Node(s) + root = s.root() + version_node = s.first_child(root) + version = bytestr_to_int(s.get_value_of_type(version_node, 'INTEGER')) + if version != 0: + raise SyntaxError("Unrecognized RSAPrivateKey version") + n = s.next_node(version_node) + e = s.next_node(n) + d = s.next_node(e) + p = s.next_node(d) + q = s.next_node(p) + dP = s.next_node(q) + dQ = s.next_node(dP) + qInv = s.next_node(dQ) + return list(map(lambda x: bytesToNumber(s.get_value_of_type(x, 'INTEGER')), [n, e, d, p, q, dP, dQ, qInv])) + diff --git a/electrum/plot.py b/electrum/plot.py @@ -0,0 +1,63 @@ +import datetime +from collections import defaultdict + +import matplotlib +matplotlib.use('Qt5Agg') +import matplotlib.pyplot as plt +import matplotlib.dates as md + +from .i18n import _ +from .bitcoin import COIN + + +class NothingToPlotException(Exception): + def __str__(self): + return _("Nothing to plot.") + + +def plot_history(history): + if len(history) == 0: + raise NothingToPlotException() + hist_in = defaultdict(int) + hist_out = defaultdict(int) + for item in history: + if not item['confirmations']: + continue + if item['timestamp'] is None: + continue + value = item['value'].value/COIN + date = item['date'] + datenum = int(md.date2num(datetime.date(date.year, date.month, 1))) + if value > 0: + hist_in[datenum] += value + else: + hist_out[datenum] -= value + + f, axarr = plt.subplots(2, sharex=True) + plt.subplots_adjust(bottom=0.2) + plt.xticks( rotation=25 ) + ax = plt.gca() + plt.ylabel('BTC') + plt.xlabel('Month') + xfmt = md.DateFormatter('%Y-%m-%d') + ax.xaxis.set_major_formatter(xfmt) + axarr[0].set_title('Monthly Volume') + xfmt = md.DateFormatter('%Y-%m') + ax.xaxis.set_major_formatter(xfmt) + width = 20 + + r1 = None + r2 = None + dates_values = list(zip(*sorted(hist_in.items()))) + if dates_values and len(dates_values) == 2: + dates, values = dates_values + r1 = axarr[0].bar(dates, values, width, label='incoming') + axarr[0].legend(loc='upper left') + dates_values = list(zip(*sorted(hist_out.items()))) + if dates_values and len(dates_values) == 2: + dates, values = dates_values + r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing') + axarr[1].legend(loc='upper left') + if r1 is None and r2 is None: + raise NothingToPlotException() + return plt diff --git a/electrum/plugin.py b/electrum/plugin.py @@ -0,0 +1,566 @@ +#!/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 collections import namedtuple +import traceback +import sys +import os +import imp +import pkgutil +import time +import threading + +from .util import print_error +from .i18n import _ +from .util import profiler, PrintError, DaemonThread, UserCancelled, ThreadJob +from . import bitcoin +from . import plugins + +plugin_loaders = {} +hook_names = set() +hooks = {} + + +class Plugins(DaemonThread): + + @profiler + def __init__(self, config, is_local, gui_name): + DaemonThread.__init__(self) + self.pkgpath = os.path.dirname(plugins.__file__) + self.config = config + self.hw_wallets = {} + self.plugins = {} + self.gui_name = gui_name + self.descriptions = {} + self.device_manager = DeviceMgr(config) + self.load_plugins() + self.add_jobs(self.device_manager.thread_jobs()) + self.start() + + def load_plugins(self): + for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]): + mod = pkgutil.find_loader('electrum.plugins.' + name) + m = mod.load_module() + d = m.__dict__ + gui_good = self.gui_name in d.get('available_for', []) + if not gui_good: + continue + details = d.get('registers_wallet_type') + if details: + self.register_wallet_type(name, gui_good, details) + details = d.get('registers_keystore') + if details: + self.register_keystore(name, gui_good, details) + self.descriptions[name] = d + if not d.get('requires_wallet_type') and self.config.get('use_' + name): + try: + self.load_plugin(name) + except BaseException as e: + traceback.print_exc(file=sys.stdout) + self.print_error("cannot initialize plugin %s:" % name, str(e)) + + def get(self, name): + return self.plugins.get(name) + + def count(self): + return len(self.plugins) + + def load_plugin(self, name): + if name in self.plugins: + return self.plugins[name] + full_name = 'electrum.plugins.' + name + '.' + self.gui_name + loader = pkgutil.find_loader(full_name) + if not loader: + raise RuntimeError("%s implementation for %s plugin not found" + % (self.gui_name, name)) + p = loader.load_module() + plugin = p.Plugin(self, self.config, name) + self.add_jobs(plugin.thread_jobs()) + self.plugins[name] = plugin + self.print_error("loaded", name) + return plugin + + def close_plugin(self, plugin): + self.remove_jobs(plugin.thread_jobs()) + + def enable(self, name): + self.config.set_key('use_' + name, True, True) + p = self.get(name) + if p: + return p + return self.load_plugin(name) + + def disable(self, name): + self.config.set_key('use_' + name, False, True) + p = self.get(name) + if not p: + return + self.plugins.pop(name) + p.close() + self.print_error("closed", name) + + def toggle(self, name): + p = self.get(name) + return self.disable(name) if p else self.enable(name) + + def is_available(self, name, w): + d = self.descriptions.get(name) + if not d: + return False + deps = d.get('requires', []) + for dep, s in deps: + try: + __import__(dep) + except ImportError: + return False + requires = d.get('requires_wallet_type', []) + return not requires or w.wallet_type in requires + + def get_hardware_support(self): + out = [] + for name, (gui_good, details) in self.hw_wallets.items(): + if gui_good: + try: + p = self.get_plugin(name) + if p.is_enabled(): + out.append([name, details[2], p]) + except: + traceback.print_exc() + self.print_error("cannot load plugin for:", name) + return out + + def register_wallet_type(self, name, gui_good, wallet_type): + from .wallet import register_wallet_type, register_constructor + self.print_error("registering wallet type", (wallet_type, name)) + def loader(): + plugin = self.get_plugin(name) + register_constructor(wallet_type, plugin.wallet_class) + register_wallet_type(wallet_type) + plugin_loaders[wallet_type] = loader + + def register_keystore(self, name, gui_good, details): + from .keystore import register_keystore + def dynamic_constructor(d): + return self.get_plugin(name).keystore_class(d) + if details[0] == 'hardware': + self.hw_wallets[name] = (gui_good, details) + self.print_error("registering hardware %s: %s" %(name, details)) + register_keystore(details[1], dynamic_constructor) + + def get_plugin(self, name): + if not name in self.plugins: + self.load_plugin(name) + return self.plugins[name] + + def run(self): + while self.is_running(): + time.sleep(0.1) + self.run_jobs() + self.on_stop() + + +def hook(func): + hook_names.add(func.__name__) + return func + +def run_hook(name, *args): + results = [] + f_list = hooks.get(name, []) + for p, f in f_list: + if p.is_enabled(): + try: + r = f(*args) + except Exception: + print_error("Plugin error") + traceback.print_exc(file=sys.stdout) + r = False + if r: + results.append(r) + + if results: + assert len(results) == 1, results + return results[0] + + +class BasePlugin(PrintError): + + def __init__(self, parent, config, name): + self.parent = parent # The plugins object + self.name = name + self.config = config + self.wallet = None + # add self to hooks + for k in dir(self): + if k in hook_names: + l = hooks.get(k, []) + l.append((self, getattr(self, k))) + hooks[k] = l + + def diagnostic_name(self): + return self.name + + def __str__(self): + return self.name + + def close(self): + # remove self from hooks + for k in dir(self): + if k in hook_names: + l = hooks.get(k, []) + l.remove((self, getattr(self, k))) + hooks[k] = l + self.parent.close_plugin(self) + self.on_close() + + def on_close(self): + pass + + def requires_settings(self): + return False + + def thread_jobs(self): + return [] + + def is_enabled(self): + return self.is_available() and self.config.get('use_'+self.name) is True + + def is_available(self): + return True + + def can_user_disable(self): + return True + + def settings_dialog(self): + pass + + +class DeviceNotFoundError(Exception): + pass + +class DeviceUnpairableError(Exception): + pass + +Device = namedtuple("Device", "path interface_number id_ product_key usage_page") +DeviceInfo = namedtuple("DeviceInfo", "device label initialized") + +class DeviceMgr(ThreadJob, PrintError): + '''Manages hardware clients. A client communicates over a hardware + channel with the device. + + In addition to tracking device HID IDs, the device manager tracks + hardware wallets and manages wallet pairing. A HID ID may be + paired with a wallet when it is confirmed that the hardware device + matches the wallet, i.e. they have the same master public key. A + HID ID can be unpaired if e.g. it is wiped. + + Because of hotplugging, a wallet must request its client + dynamically each time it is required, rather than caching it + itself. + + The device manager is shared across plugins, so just one place + does hardware scans when needed. By tracking HID IDs, if a device + is plugged into a different port the wallet is automatically + re-paired. + + Wallets are informed on connect / disconnect events. It must + implement connected(), disconnected() callbacks. Being connected + implies a pairing. Callbacks can happen in any thread context, + and we do them without holding the lock. + + Confusingly, the HID ID (serial number) reported by the HID system + doesn't match the device ID reported by the device itself. We use + the HID IDs. + + This plugin is thread-safe. Currently only devices supported by + hidapi are implemented.''' + + def __init__(self, config): + super(DeviceMgr, self).__init__() + # Keyed by xpub. The value is the device id + # has been paired, and None otherwise. + self.xpub_ids = {} + # A list of clients. The key is the client, the value is + # a (path, id_) pair. + self.clients = {} + # What we recognise. Each entry is a (vendor_id, product_id) + # pair. + self.recognised_hardware = set() + # Custom enumerate functions for devices we don't know about. + self.enumerate_func = set() + # For synchronization + self.lock = threading.RLock() + self.hid_lock = threading.RLock() + self.config = config + + def thread_jobs(self): + # Thread job to handle device timeouts + return [self] + + def run(self): + '''Handle device timeouts. Runs in the context of the Plugins + thread.''' + with self.lock: + clients = list(self.clients.keys()) + cutoff = time.time() - self.config.get_session_timeout() + for client in clients: + client.timeout(cutoff) + + def register_devices(self, device_pairs): + for pair in device_pairs: + self.recognised_hardware.add(pair) + + def register_enumerate_func(self, func): + self.enumerate_func.add(func) + + def create_client(self, device, handler, plugin): + # Get from cache first + client = self.client_lookup(device.id_) + if client: + return client + client = plugin.create_client(device, handler) + if client: + self.print_error("Registering", client) + with self.lock: + self.clients[client] = (device.path, device.id_) + return client + + def xpub_id(self, xpub): + with self.lock: + return self.xpub_ids.get(xpub) + + def xpub_by_id(self, id_): + with self.lock: + for xpub, xpub_id in self.xpub_ids.items(): + if xpub_id == id_: + return xpub + return None + + def unpair_xpub(self, xpub): + with self.lock: + if not xpub in self.xpub_ids: + return + _id = self.xpub_ids.pop(xpub) + self._close_client(_id) + + def unpair_id(self, id_): + xpub = self.xpub_by_id(id_) + if xpub: + self.unpair_xpub(xpub) + else: + self._close_client(id_) + + def _close_client(self, id_): + client = self.client_lookup(id_) + self.clients.pop(client, None) + if client: + client.close() + + def pair_xpub(self, xpub, id_): + with self.lock: + self.xpub_ids[xpub] = id_ + + def client_lookup(self, id_): + with self.lock: + for client, (path, client_id) in self.clients.items(): + if client_id == id_: + return client + return None + + def client_by_id(self, id_): + '''Returns a client for the device ID if one is registered. If + a device is wiped or in bootloader mode pairing is impossible; + in such cases we communicate by device ID and not wallet.''' + self.scan_devices() + return self.client_lookup(id_) + + def client_for_keystore(self, plugin, handler, keystore, force_pair): + self.print_error("getting client for keystore") + if handler is None: + raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing.")) + handler.update_status(False) + devices = self.scan_devices() + xpub = keystore.xpub + derivation = keystore.get_derivation() + client = self.client_by_xpub(plugin, xpub, handler, devices) + if client is None and force_pair: + info = self.select_device(plugin, handler, keystore, devices) + client = self.force_pair_xpub(plugin, handler, info, xpub, derivation, devices) + if client: + handler.update_status(True) + self.print_error("end client for keystore") + return client + + def client_by_xpub(self, plugin, xpub, handler, devices): + _id = self.xpub_id(xpub) + client = self.client_lookup(_id) + if client: + # An unpaired client might have another wallet's handler + # from a prior scan. Replace to fix dialog parenting. + client.handler = handler + return client + + for device in devices: + if device.id_ == _id: + return self.create_client(device, handler, plugin) + + + def force_pair_xpub(self, plugin, handler, info, xpub, derivation, devices): + # The wallet has not been previously paired, so let the user + # choose an unpaired device and compare its first address. + xtype = bitcoin.xpub_type(xpub) + client = self.client_lookup(info.device.id_) + if client and client.is_pairable(): + # See comment above for same code + client.handler = handler + # This will trigger a PIN/passphrase entry request + try: + client_xpub = client.get_xpub(derivation, xtype) + except (UserCancelled, RuntimeError): + # Bad / cancelled PIN / passphrase + client_xpub = None + if client_xpub == xpub: + self.pair_xpub(xpub, info.device.id_) + return client + + # The user input has wrong PIN or passphrase, or cancelled input, + # or it is not pairable + raise DeviceUnpairableError( + _('Electrum cannot pair with your {}.\n\n' + 'Before you request bitcoins to be sent to addresses in this ' + 'wallet, ensure you can pair with your device, or that you have ' + 'its seed (and passphrase, if any). Otherwise all bitcoins you ' + 'receive will be unspendable.').format(plugin.device)) + + def unpaired_device_infos(self, handler, plugin, devices=None): + '''Returns a list of DeviceInfo objects: one for each connected, + unpaired device accepted by the plugin.''' + if not plugin.libraries_available: + raise Exception('Missing libraries for {}'.format(plugin.name)) + if devices is None: + devices = self.scan_devices() + devices = [dev for dev in devices if not self.xpub_by_id(dev.id_)] + infos = [] + for device in devices: + if device.product_key not in plugin.DEVICE_IDS: + continue + client = self.create_client(device, handler, plugin) + if not client: + continue + infos.append(DeviceInfo(device, client.label(), client.is_initialized())) + + return infos + + def select_device(self, plugin, handler, keystore, devices=None): + '''Ask the user to select a device to use if there is more than one, + and return the DeviceInfo for the device.''' + while True: + infos = self.unpaired_device_infos(handler, plugin, devices) + if infos: + break + msg = _('Please insert your {}').format(plugin.device) + if keystore.label: + msg += ' ({})'.format(keystore.label) + msg += '. {}\n\n{}'.format( + _('Verify the cable is connected and that ' + 'no other application is using it.'), + _('Try to connect again?') + ) + if not handler.yes_no_question(msg): + raise UserCancelled() + devices = None + if len(infos) == 1: + return infos[0] + # select device by label + for info in infos: + if info.label == keystore.label: + return info + msg = _("Please select which {} device to use:").format(plugin.device) + descriptions = [str(info.label) + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos] + c = handler.query_choice(msg, descriptions) + if c is None: + raise UserCancelled() + info = infos[c] + # save new label + keystore.set_label(info.label) + if handler.win.wallet is not None: + handler.win.wallet.save_keystore() + return info + + def _scan_devices_with_hid(self): + try: + import hid + except ImportError: + return [] + + with self.hid_lock: + hid_list = hid.enumerate(0, 0) + + devices = [] + for d in hid_list: + product_key = (d['vendor_id'], d['product_id']) + if product_key in self.recognised_hardware: + # Older versions of hid don't provide interface_number + interface_number = d.get('interface_number', -1) + usage_page = d['usage_page'] + id_ = d['serial_number'] + if len(id_) == 0: + id_ = str(d['path']) + id_ += str(interface_number) + str(usage_page) + devices.append(Device(d['path'], interface_number, + id_, product_key, usage_page)) + return devices + + def scan_devices(self): + self.print_error("scanning devices...") + + # First see what's connected that we know about + devices = self._scan_devices_with_hid() + + # Let plugin handlers enumerate devices we don't know about + for f in self.enumerate_func: + try: + new_devices = f() + except BaseException as e: + self.print_error('custom device enum failed. func {}, error {}' + .format(str(f), str(e))) + else: + devices.extend(new_devices) + + # find out what was disconnected + pairs = [(dev.path, dev.id_) for dev in devices] + disconnected_ids = [] + with self.lock: + connected = {} + for client, pair in self.clients.items(): + if pair in pairs and client.has_usable_connection_with_device(): + connected[client] = pair + else: + disconnected_ids.append(pair[1]) + self.clients = connected + + # Unpair disconnected devices + for id_ in disconnected_ids: + self.unpair_id(id_) + + return devices diff --git a/electrum/plugins/README b/electrum/plugins/README @@ -0,0 +1,31 @@ +Plugin rules: + + * The plugin system of Electrum is designed to allow the development + of new features without increasing the core code of Electrum. + + * Electrum is written in pure python. if you want to add a feature + that requires non-python libraries, then it must be submitted as a + plugin. If the feature you want to add requires communication with + a remote server (not an Electrum server), then it should be a + plugin as well. If the feature you want to add introduces new + dependencies in the code, then it should probably be a plugin. + + * We expect plugin developers to maintain their plugin code. However, + once a plugin is merged in Electrum, we will have to maintain it + too, because changes in the Electrum code often require updates in + the plugin code. Therefore, plugins have to be easy to maintain. If + we believe that a plugin will create too much maintenance work in + the future, it will be rejected. + + * Plugins should be compatible with Electrum's conventions. If your + plugin does not fit with Electrum's architecture, or if we believe + that it will create too much maintenance work, it will not be + accepted. In particular, do not duplicate existing Electrum code in + your plugin. + + * We may decide to remove a plugin after it has been merged in + Electrum. For this reason, a plugin must be easily removable, + without putting at risk the user's bitcoins. If we feel that a + plugin cannot be removed without threatening users who rely on it, + we will not merge it. + diff --git a/electrum/plugins/__init__.py b/electrum/plugins/__init__.py @@ -0,0 +1,26 @@ +#!/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. + + diff --git a/electrum/plugins/audio_modem/__init__.py b/electrum/plugins/audio_modem/__init__.py @@ -0,0 +1,7 @@ +from electrum.i18n import _ + +fullname = _('Audio MODEM') +description = _('Provides support for air-gapped transaction signing.') +requires = [('amodem', 'http://github.com/romanz/amodem/')] +available_for = ['qt'] + diff --git a/electrum/plugins/audio_modem/qt.py b/electrum/plugins/audio_modem/qt.py @@ -0,0 +1,128 @@ +from functools import partial +import zlib +import json +from io import BytesIO +import sys +import platform + +from electrum.plugin import BasePlugin, hook +from electrum.gui.qt.util import WaitingDialog, EnterButton, WindowModalDialog +from electrum.util import print_msg, print_error +from electrum.i18n import _ + +from PyQt5.QtGui import * +from PyQt5.QtCore import * +from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QPushButton) + +try: + import amodem.audio + import amodem.main + import amodem.config + print_error('Audio MODEM is available.') + amodem.log.addHandler(amodem.logging.StreamHandler(sys.stderr)) + amodem.log.setLevel(amodem.logging.INFO) +except ImportError: + amodem = None + print_error('Audio MODEM is not found.') + + +class Plugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + if self.is_available(): + self.modem_config = amodem.config.slowest() + self.library_name = { + 'Linux': 'libportaudio.so' + }[platform.system()] + + def is_available(self): + return amodem is not None + + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Settings'), partial(self.settings_dialog, window)) + + def settings_dialog(self, window): + d = WindowModalDialog(window, _("Audio Modem Settings")) + + layout = QGridLayout(d) + layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0) + + bitrates = list(sorted(amodem.config.bitrates.keys())) + + def _index_changed(index): + bitrate = bitrates[index] + self.modem_config = amodem.config.bitrates[bitrate] + + combo = QComboBox() + combo.addItems([str(x) for x in bitrates]) + combo.currentIndexChanged.connect(_index_changed) + layout.addWidget(combo, 0, 1) + + ok_button = QPushButton(_("OK")) + ok_button.clicked.connect(d.accept) + layout.addWidget(ok_button, 1, 1) + + return bool(d.exec_()) + + @hook + def transaction_dialog(self, dialog): + b = QPushButton() + b.setIcon(QIcon(":icons/speaker.png")) + + def handler(): + blob = json.dumps(dialog.tx.as_dict()) + self._send(parent=dialog, blob=blob) + b.clicked.connect(handler) + dialog.sharing_buttons.insert(-1, b) + + @hook + def scan_text_edit(self, parent): + parent.addButton(':icons/microphone.png', partial(self._recv, parent), + _("Read from microphone")) + + @hook + def show_text_edit(self, parent): + def handler(): + blob = str(parent.toPlainText()) + self._send(parent=parent, blob=blob) + parent.addButton(':icons/speaker.png', handler, _("Send to speaker")) + + def _audio_interface(self): + interface = amodem.audio.Interface(config=self.modem_config) + return interface.load(self.library_name) + + def _send(self, parent, blob): + def sender_thread(): + with self._audio_interface() as interface: + src = BytesIO(blob) + dst = interface.player() + amodem.main.send(config=self.modem_config, src=src, dst=dst) + + print_msg('Sending:', repr(blob)) + blob = zlib.compress(blob.encode('ascii')) + + kbps = self.modem_config.modem_bps / 1e3 + msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps) + WaitingDialog(parent, msg, sender_thread) + + def _recv(self, parent): + def receiver_thread(): + with self._audio_interface() as interface: + src = interface.recorder() + dst = BytesIO() + amodem.main.recv(config=self.modem_config, src=src, dst=dst) + return dst.getvalue() + + def on_finished(blob): + if blob: + blob = zlib.decompress(blob).decode('ascii') + print_msg('Received:', repr(blob)) + parent.setText(blob) + + kbps = self.modem_config.modem_bps / 1e3 + msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps) + WaitingDialog(parent, msg, receiver_thread, on_finished) diff --git a/electrum/plugins/cosigner_pool/__init__.py b/electrum/plugins/cosigner_pool/__init__.py @@ -0,0 +1,9 @@ +from electrum.i18n import _ +fullname = _('Cosigner Pool') +description = ' '.join([ + _("This plugin facilitates the use of multi-signatures wallets."), + _("It sends and receives partially signed transactions from/to your cosigner wallet."), + _("Transactions are encrypted and stored on a remote server.") +]) +#requires_wallet_type = ['2of2', '2of3'] +available_for = ['qt'] diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py @@ -0,0 +1,228 @@ +#!/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 time +from xmlrpc.client import ServerProxy + +from PyQt5.QtGui import * +from PyQt5.QtCore import * +from PyQt5.QtWidgets import QPushButton + +from electrum import bitcoin, util, keystore, ecc +from electrum import transaction +from electrum.plugin import BasePlugin, hook +from electrum.i18n import _ +from electrum.wallet import Multisig_Wallet +from electrum.util import bh2u, bfh + +from electrum.gui.qt.transaction_dialog import show_transaction + +import sys +import traceback + + +server = ServerProxy('https://cosigner.electrum.org/', allow_none=True) + + +class Listener(util.DaemonThread): + + def __init__(self, parent): + util.DaemonThread.__init__(self) + self.daemon = True + self.parent = parent + self.received = set() + self.keyhashes = [] + + def set_keyhashes(self, keyhashes): + self.keyhashes = keyhashes + + def clear(self, keyhash): + server.delete(keyhash) + self.received.remove(keyhash) + + def run(self): + while self.running: + if not self.keyhashes: + time.sleep(2) + continue + for keyhash in self.keyhashes: + if keyhash in self.received: + continue + try: + message = server.get(keyhash) + except Exception as e: + self.print_error("cannot contact cosigner pool") + time.sleep(30) + continue + if message: + self.received.add(keyhash) + self.print_error("received message for", keyhash) + self.parent.obj.cosigner_receive_signal.emit( + keyhash, message) + # poll every 30 seconds + time.sleep(30) + + +class QReceiveSignalObject(QObject): + cosigner_receive_signal = pyqtSignal(object, object) + + +class Plugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.listener = None + self.obj = QReceiveSignalObject() + self.obj.cosigner_receive_signal.connect(self.on_receive) + self.keys = [] + self.cosigner_list = [] + + @hook + def init_qt(self, gui): + for window in gui.windows: + self.on_new_window(window) + + @hook + def on_new_window(self, window): + self.update(window) + + @hook + def on_close_window(self, window): + self.update(window) + + def is_available(self): + return True + + def update(self, window): + wallet = window.wallet + if type(wallet) != Multisig_Wallet: + return + if self.listener is None: + self.print_error("starting listener") + self.listener = Listener(self) + self.listener.start() + elif self.listener: + self.print_error("shutting down listener") + self.listener.stop() + self.listener = None + self.keys = [] + self.cosigner_list = [] + for key, keystore in wallet.keystores.items(): + xpub = keystore.get_master_public_key() + K = bitcoin.deserialize_xpub(xpub)[-1] + _hash = bh2u(bitcoin.Hash(K)) + if not keystore.is_watching_only(): + self.keys.append((key, _hash, window)) + else: + self.cosigner_list.append((window, xpub, K, _hash)) + if self.listener: + self.listener.set_keyhashes([t[1] for t in self.keys]) + + @hook + def transaction_dialog(self, d): + d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) + b.clicked.connect(lambda: self.do_send(d.tx)) + d.buttons.insert(0, b) + self.transaction_dialog_update(d) + + @hook + def transaction_dialog_update(self, d): + if d.tx.is_complete() or d.wallet.can_sign(d.tx): + d.cosigner_send_button.hide() + return + for window, xpub, K, _hash in self.cosigner_list: + if window.wallet == d.wallet and self.cosigner_can_sign(d.tx, xpub): + d.cosigner_send_button.show() + break + else: + d.cosigner_send_button.hide() + + def cosigner_can_sign(self, tx, cosigner_xpub): + from electrum.keystore import is_xpubkey, parse_xpubkey + xpub_set = set([]) + for txin in tx.inputs(): + for x_pubkey in txin['x_pubkeys']: + if is_xpubkey(x_pubkey): + xpub, s = parse_xpubkey(x_pubkey) + xpub_set.add(xpub) + return cosigner_xpub in xpub_set + + def do_send(self, tx): + for window, xpub, K, _hash in self.cosigner_list: + if not self.cosigner_can_sign(tx, xpub): + continue + raw_tx_bytes = bfh(str(tx)) + public_key = ecc.ECPubkey(K) + message = public_key.encrypt_message(raw_tx_bytes).decode('ascii') + try: + server.put(_hash, message) + except Exception as e: + traceback.print_exc(file=sys.stdout) + window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + str(e)) + return + window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' + + _("Open your cosigner wallet to retrieve it.")) + + def on_receive(self, keyhash, message): + self.print_error("signal arrived for", keyhash) + for key, _hash, window in self.keys: + if _hash == keyhash: + break + else: + self.print_error("keyhash not found") + return + + wallet = window.wallet + if isinstance(wallet.keystore, keystore.Hardware_KeyStore): + window.show_warning(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' + + _('However, hardware wallets do not support message decryption, ' + 'which makes them not compatible with the current design of cosigner pool.')) + return + elif wallet.has_keystore_encryption(): + password = window.password_dialog(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' + + _('Please enter your password to decrypt it.')) + if not password: + return + else: + password = None + if not window.question(_("An encrypted transaction was retrieved from cosigning pool.") + '\n' + + _("Do you want to open it now?")): + return + + xprv = wallet.keystore.get_master_private_key(password) + if not xprv: + return + try: + k = bitcoin.deserialize_xprv(xprv)[-1] + EC = ecc.ECPrivkey(k) + message = bh2u(EC.decrypt_message(message)) + except Exception as e: + traceback.print_exc(file=sys.stdout) + window.show_error(_('Error decrypting message') + ':\n' + str(e)) + return + + self.listener.clear(keyhash) + tx = transaction.Transaction(message) + show_transaction(tx, window, prompt_if_unsaved=True) diff --git a/electrum/plugins/digitalbitbox/__init__.py b/electrum/plugins/digitalbitbox/__init__.py @@ -0,0 +1,6 @@ +from electrum.i18n import _ + +fullname = 'Digital Bitbox' +description = _('Provides support for Digital Bitbox hardware wallet') +registers_keystore = ('hardware', 'digitalbitbox', _("Digital Bitbox wallet")) +available_for = ['qt', 'cmdline'] diff --git a/electrum/plugins/digitalbitbox/cmdline.py b/electrum/plugins/digitalbitbox/cmdline.py @@ -0,0 +1,14 @@ +from electrum.plugin import hook +from .digitalbitbox import DigitalBitboxPlugin +from ..hw_wallet import CmdLineHandler + +class Plugin(DigitalBitboxPlugin): + handler = CmdLineHandler() + @hook + def init_keystore(self, keystore): + if not isinstance(keystore, self.keystore_class): + return + keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -0,0 +1,767 @@ +# ---------------------------------------------------------------------------------- +# Electrum plugin for the Digital Bitbox hardware wallet by Shift Devices AG +# digitalbitbox.com +# + +try: + from electrum.crypto import Hash, EncodeAES, DecodeAES + from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, is_address, + serialize_xpub, deserialize_xpub) + from electrum import ecc + from electrum.ecc import msg_magic + from electrum.wallet import Standard_Wallet + from electrum import constants + from electrum.transaction import Transaction + from electrum.i18n import _ + from electrum.keystore import Hardware_KeyStore + from ..hw_wallet import HW_PluginBase + from electrum.util import print_error, to_string, UserCancelled + from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET + + import time + import hid + import json + import math + import binascii + import struct + import hashlib + import requests + import base64 + import os + import sys + DIGIBOX = True +except ImportError as e: + DIGIBOX = False + + + +# ---------------------------------------------------------------------------------- +# USB HID interface +# + +def to_hexstr(s): + return binascii.hexlify(s).decode('ascii') + +class DigitalBitbox_Client(): + + def __init__(self, plugin, hidDevice): + self.plugin = plugin + self.dbb_hid = hidDevice + self.opened = True + self.password = None + self.isInitialized = False + self.setupRunning = False + self.usbReportSize = 64 # firmware > v2.0.0 + + + def close(self): + if self.opened: + try: + self.dbb_hid.close() + except: + pass + self.opened = False + + + def timeout(self, cutoff): + pass + + + def label(self): + return " " + + + def is_pairable(self): + return True + + + def is_initialized(self): + return self.dbb_has_password() + + + def is_paired(self): + return self.password is not None + + def has_usable_connection_with_device(self): + try: + self.dbb_has_password() + except BaseException: + return False + return True + + def _get_xpub(self, bip32_path): + if self.check_device_dialog(): + return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8')) + + + def get_xpub(self, bip32_path, xtype): + assert xtype in self.plugin.SUPPORTED_XTYPES + reply = self._get_xpub(bip32_path) + if reply: + xpub = reply['xpub'] + # Change type of xpub to the requested type. The firmware + # only ever returns the mainnet standard type, but it is agnostic + # to the type when signing. + if xtype != 'standard' or constants.net.TESTNET: + _, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub, net=constants.BitcoinMainnet) + xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) + return xpub + else: + raise Exception('no reply') + + + def dbb_has_password(self): + reply = self.hid_send_plain(b'{"ping":""}') + if 'ping' not in reply: + raise Exception(_('Device communication error. Please unplug and replug your Digital Bitbox.')) + if reply['ping'] == 'password': + return True + return False + + + def stretch_key(self, key): + import pbkdf2, hmac + return to_hexstr(pbkdf2.PBKDF2(key, b'Digital Bitbox', iterations = 20480, macmodule = hmac, digestmodule = hashlib.sha512).read(64)) + + + def backup_password_dialog(self): + msg = _("Enter the password used when the backup was created:") + while True: + password = self.handler.get_passphrase(msg, False) + if password is None: + return None + if len(password) < 4: + msg = _("Password must have at least 4 characters.") \ + + "\n\n" + _("Enter password:") + elif len(password) > 64: + msg = _("Password must have less than 64 characters.") \ + + "\n\n" + _("Enter password:") + else: + return password.encode('utf8') + + + def password_dialog(self, msg): + while True: + password = self.handler.get_passphrase(msg, False) + if password is None: + return False + if len(password) < 4: + msg = _("Password must have at least 4 characters.") + \ + "\n\n" + _("Enter password:") + elif len(password) > 64: + msg = _("Password must have less than 64 characters.") + \ + "\n\n" + _("Enter password:") + else: + self.password = password.encode('utf8') + return True + + + def check_device_dialog(self): + # Set password if fresh device + if self.password is None and not self.dbb_has_password(): + if not self.setupRunning: + return False # A fresh device cannot connect to an existing wallet + msg = _("An uninitialized Digital Bitbox is detected.") + " " + \ + _("Enter a new password below.") + "\n\n" + \ + _("REMEMBER THE PASSWORD!") + "\n\n" + \ + _("You cannot access your coins or a backup without the password.") + "\n" + \ + _("A backup is saved automatically when generating a new wallet.") + if self.password_dialog(msg): + reply = self.hid_send_plain(b'{"password":"' + self.password + b'"}') + else: + return False + + # Get password from user if not yet set + msg = _("Enter your Digital Bitbox password:") + while self.password is None: + if not self.password_dialog(msg): + raise UserCancelled() + reply = self.hid_send_encrypt(b'{"led":"blink"}') + if 'error' in reply: + self.password = None + if reply['error']['code'] == 109: + msg = _("Incorrect password entered.") + "\n\n" + \ + reply['error']['message'] + "\n\n" + \ + _("Enter your Digital Bitbox password:") + else: + # Should never occur + msg = _("Unexpected error occurred.") + "\n\n" + \ + reply['error']['message'] + "\n\n" + \ + _("Enter your Digital Bitbox password:") + + # Initialize device if not yet initialized + if not self.setupRunning: + self.isInitialized = True # Wallet exists. Electrum code later checks if the device matches the wallet + elif not self.isInitialized: + reply = self.hid_send_encrypt(b'{"device":"info"}') + if reply['device']['id'] != "": + self.recover_or_erase_dialog() # Already seeded + else: + self.seed_device_dialog() # Seed if not initialized + self.mobile_pairing_dialog() + return self.isInitialized + + + def recover_or_erase_dialog(self): + msg = _("The Digital Bitbox is already seeded. Choose an option:") + "\n" + choices = [ + (_("Create a wallet using the current seed")), + (_("Load a wallet from the micro SD card (the current seed is overwritten)")), + (_("Erase the Digital Bitbox")) + ] + try: + reply = self.handler.win.query_choice(msg, choices) + except Exception: + return # Back button pushed + if reply == 2: + self.dbb_erase() + elif reply == 1: + if not self.dbb_load_backup(): + return + else: + if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']: + raise Exception(_("Full 2FA enabled. This is not supported yet.")) + # Use existing seed + self.isInitialized = True + + + def seed_device_dialog(self): + msg = _("Choose how to initialize your Digital Bitbox:") + "\n" + choices = [ + (_("Generate a new random wallet")), + (_("Load a wallet from the micro SD card")) + ] + try: + reply = self.handler.win.query_choice(msg, choices) + except Exception: + return # Back button pushed + if reply == 0: + self.dbb_generate_wallet() + else: + if not self.dbb_load_backup(show_msg=False): + return + self.isInitialized = True + + def mobile_pairing_dialog(self): + dbb_user_dir = None + if sys.platform == 'darwin': + dbb_user_dir = os.path.join(os.environ.get("HOME", ""), "Library", "Application Support", "DBB") + elif sys.platform == 'win32': + dbb_user_dir = os.path.join(os.environ["APPDATA"], "DBB") + else: + dbb_user_dir = os.path.join(os.environ["HOME"], ".dbb") + + if not dbb_user_dir: + return + + try: + # Python 3.5+ + jsonDecodeError = json.JSONDecodeError + except AttributeError: + jsonDecodeError = ValueError + try: + with open(os.path.join(dbb_user_dir, "config.dat")) as f: + dbb_config = json.load(f) + except (FileNotFoundError, jsonDecodeError): + return + + if 'encryptionprivkey' not in dbb_config or 'comserverchannelid' not in dbb_config: + return + + choices = [ + _('Do not pair'), + _('Import pairing from the Digital Bitbox desktop app'), + ] + try: + reply = self.handler.win.query_choice(_('Mobile pairing options'), choices) + except Exception: + return # Back button pushed + + if reply == 0: + if self.plugin.is_mobile_paired(): + del self.plugin.digitalbitbox_config['encryptionprivkey'] + del self.plugin.digitalbitbox_config['comserverchannelid'] + elif reply == 1: + # import pairing from dbb app + self.plugin.digitalbitbox_config['encryptionprivkey'] = dbb_config['encryptionprivkey'] + self.plugin.digitalbitbox_config['comserverchannelid'] = dbb_config['comserverchannelid'] + self.plugin.config.set_key('digitalbitbox', self.plugin.digitalbitbox_config) + + def dbb_generate_wallet(self): + key = self.stretch_key(self.password) + filename = ("Electrum-" + time.strftime("%Y-%m-%d-%H-%M-%S") + ".pdf") + msg = ('{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, 'Digital Bitbox Electrum Plugin')).encode('utf8') + reply = self.hid_send_encrypt(msg) + if 'error' in reply: + raise Exception(reply['error']['message']) + + + def dbb_erase(self): + self.handler.show_message(_("Are you sure you want to erase the Digital Bitbox?") + "\n\n" + + _("To continue, touch the Digital Bitbox's light for 3 seconds.") + "\n\n" + + _("To cancel, briefly touch the light or wait for the timeout.")) + hid_reply = self.hid_send_encrypt(b'{"reset":"__ERASE__"}') + self.handler.finished() + if 'error' in hid_reply: + raise Exception(hid_reply['error']['message']) + else: + self.password = None + raise Exception('Device erased') + + + def dbb_load_backup(self, show_msg=True): + backups = self.hid_send_encrypt(b'{"backup":"list"}') + if 'error' in backups: + raise Exception(backups['error']['message']) + try: + f = self.handler.win.query_choice(_("Choose a backup file:"), backups['backup']) + except Exception: + return False # Back button pushed + key = self.backup_password_dialog() + if key is None: + raise Exception('Canceled by user') + key = self.stretch_key(key) + if show_msg: + self.handler.show_message(_("Loading backup...") + "\n\n" + + _("To continue, touch the Digital Bitbox's light for 3 seconds.") + "\n\n" + + _("To cancel, briefly touch the light or wait for the timeout.")) + msg = ('{"seed":{"source": "backup", "key": "%s", "filename": "%s"}}' % (key, backups['backup'][f])).encode('utf8') + hid_reply = self.hid_send_encrypt(msg) + self.handler.finished() + if 'error' in hid_reply: + raise Exception(hid_reply['error']['message']) + return True + + + def hid_send_frame(self, data): + HWW_CID = 0xFF000000 + HWW_CMD = 0x80 + 0x40 + 0x01 + data_len = len(data) + seq = 0; + idx = 0; + write = [] + while idx < data_len: + if idx == 0: + # INIT frame + write = data[idx : idx + min(data_len, self.usbReportSize - 7)] + self.dbb_hid.write(b'\0' + struct.pack(">IBH", HWW_CID, HWW_CMD, data_len & 0xFFFF) + write + b'\xEE' * (self.usbReportSize - 7 - len(write))) + else: + # CONT frame + write = data[idx : idx + min(data_len, self.usbReportSize - 5)] + self.dbb_hid.write(b'\0' + struct.pack(">IB", HWW_CID, seq) + write + b'\xEE' * (self.usbReportSize - 5 - len(write))) + seq += 1 + idx += len(write) + + + def hid_read_frame(self): + # INIT response + read = bytearray(self.dbb_hid.read(self.usbReportSize)) + cid = ((read[0] * 256 + read[1]) * 256 + read[2]) * 256 + read[3] + cmd = read[4] + data_len = read[5] * 256 + read[6] + data = read[7:] + idx = len(read) - 7; + while idx < data_len: + # CONT response + read = bytearray(self.dbb_hid.read(self.usbReportSize)) + data += read[5:] + idx += len(read) - 5 + return data + + + def hid_send_plain(self, msg): + reply = "" + try: + serial_number = self.dbb_hid.get_serial_number_string() + if "v2.0." in serial_number or "v1." in serial_number: + hidBufSize = 4096 + self.dbb_hid.write('\0' + msg + '\0' * (hidBufSize - len(msg))) + r = bytearray() + while len(r) < hidBufSize: + r += bytearray(self.dbb_hid.read(hidBufSize)) + else: + self.hid_send_frame(msg) + r = self.hid_read_frame() + r = r.rstrip(b' \t\r\n\0') + r = r.replace(b"\0", b'') + r = to_string(r, 'utf8') + reply = json.loads(r) + except Exception as e: + print_error('Exception caught ' + str(e)) + return reply + + + def hid_send_encrypt(self, msg): + reply = "" + try: + secret = Hash(self.password) + msg = EncodeAES(secret, msg) + reply = self.hid_send_plain(msg) + if 'ciphertext' in reply: + reply = DecodeAES(secret, ''.join(reply["ciphertext"])) + reply = to_string(reply, 'utf8') + reply = json.loads(reply) + if 'error' in reply: + self.password = None + except Exception as e: + print_error('Exception caught ' + str(e)) + return reply + + + +# ---------------------------------------------------------------------------------- +# +# + +class DigitalBitbox_KeyStore(Hardware_KeyStore): + hw_type = 'digitalbitbox' + device = 'DigitalBitbox' + + + def __init__(self, d): + Hardware_KeyStore.__init__(self, d) + self.force_watching_only = False + self.maxInputs = 14 # maximum inputs per single sign command + + + def get_derivation(self): + return str(self.derivation) + + + def is_p2pkh(self): + return self.derivation.startswith("m/44'/") + + + def give_error(self, message, clear_client = False): + if clear_client: + self.client = None + raise Exception(message) + + + def decrypt_message(self, pubkey, message, password): + raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + + + def sign_message(self, sequence, message, password): + sig = None + try: + message = message.encode('utf8') + inputPath = self.get_derivation() + "/%d/%d" % sequence + msg_hash = Hash(msg_magic(message)) + inputHash = to_hexstr(msg_hash) + hasharray = [] + hasharray.append({'hash': inputHash, 'keypath': inputPath}) + hasharray = json.dumps(hasharray) + + msg = ('{"sign":{"meta":"sign message", "data":%s}}' % hasharray).encode('utf8') + + dbb_client = self.plugin.get_client(self) + + if not dbb_client.is_paired(): + raise Exception(_("Could not sign message.")) + + reply = dbb_client.hid_send_encrypt(msg) + self.handler.show_message(_("Signing message ...") + "\n\n" + + _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + "\n\n" + + _("To cancel, briefly touch the blinking light or wait for the timeout.")) + reply = dbb_client.hid_send_encrypt(msg) # Send twice, first returns an echo for smart verification (not implemented) + self.handler.finished() + + if 'error' in reply: + raise Exception(reply['error']['message']) + + if 'sign' not in reply: + raise Exception(_("Could not sign message.")) + + if 'recid' in reply['sign'][0]: + # firmware > v2.1.1 + sig_string = binascii.unhexlify(reply['sign'][0]['sig']) + recid = int(reply['sign'][0]['recid'], 16) + sig = ecc.construct_sig65(sig_string, recid, True) + pubkey, compressed = ecc.ECPubkey.from_signature65(sig, msg_hash) + addr = public_key_to_p2pkh(pubkey.get_public_key_bytes(compressed=compressed)) + if ecc.verify_message_with_address(addr, sig, message) is False: + raise Exception(_("Could not sign message")) + elif 'pubkey' in reply['sign'][0]: + # firmware <= v2.1.1 + for recid in range(4): + sig_string = binascii.unhexlify(reply['sign'][0]['sig']) + sig = ecc.construct_sig65(sig_string, recid, True) + try: + addr = public_key_to_p2pkh(binascii.unhexlify(reply['sign'][0]['pubkey'])) + if ecc.verify_message_with_address(addr, sig, message): + break + except Exception: + continue + else: + raise Exception(_("Could not sign message")) + + + except BaseException as e: + self.give_error(e) + return sig + + + def sign_transaction(self, tx, password): + if tx.is_complete(): + return + + try: + p2pkhTransaction = True + derivations = self.get_tx_derivations(tx) + inputhasharray = [] + hasharray = [] + pubkeyarray = [] + + # Build hasharray from inputs + for i, txin in enumerate(tx.inputs()): + if txin['type'] == 'coinbase': + self.give_error("Coinbase not supported") # should never happen + + if txin['type'] != 'p2pkh': + p2pkhTransaction = False + + for x_pubkey in txin['x_pubkeys']: + if x_pubkey in derivations: + index = derivations.get(x_pubkey) + inputPath = "%s/%d/%d" % (self.get_derivation(), index[0], index[1]) + inputHash = Hash(binascii.unhexlify(tx.serialize_preimage(i))) + hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath} + hasharray.append(hasharray_i) + inputhasharray.append(inputHash) + break + else: + self.give_error("No matching x_key for sign_transaction") # should never happen + + # Build pubkeyarray from outputs + for _type, address, amount in tx.outputs(): + assert _type == TYPE_ADDRESS + info = tx.output_info.get(address) + if info is not None: + index, xpubs, m = info + changePath = self.get_derivation() + "/%d/%d" % index + changePubkey = self.derive_pubkey(index[0], index[1]) + pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath} + pubkeyarray.append(pubkeyarray_i) + + # Special serialization of the unsigned transaction for + # the mobile verification app. + # At the moment, verification only works for p2pkh transactions. + if p2pkhTransaction: + class CustomTXSerialization(Transaction): + @classmethod + def input_script(self, txin, estimate_size=False): + if txin['type'] == 'p2pkh': + return Transaction.get_preimage_script(txin) + if txin['type'] == 'p2sh': + # Multisig verification has partial support, but is disabled. This is the + # expected serialization though, so we leave it here until we activate it. + return '00' + push_script(Transaction.get_preimage_script(txin)) + raise Exception("unsupported type %s" % txin['type']) + tx_dbb_serialized = CustomTXSerialization(tx.serialize()).serialize_to_network() + else: + # We only need this for the signing echo / verification. + tx_dbb_serialized = None + + # Build sign command + dbb_signatures = [] + steps = math.ceil(1.0 * len(hasharray) / self.maxInputs) + for step in range(int(steps)): + hashes = hasharray[step * self.maxInputs : (step + 1) * self.maxInputs] + + msg = { + "sign": { + "data": hashes, + "checkpub": pubkeyarray, + }, + } + if tx_dbb_serialized is not None: + msg["sign"]["meta"] = to_hexstr(Hash(tx_dbb_serialized)) + msg = json.dumps(msg).encode('ascii') + dbb_client = self.plugin.get_client(self) + + if not dbb_client.is_paired(): + raise Exception("Could not sign transaction.") + + reply = dbb_client.hid_send_encrypt(msg) + if 'error' in reply: + raise Exception(reply['error']['message']) + + if 'echo' not in reply: + raise Exception("Could not sign transaction.") + + if self.plugin.is_mobile_paired() and tx_dbb_serialized is not None: + reply['tx'] = tx_dbb_serialized + self.plugin.comserver_post_notification(reply) + + if steps > 1: + self.handler.show_message(_("Signing large transaction. Please be patient ...") + "\n\n" + + _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + " " + + _("(Touch {} of {})").format((step + 1), steps) + "\n\n" + + _("To cancel, briefly touch the blinking light or wait for the timeout.") + "\n\n") + else: + self.handler.show_message(_("Signing transaction...") + "\n\n" + + _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + "\n\n" + + _("To cancel, briefly touch the blinking light or wait for the timeout.")) + + # Send twice, first returns an echo for smart verification + reply = dbb_client.hid_send_encrypt(msg) + self.handler.finished() + + if 'error' in reply: + if reply["error"].get('code') in (600, 601): + # aborted via LED short touch or timeout + raise UserCancelled() + raise Exception(reply['error']['message']) + + if 'sign' not in reply: + raise Exception("Could not sign transaction.") + + dbb_signatures.extend(reply['sign']) + + # Fill signatures + if len(dbb_signatures) != len(tx.inputs()): + raise Exception("Incorrect number of transactions signed.") # Should never occur + for i, txin in enumerate(tx.inputs()): + num = txin['num_sig'] + for pubkey in txin['pubkeys']: + signatures = list(filter(None, txin['signatures'])) + if len(signatures) == num: + break # txin is complete + ii = txin['pubkeys'].index(pubkey) + signed = dbb_signatures[i] + if 'recid' in signed: + # firmware > v2.1.1 + recid = int(signed['recid'], 16) + s = binascii.unhexlify(signed['sig']) + h = inputhasharray[i] + pk = ecc.ECPubkey.from_sig_string(s, recid, h) + pk = pk.get_public_key_hex(compressed=True) + elif 'pubkey' in signed: + # firmware <= v2.1.1 + pk = signed['pubkey'] + if pk != pubkey: + continue + sig_r = int(signed['sig'][:64], 16) + sig_s = int(signed['sig'][64:], 16) + sig = ecc.der_sig_from_r_and_s(sig_r, sig_s) + sig = to_hexstr(sig) + '01' + tx.add_signature_to_txin(i, ii, sig) + except UserCancelled: + raise + except BaseException as e: + self.give_error(e, True) + else: + print_error("Transaction is_complete", tx.is_complete()) + tx.raw = tx.serialize() + + +class DigitalBitboxPlugin(HW_PluginBase): + + libraries_available = DIGIBOX + keystore_class = DigitalBitbox_KeyStore + client = None + DEVICE_IDS = [ + (0x03eb, 0x2402) # Digital Bitbox + ] + SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') + + def __init__(self, parent, config, name): + HW_PluginBase.__init__(self, parent, config, name) + if self.libraries_available: + self.device_manager().register_devices(self.DEVICE_IDS) + + self.digitalbitbox_config = self.config.get('digitalbitbox', {}) + + + def get_dbb_device(self, device): + dev = hid.device() + dev.open_path(device.path) + return dev + + + def create_client(self, device, handler): + if device.interface_number == 0 or device.usage_page == 0xffff: + if handler: + self.handler = handler + client = self.get_dbb_device(device) + if client is not None: + client = DigitalBitbox_Client(self, client) + return client + else: + return None + + + def setup_device(self, device_info, wizard, purpose): + devmgr = self.device_manager() + device_id = device_info.device.id_ + client = devmgr.client_by_id(device_id) + if client is None: + raise Exception(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) + client.handler = self.create_handler(wizard) + if purpose == HWD_SETUP_NEW_WALLET: + client.setupRunning = True + client.get_xpub("m/44'/0'", 'standard') + + + def is_mobile_paired(self): + return 'encryptionprivkey' in self.digitalbitbox_config + + + def comserver_post_notification(self, payload): + assert self.is_mobile_paired(), "unexpected mobile pairing error" + url = 'https://digitalbitbox.com/smartverification/index.php' + key_s = base64.b64decode(self.digitalbitbox_config['encryptionprivkey']) + args = 'c=data&s=0&dt=0&uuid=%s&pl=%s' % ( + self.digitalbitbox_config['comserverchannelid'], + EncodeAES(key_s, json.dumps(payload).encode('ascii')).decode('ascii'), + ) + try: + requests.post(url, args) + except Exception as e: + self.handler.show_error(str(e)) + + + def get_xpub(self, device_id, derivation, xtype, wizard): + if xtype not in self.SUPPORTED_XTYPES: + raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + client.handler = self.create_handler(wizard) + client.check_device_dialog() + xpub = client.get_xpub(derivation, xtype) + return xpub + + + def get_client(self, keystore, force_pair=True): + devmgr = self.device_manager() + handler = keystore.handler + with devmgr.hid_lock: + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) + if client is not None: + client.check_device_dialog() + return client + + def show_address(self, wallet, address, keystore=None): + if keystore is None: + keystore = wallet.get_keystore() + if not self.show_address_helper(wallet, address, keystore): + return + if type(wallet) is not Standard_Wallet: + keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) + return + if not self.is_mobile_paired(): + keystore.handler.show_error(_('This function is only available after pairing your {} with a mobile device.').format(self.device)) + return + if not keystore.is_p2pkh(): + keystore.handler.show_error(_('This function is only available for p2pkh keystores when using {}.').format(self.device)) + return + change, index = wallet.get_address_index(address) + keypath = '%s/%d/%d' % (keystore.derivation, change, index) + xpub = self.get_client(keystore)._get_xpub(keypath) + verify_request_payload = { + "type": 'p2pkh', + "echo": xpub['echo'], + } + self.comserver_post_notification(verify_request_payload) diff --git a/electrum/plugins/digitalbitbox/qt.py b/electrum/plugins/digitalbitbox/qt.py @@ -0,0 +1,43 @@ +from functools import partial + +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase +from .digitalbitbox import DigitalBitboxPlugin + +from electrum.i18n import _ +from electrum.plugin import hook +from electrum.wallet import Standard_Wallet + + +class Plugin(DigitalBitboxPlugin, QtPluginBase): + icon_unpaired = ":icons/digitalbitbox_unpaired.png" + icon_paired = ":icons/digitalbitbox.png" + + def create_handler(self, window): + return DigitalBitbox_Handler(window) + + @hook + def receive_menu(self, menu, addrs, wallet): + if type(wallet) is not Standard_Wallet: + return + + keystore = wallet.get_keystore() + if type(keystore) is not self.keystore_class: + return + + if not self.is_mobile_paired(): + return + + if not keystore.is_p2pkh(): + return + + if len(addrs) == 1: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) + + menu.addAction(_("Show on {}").format(self.device), show_address) + + +class DigitalBitbox_Handler(QtHandlerBase): + + def __init__(self, win): + super(DigitalBitbox_Handler, self).__init__(win, 'Digital Bitbox') diff --git a/electrum/plugins/email_requests/__init__.py b/electrum/plugins/email_requests/__init__.py @@ -0,0 +1,5 @@ +from electrum.i18n import _ + +fullname = _('Email') +description = _("Send and receive payment request with an email account") +available_for = ['qt'] diff --git a/electrum/plugins/email_requests/qt.py b/electrum/plugins/email_requests/qt.py @@ -0,0 +1,271 @@ +#!/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 random +import time +import threading +import base64 +from functools import partial +import traceback +import sys + +import smtplib +import imaplib +import email +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.encoders import encode_base64 + +from PyQt5.QtGui import * +from PyQt5.QtCore import * +from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit, + QInputDialog) + +from electrum.plugin import BasePlugin, hook +from electrum.paymentrequest import PaymentRequest +from electrum.i18n import _ +from electrum.util import PrintError +from ...gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, + WindowModalDialog, get_parent_main_window) + + +class Processor(threading.Thread, PrintError): + polling_interval = 5*60 + + def __init__(self, imap_server, username, password, callback): + threading.Thread.__init__(self) + self.daemon = True + self.username = username + self.password = password + self.imap_server = imap_server + self.on_receive = callback + self.M = None + self.reset_connect_wait() + + def reset_connect_wait(self): + self.connect_wait = 100 # ms, between failed connection attempts + + def poll(self): + try: + self.M.select() + except: + return + typ, data = self.M.search(None, 'ALL') + for num in str(data[0], 'utf8').split(): + typ, msg_data = self.M.fetch(num, '(RFC822)') + msg = email.message_from_bytes(msg_data[0][1]) + p = msg.get_payload() + if not msg.is_multipart(): + p = [p] + continue + for item in p: + if item.get_content_type() == "application/bitcoin-paymentrequest": + pr_str = item.get_payload() + pr_str = base64.b64decode(pr_str) + self.on_receive(pr_str) + + def run(self): + while True: + try: + self.M = imaplib.IMAP4_SSL(self.imap_server) + self.M.login(self.username, self.password) + except BaseException as e: + self.print_error('connecting failed: {}'.format(e)) + self.connect_wait *= 2 + else: + self.reset_connect_wait() + # Reconnect when host changes + while self.M and self.M.host == self.imap_server: + try: + self.poll() + except BaseException as e: + self.print_error('polling failed: {}'.format(e)) + break + time.sleep(self.polling_interval) + time.sleep(random.randint(0, self.connect_wait)) + + def send(self, recipient, message, payment_request): + msg = MIMEMultipart() + msg['Subject'] = message + msg['To'] = recipient + msg['From'] = self.username + part = MIMEBase('application', "bitcoin-paymentrequest") + part.set_payload(payment_request) + encode_base64(part) + part.add_header('Content-Disposition', 'attachment; filename="payreq.btc"') + msg.attach(part) + try: + s = smtplib.SMTP_SSL(self.imap_server, timeout=2) + s.login(self.username, self.password) + s.sendmail(self.username, [recipient], msg.as_string()) + s.quit() + except BaseException as e: + self.print_error(e) + + +class QEmailSignalObject(QObject): + email_new_invoice_signal = pyqtSignal() + + +class Plugin(BasePlugin): + + def fullname(self): + return 'Email' + + def description(self): + return _("Send and receive payment requests via email") + + def is_available(self): + return True + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.imap_server = self.config.get('email_server', '') + self.username = self.config.get('email_username', '') + self.password = self.config.get('email_password', '') + if self.imap_server and self.username and self.password: + self.processor = Processor(self.imap_server, self.username, self.password, self.on_receive) + self.processor.start() + self.obj = QEmailSignalObject() + self.obj.email_new_invoice_signal.connect(self.new_invoice) + self.wallets = set() + + def on_receive(self, pr_str): + self.print_error('received payment request') + self.pr = PaymentRequest(pr_str) + self.obj.email_new_invoice_signal.emit() + + @hook + def load_wallet(self, wallet, main_window): + self.wallets |= {wallet} + + @hook + def close_wallet(self, wallet): + self.wallets -= {wallet} + + def new_invoice(self): + for wallet in self.wallets: + wallet.invoices.add(self.pr) + #main_window.invoice_list.update() + + @hook + def receive_list_menu(self, menu, addr): + window = get_parent_main_window(menu) + menu.addAction(_("Send via e-mail"), lambda: self.send(window, addr)) + + def send(self, window, addr): + from electrum import paymentrequest + r = window.wallet.receive_requests.get(addr) + message = r.get('memo', '') + if r.get('signature'): + pr = paymentrequest.serialize_request(r) + else: + pr = paymentrequest.make_request(self.config, r) + if not pr: + return + recipient, ok = QInputDialog.getText(window, 'Send request', 'Email invoice to:') + if not ok: + return + recipient = str(recipient) + payload = pr.SerializeToString() + self.print_error('sending mail to', recipient) + try: + # FIXME this runs in the GUI thread and blocks it... + self.processor.send(recipient, message, payload) + except BaseException as e: + traceback.print_exc(file=sys.stderr) + window.show_message(str(e)) + else: + window.show_message(_('Request sent.')) + + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Settings'), partial(self.settings_dialog, window)) + + def settings_dialog(self, window): + d = WindowModalDialog(window, _("Email settings")) + d.setMinimumSize(500, 200) + + vbox = QVBoxLayout(d) + vbox.addWidget(QLabel(_('Server hosting your email account'))) + grid = QGridLayout() + vbox.addLayout(grid) + grid.addWidget(QLabel('Server (IMAP)'), 0, 0) + server_e = QLineEdit() + server_e.setText(self.imap_server) + grid.addWidget(server_e, 0, 1) + + grid.addWidget(QLabel('Username'), 1, 0) + username_e = QLineEdit() + username_e.setText(self.username) + grid.addWidget(username_e, 1, 1) + + grid.addWidget(QLabel('Password'), 2, 0) + password_e = QLineEdit() + password_e.setText(self.password) + grid.addWidget(password_e, 2, 1) + + vbox.addStretch() + vbox.addLayout(Buttons(CloseButton(d), OkButton(d))) + + if not d.exec_(): + return + + server = str(server_e.text()) + self.config.set_key('email_server', server) + self.imap_server = server + + username = str(username_e.text()) + self.config.set_key('email_username', username) + self.username = username + + password = str(password_e.text()) + self.config.set_key('email_password', password) + self.password = password + + check_connection = CheckConnectionThread(server, username, password) + check_connection.connection_error_signal.connect(lambda e: window.show_message( + _("Unable to connect to mail server:\n {}").format(e) + "\n" + + _("Please check your connection and credentials.") + )) + check_connection.start() + + +class CheckConnectionThread(QThread): + connection_error_signal = pyqtSignal(str) + + def __init__(self, server, username, password): + super().__init__() + self.server = server + self.username = username + self.password = password + + def run(self): + try: + conn = imaplib.IMAP4_SSL(self.server) + conn.login(self.username, self.password) + except BaseException as e: + self.connection_error_signal.emit(str(e)) diff --git a/electrum/plugins/greenaddress_instant/__init__.py b/electrum/plugins/greenaddress_instant/__init__.py @@ -0,0 +1,5 @@ +from electrum.i18n import _ + +fullname = 'GreenAddress instant' +description = _("Allows validating if your transactions have instant confirmations by GreenAddress") +available_for = ['qt'] diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py @@ -0,0 +1,107 @@ +#!/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 base64 +import urllib.parse +import sys +import requests + +from PyQt5.QtWidgets import QApplication, QPushButton + +from electrum.plugin import BasePlugin, hook +from electrum.i18n import _ + + + +class Plugin(BasePlugin): + + button_label = _("Verify GA instant") + + @hook + def transaction_dialog(self, d): + d.verify_button = QPushButton(self.button_label) + d.verify_button.clicked.connect(lambda: self.do_verify(d)) + d.buttons.insert(0, d.verify_button) + self.transaction_dialog_update(d) + + def get_my_addr(self, d): + """Returns the address for given tx which can be used to request + instant confirmation verification from GreenAddress""" + for addr, _ in d.tx.get_outputs(): + if d.wallet.is_mine(addr): + return addr + return None + + @hook + def transaction_dialog_update(self, d): + if d.tx.is_complete() and self.get_my_addr(d): + d.verify_button.show() + else: + d.verify_button.hide() + + def do_verify(self, d): + tx = d.tx + wallet = d.wallet + window = d.main_window + + if wallet.is_watching_only(): + d.show_critical(_('This feature is not available for watch-only wallets.')) + return + + # 1. get the password and sign the verification request + password = None + if wallet.has_keystore_encryption(): + msg = _('GreenAddress requires your signature \n' + 'to verify that transaction is instant.\n' + 'Please enter your password to sign a\n' + 'verification request.') + password = window.password_dialog(msg, parent=d) + if not password: + return + try: + d.verify_button.setText(_('Verifying...')) + QApplication.processEvents() # update the button label + + addr = self.get_my_addr(d) + message = "Please verify if %s is GreenAddress instant confirmed" % tx.txid() + sig = wallet.sign_message(addr, message, password) + sig = base64.b64encode(sig).decode('ascii') + + # 2. send the request + response = requests.request("GET", ("https://greenaddress.it/verify/?signature=%s&txhash=%s" % (urllib.parse.quote(sig), tx.txid())), + headers = {'User-Agent': 'Electrum'}) + response = response.json() + + # 3. display the result + if response.get('verified'): + d.show_message(_('{} is covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification successful!')) + else: + d.show_critical(_('{} is not covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification failed!')) + except BaseException as e: + import traceback + traceback.print_exc(file=sys.stdout) + d.show_error(str(e)) + finally: + d.verify_button.setText(self.button_label) diff --git a/electrum/plugins/hw_wallet/__init__.py b/electrum/plugins/hw_wallet/__init__.py @@ -0,0 +1,2 @@ +from .plugin import HW_PluginBase +from .cmdline import CmdLineHandler diff --git a/electrum/plugins/hw_wallet/cmdline.py b/electrum/plugins/hw_wallet/cmdline.py @@ -0,0 +1,46 @@ +from electrum.util import print_msg, print_error, raw_input + + +class CmdLineHandler: + + def get_passphrase(self, msg, confirm): + import getpass + print_msg(msg) + return getpass.getpass('') + + def get_pin(self, msg): + t = { 'a':'7', 'b':'8', 'c':'9', 'd':'4', 'e':'5', 'f':'6', 'g':'1', 'h':'2', 'i':'3'} + print_msg(msg) + print_msg("a b c\nd e f\ng h i\n-----") + o = raw_input() + try: + return ''.join(map(lambda x: t[x], o)) + except KeyError as e: + raise Exception("Character {} not in matrix!".format(e)) from e + + def prompt_auth(self, msg): + import getpass + print_msg(msg) + response = getpass.getpass('') + if len(response) == 0: + return None + return response + + def yes_no_question(self, msg): + print_msg(msg) + return raw_input() in 'yY' + + def stop(self): + pass + + def show_message(self, msg, on_cancel=None): + print_msg(msg) + + def show_error(self, msg, blocking=False): + print_msg(msg) + + def update_status(self, b): + print_error('hw device status', b) + + def finished(self): + pass diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py @@ -0,0 +1,89 @@ +#!/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 electrum.plugin import BasePlugin, hook +from electrum.i18n import _ +from electrum.bitcoin import is_address + + +class HW_PluginBase(BasePlugin): + # Derived classes provide: + # + # class-static variables: client_class, firmware_URL, handler_class, + # libraries_available, libraries_URL, minimum_firmware, + # wallet_class, ckd_public, types, HidTransport + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.device = self.keystore_class.device + self.keystore_class.plugin = self + + def is_enabled(self): + return True + + def device_manager(self): + return self.parent.device_manager + + @hook + def close_wallet(self, wallet): + for keystore in wallet.get_keystores(): + if isinstance(keystore, self.keystore_class): + self.device_manager().unpair_xpub(keystore.xpub) + + def setup_device(self, device_info, wizard, purpose): + """Called when creating a new wallet or when using the device to decrypt + an existing wallet. Select the device to use. If the device is + uninitialized, go through the initialization process. + """ + raise NotImplementedError() + + def show_address(self, wallet, address, keystore=None): + pass # implemented in child classes + + def show_address_helper(self, wallet, address, keystore=None): + if keystore is None: + keystore = wallet.get_keystore() + if not is_address(address): + keystore.handler.show_error(_('Invalid Bitcoin Address')) + return False + if not wallet.is_mine(address): + keystore.handler.show_error(_('Address not in wallet.')) + return False + if type(keystore) != self.keystore_class: + return False + return True + + +def is_any_tx_output_on_change_branch(tx): + if not hasattr(tx, 'output_info'): + return False + for _type, address, amount in tx.outputs(): + info = tx.output_info.get(address) + if info is not None: + index, xpubs, m = info + if index[0] == 1: + return True + return False diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# -*- 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. + +import threading + +from PyQt5.Qt import QVBoxLayout, QLabel +from electrum.gui.qt.password_dialog import PasswordDialog, PW_PASSPHRASE +from electrum.gui.qt.util import * + +from electrum.i18n import _ +from electrum.util import PrintError + +# The trickiest thing about this handler was getting windows properly +# parented on macOS. +class QtHandlerBase(QObject, PrintError): + '''An interface between the GUI (here, QT) and the device handling + logic for handling I/O.''' + + passphrase_signal = pyqtSignal(object, object) + message_signal = pyqtSignal(object, object) + error_signal = pyqtSignal(object, object) + word_signal = pyqtSignal(object) + clear_signal = pyqtSignal() + query_signal = pyqtSignal(object, object) + yes_no_signal = pyqtSignal(object) + status_signal = pyqtSignal(object) + + def __init__(self, win, device): + super(QtHandlerBase, self).__init__() + self.clear_signal.connect(self.clear_dialog) + self.error_signal.connect(self.error_dialog) + self.message_signal.connect(self.message_dialog) + self.passphrase_signal.connect(self.passphrase_dialog) + self.word_signal.connect(self.word_dialog) + self.query_signal.connect(self.win_query_choice) + self.yes_no_signal.connect(self.win_yes_no_question) + self.status_signal.connect(self._update_status) + self.win = win + self.device = device + self.dialog = None + self.done = threading.Event() + + def top_level_window(self): + return self.win.top_level_window() + + def update_status(self, paired): + self.status_signal.emit(paired) + + def _update_status(self, paired): + if hasattr(self, 'button'): + button = self.button + icon = button.icon_paired if paired else button.icon_unpaired + button.setIcon(QIcon(icon)) + + def query_choice(self, msg, labels): + self.done.clear() + self.query_signal.emit(msg, labels) + self.done.wait() + return self.choice + + def yes_no_question(self, msg): + self.done.clear() + self.yes_no_signal.emit(msg) + self.done.wait() + return self.ok + + def show_message(self, msg, on_cancel=None): + self.message_signal.emit(msg, on_cancel) + + def show_error(self, msg, blocking=False): + self.done.clear() + self.error_signal.emit(msg, blocking) + if blocking: + self.done.wait() + + def finished(self): + self.clear_signal.emit() + + def get_word(self, msg): + self.done.clear() + self.word_signal.emit(msg) + self.done.wait() + return self.word + + def get_passphrase(self, msg, confirm): + self.done.clear() + self.passphrase_signal.emit(msg, confirm) + self.done.wait() + return self.passphrase + + def passphrase_dialog(self, msg, confirm): + # If confirm is true, require the user to enter the passphrase twice + parent = self.top_level_window() + if confirm: + d = PasswordDialog(parent, None, msg, PW_PASSPHRASE) + confirmed, p, passphrase = d.run() + else: + d = WindowModalDialog(parent, _("Enter Passphrase")) + pw = QLineEdit() + pw.setEchoMode(2) + pw.setMinimumWidth(200) + vbox = QVBoxLayout() + vbox.addWidget(WWLabel(msg)) + vbox.addWidget(pw) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + d.setLayout(vbox) + passphrase = pw.text() if d.exec_() else None + self.passphrase = passphrase + self.done.set() + + def word_dialog(self, msg): + dialog = WindowModalDialog(self.top_level_window(), "") + hbox = QHBoxLayout(dialog) + hbox.addWidget(QLabel(msg)) + text = QLineEdit() + text.setMaximumWidth(100) + text.returnPressed.connect(dialog.accept) + hbox.addWidget(text) + hbox.addStretch(1) + dialog.exec_() # Firmware cannot handle cancellation + self.word = text.text() + self.done.set() + + def message_dialog(self, msg, on_cancel): + # Called more than once during signing, to confirm output and fee + self.clear_dialog() + title = _('Please check your {} device').format(self.device) + self.dialog = dialog = WindowModalDialog(self.top_level_window(), title) + l = QLabel(msg) + vbox = QVBoxLayout(dialog) + vbox.addWidget(l) + if on_cancel: + dialog.rejected.connect(on_cancel) + vbox.addLayout(Buttons(CancelButton(dialog))) + dialog.show() + + def error_dialog(self, msg, blocking): + self.win.show_error(msg, parent=self.top_level_window()) + if blocking: + self.done.set() + + def clear_dialog(self): + if self.dialog: + self.dialog.accept() + self.dialog = None + + def win_query_choice(self, msg, labels): + self.choice = self.win.query_choice(msg, labels) + self.done.set() + + def win_yes_no_question(self, msg): + self.ok = self.win.question(msg) + self.done.set() + + + +from electrum.plugin import hook +from electrum.util import UserCancelled +from electrum.gui.qt.main_window import StatusBarButton + +class QtPluginBase(object): + + @hook + def load_wallet(self, wallet, window): + for keystore in wallet.get_keystores(): + if not isinstance(keystore, self.keystore_class): + continue + if not self.libraries_available: + if hasattr(self, 'libraries_available_message'): + message = self.libraries_available_message + '\n' + else: + message = _("Cannot find python library for") + " '%s'.\n" % self.name + message += _("Make sure you install it with python3") + window.show_error(message) + return + tooltip = self.device + '\n' + (keystore.label or 'unnamed') + cb = partial(self.show_settings_dialog, window, keystore) + button = StatusBarButton(QIcon(self.icon_unpaired), tooltip, cb) + button.icon_paired = self.icon_paired + button.icon_unpaired = self.icon_unpaired + window.statusBar().addPermanentWidget(button) + handler = self.create_handler(window) + handler.button = button + keystore.handler = handler + keystore.thread = TaskThread(window, window.on_error) + self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window) + # Trigger a pairing + keystore.thread.add(partial(self.get_client, keystore)) + + def choose_device(self, window, keystore): + '''This dialog box should be usable even if the user has + forgotten their PIN or it is in bootloader mode.''' + device_id = self.device_manager().xpub_id(keystore.xpub) + if not device_id: + try: + info = self.device_manager().select_device(self, keystore.handler, keystore) + except UserCancelled: + return + device_id = info.device.id_ + return device_id + + def show_settings_dialog(self, window, keystore): + device_id = self.choose_device(window, keystore) + + def add_show_address_on_hw_device_button_for_receive_addr(self, wallet, keystore, main_window): + plugin = keystore.plugin + receive_address_e = main_window.receive_address_e + + def show_address(): + addr = receive_address_e.text() + keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore)) + receive_address_e.addButton(":icons/eye1.png", show_address, _("Show on {}").format(plugin.device)) diff --git a/electrum/plugins/keepkey/__init__.py b/electrum/plugins/keepkey/__init__.py @@ -0,0 +1,7 @@ +from electrum.i18n import _ + +fullname = 'KeepKey' +description = _('Provides support for KeepKey hardware wallet') +requires = [('keepkeylib','github.com/keepkey/python-keepkey')] +registers_keystore = ('hardware', 'keepkey', _("KeepKey wallet")) +available_for = ['qt', 'cmdline'] diff --git a/electrum/plugins/keepkey/client.py b/electrum/plugins/keepkey/client.py @@ -0,0 +1,14 @@ +from keepkeylib.client import proto, BaseClient, ProtocolMixin +from .clientbase import KeepKeyClientBase + +class KeepKeyClient(KeepKeyClientBase, ProtocolMixin, BaseClient): + def __init__(self, transport, handler, plugin): + BaseClient.__init__(self, transport) + ProtocolMixin.__init__(self, transport) + KeepKeyClientBase.__init__(self, handler, plugin, proto) + + def recovery_device(self, *args): + ProtocolMixin.recovery_device(self, False, *args) + + +KeepKeyClientBase.wrap_methods(KeepKeyClient) diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py @@ -0,0 +1,250 @@ +import time +from struct import pack + +from electrum.i18n import _ +from electrum.util import PrintError, UserCancelled +from electrum.keystore import bip39_normalize_passphrase +from electrum.bitcoin import serialize_xpub + + +class GuiMixin(object): + # Requires: self.proto, self.device + + messages = { + 3: _("Confirm the transaction output on your {} device"), + 4: _("Confirm internal entropy on your {} device to begin"), + 5: _("Write down the seed word shown on your {}"), + 6: _("Confirm on your {} that you want to wipe it clean"), + 7: _("Confirm on your {} device the message to sign"), + 8: _("Confirm the total amount spent and the transaction fee on your " + "{} device"), + 10: _("Confirm wallet address on your {} device"), + 'default': _("Check your {} device to continue"), + } + + def callback_Failure(self, msg): + # BaseClient's unfortunate call() implementation forces us to + # raise exceptions on failure in order to unwind the stack. + # However, making the user acknowledge they cancelled + # gets old very quickly, so we suppress those. The NotInitialized + # one is misnamed and indicates a passphrase request was cancelled. + if msg.code in (self.types.Failure_PinCancelled, + self.types.Failure_ActionCancelled, + self.types.Failure_NotInitialized): + raise UserCancelled() + raise RuntimeError(msg.message) + + def callback_ButtonRequest(self, msg): + message = self.msg + if not message: + message = self.messages.get(msg.code, self.messages['default']) + self.handler.show_message(message.format(self.device), self.cancel) + return self.proto.ButtonAck() + + def callback_PinMatrixRequest(self, msg): + if msg.type == 2: + msg = _("Enter a new PIN for your {}:") + elif msg.type == 3: + msg = (_("Re-enter the new PIN for your {}.\n\n" + "NOTE: the positions of the numbers have changed!")) + else: + msg = _("Enter your current {} PIN:") + pin = self.handler.get_pin(msg.format(self.device)) + if len(pin) > 9: + self.handler.show_error(_('The PIN cannot be longer than 9 characters.')) + pin = '' # to cancel below + if not pin: + return self.proto.Cancel() + return self.proto.PinMatrixAck(pin=pin) + + def callback_PassphraseRequest(self, req): + if self.creating_wallet: + msg = _("Enter a passphrase to generate this wallet. Each time " + "you use this wallet your {} will prompt you for the " + "passphrase. If you forget the passphrase you cannot " + "access the bitcoins in the wallet.").format(self.device) + else: + msg = _("Enter the passphrase to unlock this wallet:") + passphrase = self.handler.get_passphrase(msg, self.creating_wallet) + if passphrase is None: + return self.proto.Cancel() + passphrase = bip39_normalize_passphrase(passphrase) + + ack = self.proto.PassphraseAck(passphrase=passphrase) + length = len(ack.passphrase) + if length > 50: + self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length)) + return self.proto.Cancel() + return ack + + def callback_WordRequest(self, msg): + self.step += 1 + msg = _("Step {}/24. Enter seed word as explained on " + "your {}:").format(self.step, self.device) + word = self.handler.get_word(msg) + # Unfortunately the device can't handle self.proto.Cancel() + return self.proto.WordAck(word=word) + + def callback_CharacterRequest(self, msg): + char_info = self.handler.get_char(msg) + if not char_info: + return self.proto.Cancel() + return self.proto.CharacterAck(**char_info) + + +class KeepKeyClientBase(GuiMixin, PrintError): + + def __init__(self, handler, plugin, proto): + assert hasattr(self, 'tx_api') # ProtocolMixin already constructed? + self.proto = proto + self.device = plugin.device + self.handler = handler + self.tx_api = plugin + self.types = plugin.types + self.msg = None + self.creating_wallet = False + self.used() + + def __str__(self): + return "%s/%s" % (self.label(), self.features.device_id) + + def label(self): + '''The name given by the user to the device.''' + return self.features.label + + def is_initialized(self): + '''True if initialized, False if wiped.''' + return self.features.initialized + + def is_pairable(self): + return not self.features.bootloader_mode + + def has_usable_connection_with_device(self): + try: + res = self.ping("electrum pinging device") + assert res == "electrum pinging device" + except BaseException: + return False + return True + + def used(self): + self.last_operation = time.time() + + def prevent_timeouts(self): + self.last_operation = float('inf') + + def timeout(self, cutoff): + '''Time out the client if the last operation was before cutoff.''' + if self.last_operation < cutoff: + self.print_error("timed out") + self.clear_session() + + @staticmethod + def expand_path(n): + '''Convert bip32 path to list of uint32 integers with prime flags + 0/-1/1' -> [0, 0x80000001, 0x80000001]''' + # This code is similar to code in trezorlib where it unfortunately + # is not declared as a staticmethod. Our n has an extra element. + PRIME_DERIVATION_FLAG = 0x80000000 + path = [] + for x in n.split('/')[1:]: + prime = 0 + if x.endswith("'"): + x = x.replace('\'', '') + prime = PRIME_DERIVATION_FLAG + if x.startswith('-'): + prime = PRIME_DERIVATION_FLAG + path.append(abs(int(x)) | prime) + return path + + def cancel(self): + '''Provided here as in keepkeylib but not trezorlib.''' + self.transport.write(self.proto.Cancel()) + + def i4b(self, x): + return pack('>I', x) + + def get_xpub(self, bip32_path, xtype): + address_n = self.expand_path(bip32_path) + creating = False + node = self.get_public_node(address_n, creating).node + return serialize_xpub(xtype, node.chain_code, node.public_key, node.depth, self.i4b(node.fingerprint), self.i4b(node.child_num)) + + def toggle_passphrase(self): + if self.features.passphrase_protection: + self.msg = _("Confirm on your {} device to disable passphrases") + else: + self.msg = _("Confirm on your {} device to enable passphrases") + enabled = not self.features.passphrase_protection + self.apply_settings(use_passphrase=enabled) + + def change_label(self, label): + self.msg = _("Confirm the new label on your {} device") + self.apply_settings(label=label) + + def change_homescreen(self, homescreen): + self.msg = _("Confirm on your {} device to change your home screen") + self.apply_settings(homescreen=homescreen) + + def set_pin(self, remove): + if remove: + self.msg = _("Confirm on your {} device to disable PIN protection") + elif self.features.pin_protection: + self.msg = _("Confirm on your {} device to change your PIN") + else: + self.msg = _("Confirm on your {} device to set a PIN") + self.change_pin(remove) + + def clear_session(self): + '''Clear the session to force pin (and passphrase if enabled) + re-entry. Does not leak exceptions.''' + self.print_error("clear session:", self) + self.prevent_timeouts() + try: + super(KeepKeyClientBase, self).clear_session() + except BaseException as e: + # If the device was removed it has the same effect... + self.print_error("clear_session: ignoring error", str(e)) + + def get_public_node(self, address_n, creating): + self.creating_wallet = creating + return super(KeepKeyClientBase, self).get_public_node(address_n) + + def close(self): + '''Called when Our wallet was closed or the device removed.''' + self.print_error("closing client") + self.clear_session() + # Release the device + self.transport.close() + + def firmware_version(self): + f = self.features + return (f.major_version, f.minor_version, f.patch_version) + + def atleast_version(self, major, minor=0, patch=0): + return self.firmware_version() >= (major, minor, patch) + + @staticmethod + def wrapper(func): + '''Wrap methods to clear any message box they opened.''' + + def wrapped(self, *args, **kwargs): + try: + self.prevent_timeouts() + return func(self, *args, **kwargs) + finally: + self.used() + self.handler.finished() + self.creating_wallet = False + self.msg = None + + return wrapped + + @staticmethod + def wrap_methods(cls): + for method in ['apply_settings', 'change_pin', + 'get_address', 'get_public_node', + 'load_device_by_mnemonic', 'load_device_by_xprv', + 'recovery_device', 'reset_device', 'sign_message', + 'sign_tx', 'wipe_device']: + setattr(cls, method, cls.wrapper(getattr(cls, method))) diff --git a/electrum/plugins/keepkey/cmdline.py b/electrum/plugins/keepkey/cmdline.py @@ -0,0 +1,14 @@ +from electrum.plugin import hook +from .keepkey import KeepKeyPlugin +from ..hw_wallet import CmdLineHandler + +class Plugin(KeepKeyPlugin): + handler = CmdLineHandler() + @hook + def init_keystore(self, keystore): + if not isinstance(keystore, self.keystore_class): + return + keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py @@ -0,0 +1,438 @@ +from binascii import hexlify, unhexlify +import traceback +import sys + +from electrum.util import bfh, bh2u, UserCancelled +from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, + TYPE_ADDRESS, TYPE_SCRIPT, + is_segwit_address) +from electrum import constants +from electrum.i18n import _ +from electrum.plugin import BasePlugin +from electrum.transaction import deserialize, Transaction +from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey +from electrum.wallet import Standard_Wallet +from electrum.base_wizard import ScriptTypeNotSupported + +from ..hw_wallet import HW_PluginBase +from ..hw_wallet.plugin import is_any_tx_output_on_change_branch + + +# TREZOR initialization methods +TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) + + +class KeepKey_KeyStore(Hardware_KeyStore): + hw_type = 'keepkey' + device = 'KeepKey' + + def get_derivation(self): + return self.derivation + + def is_segwit(self): + return self.derivation.startswith("m/49'/") + + def get_client(self, force_pair=True): + return self.plugin.get_client(self, force_pair) + + def decrypt_message(self, sequence, message, password): + raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + + def sign_message(self, sequence, message, password): + client = self.get_client() + address_path = self.get_derivation() + "/%d/%d"%sequence + address_n = client.expand_path(address_path) + msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) + return msg_sig.signature + + def sign_transaction(self, tx, password): + if tx.is_complete(): + return + # previous transactions used as inputs + prev_tx = {} + # path of the xpubs that are involved + xpub_path = {} + for txin in tx.inputs(): + pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) + tx_hash = txin['prevout_hash'] + if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): + raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + prev_tx[tx_hash] = txin['prev_tx'] + for x_pubkey in x_pubkeys: + if not is_xpubkey(x_pubkey): + continue + xpub, s = parse_xpubkey(x_pubkey) + if xpub == self.get_master_public_key(): + xpub_path[xpub] = self.get_derivation() + + self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) + + +class KeepKeyPlugin(HW_PluginBase): + # Derived classes provide: + # + # class-static variables: client_class, firmware_URL, handler_class, + # libraries_available, libraries_URL, minimum_firmware, + # wallet_class, ckd_public, types, HidTransport + + firmware_URL = 'https://www.keepkey.com' + libraries_URL = 'https://github.com/keepkey/python-keepkey' + minimum_firmware = (1, 0, 0) + keystore_class = KeepKey_KeyStore + SUPPORTED_XTYPES = ('standard', ) + + MAX_LABEL_LEN = 32 + + def __init__(self, parent, config, name): + HW_PluginBase.__init__(self, parent, config, name) + + try: + from . import client + import keepkeylib + import keepkeylib.ckd_public + import keepkeylib.transport_hid + self.client_class = client.KeepKeyClient + self.ckd_public = keepkeylib.ckd_public + self.types = keepkeylib.client.types + self.DEVICE_IDS = keepkeylib.transport_hid.DEVICE_IDS + self.device_manager().register_devices(self.DEVICE_IDS) + self.libraries_available = True + except ImportError: + self.libraries_available = False + + def hid_transport(self, pair): + from keepkeylib.transport_hid import HidTransport + return HidTransport(pair) + + def _try_hid(self, device): + self.print_error("Trying to connect over USB...") + if device.interface_number == 1: + pair = [None, device.path] + else: + pair = [device.path, None] + + try: + return self.hid_transport(pair) + except BaseException as e: + # see fdb810ba622dc7dbe1259cbafb5b28e19d2ab114 + # raise + self.print_error("cannot connect at", device.path, str(e)) + return None + + def create_client(self, device, handler): + transport = self._try_hid(device) + if not transport: + self.print_error("cannot connect to device") + return + + self.print_error("connected to device at", device.path) + + client = self.client_class(transport, handler, self) + + # Try a ping for device sanity + try: + client.ping('t') + except BaseException as e: + self.print_error("ping failed", str(e)) + return None + + if not client.atleast_version(*self.minimum_firmware): + msg = (_('Outdated {} firmware for device labelled {}. Please ' + 'download the updated firmware from {}') + .format(self.device, client.label(), self.firmware_URL)) + self.print_error(msg) + handler.show_error(msg) + return None + + return client + + def get_client(self, keystore, force_pair=True): + devmgr = self.device_manager() + handler = keystore.handler + with devmgr.hid_lock: + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) + # returns the client for a given keystore. can use xpub + if client: + client.used() + return client + + def get_coin_name(self): + return "Testnet" if constants.net.TESTNET else "Bitcoin" + + def initialize_device(self, device_id, wizard, handler): + # Initialization method + msg = _("Choose how you want to initialize your {}.\n\n" + "The first two methods are secure as no secret information " + "is entered into your computer.\n\n" + "For the last two methods you input secrets on your keyboard " + "and upload them to your {}, and so you should " + "only do those on a computer you know to be trustworthy " + "and free of malware." + ).format(self.device, self.device) + choices = [ + # Must be short as QT doesn't word-wrap radio button text + (TIM_NEW, _("Let the device generate a completely new seed randomly")), + (TIM_RECOVER, _("Recover from a seed you have previously written down")), + (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), + (TIM_PRIVKEY, _("Upload a master private key")) + ] + def f(method): + import threading + settings = self.request_trezor_init_settings(wizard, method, self.device) + t = threading.Thread(target=self._initialize_device_safe, args=(settings, method, device_id, wizard, handler)) + t.setDaemon(True) + t.start() + exit_code = wizard.loop.exec_() + if exit_code != 0: + # this method (initialize_device) was called with the expectation + # of leaving the device in an initialized state when finishing. + # signal that this is not the case: + raise UserCancelled() + wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f) + + def _initialize_device_safe(self, settings, method, device_id, wizard, handler): + exit_code = 0 + try: + self._initialize_device(settings, method, device_id, wizard, handler) + except UserCancelled: + exit_code = 1 + except BaseException as e: + traceback.print_exc(file=sys.stderr) + handler.show_error(str(e)) + exit_code = 1 + finally: + wizard.loop.exit(exit_code) + + def _initialize_device(self, settings, method, device_id, wizard, handler): + item, label, pin_protection, passphrase_protection = settings + + language = 'english' + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + + if method == TIM_NEW: + strength = 64 * (item + 2) # 128, 192 or 256 + client.reset_device(True, strength, passphrase_protection, + pin_protection, label, language) + elif method == TIM_RECOVER: + word_count = 6 * (item + 2) # 12, 18 or 24 + client.step = 0 + client.recovery_device(word_count, passphrase_protection, + pin_protection, label, language) + elif method == TIM_MNEMONIC: + pin = pin_protection # It's the pin, not a boolean + client.load_device_by_mnemonic(str(item), pin, + passphrase_protection, + label, language) + else: + pin = pin_protection # It's the pin, not a boolean + client.load_device_by_xprv(item, pin, passphrase_protection, + label, language) + + def setup_device(self, device_info, wizard, purpose): + devmgr = self.device_manager() + device_id = device_info.device.id_ + client = devmgr.client_by_id(device_id) + if client is None: + raise Exception(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) + # fixme: we should use: client.handler = wizard + client.handler = self.create_handler(wizard) + if not device_info.initialized: + self.initialize_device(device_id, wizard, client.handler) + client.get_xpub('m', 'standard') + client.used() + + def get_xpub(self, device_id, derivation, xtype, wizard): + if xtype not in self.SUPPORTED_XTYPES: + raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + client.handler = wizard + xpub = client.get_xpub(derivation, xtype) + client.used() + return xpub + + def sign_transaction(self, keystore, tx, prev_tx, xpub_path): + self.prev_tx = prev_tx + self.xpub_path = xpub_path + client = self.get_client(keystore) + inputs = self.tx_inputs(tx, True, keystore.is_segwit()) + outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.is_segwit()) + signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0] + signatures = [(bh2u(x) + '01') for x in signatures] + tx.update_signatures(signatures) + + def show_address(self, wallet, address, keystore=None): + if keystore is None: + keystore = wallet.get_keystore() + if not self.show_address_helper(wallet, address, keystore): + return + if type(wallet) is not Standard_Wallet: + keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) + return + client = self.get_client(wallet.keystore) + if not client.atleast_version(1, 3): + wallet.keystore.handler.show_error(_("Your device firmware is too old")) + return + change, index = wallet.get_address_index(address) + derivation = wallet.keystore.derivation + address_path = "%s/%d/%d"%(derivation, change, index) + address_n = client.expand_path(address_path) + segwit = wallet.keystore.is_segwit() + script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDADDRESS + client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) + + def tx_inputs(self, tx, for_sig=False, segwit=False): + inputs = [] + for txin in tx.inputs(): + txinputtype = self.types.TxInputType() + if txin['type'] == 'coinbase': + prev_hash = "\0"*32 + prev_index = 0xffffffff # signed int -1 + else: + if for_sig: + x_pubkeys = txin['x_pubkeys'] + if len(x_pubkeys) == 1: + x_pubkey = x_pubkeys[0] + xpub, s = parse_xpubkey(x_pubkey) + xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + txinputtype.script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDADDRESS + else: + def f(x_pubkey): + if is_xpubkey(x_pubkey): + xpub, s = parse_xpubkey(x_pubkey) + else: + xpub = xpub_from_pubkey(0, bfh(x_pubkey)) + s = [] + node = self.ckd_public.deserialize(xpub) + return self.types.HDNodePathType(node=node, address_n=s) + pubkeys = map(f, x_pubkeys) + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures')), + m=txin.get('num_sig'), + ) + script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDMULTISIG + txinputtype = self.types.TxInputType( + script_type=script_type, + multisig=multisig + ) + # find which key is mine + for x_pubkey in x_pubkeys: + if is_xpubkey(x_pubkey): + xpub, s = parse_xpubkey(x_pubkey) + if xpub in self.xpub_path: + xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + break + + prev_hash = unhexlify(txin['prevout_hash']) + prev_index = txin['prevout_n'] + + if 'value' in txin: + txinputtype.amount = txin['value'] + txinputtype.prev_hash = prev_hash + txinputtype.prev_index = prev_index + + if txin.get('scriptSig') is not None: + script_sig = bfh(txin['scriptSig']) + txinputtype.script_sig = script_sig + + txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) + + inputs.append(txinputtype) + + return inputs + + def tx_outputs(self, derivation, tx, segwit=False): + + def create_output_by_derivation(info): + index, xpubs, m = info + if len(xpubs) == 1: + script_type = self.types.PAYTOP2SHWITNESS if segwit else self.types.PAYTOADDRESS + address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) + txoutputtype = self.types.TxOutputType( + amount=amount, + script_type=script_type, + address_n=address_n, + ) + else: + script_type = self.types.PAYTOP2SHWITNESS if segwit else self.types.PAYTOMULTISIG + address_n = self.client_class.expand_path("/%d/%d" % index) + nodes = map(self.ckd_public.deserialize, xpubs) + pubkeys = [self.types.HDNodePathType(node=node, address_n=address_n) for node in nodes] + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=[b''] * len(pubkeys), + m=m) + txoutputtype = self.types.TxOutputType( + multisig=multisig, + amount=amount, + address_n=self.client_class.expand_path(derivation + "/%d/%d" % index), + script_type=script_type) + return txoutputtype + + def create_output_by_address(): + txoutputtype = self.types.TxOutputType() + txoutputtype.amount = amount + if _type == TYPE_SCRIPT: + txoutputtype.script_type = self.types.PAYTOOPRETURN + txoutputtype.op_return_data = address[2:] + elif _type == TYPE_ADDRESS: + if is_segwit_address(address): + txoutputtype.script_type = self.types.PAYTOWITNESS + else: + addrtype, hash_160 = b58_address_to_hash160(address) + if addrtype == constants.net.ADDRTYPE_P2PKH: + txoutputtype.script_type = self.types.PAYTOADDRESS + elif addrtype == constants.net.ADDRTYPE_P2SH: + txoutputtype.script_type = self.types.PAYTOSCRIPTHASH + else: + raise Exception('addrtype: ' + str(addrtype)) + txoutputtype.address = address + return txoutputtype + + outputs = [] + has_change = False + any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) + + for _type, address, amount in tx.outputs(): + use_create_by_derivation = False + + info = tx.output_info.get(address) + if info is not None and not has_change: + index, xpubs, m = info + on_change_branch = index[0] == 1 + # prioritise hiding outputs on the 'change' branch from user + # because no more than one change address allowed + if on_change_branch == any_output_on_change_branch: + use_create_by_derivation = True + has_change = True + + if use_create_by_derivation: + txoutputtype = create_output_by_derivation(info) + else: + txoutputtype = create_output_by_address() + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = self.types.TransactionType() + d = deserialize(tx.raw) + t.version = d['version'] + t.lock_time = d['lockTime'] + inputs = self.tx_inputs(tx) + t.inputs.extend(inputs) + for vout in d['outputs']: + o = t.bin_outputs.add() + o.amount = vout['value'] + o.script_pubkey = bfh(vout['scriptPubKey']) + return t + + # This function is called from the TREZOR libraries (via tx_api) + def get_tx(self, tx_hash): + tx = self.prev_tx[tx_hash] + return self.electrum_tx_to_txtype(tx) diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py @@ -0,0 +1,586 @@ +from functools import partial +import threading + +from PyQt5.Qt import Qt +from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton +from PyQt5.Qt import QVBoxLayout, QLabel + +from electrum.gui.qt.util import * +from electrum.i18n import _ +from electrum.plugin import hook, DeviceMgr +from electrum.util import PrintError, UserCancelled, bh2u +from electrum.wallet import Wallet, Standard_Wallet + +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase +from .keepkey import KeepKeyPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC + + +PASSPHRASE_HELP_SHORT =_( + "Passphrases allow you to access new wallets, each " + "hidden behind a particular case-sensitive passphrase.") +PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _( + "You need to create a separate Electrum wallet for each passphrase " + "you use as they each generate different addresses. Changing " + "your passphrase does not lose other wallets, each is still " + "accessible behind its own passphrase.") +RECOMMEND_PIN = _( + "You should enable PIN protection. Your PIN is the only protection " + "for your bitcoins if your device is lost or stolen.") +PASSPHRASE_NOT_PIN = _( + "If you forget a passphrase you will be unable to access any " + "bitcoins in the wallet behind it. A passphrase is not a PIN. " + "Only change this if you are sure you understand it.") +CHARACTER_RECOVERY = ( + "Use the recovery cipher shown on your device to input your seed words. " + "The cipher changes with every keypress.\n" + "After at most 4 letters the device will auto-complete a word.\n" + "Press SPACE or the Accept Word button to accept the device's auto-" + "completed word and advance to the next one.\n" + "Press BACKSPACE to go back a character or word.\n" + "Press ENTER or the Seed Entered button once the last word in your " + "seed is auto-completed.") + +class CharacterButton(QPushButton): + def __init__(self, text=None): + QPushButton.__init__(self, text) + + def keyPressEvent(self, event): + event.setAccepted(False) # Pass through Enter and Space keys + + +class CharacterDialog(WindowModalDialog): + + def __init__(self, parent): + super(CharacterDialog, self).__init__(parent) + self.setWindowTitle(_("KeepKey Seed Recovery")) + self.character_pos = 0 + self.word_pos = 0 + self.loop = QEventLoop() + self.word_help = QLabel() + self.char_buttons = [] + + vbox = QVBoxLayout(self) + vbox.addWidget(WWLabel(CHARACTER_RECOVERY)) + hbox = QHBoxLayout() + hbox.addWidget(self.word_help) + for i in range(4): + char_button = CharacterButton('*') + char_button.setMaximumWidth(36) + self.char_buttons.append(char_button) + hbox.addWidget(char_button) + self.accept_button = CharacterButton(_("Accept Word")) + self.accept_button.clicked.connect(partial(self.process_key, 32)) + self.rejected.connect(partial(self.loop.exit, 1)) + hbox.addWidget(self.accept_button) + hbox.addStretch(1) + vbox.addLayout(hbox) + + self.finished_button = QPushButton(_("Seed Entered")) + self.cancel_button = QPushButton(_("Cancel")) + self.finished_button.clicked.connect(partial(self.process_key, + Qt.Key_Return)) + self.cancel_button.clicked.connect(self.rejected) + buttons = Buttons(self.finished_button, self.cancel_button) + vbox.addSpacing(40) + vbox.addLayout(buttons) + self.refresh() + self.show() + + def refresh(self): + self.word_help.setText("Enter seed word %2d:" % (self.word_pos + 1)) + self.accept_button.setEnabled(self.character_pos >= 3) + self.finished_button.setEnabled((self.word_pos in (11, 17, 23) + and self.character_pos >= 3)) + for n, button in enumerate(self.char_buttons): + button.setEnabled(n == self.character_pos) + if n == self.character_pos: + button.setFocus() + + def is_valid_alpha_space(self, key): + # Auto-completion requires at least 3 characters + if key == ord(' ') and self.character_pos >= 3: + return True + # Firmware aborts protocol if the 5th character is non-space + if self.character_pos >= 4: + return False + return (key >= ord('a') and key <= ord('z') + or (key >= ord('A') and key <= ord('Z'))) + + def process_key(self, key): + self.data = None + if key == Qt.Key_Return and self.finished_button.isEnabled(): + self.data = {'done': True} + elif key == Qt.Key_Backspace and (self.word_pos or self.character_pos): + self.data = {'delete': True} + elif self.is_valid_alpha_space(key): + self.data = {'character': chr(key).lower()} + if self.data: + self.loop.exit(0) + + def keyPressEvent(self, event): + self.process_key(event.key()) + if not self.data: + QDialog.keyPressEvent(self, event) + + def get_char(self, word_pos, character_pos): + self.word_pos = word_pos + self.character_pos = character_pos + self.refresh() + if self.loop.exec_(): + self.data = None # User cancelled + + +class QtHandler(QtHandlerBase): + + char_signal = pyqtSignal(object) + pin_signal = pyqtSignal(object) + close_char_dialog_signal = pyqtSignal() + + def __init__(self, win, pin_matrix_widget_class, device): + super(QtHandler, self).__init__(win, device) + self.char_signal.connect(self.update_character_dialog) + self.pin_signal.connect(self.pin_dialog) + self.close_char_dialog_signal.connect(self._close_char_dialog) + self.pin_matrix_widget_class = pin_matrix_widget_class + self.character_dialog = None + + def get_char(self, msg): + self.done.clear() + self.char_signal.emit(msg) + self.done.wait() + data = self.character_dialog.data + if not data or 'done' in data: + self.close_char_dialog_signal.emit() + return data + + def _close_char_dialog(self): + if self.character_dialog: + self.character_dialog.accept() + self.character_dialog = None + + def get_pin(self, msg): + self.done.clear() + self.pin_signal.emit(msg) + self.done.wait() + return self.response + + def pin_dialog(self, msg): + # Needed e.g. when resetting a device + self.clear_dialog() + dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN")) + matrix = self.pin_matrix_widget_class() + vbox = QVBoxLayout() + vbox.addWidget(QLabel(msg)) + vbox.addWidget(matrix) + vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) + dialog.setLayout(vbox) + dialog.exec_() + self.response = str(matrix.get_value()) + self.done.set() + + def update_character_dialog(self, msg): + if not self.character_dialog: + self.character_dialog = CharacterDialog(self.top_level_window()) + self.character_dialog.get_char(msg.word_pos, msg.character_pos) + self.done.set() + + + +class QtPlugin(QtPluginBase): + # Derived classes must provide the following class-static variables: + # icon_file + # pin_matrix_widget_class + + def create_handler(self, window): + return QtHandler(window, self.pin_matrix_widget_class(), self.device) + + @hook + def receive_menu(self, menu, addrs, wallet): + if type(wallet) is not Standard_Wallet: + return + keystore = wallet.get_keystore() + if type(keystore) == self.keystore_class and len(addrs) == 1: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0])) + menu.addAction(_("Show on {}").format(self.device), show_address) + + def show_settings_dialog(self, window, keystore): + device_id = self.choose_device(window, keystore) + if device_id: + SettingsDialog(window, self, keystore, device_id).exec_() + + def request_trezor_init_settings(self, wizard, method, device): + vbox = QVBoxLayout() + next_enabled = True + label = QLabel(_("Enter a label to name your device:")) + name = QLineEdit() + hl = QHBoxLayout() + hl.addWidget(label) + hl.addWidget(name) + hl.addStretch(1) + vbox.addLayout(hl) + + def clean_text(widget): + text = widget.toPlainText().strip() + return ' '.join(text.split()) + + if method in [TIM_NEW, TIM_RECOVER]: + gb = QGroupBox() + hbox1 = QHBoxLayout() + gb.setLayout(hbox1) + # KeepKey recovery doesn't need a word count + if method == TIM_NEW: + vbox.addWidget(gb) + gb.setTitle(_("Select your seed length:")) + bg = QButtonGroup() + for i, count in enumerate([12, 18, 24]): + rb = QRadioButton(gb) + rb.setText(_("{} words").format(count)) + bg.addButton(rb) + bg.setId(rb, i) + hbox1.addWidget(rb) + rb.setChecked(True) + cb_pin = QCheckBox(_('Enable PIN protection')) + cb_pin.setChecked(True) + else: + text = QTextEdit() + text.setMaximumHeight(60) + if method == TIM_MNEMONIC: + msg = _("Enter your BIP39 mnemonic:") + else: + msg = _("Enter the master private key beginning with xprv:") + def set_enabled(): + from keystore import is_xprv + wizard.next_button.setEnabled(is_xprv(clean_text(text))) + text.textChanged.connect(set_enabled) + next_enabled = False + + vbox.addWidget(QLabel(msg)) + vbox.addWidget(text) + pin = QLineEdit() + pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) + pin.setMaximumWidth(100) + hbox_pin = QHBoxLayout() + hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) + hbox_pin.addWidget(pin) + hbox_pin.addStretch(1) + + if method in [TIM_NEW, TIM_RECOVER]: + vbox.addWidget(WWLabel(RECOMMEND_PIN)) + vbox.addWidget(cb_pin) + else: + vbox.addLayout(hbox_pin) + + passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + cb_phrase = QCheckBox(_('Enable passphrases')) + cb_phrase.setChecked(False) + vbox.addWidget(passphrase_msg) + vbox.addWidget(passphrase_warning) + vbox.addWidget(cb_phrase) + + wizard.exec_layout(vbox, next_enabled=next_enabled) + + if method in [TIM_NEW, TIM_RECOVER]: + item = bg.checkedId() + pin = cb_pin.isChecked() + else: + item = ' '.join(str(clean_text(text)).split()) + pin = str(pin.text()) + + return (item, name.text(), pin, cb_phrase.isChecked()) + + +class Plugin(KeepKeyPlugin, QtPlugin): + icon_paired = ":icons/keepkey.png" + icon_unpaired = ":icons/keepkey_unpaired.png" + + @classmethod + def pin_matrix_widget_class(self): + from keepkeylib.qt.pinmatrix import PinMatrixWidget + return PinMatrixWidget + + +class SettingsDialog(WindowModalDialog): + '''This dialog doesn't require a device be paired with a wallet. + We want users to be able to wipe a device even if they've forgotten + their PIN.''' + + def __init__(self, window, plugin, keystore, device_id): + title = _("{} Settings").format(plugin.device) + super(SettingsDialog, self).__init__(window, title) + self.setMaximumWidth(540) + + devmgr = plugin.device_manager() + config = devmgr.config + handler = keystore.handler + thread = keystore.thread + hs_rows, hs_cols = (64, 128) + + def invoke_client(method, *args, **kw_args): + unpair_after = kw_args.pop('unpair_after', False) + + def task(): + client = devmgr.client_by_id(device_id) + if not client: + raise RuntimeError("Device not connected") + if method: + getattr(client, method)(*args, **kw_args) + if unpair_after: + devmgr.unpair_id(device_id) + return client.features + + thread.add(task, on_success=update) + + def update(features): + self.features = features + set_label_enabled() + bl_hash = bh2u(features.bootloader_hash) + bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) + noyes = [_("No"), _("Yes")] + endis = [_("Enable Passphrases"), _("Disable Passphrases")] + disen = [_("Disabled"), _("Enabled")] + setchange = [_("Set a PIN"), _("Change PIN")] + + version = "%d.%d.%d" % (features.major_version, + features.minor_version, + features.patch_version) + coins = ", ".join(coin.coin_name for coin in features.coins) + + device_label.setText(features.label) + pin_set_label.setText(noyes[features.pin_protection]) + passphrases_label.setText(disen[features.passphrase_protection]) + bl_hash_label.setText(bl_hash) + label_edit.setText(features.label) + device_id_label.setText(features.device_id) + initialized_label.setText(noyes[features.initialized]) + version_label.setText(version) + coins_label.setText(coins) + clear_pin_button.setVisible(features.pin_protection) + clear_pin_warning.setVisible(features.pin_protection) + pin_button.setText(setchange[features.pin_protection]) + pin_msg.setVisible(not features.pin_protection) + passphrase_button.setText(endis[features.passphrase_protection]) + language_label.setText(features.language) + + def set_label_enabled(): + label_apply.setEnabled(label_edit.text() != self.features.label) + + def rename(): + invoke_client('change_label', label_edit.text()) + + def toggle_passphrase(): + title = _("Confirm Toggle Passphrase Protection") + currently_enabled = self.features.passphrase_protection + if currently_enabled: + msg = _("After disabling passphrases, you can only pair this " + "Electrum wallet if it had an empty passphrase. " + "If its passphrase was not empty, you will need to " + "create a new wallet with the install wizard. You " + "can use this wallet again at any time by re-enabling " + "passphrases and entering its passphrase.") + else: + msg = _("Your current Electrum wallet can only be used with " + "an empty passphrase. You must create a separate " + "wallet with the install wizard for other passphrases " + "as each one generates a new set of addresses.") + msg += "\n\n" + _("Are you sure you want to proceed?") + if not self.question(msg, title=title): + return + invoke_client('toggle_passphrase', unpair_after=currently_enabled) + + def change_homescreen(): + from PIL import Image # FIXME + dialog = QFileDialog(self, _("Choose Homescreen")) + filename, __ = dialog.getOpenFileName() + if filename: + im = Image.open(str(filename)) + if im.size != (hs_cols, hs_rows): + raise Exception('Image must be 64 x 128 pixels') + im = im.convert('1') + pix = im.load() + img = '' + for j in range(hs_rows): + for i in range(hs_cols): + img += '1' if pix[i, j] else '0' + img = ''.join(chr(int(img[i:i + 8], 2)) + for i in range(0, len(img), 8)) + invoke_client('change_homescreen', img) + + def clear_homescreen(): + invoke_client('change_homescreen', '\x00') + + def set_pin(): + invoke_client('set_pin', remove=False) + + def clear_pin(): + invoke_client('set_pin', remove=True) + + def wipe_device(): + wallet = window.wallet + if wallet and sum(wallet.get_balance()): + title = _("Confirm Device Wipe") + msg = _("Are you SURE you want to wipe the device?\n" + "Your wallet still has bitcoins in it!") + if not self.question(msg, title=title, + icon=QMessageBox.Critical): + return + invoke_client('wipe_device', unpair_after=True) + + def slider_moved(): + mins = timeout_slider.sliderPosition() + timeout_minutes.setText(_("%2d minutes") % mins) + + def slider_released(): + config.set_session_timeout(timeout_slider.sliderPosition() * 60) + + # Information tab + info_tab = QWidget() + info_layout = QVBoxLayout(info_tab) + info_glayout = QGridLayout() + info_glayout.setColumnStretch(2, 1) + device_label = QLabel() + pin_set_label = QLabel() + passphrases_label = QLabel() + version_label = QLabel() + device_id_label = QLabel() + bl_hash_label = QLabel() + bl_hash_label.setWordWrap(True) + coins_label = QLabel() + coins_label.setWordWrap(True) + language_label = QLabel() + initialized_label = QLabel() + rows = [ + (_("Device Label"), device_label), + (_("PIN set"), pin_set_label), + (_("Passphrases"), passphrases_label), + (_("Firmware Version"), version_label), + (_("Device ID"), device_id_label), + (_("Bootloader Hash"), bl_hash_label), + (_("Supported Coins"), coins_label), + (_("Language"), language_label), + (_("Initialized"), initialized_label), + ] + for row_num, (label, widget) in enumerate(rows): + info_glayout.addWidget(QLabel(label), row_num, 0) + info_glayout.addWidget(widget, row_num, 1) + info_layout.addLayout(info_glayout) + + # Settings tab + settings_tab = QWidget() + settings_layout = QVBoxLayout(settings_tab) + settings_glayout = QGridLayout() + + # Settings tab - Label + label_msg = QLabel(_("Name this {}. If you have multiple devices " + "their labels help distinguish them.") + .format(plugin.device)) + label_msg.setWordWrap(True) + label_label = QLabel(_("Device Label")) + label_edit = QLineEdit() + label_edit.setMinimumWidth(150) + label_edit.setMaxLength(plugin.MAX_LABEL_LEN) + label_apply = QPushButton(_("Apply")) + label_apply.clicked.connect(rename) + label_edit.textChanged.connect(set_label_enabled) + settings_glayout.addWidget(label_label, 0, 0) + settings_glayout.addWidget(label_edit, 0, 1, 1, 2) + settings_glayout.addWidget(label_apply, 0, 3) + settings_glayout.addWidget(label_msg, 1, 1, 1, -1) + + # Settings tab - PIN + pin_label = QLabel(_("PIN Protection")) + pin_button = QPushButton() + pin_button.clicked.connect(set_pin) + settings_glayout.addWidget(pin_label, 2, 0) + settings_glayout.addWidget(pin_button, 2, 1) + pin_msg = QLabel(_("PIN protection is strongly recommended. " + "A PIN is your only protection against someone " + "stealing your bitcoins if they obtain physical " + "access to your {}.").format(plugin.device)) + pin_msg.setWordWrap(True) + pin_msg.setStyleSheet("color: red") + settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) + + # Settings tab - Session Timeout + timeout_label = QLabel(_("Session Timeout")) + timeout_minutes = QLabel() + timeout_slider = QSlider(Qt.Horizontal) + timeout_slider.setRange(1, 60) + timeout_slider.setSingleStep(1) + timeout_slider.setTickInterval(5) + timeout_slider.setTickPosition(QSlider.TicksBelow) + timeout_slider.setTracking(True) + timeout_msg = QLabel( + _("Clear the session after the specified period " + "of inactivity. Once a session has timed out, " + "your PIN and passphrase (if enabled) must be " + "re-entered to use the device.")) + timeout_msg.setWordWrap(True) + timeout_slider.setSliderPosition(config.get_session_timeout() // 60) + slider_moved() + timeout_slider.valueChanged.connect(slider_moved) + timeout_slider.sliderReleased.connect(slider_released) + settings_glayout.addWidget(timeout_label, 6, 0) + settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3) + settings_glayout.addWidget(timeout_minutes, 6, 4) + settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1) + settings_layout.addLayout(settings_glayout) + settings_layout.addStretch(1) + + # Advanced tab + advanced_tab = QWidget() + advanced_layout = QVBoxLayout(advanced_tab) + advanced_glayout = QGridLayout() + + # Advanced tab - clear PIN + clear_pin_button = QPushButton(_("Disable PIN")) + clear_pin_button.clicked.connect(clear_pin) + clear_pin_warning = QLabel( + _("If you disable your PIN, anyone with physical access to your " + "{} device can spend your bitcoins.").format(plugin.device)) + clear_pin_warning.setWordWrap(True) + clear_pin_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(clear_pin_button, 0, 2) + advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5) + + # Advanced tab - toggle passphrase protection + passphrase_button = QPushButton() + passphrase_button.clicked.connect(toggle_passphrase) + passphrase_msg = WWLabel(PASSPHRASE_HELP) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(passphrase_button, 3, 2) + advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5) + advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5) + + # Advanced tab - wipe device + wipe_device_button = QPushButton(_("Wipe Device")) + wipe_device_button.clicked.connect(wipe_device) + wipe_device_msg = QLabel( + _("Wipe the device, removing all data from it. The firmware " + "is left unchanged.")) + wipe_device_msg.setWordWrap(True) + wipe_device_warning = QLabel( + _("Only wipe a device if you have the recovery seed written down " + "and the device wallet(s) are empty, otherwise the bitcoins " + "will be lost forever.")) + wipe_device_warning.setWordWrap(True) + wipe_device_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(wipe_device_button, 6, 2) + advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5) + advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5) + advanced_layout.addLayout(advanced_glayout) + advanced_layout.addStretch(1) + + tabs = QTabWidget(self) + tabs.addTab(info_tab, _("Information")) + tabs.addTab(settings_tab, _("Settings")) + tabs.addTab(advanced_tab, _("Advanced")) + dialog_vbox = QVBoxLayout(self) + dialog_vbox.addWidget(tabs) + dialog_vbox.addLayout(Buttons(CloseButton(self))) + + # Update information + invoke_client(None) diff --git a/electrum/plugins/labels/__init__.py b/electrum/plugins/labels/__init__.py @@ -0,0 +1,9 @@ +from electrum.i18n import _ + +fullname = _('LabelSync') +description = ' '.join([ + _("Save your wallet labels on a remote server, and synchronize them across multiple devices where you use Electrum."), + _("Labels, transactions IDs and addresses are encrypted before they are sent to the remote server.") +]) +available_for = ['qt', 'kivy', 'cmdline'] + diff --git a/electrum/plugins/labels/cmdline.py b/electrum/plugins/labels/cmdline.py @@ -0,0 +1,11 @@ +from .labels import LabelsPlugin +from electrum.plugin import hook + +class Plugin(LabelsPlugin): + + @hook + def load_wallet(self, wallet, window): + self.start_wallet(wallet) + + def on_pulled(self, wallet): + self.print_error('labels pulled from server') diff --git a/electrum/plugins/labels/kivy.py b/electrum/plugins/labels/kivy.py @@ -0,0 +1,14 @@ +from .labels import LabelsPlugin +from electrum.plugin import hook + +class Plugin(LabelsPlugin): + + @hook + def load_wallet(self, wallet, window): + self.window = window + self.start_wallet(wallet) + + def on_pulled(self, wallet): + self.print_error('on pulled') + self.window._trigger_update_history() + diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py @@ -0,0 +1,167 @@ +import hashlib +import requests +import threading +import json +import sys +import traceback + +import base64 + +from electrum.plugin import BasePlugin, hook +from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv +from electrum.i18n import _ + + +class LabelsPlugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.target_host = 'labels.electrum.org' + self.wallets = {} + + def encode(self, wallet, msg): + password, iv, wallet_id = self.wallets[wallet] + encrypted = aes_encrypt_with_iv(password, iv, + msg.encode('utf8')) + return base64.b64encode(encrypted).decode() + + def decode(self, wallet, message): + password, iv, wallet_id = self.wallets[wallet] + decoded = base64.b64decode(message) + decrypted = aes_decrypt_with_iv(password, iv, decoded) + return decrypted.decode('utf8') + + def get_nonce(self, wallet): + # nonce is the nonce to be used with the next change + nonce = wallet.storage.get('wallet_nonce') + if nonce is None: + nonce = 1 + self.set_nonce(wallet, nonce) + return nonce + + def set_nonce(self, wallet, nonce): + self.print_error("set", wallet.basename(), "nonce to", nonce) + wallet.storage.put("wallet_nonce", nonce) + + @hook + def set_label(self, wallet, item, label): + if wallet not in self.wallets: + return + if not item: + return + nonce = self.get_nonce(wallet) + wallet_id = self.wallets[wallet][2] + bundle = {"walletId": wallet_id, + "walletNonce": nonce, + "externalId": self.encode(wallet, item), + "encryptedLabel": self.encode(wallet, label)} + t = threading.Thread(target=self.do_request_safe, + args=["POST", "/label", False, bundle]) + t.setDaemon(True) + t.start() + # Caller will write the wallet + self.set_nonce(wallet, nonce + 1) + + def do_request(self, method, url = "/labels", is_batch=False, data=None): + url = 'https://' + self.target_host + url + kwargs = {'headers': {}} + if method == 'GET' and data: + kwargs['params'] = data + elif method == 'POST' and data: + kwargs['data'] = json.dumps(data) + kwargs['headers']['Content-Type'] = 'application/json' + response = requests.request(method, url, **kwargs) + if response.status_code != 200: + raise Exception(response.status_code, response.text) + response = response.json() + if "error" in response: + raise Exception(response["error"]) + return response + + def do_request_safe(self, *args, **kwargs): + try: + self.do_request(*args, **kwargs) + except BaseException as e: + #traceback.print_exc(file=sys.stderr) + self.print_error('error doing request') + + def push_thread(self, wallet): + wallet_data = self.wallets.get(wallet, None) + if not wallet_data: + raise Exception('Wallet {} not loaded'.format(wallet)) + wallet_id = wallet_data[2] + bundle = {"labels": [], + "walletId": wallet_id, + "walletNonce": self.get_nonce(wallet)} + for key, value in wallet.labels.items(): + try: + encoded_key = self.encode(wallet, key) + encoded_value = self.encode(wallet, value) + except: + self.print_error('cannot encode', repr(key), repr(value)) + continue + bundle["labels"].append({'encryptedLabel': encoded_value, + 'externalId': encoded_key}) + self.do_request("POST", "/labels", True, bundle) + + def pull_thread(self, wallet, force): + wallet_data = self.wallets.get(wallet, None) + if not wallet_data: + raise Exception('Wallet {} not loaded'.format(wallet)) + wallet_id = wallet_data[2] + nonce = 1 if force else self.get_nonce(wallet) - 1 + self.print_error("asking for labels since nonce", nonce) + response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) )) + if response["labels"] is None: + self.print_error('no new labels') + return + result = {} + for label in response["labels"]: + try: + key = self.decode(wallet, label["externalId"]) + value = self.decode(wallet, label["encryptedLabel"]) + except: + continue + try: + json.dumps(key) + json.dumps(value) + except: + self.print_error('error: no json', key) + continue + result[key] = value + + for key, value in result.items(): + if force or not wallet.labels.get(key): + wallet.labels[key] = value + + self.print_error("received %d labels" % len(response)) + # do not write to disk because we're in a daemon thread + wallet.storage.put('labels', wallet.labels) + self.set_nonce(wallet, response["nonce"] + 1) + self.on_pulled(wallet) + + def pull_thread_safe(self, wallet, force): + try: + self.pull_thread(wallet, force) + except BaseException as e: + # traceback.print_exc(file=sys.stderr) + self.print_error('could not retrieve labels') + + def start_wallet(self, wallet): + nonce = self.get_nonce(wallet) + self.print_error("wallet", wallet.basename(), "nonce is", nonce) + mpk = wallet.get_fingerprint() + if not mpk: + return + mpk = mpk.encode('ascii') + password = hashlib.sha1(mpk).hexdigest()[:32].encode('ascii') + iv = hashlib.sha256(password).digest()[:16] + wallet_id = hashlib.sha256(mpk).hexdigest() + self.wallets[wallet] = (password, iv, wallet_id) + # If there is an auth token we can try to actually start syncing + t = threading.Thread(target=self.pull_thread_safe, args=(wallet, False)) + t.setDaemon(True) + t.start() + + def stop_wallet(self, wallet): + self.wallets.pop(wallet, None) diff --git a/electrum/plugins/labels/qt.py b/electrum/plugins/labels/qt.py @@ -0,0 +1,78 @@ +from functools import partial +import traceback +import sys + +from PyQt5.QtGui import * +from PyQt5.QtCore import * +from PyQt5.QtWidgets import (QHBoxLayout, QLabel, QVBoxLayout) + +from electrum.plugin import hook +from electrum.i18n import _ +from electrum.gui.qt import EnterButton +from electrum.gui.qt.util import ThreadedButton, Buttons +from electrum.gui.qt.util import WindowModalDialog, OkButton + +from .labels import LabelsPlugin + + +class QLabelsSignalObject(QObject): + labels_changed_signal = pyqtSignal(object) + + +class Plugin(LabelsPlugin): + + def __init__(self, *args): + LabelsPlugin.__init__(self, *args) + self.obj = QLabelsSignalObject() + + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Settings'), + partial(self.settings_dialog, window)) + + def settings_dialog(self, window): + wallet = window.parent().wallet + d = WindowModalDialog(window, _("Label Settings")) + hbox = QHBoxLayout() + hbox.addWidget(QLabel("Label sync options:")) + upload = ThreadedButton("Force upload", + partial(self.push_thread, wallet), + partial(self.done_processing_success, d), + partial(self.done_processing_error, d)) + download = ThreadedButton("Force download", + partial(self.pull_thread, wallet, True), + partial(self.done_processing_success, d), + partial(self.done_processing_error, d)) + vbox = QVBoxLayout() + vbox.addWidget(upload) + vbox.addWidget(download) + hbox.addLayout(vbox) + vbox = QVBoxLayout(d) + vbox.addLayout(hbox) + vbox.addSpacing(20) + vbox.addLayout(Buttons(OkButton(d))) + return bool(d.exec_()) + + def on_pulled(self, wallet): + self.obj.labels_changed_signal.emit(wallet) + + def done_processing_success(self, dialog, result): + dialog.show_message(_("Your labels have been synchronised.")) + + def done_processing_error(self, dialog, result): + traceback.print_exception(*result, file=sys.stderr) + dialog.show_error(_("Error synchronising labels") + ':\n' + str(result[:2])) + + @hook + def load_wallet(self, wallet, window): + # FIXME if the user just enabled the plugin, this hook won't be called + # as the wallet is already loaded, and hence the plugin will be in + # a non-functional state for that window + self.obj.labels_changed_signal.connect(window.update_tabs) + self.start_wallet(wallet) + + @hook + def on_close_window(self, window): + self.stop_wallet(window.wallet) diff --git a/electrum/plugins/ledger/__init__.py b/electrum/plugins/ledger/__init__.py @@ -0,0 +1,7 @@ +from electrum.i18n import _ + +fullname = 'Ledger Wallet' +description = 'Provides support for Ledger hardware wallet' +requires = [('btchip', 'github.com/ledgerhq/btchip-python')] +registers_keystore = ('hardware', 'ledger', _("Ledger wallet")) +available_for = ['qt', 'cmdline'] diff --git a/electrum/plugins/ledger/auth2fa.py b/electrum/plugins/ledger/auth2fa.py @@ -0,0 +1,358 @@ +import os +import hashlib +import logging +import json +import copy +from binascii import hexlify, unhexlify + +import websocket + +from PyQt5.Qt import QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel +import PyQt5.QtCore as QtCore +from PyQt5.QtWidgets import * + +from btchip.btchip import * + +from electrum.i18n import _ +from electrum.util import print_msg +from electrum import constants, bitcoin +from electrum.gui.qt.qrcodewidget import QRCodeWidget +from electrum.gui.qt.util import * + + +DEBUG = False + +helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code.<br><br>" \ + "For best security you should unplug your device, open a text editor on another computer, " \ + "put your cursor into it, and plug your device into that computer. " \ + "It will output a summary of the transaction being signed and a one-time PIN.<br><br>" \ + "Verify the transaction summary and type the PIN code here.<br><br>" \ + "Before pressing enter, plug the device back into this computer.<br>" ), + _("Verify the address below.<br>Type the character from your security card corresponding to the <u><b>BOLD</b></u> character."), + _("Waiting for authentication on your mobile phone"), + _("Transaction accepted by mobile phone. Waiting for confirmation."), + _("Click Pair button to begin pairing a mobile phone."), + _("Scan this QR code with your Ledger Wallet phone app to pair it with this Ledger device.<br>" + "To complete pairing you will need your security card to answer a challenge." ) + ] + +class LedgerAuthDialog(QDialog): + def __init__(self, handler, data): + '''Ask user for 2nd factor authentication. Support text, security card and paired mobile methods. + Use last method from settings, but support new pairing and downgrade. + ''' + QDialog.__init__(self, handler.top_level_window()) + self.handler = handler + self.txdata = data + self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else '' + self.setMinimumWidth(650) + self.setWindowTitle(_("Ledger Wallet Authentication")) + self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg) + self.dongle = self.handler.win.wallet.get_keystore().get_client().dongle + self.ws = None + self.pin = '' + + self.devmode = self.getDevice2FAMode() + if self.devmode == 0x11 or self.txdata['confirmationType'] == 1: + self.cfg['mode'] = 0 + + vbox = QVBoxLayout() + self.setLayout(vbox) + + def on_change_mode(idx): + if idx < 2 and self.ws: + self.ws.stop() + self.ws = None + self.cfg['mode'] = 0 if self.devmode == 0x11 else idx if idx > 0 else 1 + if self.cfg['mode'] > 1 and self.cfg['pair'] and not self.ws: + self.req_validation() + if self.cfg['mode'] > 0: + self.handler.win.wallet.get_keystore().cfg = self.cfg + self.handler.win.wallet.save_keystore() + self.update_dlg() + def add_pairing(): + self.do_pairing() + def return_pin(): + self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text() + if self.cfg['mode'] == 1: + self.pin = ''.join(chr(int(str(i),16)) for i in self.pin) + self.accept() + + self.modebox = QWidget() + modelayout = QHBoxLayout() + self.modebox.setLayout(modelayout) + modelayout.addWidget(QLabel(_("Method:"))) + self.modes = QComboBox() + modelayout.addWidget(self.modes, 2) + self.addPair = QPushButton(_("Pair")) + self.addPair.setMaximumWidth(60) + modelayout.addWidget(self.addPair) + modelayout.addStretch(1) + self.modebox.setMaximumHeight(50) + vbox.addWidget(self.modebox) + + self.populate_modes() + self.modes.currentIndexChanged.connect(on_change_mode) + self.addPair.clicked.connect(add_pairing) + + self.helpmsg = QTextEdit() + self.helpmsg.setStyleSheet("QTextEdit { background-color: lightgray; }") + self.helpmsg.setReadOnly(True) + vbox.addWidget(self.helpmsg) + + self.pinbox = QWidget() + pinlayout = QHBoxLayout() + self.pinbox.setLayout(pinlayout) + self.pintxt = QLineEdit() + self.pintxt.setEchoMode(2) + self.pintxt.setMaxLength(4) + self.pintxt.returnPressed.connect(return_pin) + pinlayout.addWidget(QLabel(_("Enter PIN:"))) + pinlayout.addWidget(self.pintxt) + pinlayout.addWidget(QLabel(_("NOT DEVICE PIN - see above"))) + pinlayout.addStretch(1) + self.pinbox.setVisible(self.cfg['mode'] == 0) + vbox.addWidget(self.pinbox) + + self.cardbox = QWidget() + card = QVBoxLayout() + self.cardbox.setLayout(card) + self.addrtext = QTextEdit() + self.addrtext.setStyleSheet("QTextEdit { color:blue; background-color:lightgray; padding:15px 10px; border:none; font-size:20pt; font-family:monospace; }") + self.addrtext.setReadOnly(True) + self.addrtext.setMaximumHeight(130) + card.addWidget(self.addrtext) + + def pin_changed(s): + if len(s) < len(self.idxs): + i = self.idxs[len(s)] + addr = self.txdata['address'] + if not constants.net.TESTNET: + text = addr[:i] + '<u><b>' + addr[i:i+1] + '</u></b>' + addr[i+1:] + else: + # pin needs to be created from mainnet address + addr_mainnet = bitcoin.script_to_address(bitcoin.address_to_script(addr), net=constants.BitcoinMainnet) + addr_mainnet = addr_mainnet[:i] + '<u><b>' + addr_mainnet[i:i+1] + '</u></b>' + addr_mainnet[i+1:] + text = str(addr) + '\n' + str(addr_mainnet) + self.addrtext.setHtml(str(text)) + else: + self.addrtext.setHtml(_("Press Enter")) + + pin_changed('') + cardpin = QHBoxLayout() + cardpin.addWidget(QLabel(_("Enter PIN:"))) + self.cardtxt = QLineEdit() + self.cardtxt.setEchoMode(2) + self.cardtxt.setMaxLength(len(self.idxs)) + self.cardtxt.textChanged.connect(pin_changed) + self.cardtxt.returnPressed.connect(return_pin) + cardpin.addWidget(self.cardtxt) + cardpin.addWidget(QLabel(_("NOT DEVICE PIN - see above"))) + cardpin.addStretch(1) + card.addLayout(cardpin) + self.cardbox.setVisible(self.cfg['mode'] == 1) + vbox.addWidget(self.cardbox) + + self.pairbox = QWidget() + pairlayout = QVBoxLayout() + self.pairbox.setLayout(pairlayout) + pairhelp = QTextEdit(helpTxt[5]) + pairhelp.setStyleSheet("QTextEdit { background-color: lightgray; }") + pairhelp.setReadOnly(True) + pairlayout.addWidget(pairhelp, 1) + self.pairqr = QRCodeWidget() + pairlayout.addWidget(self.pairqr, 4) + self.pairbox.setVisible(False) + vbox.addWidget(self.pairbox) + self.update_dlg() + + if self.cfg['mode'] > 1 and not self.ws: + self.req_validation() + + def populate_modes(self): + self.modes.blockSignals(True) + self.modes.clear() + self.modes.addItem(_("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _("Summary Text PIN is Disabled")) + if self.txdata['confirmationType'] > 1: + self.modes.addItem(_("Security Card Challenge")) + if not self.cfg['pair']: + self.modes.addItem(_("Mobile - Not paired")) + else: + self.modes.addItem(_("Mobile - {}").format(self.cfg['pair'][1])) + self.modes.blockSignals(False) + + def update_dlg(self): + self.modes.setCurrentIndex(self.cfg['mode']) + self.modebox.setVisible(True) + self.addPair.setText(_("Pair") if not self.cfg['pair'] else _("Re-Pair")) + self.addPair.setVisible(self.txdata['confirmationType'] > 2) + self.helpmsg.setText(helpTxt[self.cfg['mode'] if self.cfg['mode'] < 2 else 2 if self.cfg['pair'] else 4]) + self.helpmsg.setMinimumHeight(180 if self.txdata['confirmationType'] == 1 else 100) + self.pairbox.setVisible(False) + self.helpmsg.setVisible(True) + self.pinbox.setVisible(self.cfg['mode'] == 0) + self.cardbox.setVisible(self.cfg['mode'] == 1) + self.pintxt.setFocus(True) if self.cfg['mode'] == 0 else self.cardtxt.setFocus(True) + self.setMaximumHeight(400) + + def do_pairing(self): + rng = os.urandom(16) + pairID = (hexlify(rng) + hexlify(hashlib.sha256(rng).digest()[0:1])).decode('utf-8') + self.pairqr.setData(pairID) + self.modebox.setVisible(False) + self.helpmsg.setVisible(False) + self.pinbox.setVisible(False) + self.cardbox.setVisible(False) + self.pairbox.setVisible(True) + self.pairqr.setMinimumSize(300,300) + if self.ws: + self.ws.stop() + self.ws = LedgerWebSocket(self, pairID) + self.ws.pairing_done.connect(self.pairing_done) + self.ws.start() + + def pairing_done(self, data): + if data is not None: + self.cfg['pair'] = [ data['pairid'], data['name'], data['platform'] ] + self.cfg['mode'] = 2 + self.handler.win.wallet.get_keystore().cfg = self.cfg + self.handler.win.wallet.save_keystore() + self.pin = 'paired' + self.accept() + + def req_validation(self): + if self.cfg['pair'] and 'secureScreenData' in self.txdata: + if self.ws: + self.ws.stop() + self.ws = LedgerWebSocket(self, self.cfg['pair'][0], self.txdata) + self.ws.req_updated.connect(self.req_updated) + self.ws.start() + + def req_updated(self, pin): + if pin == 'accepted': + self.helpmsg.setText(helpTxt[3]) + else: + self.pin = str(pin) + self.accept() + + def getDevice2FAMode(self): + apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode + try: + mode = self.dongle.exchange( bytearray(apdu) ) + return mode + except BTChipException as e: + debug_msg('Device getMode Failed') + return 0x11 + + def closeEvent(self, evnt): + debug_msg("CLOSE - Stop WS") + if self.ws: + self.ws.stop() + if self.pairbox.isVisible(): + evnt.ignore() + self.update_dlg() + +class LedgerWebSocket(QThread): + pairing_done = pyqtSignal(object) + req_updated = pyqtSignal(str) + + def __init__(self, dlg, pairID, txdata=None): + QThread.__init__(self) + self.stopping = False + self.pairID = pairID + self.txreq = '{"type":"request","second_factor_data":"' + hexlify(txdata['secureScreenData']).decode('utf-8') + '"}' if txdata else None + self.dlg = dlg + self.dongle = self.dlg.dongle + self.data = None + + #websocket.enableTrace(True) + logging.basicConfig(level=logging.INFO) + self.ws = websocket.WebSocketApp('wss://ws.ledgerwallet.com/2fa/channels', + on_message = self.on_message, on_error = self.on_error, + on_close = self.on_close, on_open = self.on_open) + + def run(self): + while not self.stopping: + self.ws.run_forever() + def stop(self): + debug_msg("WS: Stopping") + self.stopping = True + self.ws.close() + + def on_message(self, ws, msg): + data = json.loads(msg) + if data['type'] == 'identify': + debug_msg('Identify') + apdu = [0xe0, 0x12, 0x01, 0x00, 0x41] # init pairing + apdu.extend(unhexlify(data['public_key'])) + try: + challenge = self.dongle.exchange( bytearray(apdu) ) + ws.send( '{"type":"challenge","data":"%s" }' % hexlify(challenge).decode('utf-8') ) + self.data = data + except BTChipException as e: + debug_msg('Identify Failed') + + if data['type'] == 'challenge': + debug_msg('Challenge') + apdu = [0xe0, 0x12, 0x02, 0x00, 0x10] # confirm pairing + apdu.extend(unhexlify(data['data'])) + try: + self.dongle.exchange( bytearray(apdu) ) + debug_msg('Pairing Successful') + ws.send( '{"type":"pairing","is_successful":"true"}' ) + self.data['pairid'] = self.pairID + self.pairing_done.emit(self.data) + except BTChipException as e: + debug_msg('Pairing Failed') + ws.send( '{"type":"pairing","is_successful":"false"}' ) + self.pairing_done.emit(None) + ws.send( '{"type":"disconnect"}' ) + self.stopping = True + ws.close() + + if data['type'] == 'accept': + debug_msg('Accepted') + self.req_updated.emit('accepted') + if data['type'] == 'response': + debug_msg('Responded', data) + self.req_updated.emit(str(data['pin']) if data['is_accepted'] else '') + self.txreq = None + self.stopping = True + ws.close() + + if data['type'] == 'repeat': + debug_msg('Repeat') + if self.txreq: + ws.send( self.txreq ) + debug_msg("Req Sent", self.txreq) + if data['type'] == 'connect': + debug_msg('Connected') + if self.txreq: + ws.send( self.txreq ) + debug_msg("Req Sent", self.txreq) + if data['type'] == 'disconnect': + debug_msg('Disconnected') + ws.close() + + def on_error(self, ws, error): + message = getattr(error, 'strerror', '') + if not message: + message = getattr(error, 'message', '') + debug_msg("WS: %s" % message) + + def on_close(self, ws): + debug_msg("WS: ### socket closed ###") + + def on_open(self, ws): + debug_msg("WS: ### socket open ###") + debug_msg("Joining with pairing ID", self.pairID) + ws.send( '{"type":"join","room":"%s"}' % self.pairID ) + ws.send( '{"type":"repeat"}' ) + if self.txreq: + ws.send( self.txreq ) + debug_msg("Req Sent", self.txreq) + + +def debug_msg(*args): + if DEBUG: + print_msg(*args) diff --git a/electrum/plugins/ledger/cmdline.py b/electrum/plugins/ledger/cmdline.py @@ -0,0 +1,14 @@ +from electrum.plugin import hook +from .ledger import LedgerPlugin +from ..hw_wallet import CmdLineHandler + +class Plugin(LedgerPlugin): + handler = CmdLineHandler() + @hook + def init_keystore(self, keystore): + if not isinstance(keystore, self.keystore_class): + return + keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py @@ -0,0 +1,637 @@ +from struct import pack, unpack +import hashlib +import sys +import traceback + +from electrum import bitcoin +from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int +from electrum.i18n import _ +from electrum.plugin import BasePlugin +from electrum.keystore import Hardware_KeyStore +from electrum.transaction import Transaction +from electrum.wallet import Standard_Wallet +from ..hw_wallet import HW_PluginBase +from ..hw_wallet.plugin import is_any_tx_output_on_change_branch +from electrum.util import print_error, is_verbose, bfh, bh2u, versiontuple +from electrum.base_wizard import ScriptTypeNotSupported + +try: + import hid + from btchip.btchipComm import HIDDongleHIDAPI, DongleWait + from btchip.btchip import btchip + from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script + from btchip.bitcoinTransaction import bitcoinTransaction + from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware + from btchip.btchipException import BTChipException + BTCHIP = True + BTCHIP_DEBUG = is_verbose +except ImportError: + BTCHIP = False + +MSG_NEEDS_FW_UPDATE_GENERIC = _('Firmware version too old. Please update at') + \ + ' https://www.ledgerwallet.com' +MSG_NEEDS_FW_UPDATE_SEGWIT = _('Firmware version (or "Bitcoin" app) too old for Segwit support. Please update at') + \ + ' https://www.ledgerwallet.com' +MULTI_OUTPUT_SUPPORT = '1.1.4' +SEGWIT_SUPPORT = '1.1.10' +SEGWIT_SUPPORT_SPECIAL = '1.0.4' + + +def test_pin_unlocked(func): + """Function decorator to test the Ledger for being unlocked, and if not, + raise a human-readable exception. + """ + def catch_exception(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except BTChipException as e: + if e.sw == 0x6982: + raise Exception(_('Your Ledger is locked. Please unlock it.')) + else: + raise + return catch_exception + + +class Ledger_Client(): + def __init__(self, hidDevice): + self.dongleObject = btchip(hidDevice) + self.preflightDone = False + + def is_pairable(self): + return True + + def close(self): + self.dongleObject.dongle.close() + + def timeout(self, cutoff): + pass + + def is_initialized(self): + return True + + def label(self): + return "" + + def i4b(self, x): + return pack('>I', x) + + def has_usable_connection_with_device(self): + try: + self.dongleObject.getFirmwareVersion() + except BaseException: + return False + return True + + @test_pin_unlocked + def get_xpub(self, bip32_path, xtype): + self.checkDevice() + # bip32_path is of the form 44'/0'/1' + # S-L-O-W - we don't handle the fingerprint directly, so compute + # it manually from the previous node + # This only happens once so it's bearable + #self.get_client() # prompt for the PIN before displaying the dialog if necessary + #self.handler.show_message("Computing master public key") + if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit(): + raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT) + if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): + raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT) + splitPath = bip32_path.split('/') + if splitPath[0] == 'm': + splitPath = splitPath[1:] + bip32_path = bip32_path[2:] + fingerprint = 0 + if len(splitPath) > 1: + prevPath = "/".join(splitPath[0:len(splitPath) - 1]) + nodeData = self.dongleObject.getWalletPublicKey(prevPath) + publicKey = compress_public_key(nodeData['publicKey']) + h = hashlib.new('ripemd160') + h.update(hashlib.sha256(publicKey).digest()) + fingerprint = unpack(">I", h.digest()[0:4])[0] + nodeData = self.dongleObject.getWalletPublicKey(bip32_path) + publicKey = compress_public_key(nodeData['publicKey']) + depth = len(splitPath) + lastChild = splitPath[len(splitPath) - 1].split('\'') + childnum = int(lastChild[0]) if len(lastChild) == 1 else 0x80000000 | int(lastChild[0]) + xpub = bitcoin.serialize_xpub(xtype, nodeData['chainCode'], publicKey, depth, self.i4b(fingerprint), self.i4b(childnum)) + return xpub + + def has_detached_pin_support(self, client): + try: + client.getVerifyPinRemainingAttempts() + return True + except BTChipException as e: + if e.sw == 0x6d00: + return False + raise e + + def is_pin_validated(self, client): + try: + # Invalid SET OPERATION MODE to verify the PIN status + client.dongle.exchange(bytearray([0xe0, 0x26, 0x00, 0x00, 0x01, 0xAB])) + except BTChipException as e: + if (e.sw == 0x6982): + return False + if (e.sw == 0x6A80): + return True + raise e + + def supports_multi_output(self): + return self.multiOutputSupported + + def supports_segwit(self): + return self.segwitSupported + + def supports_native_segwit(self): + return self.nativeSegwitSupported + + def perform_hw1_preflight(self): + try: + firmwareInfo = self.dongleObject.getFirmwareVersion() + firmware = firmwareInfo['version'] + self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT) + self.nativeSegwitSupported = versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT) + self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL)) + + if not checkFirmware(firmwareInfo): + self.dongleObject.dongle.close() + raise Exception(MSG_NEEDS_FW_UPDATE_GENERIC) + try: + self.dongleObject.getOperationMode() + except BTChipException as e: + if (e.sw == 0x6985): + self.dongleObject.dongle.close() + self.handler.get_setup( ) + # Acquire the new client on the next run + else: + raise e + if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject) and (self.handler is not None): + remaining_attempts = self.dongleObject.getVerifyPinRemainingAttempts() + if remaining_attempts != 1: + msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) + else: + msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." + confirmed, p, pin = self.password_dialog(msg) + if not confirmed: + raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') + pin = pin.encode() + self.dongleObject.verifyPin(pin) + except BTChipException as e: + if (e.sw == 0x6faa): + raise Exception("Dongle is temporarily locked - please unplug it and replug it again") + if ((e.sw & 0xFFF0) == 0x63c0): + raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") + if e.sw == 0x6f00 and e.message == 'Invalid channel': + # based on docs 0x6f00 might be a more general error, hence we also compare message to be sure + raise Exception("Invalid channel.\n" + "Please make sure that 'Browser support' is disabled on your device.") + raise e + + def checkDevice(self): + if not self.preflightDone: + try: + self.perform_hw1_preflight() + except BTChipException as e: + if (e.sw == 0x6d00 or e.sw == 0x6700): + raise Exception(_("Device not in Bitcoin mode")) from e + raise e + self.preflightDone = True + + def password_dialog(self, msg=None): + response = self.handler.get_word(msg) + if response is None: + return False, None, None + return True, response, response + + +class Ledger_KeyStore(Hardware_KeyStore): + hw_type = 'ledger' + device = 'Ledger' + + def __init__(self, d): + Hardware_KeyStore.__init__(self, d) + # Errors and other user interaction is done through the wallet's + # handler. The handler is per-window and preserved across + # device reconnects + self.force_watching_only = False + self.signing = False + self.cfg = d.get('cfg', {'mode':0,'pair':''}) + + def dump(self): + obj = Hardware_KeyStore.dump(self) + obj['cfg'] = self.cfg + return obj + + def get_derivation(self): + return self.derivation + + def get_client(self): + return self.plugin.get_client(self).dongleObject + + def get_client_electrum(self): + return self.plugin.get_client(self) + + def give_error(self, message, clear_client = False): + print_error(message) + if not self.signing: + self.handler.show_error(message) + else: + self.signing = False + if clear_client: + self.client = None + raise Exception(message) + + def set_and_unset_signing(func): + """Function decorator to set and unset self.signing.""" + def wrapper(self, *args, **kwargs): + try: + self.signing = True + return func(self, *args, **kwargs) + finally: + self.signing = False + return wrapper + + def address_id_stripped(self, address): + # Strip the leading "m/" + change, index = self.get_address_index(address) + derivation = self.derivation + address_path = "%s/%d/%d"%(derivation, change, index) + return address_path[2:] + + def decrypt_message(self, pubkey, message, password): + raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + + @test_pin_unlocked + @set_and_unset_signing + def sign_message(self, sequence, message, password): + message = message.encode('utf8') + message_hash = hashlib.sha256(message).hexdigest().upper() + # prompt for the PIN before displaying the dialog if necessary + client = self.get_client() + address_path = self.get_derivation()[2:] + "/%d/%d"%sequence + self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash) + try: + info = self.get_client().signMessagePrepare(address_path, message) + pin = "" + if info['confirmationNeeded']: + pin = self.handler.get_auth( info ) # does the authenticate dialog and returns pin + if not pin: + raise UserWarning(_('Cancelled by user')) + pin = str(pin).encode() + signature = self.get_client().signMessageSign(pin) + except BTChipException as e: + if e.sw == 0x6a80: + self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") + elif e.sw == 0x6985: # cancelled by user + return b'' + elif e.sw == 0x6982: + raise # pin lock. decorator will catch it + else: + self.give_error(e, True) + except UserWarning: + self.handler.show_error(_('Cancelled by user')) + return b'' + except Exception as e: + self.give_error(e, True) + finally: + self.handler.finished() + # Parse the ASN.1 signature + rLength = signature[3] + r = signature[4 : 4 + rLength] + sLength = signature[4 + rLength + 1] + s = signature[4 + rLength + 2:] + if rLength == 33: + r = r[1:] + if sLength == 33: + s = s[1:] + # And convert it + return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s + + @test_pin_unlocked + @set_and_unset_signing + def sign_transaction(self, tx, password): + if tx.is_complete(): + return + client = self.get_client() + inputs = [] + inputsPaths = [] + pubKeys = [] + chipInputs = [] + redeemScripts = [] + signatures = [] + preparedTrustedInputs = [] + changePath = "" + output = None + p2shTransaction = False + segwitTransaction = False + pin = "" + self.get_client() # prompt for the PIN before displaying the dialog if necessary + + # Fetch inputs of the transaction to sign + derivations = self.get_tx_derivations(tx) + for txin in tx.inputs(): + if txin['type'] == 'coinbase': + self.give_error("Coinbase not supported") # should never happen + + if txin['type'] in ['p2sh']: + p2shTransaction = True + + if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']: + if not self.get_client_electrum().supports_segwit(): + self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) + segwitTransaction = True + + if txin['type'] in ['p2wpkh', 'p2wsh']: + if not self.get_client_electrum().supports_native_segwit(): + self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) + segwitTransaction = True + + pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) + for i, x_pubkey in enumerate(x_pubkeys): + if x_pubkey in derivations: + signingPos = i + s = derivations.get(x_pubkey) + hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1]) + break + else: + self.give_error("No matching x_key for sign_transaction") # should never happen + + redeemScript = Transaction.get_preimage_script(txin) + txin_prev_tx = txin.get('prev_tx') + if txin_prev_tx is None and not Transaction.is_segwit_input(txin): + raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None + inputs.append([txin_prev_tx_raw, + txin['prevout_n'], + redeemScript, + txin['prevout_hash'], + signingPos, + txin.get('sequence', 0xffffffff - 1), + txin.get('value')]) + inputsPaths.append(hwAddress) + pubKeys.append(pubkeys) + + # Sanity check + if p2shTransaction: + for txin in tx.inputs(): + if txin['type'] != 'p2sh': + self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen + + txOutput = var_int(len(tx.outputs())) + for txout in tx.outputs(): + output_type, addr, amount = txout + txOutput += int_to_hex(amount, 8) + script = tx.pay_script(output_type, addr) + txOutput += var_int(len(script)//2) + txOutput += script + txOutput = bfh(txOutput) + + # Recognize outputs + # - only one output and one change is authorized (for hw.1 and nano) + # - at most one output can bypass confirmation (~change) (for all) + if not p2shTransaction: + if not self.get_client_electrum().supports_multi_output(): + if len(tx.outputs()) > 2: + self.give_error("Transaction with more than 2 outputs not supported") + has_change = False + any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) + for _type, address, amount in tx.outputs(): + assert _type == TYPE_ADDRESS + info = tx.output_info.get(address) + if (info is not None) and len(tx.outputs()) > 1 \ + and not has_change: + index, xpubs, m = info + on_change_branch = index[0] == 1 + # prioritise hiding outputs on the 'change' branch from user + # because no more than one change address allowed + if on_change_branch == any_output_on_change_branch: + changePath = self.get_derivation()[2:] + "/%d/%d"%index + has_change = True + else: + output = address + else: + output = address + + self.handler.show_message(_("Confirm Transaction on your Ledger device...")) + try: + # Get trusted inputs from the original transactions + for utxo in inputs: + sequence = int_to_hex(utxo[5], 4) + if segwitTransaction: + tmp = bfh(utxo[3])[::-1] + tmp += bfh(int_to_hex(utxo[1], 4)) + tmp += bfh(int_to_hex(utxo[6], 8)) # txin['value'] + chipInputs.append({'value' : tmp, 'witness' : True, 'sequence' : sequence}) + redeemScripts.append(bfh(utxo[2])) + elif not p2shTransaction: + txtmp = bitcoinTransaction(bfh(utxo[0])) + trustedInput = self.get_client().getTrustedInput(txtmp, utxo[1]) + trustedInput['sequence'] = sequence + chipInputs.append(trustedInput) + redeemScripts.append(txtmp.outputs[utxo[1]].script) + else: + tmp = bfh(utxo[3])[::-1] + tmp += bfh(int_to_hex(utxo[1], 4)) + chipInputs.append({'value' : tmp, 'sequence' : sequence}) + redeemScripts.append(bfh(utxo[2])) + + # Sign all inputs + firstTransaction = True + inputIndex = 0 + rawTx = tx.serialize_to_network() + self.get_client().enableAlternate2fa(False) + if segwitTransaction: + self.get_client().startUntrustedTransaction(True, inputIndex, + chipInputs, redeemScripts[inputIndex]) + if changePath: + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) + else: + outputData = self.get_client().finalizeInputFull(txOutput) + outputData['outputData'] = txOutput + transactionOutput = outputData['outputData'] + if outputData['confirmationNeeded']: + outputData['address'] = output + self.handler.finished() + pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin + if not pin: + raise UserWarning() + if pin != 'paired': + self.handler.show_message(_("Confirmed. Signing Transaction...")) + while inputIndex < len(inputs): + singleInput = [ chipInputs[inputIndex] ] + self.get_client().startUntrustedTransaction(False, 0, + singleInput, redeemScripts[inputIndex]) + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) + inputSignature[0] = 0x30 # force for 1.4.9+ + signatures.append(inputSignature) + inputIndex = inputIndex + 1 + else: + while inputIndex < len(inputs): + self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, + chipInputs, redeemScripts[inputIndex]) + if changePath: + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) + else: + outputData = self.get_client().finalizeInputFull(txOutput) + outputData['outputData'] = txOutput + if firstTransaction: + transactionOutput = outputData['outputData'] + if outputData['confirmationNeeded']: + outputData['address'] = output + self.handler.finished() + pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin + if not pin: + raise UserWarning() + if pin != 'paired': + self.handler.show_message(_("Confirmed. Signing Transaction...")) + else: + # Sign input with the provided PIN + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) + inputSignature[0] = 0x30 # force for 1.4.9+ + signatures.append(inputSignature) + inputIndex = inputIndex + 1 + if pin != 'paired': + firstTransaction = False + except UserWarning: + self.handler.show_error(_('Cancelled by user')) + return + except BTChipException as e: + if e.sw == 0x6985: # cancelled by user + return + elif e.sw == 0x6982: + raise # pin lock. decorator will catch it + else: + traceback.print_exc(file=sys.stderr) + self.give_error(e, True) + except BaseException as e: + traceback.print_exc(file=sys.stdout) + self.give_error(e, True) + finally: + self.handler.finished() + + for i, txin in enumerate(tx.inputs()): + signingPos = inputs[i][4] + tx.add_signature_to_txin(i, signingPos, bh2u(signatures[i])) + tx.raw = tx.serialize() + + @test_pin_unlocked + @set_and_unset_signing + def show_address(self, sequence, txin_type): + client = self.get_client() + address_path = self.get_derivation()[2:] + "/%d/%d"%sequence + self.handler.show_message(_("Showing address ...")) + segwit = Transaction.is_segwit_inputtype(txin_type) + segwitNative = txin_type == 'p2wpkh' + try: + client.getWalletPublicKey(address_path, showOnScreen=True, segwit=segwit, segwitNative=segwitNative) + except BTChipException as e: + if e.sw == 0x6985: # cancelled by user + pass + elif e.sw == 0x6982: + raise # pin lock. decorator will catch it + elif e.sw == 0x6b00: # hw.1 raises this + self.handler.show_error('{}\n{}\n{}'.format( + _('Error showing address') + ':', + e, + _('Your device might not have support for this functionality.'))) + else: + traceback.print_exc(file=sys.stderr) + self.handler.show_error(e) + except BaseException as e: + traceback.print_exc(file=sys.stderr) + self.handler.show_error(e) + finally: + self.handler.finished() + +class LedgerPlugin(HW_PluginBase): + libraries_available = BTCHIP + keystore_class = Ledger_KeyStore + client = None + DEVICE_IDS = [ + (0x2581, 0x1807), # HW.1 legacy btchip + (0x2581, 0x2b7c), # HW.1 transitional production + (0x2581, 0x3b7c), # HW.1 ledger production + (0x2581, 0x4b7c), # HW.1 ledger test + (0x2c97, 0x0000), # Blue + (0x2c97, 0x0001) # Nano-S + ] + SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') + + def __init__(self, parent, config, name): + self.segwit = config.get("segwit") + HW_PluginBase.__init__(self, parent, config, name) + if self.libraries_available: + self.device_manager().register_devices(self.DEVICE_IDS) + + def get_btchip_device(self, device): + ledger = False + if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c: + ledger = True + if device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c: + ledger = True + if device.product_key[0] == 0x2c97: + if device.interface_number == 0 or device.usage_page == 0xffa0: + ledger = True + else: + return None # non-compatible interface of a Nano S or Blue + dev = hid.device() + dev.open_path(device.path) + dev.set_nonblocking(True) + return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) + + def create_client(self, device, handler): + if handler: + self.handler = handler + + client = self.get_btchip_device(device) + if client is not None: + client = Ledger_Client(client) + return client + + def setup_device(self, device_info, wizard, purpose): + devmgr = self.device_manager() + device_id = device_info.device.id_ + client = devmgr.client_by_id(device_id) + if client is None: + raise Exception(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) + client.handler = self.create_handler(wizard) + client.get_xpub("m/44'/0'", 'standard') # TODO replace by direct derivation once Nano S > 1.1 + + def get_xpub(self, device_id, derivation, xtype, wizard): + if xtype not in self.SUPPORTED_XTYPES: + raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + client.handler = self.create_handler(wizard) + client.checkDevice() + xpub = client.get_xpub(derivation, xtype) + return xpub + + def get_client(self, keystore, force_pair=True): + # All client interaction should not be in the main GUI thread + devmgr = self.device_manager() + handler = keystore.handler + with devmgr.hid_lock: + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) + # returns the client for a given keystore. can use xpub + #if client: + # client.used() + if client is not None: + client.checkDevice() + return client + + def show_address(self, wallet, address, keystore=None): + if keystore is None: + keystore = wallet.get_keystore() + if not self.show_address_helper(wallet, address, keystore): + return + if type(wallet) is not Standard_Wallet: + keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) + return + sequence = wallet.get_address_index(address) + txin_type = wallet.get_txin_type(address) + keystore.show_address(sequence, txin_type) diff --git a/electrum/plugins/ledger/qt.py b/electrum/plugins/ledger/qt.py @@ -0,0 +1,81 @@ +#from btchip.btchipPersoWizard import StartBTChipPersoDialog + +from electrum.i18n import _ +from electrum.plugin import hook +from electrum.wallet import Standard_Wallet +from electrum.gui.qt.util import * + +from .ledger import LedgerPlugin +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase + + +class Plugin(LedgerPlugin, QtPluginBase): + icon_unpaired = ":icons/ledger_unpaired.png" + icon_paired = ":icons/ledger.png" + + def create_handler(self, window): + return Ledger_Handler(window) + + @hook + def receive_menu(self, menu, addrs, wallet): + if type(wallet) is not Standard_Wallet: + return + keystore = wallet.get_keystore() + if type(keystore) == self.keystore_class and len(addrs) == 1: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0])) + menu.addAction(_("Show on Ledger"), show_address) + +class Ledger_Handler(QtHandlerBase): + setup_signal = pyqtSignal() + auth_signal = pyqtSignal(object) + + def __init__(self, win): + super(Ledger_Handler, self).__init__(win, 'Ledger') + self.setup_signal.connect(self.setup_dialog) + self.auth_signal.connect(self.auth_dialog) + + def word_dialog(self, msg): + response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, QLineEdit.Password) + if not response[1]: + self.word = None + else: + self.word = str(response[0]) + self.done.set() + + def message_dialog(self, msg): + self.clear_dialog() + self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Ledger Status")) + l = QLabel(msg) + vbox = QVBoxLayout(dialog) + vbox.addWidget(l) + dialog.show() + + def auth_dialog(self, data): + try: + from .auth2fa import LedgerAuthDialog + except ImportError as e: + self.message_dialog(str(e)) + return + dialog = LedgerAuthDialog(self, data) + dialog.exec_() + self.word = dialog.pin + self.done.set() + + def get_auth(self, data): + self.done.clear() + self.auth_signal.emit(data) + self.done.wait() + return self.word + + def get_setup(self): + self.done.clear() + self.setup_signal.emit() + self.done.wait() + return + + def setup_dialog(self): + self.show_error(_('Initialization of Ledger HW devices is currently disabled.')) + return + dialog = StartBTChipPersoDialog() + dialog.exec_() diff --git a/electrum/plugins/revealer/DejaVuSansMono-Bold.ttf b/electrum/plugins/revealer/DejaVuSansMono-Bold.ttf Binary files differ. diff --git a/electrum/plugins/revealer/LICENSE_DEJAVU.txt b/electrum/plugins/revealer/LICENSE_DEJAVU.txt @@ -0,0 +1,99 @@ +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $ diff --git a/electrum/plugins/revealer/SIL Open Font License.txt b/electrum/plugins/revealer/SIL Open Font License.txt @@ -0,0 +1,43 @@ +Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.+ \ No newline at end of file diff --git a/electrum/plugins/revealer/SourceSansPro-Bold.otf b/electrum/plugins/revealer/SourceSansPro-Bold.otf Binary files differ. diff --git a/electrum/plugins/revealer/__init__.py b/electrum/plugins/revealer/__init__.py @@ -0,0 +1,16 @@ +from electrum.i18n import _ + +fullname = _('Revealer') +description = ''.join(["<br/>", + "<b>"+_("Do you have something to hide ?")+"</b>", '<br/>', '<br/>', + _("Revealer is a seed phrase back-up solution. It allows you to create a cold, analog, multi-factor backup of your wallet seeds, or of any arbitrary secret."), '<br/>', '<br/>', + _("Using a Revealer is better than writing your seed phrases on paper: a revealer is invulnerable to physical access and allows creation of trustless redundancy."), '<br/>', '<br/>', + _("This plug-in allows you to generate a pdf file of your secret phrase encrypted visually for your physical Revealer. You can print it trustlessly - it can only be decrypted optically with your Revealer."), '<br/>', '<br/>', + _("The plug-in also allows you to generate a digital Revealer file and print it yourself on a transparent overhead foil."), '<br/>', '<br/>', + _("Once activated you can access the plug-in through the icon at the seed dialog."), '<br/>', '<br/>', + _("For more information, visit"), + " <a href=\"https://revealer.cc\">https://revealer.cc</a>", '<br/>', '<br/>', +]) +available_for = ['qt'] + + diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py @@ -0,0 +1,724 @@ +''' + +Revealer +So you have something to hide? + +plug-in for the electrum wallet. + +Features: + - Deep Cold multi-factor backup solution + - Safety - One time pad security + - Redundancy - Trustless printing & distribution + - Encrypt your seedphrase or any secret you want for your revealer + - Based on crypto by legendary cryptographers Naor and Shamir + +Tiago Romagnani Silveira, 2017 + +''' + +import os +import random +import qrcode +import traceback +from hashlib import sha256 +from decimal import Decimal + +from PyQt5.QtPrintSupport import QPrinter + +from electrum.plugin import BasePlugin, hook +from electrum.i18n import _ +from electrum.util import to_bytes, make_dir + +from electrum.gui.qt.util import * +from electrum.gui.qt.qrtextedit import ScanQRTextEdit + + +class Plugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.base_dir = config.electrum_path()+'/revealer/' + + if self.config.get('calibration_h') == None: + self.config.set_key('calibration_h', 0) + if self.config.get('calibration_v') == None: + self.config.set_key('calibration_v', 0) + + self.calibration_h = self.config.get('calibration_h') + self.calibration_v = self.config.get('calibration_v') + + self.version = '0' + self.size = (159, 97) + self.f_size = QSize(1014*2, 642*2) + self.abstand_h = 21 + self.abstand_v = 34 + self.calibration_noise = int('10' * 128) + self.rawnoise = False + make_dir(self.base_dir) + + @hook + def set_seed(self, seed, parent): + self.cseed = seed.upper() + parent.addButton(':icons/revealer.png', partial(self.setup_dialog, parent), "Revealer"+_(" secret backup utility")) + + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Printer Calibration'), partial(self.calibration_dialog, window)) + + def setup_dialog(self, window): + self.update_wallet_name(window.parent().parent().wallet) + self.user_input = False + self.noise_seed = False + self.d = WindowModalDialog(window, "Revealer") + self.d.setMinimumWidth(420) + vbox = QVBoxLayout(self.d) + vbox.addSpacing(21) + logo = QLabel() + vbox.addWidget(logo) + logo.setPixmap(QPixmap(':icons/revealer.png')) + logo.setAlignment(Qt.AlignCenter) + vbox.addSpacing(42) + + self.load_noise = ScanQRTextEdit() + self.load_noise.setTabChangesFocus(True) + self.load_noise.textChanged.connect(self.on_edit) + self.load_noise.setMaximumHeight(33) + + vbox.addWidget(WWLabel("<b>"+_("Enter your physical revealer code:")+"<b>")) + vbox.addWidget(self.load_noise) + vbox.addSpacing(11) + + self.next_button = QPushButton(_("Next"), self.d) + self.next_button.setDefault(True) + self.next_button.setEnabled(False) + vbox.addLayout(Buttons(self.next_button)) + self.next_button.clicked.connect(self.d.close) + self.next_button.clicked.connect(partial(self.cypherseed_dialog, window)) + vbox.addSpacing(21) + + vbox.addWidget(WWLabel(_("or, alternatively: "))) + bcreate = QPushButton(_("Create a digital Revealer")) + + def mk_digital(): + try: + self.make_digital(self.d) + except Exception: + traceback.print_exc(file=sys.stdout) + else: + self.cypherseed_dialog(window) + + bcreate.clicked.connect(mk_digital) + + vbox.addWidget(bcreate) + vbox.addSpacing(11) + vbox.addWidget(QLabel(''.join([ "<b>"+_("WARNING")+ "</b>:" + _("Printing a revealer and encrypted seed"), '<br/>', + _("on the same printer is not trustless towards the printer."), '<br/>', + ]))) + vbox.addSpacing(11) + vbox.addLayout(Buttons(CloseButton(self.d))) + + return bool(self.d.exec_()) + + def get_noise(self): + text = self.load_noise.text() + return ''.join(text.split()).lower() + + def on_edit(self): + s = self.get_noise() + b = self.is_noise(s) + if b: + self.noise_seed = s[:-3] + self.user_input = True + self.next_button.setEnabled(b) + + def code_hashid(self, txt): + x = to_bytes(txt, 'utf8') + hash = sha256(x).hexdigest() + return hash[-3:].upper() + + def is_noise(self, txt): + if (len(txt) >= 34): + try: + int(txt, 16) + except: + self.user_input = False + return False + else: + id = self.code_hashid(txt[:-3]) + if (txt[-3:].upper() == id.upper()): + self.code_id = id + self.user_input = True + return True + else: + return False + else: + self.user_input = False + return False + + def make_digital(self, dialog): + self.make_rawnoise(True) + self.bdone(dialog) + self.d.close() + + def bcrypt(self, dialog): + self.rawnoise = False + dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at:").format(self.was, self.version, self.code_id), + "<br/>","<b>", self.base_dir+ self.filename+self.version+"_"+self.code_id,"</b>"])) + dialog.close() + + def bdone(self, dialog): + dialog.show_message(''.join([_("Digital Revealer ({}_{}) saved as PNG and PDF at:").format(self.version, self.code_id), + "<br/>","<b>", self.base_dir + 'revealer_' +self.version + '_'+ self.code_id, '</b>'])) + + + def customtxt_limits(self): + txt = self.text.text() + self.max_chars.setVisible(False) + self.char_count.setText("("+str(len(txt))+"/216)") + if len(txt)>0: + self.ctext.setEnabled(True) + if len(txt) > 216: + self.text.setPlainText(self.text.toPlainText()[:216]) + self.max_chars.setVisible(True) + + def t(self): + self.txt = self.text.text() + self.seed_img(is_seed=False) + + def cypherseed_dialog(self, window): + + d = WindowModalDialog(window, "Revealer") + d.setMinimumWidth(420) + + self.c_dialog = d + + self.vbox = QVBoxLayout(d) + self.vbox.addSpacing(21) + + logo = QLabel() + self.vbox.addWidget(logo) + logo.setPixmap(QPixmap(':icons/revealer.png')) + logo.setAlignment(Qt.AlignCenter) + self.vbox.addSpacing(42) + + grid = QGridLayout() + self.vbox.addLayout(grid) + + cprint = QPushButton(_("Generate encrypted seed PDF")) + cprint.clicked.connect(partial(self.seed_img, True)) + self.vbox.addWidget(cprint) + self.vbox.addSpacing(14) + + self.vbox.addWidget(WWLabel(_("and/or type any secret below:"))) + self.text = ScanQRTextEdit() + self.text.setTabChangesFocus(True) + self.text.setMaximumHeight(70) + self.text.textChanged.connect(self.customtxt_limits) + self.vbox.addWidget(self.text) + + self.char_count = WWLabel("") + self.char_count.setAlignment(Qt.AlignRight) + self.vbox.addWidget(self.char_count) + + self.max_chars = WWLabel("<font color='red'>" + _("This version supports a maximum of 216 characters.")+"</font>") + self.vbox.addWidget(self.max_chars) + self.max_chars.setVisible(False) + + self.ctext = QPushButton(_("Generate custom secret encrypted PDF")) + self.ctext.clicked.connect(self.t) + + self.vbox.addWidget(self.ctext) + self.ctext.setEnabled(False) + + self.vbox.addSpacing(21) + self.vbox.addLayout(Buttons(CloseButton(d))) + return bool(d.exec_()) + + + def update_wallet_name (self, name): + self.wallet_name = str(name) + self.base_name = self.base_dir + self.wallet_name + + def seed_img(self, is_seed = True): + + if not self.cseed and self.txt == False: + return + + if is_seed: + txt = self.cseed + else: + txt = self.txt.upper() + + img = QImage(self.size[0],self.size[1], QImage.Format_Mono) + bitmap = QBitmap.fromImage(img, Qt.MonoOnly) + bitmap.fill(Qt.white) + painter = QPainter() + painter.begin(bitmap) + QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(__file__), 'SourceSansPro-Bold.otf') ) + if len(txt) < 102 : + fontsize = 12 + linespace = 15 + max_letters = 17 + max_lines = 6 + max_words = 3 + if len(txt) > 102: + fontsize = 9 + linespace = 10 + max_letters = 24 + max_lines = 9 + max_words = int(max_letters/4) + + font = QFont('Source Sans Pro', fontsize, QFont.Bold) + font.setLetterSpacing(QFont.PercentageSpacing, 100) + painter.setFont(font) + seed_array = txt.split(' ') + + for n in range(max_lines): + nwords = max_words + temp_seed = seed_array[:nwords] + while len(' '.join(map(str, temp_seed))) > max_letters: + nwords = nwords - 1 + temp_seed = seed_array[:nwords] + painter.drawText(QRect(0, linespace*n , self.size[0], self.size[1]), Qt.AlignHCenter, ' '.join(map(str, temp_seed))) + del seed_array[:nwords] + + painter.end() + img = bitmap.toImage() + if (self.rawnoise == False): + self.make_rawnoise() + + self.make_cypherseed(img, self.rawnoise, False, is_seed) + return img + + def make_rawnoise(self, create_revealer=False): + w = self.size[0] + h = self.size[1] + rawnoise = QImage(w, h, QImage.Format_Mono) + + if(self.noise_seed == False): + self.noise_seed = random.SystemRandom().getrandbits(128) + self.hex_noise = format(self.noise_seed, '02x') + self.hex_noise = self.version + str(self.hex_noise) + + if (self.user_input == True): + self.noise_seed = int(self.noise_seed, 16) + self.hex_noise = self.version + str(format(self.noise_seed, '02x')) + + + self.code_id = self.code_hashid(self.hex_noise) + self.hex_noise = ' '.join(self.hex_noise[i:i+4] for i in range(0,len(self.hex_noise),4)) + random.seed(self.noise_seed) + + for x in range(w): + for y in range(h): + rawnoise.setPixel(x,y,random.randint(0, 1)) + + self.rawnoise = rawnoise + if create_revealer==True: + self.make_revealer() + self.noise_seed = False + + def make_calnoise(self): + random.seed(self.calibration_noise) + w = self.size[0] + h = self.size[1] + rawnoise = QImage(w, h, QImage.Format_Mono) + for x in range(w): + for y in range(h): + rawnoise.setPixel(x,y,random.randint(0, 1)) + self.calnoise = self.pixelcode_2x2(rawnoise) + + def make_revealer(self): + revealer = self.pixelcode_2x2(self.rawnoise) + revealer.invertPixels() + revealer = QBitmap.fromImage(revealer) + revealer = self.overlay_marks(revealer) + revealer = revealer.scaled(1014, 642) + self.filename = 'Revealer - ' + revealer.save(self.base_dir + self.filename + self.version+'_'+self.code_id + '.png') + self.toPdf(QImage(revealer)) + QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.abspath(self.base_dir + self.filename + self.version+'_'+ self.code_id + '.pdf'))) + + def make_cypherseed(self, img, rawnoise, calibration=False, is_seed = True): + img = img.convertToFormat(QImage.Format_Mono) + p = QPainter() + p.begin(img) + p.setCompositionMode(26) #xor + p.drawImage(0, 0, rawnoise) + p.end() + cypherseed = self.pixelcode_2x2(img) + cypherseed = QBitmap.fromImage(cypherseed) + cypherseed = cypherseed.scaled(self.f_size, Qt.KeepAspectRatio) + cypherseed = self.overlay_marks(cypherseed, True, calibration) + + if not is_seed: + self.filename = _('custom_secret')+'_' + self.was = _('Custom secret') + else: + self.filename = self.wallet_name+'_'+ _('seed')+'_' + self.was = self.wallet_name +' ' + _('seed') + + if not calibration: + self.toPdf(QImage(cypherseed)) + QDesktopServices.openUrl (QUrl.fromLocalFile(os.path.abspath(self.base_dir+self.filename+self.version+'_'+self.code_id+'.pdf'))) + cypherseed.save(self.base_dir + self.filename +self.version + '_'+ self.code_id + '.png') + self.bcrypt(self.c_dialog) + return cypherseed + + def calibration(self): + img = QImage(self.size[0],self.size[1], QImage.Format_Mono) + bitmap = QBitmap.fromImage(img, Qt.MonoOnly) + bitmap.fill(Qt.black) + self.make_calnoise() + img = self.overlay_marks(self.calnoise.scaledToHeight(self.f_size.height()), False, True) + self.calibration_pdf(img) + QDesktopServices.openUrl (QUrl.fromLocalFile(os.path.abspath(self.base_dir+_('calibration')+'.pdf'))) + return img + + def toPdf(self, image): + printer = QPrinter() + printer.setPaperSize(QSizeF(210, 297), QPrinter.Millimeter) + printer.setResolution(600) + printer.setOutputFormat(QPrinter.PdfFormat) + printer.setOutputFileName(self.base_dir+self.filename+self.version + '_'+self.code_id+'.pdf') + printer.setPageMargins(0,0,0,0,6) + painter = QPainter() + painter.begin(printer) + + delta_h = round(image.width()/self.abstand_v) + delta_v = round(image.height()/self.abstand_h) + + size_h = 2028+((int(self.calibration_h)*2028/(2028-(delta_h*2)+int(self.calibration_h)))/2) + size_v = 1284+((int(self.calibration_v)*1284/(1284-(delta_v*2)+int(self.calibration_v)))/2) + + image = image.scaled(size_h, size_v) + + painter.drawImage(553,533, image) + wpath = QPainterPath() + wpath.addRoundedRect(QRectF(553,533, size_h, size_v), 19, 19) + painter.setPen(QPen(Qt.black, 1)) + painter.drawPath(wpath) + painter.end() + + def calibration_pdf(self, image): + printer = QPrinter() + printer.setPaperSize(QSizeF(210, 297), QPrinter.Millimeter) + printer.setResolution(600) + printer.setOutputFormat(QPrinter.PdfFormat) + printer.setOutputFileName(self.base_dir+_('calibration')+'.pdf') + printer.setPageMargins(0,0,0,0,6) + + painter = QPainter() + painter.begin(printer) + painter.drawImage(553,533, image) + font = QFont('Source Sans Pro', 10, QFont.Bold) + painter.setFont(font) + painter.drawText(254,277, _("Calibration sheet")) + font = QFont('Source Sans Pro', 7, QFont.Bold) + painter.setFont(font) + painter.drawText(600,2077, _("Instructions:")) + font = QFont('Source Sans Pro', 7, QFont.Normal) + painter.setFont(font) + painter.drawText(700, 2177, _("1. Place this paper on a flat and well iluminated surface.")) + painter.drawText(700, 2277, _("2. Align your Revealer borderlines to the dashed lines on the top and left.")) + painter.drawText(700, 2377, _("3. Press slightly the Revealer against the paper and read the numbers that best " + "match on the opposite sides. ")) + painter.drawText(700, 2477, _("4. Type the numbers in the software")) + painter.end() + + def pixelcode_2x2(self, img): + result = QImage(img.width()*2, img.height()*2, QImage.Format_ARGB32 ) + white = qRgba(255,255,255,0) + black = qRgba(0,0,0,255) + + for x in range(img.width()): + for y in range(img.height()): + c = img.pixel(QPoint(x,y)) + colors = QColor(c).getRgbF() + if colors[0]: + result.setPixel(x*2+1,y*2+1, black) + result.setPixel(x*2,y*2+1, white) + result.setPixel(x*2+1,y*2, white) + result.setPixel(x*2, y*2, black) + + else: + result.setPixel(x*2+1,y*2+1, white) + result.setPixel(x*2,y*2+1, black) + result.setPixel(x*2+1,y*2, black) + result.setPixel(x*2, y*2, white) + return result + + def overlay_marks(self, img, is_cseed=False, calibration_sheet=False): + border_color = Qt.white + base_img = QImage(self.f_size.width(),self.f_size.height(), QImage.Format_ARGB32) + base_img.fill(border_color) + img = QImage(img) + + painter = QPainter() + painter.begin(base_img) + + total_distance_h = round(base_img.width() / self.abstand_v) + dist_v = round(total_distance_h) / 2 + dist_h = round(total_distance_h) / 2 + + img = img.scaledToWidth(base_img.width() - (2 * (total_distance_h))) + painter.drawImage(total_distance_h, + total_distance_h, + img) + + #frame around image + pen = QPen(Qt.black, 2) + painter.setPen(pen) + + #horz + painter.drawLine(0, total_distance_h, base_img.width(), total_distance_h) + painter.drawLine(0, base_img.height()-(total_distance_h), base_img.width(), base_img.height()-(total_distance_h)) + #vert + painter.drawLine(total_distance_h, 0, total_distance_h, base_img.height()) + painter.drawLine(base_img.width()-(total_distance_h), 0, base_img.width()-(total_distance_h), base_img.height()) + + #border around img + border_thick = 6 + Rpath = QPainterPath() + Rpath.addRect(QRectF((total_distance_h)+(border_thick/2), + (total_distance_h)+(border_thick/2), + base_img.width()-((total_distance_h)*2)-((border_thick)-1), + (base_img.height()-((total_distance_h))*2)-((border_thick)-1))) + pen = QPen(Qt.black, border_thick) + pen.setJoinStyle (Qt.MiterJoin) + + painter.setPen(pen) + painter.drawPath(Rpath) + + Bpath = QPainterPath() + Bpath.addRect(QRectF((total_distance_h), (total_distance_h), + base_img.width()-((total_distance_h)*2), (base_img.height()-((total_distance_h))*2))) + pen = QPen(Qt.black, 1) + painter.setPen(pen) + painter.drawPath(Bpath) + + pen = QPen(Qt.black, 1) + painter.setPen(pen) + painter.drawLine(0, base_img.height()/2, total_distance_h, base_img.height()/2) + painter.drawLine(base_img.width()/2, 0, base_img.width()/2, total_distance_h) + + painter.drawLine(base_img.width()-total_distance_h, base_img.height()/2, base_img.width(), base_img.height()/2) + painter.drawLine(base_img.width()/2, base_img.height(), base_img.width()/2, base_img.height() - total_distance_h) + + #print code + f_size = 37 + QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(__file__), 'DejaVuSansMono-Bold.ttf')) + font = QFont("DejaVu Sans Mono", f_size-11, QFont.Bold) + painter.setFont(font) + + if not calibration_sheet: + if is_cseed: #its a secret + painter.setPen(QPen(Qt.black, 1, Qt.DashDotDotLine)) + painter.drawLine(0, dist_v, base_img.width(), dist_v) + painter.drawLine(dist_h, 0, dist_h, base_img.height()) + painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v)) + painter.drawLine(base_img.width()-(dist_h), 0, base_img.width()-(dist_h), base_img.height()) + + painter.drawImage(((total_distance_h))+11, ((total_distance_h))+11, + QImage(':icons/electrumb.png').scaledToWidth(2.1*(total_distance_h), Qt.SmoothTransformation)) + + painter.setPen(QPen(Qt.white, border_thick*8)) + painter.drawLine(base_img.width()-((total_distance_h))-(border_thick*8)/2-(border_thick/2)-2, + (base_img.height()-((total_distance_h)))-((border_thick*8)/2)-(border_thick/2)-2, + base_img.width()-((total_distance_h))-(border_thick*8)/2-(border_thick/2)-2 - 77, + (base_img.height()-((total_distance_h)))-((border_thick*8)/2)-(border_thick/2)-2) + painter.setPen(QColor(0,0,0,255)) + painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick - 11, + base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.version + '_'+self.code_id) + painter.end() + + else: # revealer + + painter.setPen(QPen(border_color, 17)) + painter.drawLine(0, dist_v, base_img.width(), dist_v) + painter.drawLine(dist_h, 0, dist_h, base_img.height()) + painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v)) + painter.drawLine(base_img.width()-(dist_h), 0, base_img.width()-(dist_h), base_img.height()) + + painter.setPen(QPen(Qt.black, 2)) + painter.drawLine(0, dist_v, base_img.width(), dist_v) + painter.drawLine(dist_h, 0, dist_h, base_img.height()) + painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v)) + painter.drawLine(base_img.width()-(dist_h), 0, base_img.width()-(dist_h), base_img.height()) + logo = QImage(':icons/revealer_c.png').scaledToWidth(1.3*(total_distance_h)) + painter.drawImage((total_distance_h)+ (border_thick), ((total_distance_h))+ (border_thick), logo, Qt.SmoothTransformation) + + #frame around logo + painter.setPen(QPen(Qt.black, border_thick)) + painter.drawLine(total_distance_h+border_thick, total_distance_h+logo.height()+3*(border_thick/2), + total_distance_h+logo.width()+border_thick, total_distance_h+logo.height()+3*(border_thick/2)) + painter.drawLine(logo.width()+total_distance_h+3*(border_thick/2), total_distance_h+(border_thick), + total_distance_h+logo.width()+3*(border_thick/2), total_distance_h+logo.height()+(border_thick)) + + #frame around code/qr + qr_size = 179 + + painter.drawLine((base_img.width()-((total_distance_h))-(border_thick/2)-2)-qr_size, + (base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2, + (base_img.width()/2+(total_distance_h/2)-border_thick-(border_thick*8)/2)-qr_size, + (base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2) + + painter.drawLine((base_img.width()/2+(total_distance_h/2)-border_thick-(border_thick*8)/2)-qr_size, + (base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2, + base_img.width()/2 + (total_distance_h/2)-border_thick-(border_thick*8)/2-qr_size, + ((base_img.height()-((total_distance_h)))-(border_thick/2)-2)) + + painter.setPen(QPen(Qt.white, border_thick * 8)) + painter.drawLine( + base_img.width() - ((total_distance_h)) - (border_thick * 8) / 2 - (border_thick / 2) - 2, + (base_img.height() - ((total_distance_h))) - ((border_thick * 8) / 2) - (border_thick / 2) - 2, + base_img.width() / 2 + (total_distance_h / 2) - border_thick - qr_size, + (base_img.height() - ((total_distance_h))) - ((border_thick * 8) / 2) - (border_thick / 2) - 2) + + painter.setPen(QColor(0,0,0,255)) + painter.drawText(QRect(((base_img.width()/2) +21)-qr_size, base_img.height()-107, + base_img.width()-total_distance_h - border_thick -93, + base_img.height()-total_distance_h - border_thick), Qt.AlignLeft, self.hex_noise.upper()) + painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick -3 -qr_size, + base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.code_id) + + # draw qr code + qr_qt = self.paintQR(self.hex_noise.upper() +self.code_id) + target = QRectF(base_img.width()-65-qr_size, + base_img.height()-65-qr_size, + qr_size, qr_size ); + painter.drawImage(target, qr_qt); + painter.setPen(QPen(Qt.black, 4)) + painter.drawLine(base_img.width()-65-qr_size, + base_img.height()-65-qr_size, + base_img.width() - 65 - qr_size, + (base_img.height() - ((total_distance_h))) - ((border_thick * 8)) - (border_thick / 2) - 4 + ) + painter.drawLine(base_img.width()-65-qr_size, + base_img.height()-65-qr_size, + base_img.width() - 65, + base_img.height()-65-qr_size + ) + painter.end() + + else: # calibration only + painter.end() + cal_img = QImage(self.f_size.width() + 100, self.f_size.height() + 100, + QImage.Format_ARGB32) + cal_img.fill(Qt.white) + + cal_painter = QPainter() + cal_painter.begin(cal_img) + cal_painter.drawImage(0,0, base_img) + + #black lines in the middle of border top left only + cal_painter.setPen(QPen(Qt.black, 1, Qt.DashDotDotLine)) + cal_painter.drawLine(0, dist_v, base_img.width(), dist_v) + cal_painter.drawLine(dist_h, 0, dist_h, base_img.height()) + + pen = QPen(Qt.black, 2, Qt.DashDotDotLine) + cal_painter.setPen(pen) + n=15 + + cal_painter.setFont(QFont("DejaVu Sans Mono", 21, QFont.Bold)) + for x in range(-n,n): + #lines on bottom (vertical calibration) + cal_painter.drawLine((((base_img.width())/(n*2)) *(x))+ (base_img.width()/2)-13, + x+2+base_img.height()-(dist_v), + (((base_img.width())/(n*2)) *(x))+ (base_img.width()/2)+13, + x+2+base_img.height()-(dist_v)) + + num_pos = 9 + if x > 9 : num_pos = 17 + if x < 0 : num_pos = 20 + if x < -9: num_pos = 27 + + cal_painter.drawText((((base_img.width())/(n*2)) *(x))+ (base_img.width()/2)-num_pos, + 50+base_img.height()-(dist_v), + str(x)) + + #lines on the right (horizontal calibrations) + + cal_painter.drawLine(x+2+(base_img.width()-(dist_h)), + ((base_img.height()/(2*n)) *(x))+ (base_img.height()/n)+(base_img.height()/2)-13, + x+2+(base_img.width()-(dist_h)), + ((base_img.height()/(2*n)) *(x))+ (base_img.height()/n)+(base_img.height()/2)+13) + + + cal_painter.drawText(30+(base_img.width()-(dist_h)), + ((base_img.height()/(2*n)) *(x))+ (base_img.height()/2)+13, str(x)) + + cal_painter.end() + base_img = cal_img + + return base_img + + def paintQR(self, data): + if not data: + return + qr = qrcode.QRCode() + qr.add_data(data) + matrix = qr.get_matrix() + k = len(matrix) + border_color = Qt.white + base_img = QImage(k * 5, k * 5, QImage.Format_ARGB32) + base_img.fill(border_color) + qrpainter = QPainter() + qrpainter.begin(base_img) + boxsize = 5 + size = k * boxsize + left = (base_img.width() - size)/2 + top = (base_img.height() - size)/2 + qrpainter.setBrush(Qt.black) + qrpainter.setPen(Qt.black) + + for r in range(k): + for c in range(k): + if matrix[r][c]: + qrpainter.drawRect(left+c*boxsize, top+r*boxsize, boxsize - 1, boxsize - 1) + qrpainter.end() + return base_img + + def calibration_dialog(self, window): + d = WindowModalDialog(window, _("Revealer - Printer calibration settings")) + + d.setMinimumSize(100, 200) + + vbox = QVBoxLayout(d) + vbox.addWidget(QLabel(''.join(["<br/>", _("If you have an old printer, or want optimal precision"),"<br/>", + _("print the calibration pdf and follow the instructions "), "<br/>","<br/>", + ]))) + self.calibration_h = self.config.get('calibration_h') + self.calibration_v = self.config.get('calibration_v') + cprint = QPushButton(_("Open calibration pdf")) + cprint.clicked.connect(self.calibration) + vbox.addWidget(cprint) + + vbox.addWidget(QLabel(_('Calibration values:'))) + grid = QGridLayout() + vbox.addLayout(grid) + grid.addWidget(QLabel(_('Right side')), 0, 0) + horizontal = QLineEdit() + horizontal.setText(str(self.calibration_h)) + grid.addWidget(horizontal, 0, 1) + + grid.addWidget(QLabel(_('Bottom')), 1, 0) + vertical = QLineEdit() + vertical.setText(str(self.calibration_v)) + grid.addWidget(vertical, 1, 1) + + vbox.addStretch() + vbox.addSpacing(13) + vbox.addLayout(Buttons(CloseButton(d), OkButton(d))) + + if not d.exec_(): + return + + self.calibration_h = int(Decimal(horizontal.text())) + self.config.set_key('calibration_h', self.calibration_h) + self.calibration_v = int(Decimal(vertical.text())) + self.config.set_key('calibration_v', self.calibration_v) + + diff --git a/electrum/plugins/trezor/__init__.py b/electrum/plugins/trezor/__init__.py @@ -0,0 +1,8 @@ +from electrum.i18n import _ + +fullname = 'TREZOR Wallet' +description = _('Provides support for TREZOR hardware wallet') +requires = [('trezorlib','github.com/trezor/python-trezor')] +registers_keystore = ('hardware', 'trezor', _("TREZOR wallet")) +available_for = ['qt', 'cmdline'] + diff --git a/electrum/plugins/trezor/client.py b/electrum/plugins/trezor/client.py @@ -0,0 +1,11 @@ +from trezorlib.client import proto, BaseClient, ProtocolMixin +from .clientbase import TrezorClientBase + +class TrezorClient(TrezorClientBase, ProtocolMixin, BaseClient): + def __init__(self, transport, handler, plugin): + BaseClient.__init__(self, transport=transport) + ProtocolMixin.__init__(self, transport=transport) + TrezorClientBase.__init__(self, handler, plugin, proto) + + +TrezorClientBase.wrap_methods(TrezorClient) diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py @@ -0,0 +1,265 @@ +import time +from struct import pack + +from electrum.i18n import _ +from electrum.util import PrintError, UserCancelled +from electrum.keystore import bip39_normalize_passphrase +from electrum.bitcoin import serialize_xpub + + +class GuiMixin(object): + # Requires: self.proto, self.device + + # ref: https://github.com/trezor/trezor-common/blob/44dfb07cfaafffada4b2ce0d15ba1d90d17cf35e/protob/types.proto#L89 + messages = { + 3: _("Confirm the transaction output on your {} device"), + 4: _("Confirm internal entropy on your {} device to begin"), + 5: _("Write down the seed word shown on your {}"), + 6: _("Confirm on your {} that you want to wipe it clean"), + 7: _("Confirm on your {} device the message to sign"), + 8: _("Confirm the total amount spent and the transaction fee on your " + "{} device"), + 10: _("Confirm wallet address on your {} device"), + 14: _("Choose on your {} device where to enter your passphrase"), + 'default': _("Check your {} device to continue"), + } + + def callback_Failure(self, msg): + # BaseClient's unfortunate call() implementation forces us to + # raise exceptions on failure in order to unwind the stack. + # However, making the user acknowledge they cancelled + # gets old very quickly, so we suppress those. The NotInitialized + # one is misnamed and indicates a passphrase request was cancelled. + if msg.code in (self.types.FailureType.PinCancelled, + self.types.FailureType.ActionCancelled, + self.types.FailureType.NotInitialized): + raise UserCancelled() + raise RuntimeError(msg.message) + + def callback_ButtonRequest(self, msg): + message = self.msg + if not message: + message = self.messages.get(msg.code, self.messages['default']) + self.handler.show_message(message.format(self.device), self.cancel) + return self.proto.ButtonAck() + + def callback_PinMatrixRequest(self, msg): + if msg.type == 2: + msg = _("Enter a new PIN for your {}:") + elif msg.type == 3: + msg = (_("Re-enter the new PIN for your {}.\n\n" + "NOTE: the positions of the numbers have changed!")) + else: + msg = _("Enter your current {} PIN:") + pin = self.handler.get_pin(msg.format(self.device)) + if len(pin) > 9: + self.handler.show_error(_('The PIN cannot be longer than 9 characters.')) + pin = '' # to cancel below + if not pin: + return self.proto.Cancel() + return self.proto.PinMatrixAck(pin=pin) + + def callback_PassphraseRequest(self, req): + if req and hasattr(req, 'on_device') and req.on_device is True: + return self.proto.PassphraseAck() + + if self.creating_wallet: + msg = _("Enter a passphrase to generate this wallet. Each time " + "you use this wallet your {} will prompt you for the " + "passphrase. If you forget the passphrase you cannot " + "access the bitcoins in the wallet.").format(self.device) + else: + msg = _("Enter the passphrase to unlock this wallet:") + passphrase = self.handler.get_passphrase(msg, self.creating_wallet) + if passphrase is None: + return self.proto.Cancel() + passphrase = bip39_normalize_passphrase(passphrase) + + ack = self.proto.PassphraseAck(passphrase=passphrase) + length = len(ack.passphrase) + if length > 50: + self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length)) + return self.proto.Cancel() + return ack + + def callback_PassphraseStateRequest(self, msg): + return self.proto.PassphraseStateAck() + + def callback_WordRequest(self, msg): + if (msg.type is not None + and msg.type in (self.types.WordRequestType.Matrix9, + self.types.WordRequestType.Matrix6)): + num = 9 if msg.type == self.types.WordRequestType.Matrix9 else 6 + char = self.handler.get_matrix(num) + if char == 'x': + return self.proto.Cancel() + return self.proto.WordAck(word=char) + + self.step += 1 + msg = _("Step {}/24. Enter seed word as explained on " + "your {}:").format(self.step, self.device) + word = self.handler.get_word(msg) + # Unfortunately the device can't handle self.proto.Cancel() + return self.proto.WordAck(word=word) + + +class TrezorClientBase(GuiMixin, PrintError): + + def __init__(self, handler, plugin, proto): + assert hasattr(self, 'tx_api') # ProtocolMixin already constructed? + self.proto = proto + self.device = plugin.device + self.handler = handler + self.tx_api = plugin + self.types = plugin.types + self.msg = None + self.creating_wallet = False + self.used() + + def __str__(self): + return "%s/%s" % (self.label(), self.features.device_id) + + def label(self): + '''The name given by the user to the device.''' + return self.features.label + + def is_initialized(self): + '''True if initialized, False if wiped.''' + return self.features.initialized + + def is_pairable(self): + return not self.features.bootloader_mode + + def has_usable_connection_with_device(self): + try: + res = self.ping("electrum pinging device") + assert res == "electrum pinging device" + except BaseException: + return False + return True + + def used(self): + self.last_operation = time.time() + + def prevent_timeouts(self): + self.last_operation = float('inf') + + def timeout(self, cutoff): + '''Time out the client if the last operation was before cutoff.''' + if self.last_operation < cutoff: + self.print_error("timed out") + self.clear_session() + + @staticmethod + def expand_path(n): + '''Convert bip32 path to list of uint32 integers with prime flags + 0/-1/1' -> [0, 0x80000001, 0x80000001]''' + # This code is similar to code in trezorlib where it unfortunately + # is not declared as a staticmethod. Our n has an extra element. + PRIME_DERIVATION_FLAG = 0x80000000 + path = [] + for x in n.split('/')[1:]: + prime = 0 + if x.endswith("'"): + x = x.replace('\'', '') + prime = PRIME_DERIVATION_FLAG + if x.startswith('-'): + prime = PRIME_DERIVATION_FLAG + path.append(abs(int(x)) | prime) + return path + + def cancel(self): + '''Provided here as in keepkeylib but not trezorlib.''' + self.transport.write(self.proto.Cancel()) + + def i4b(self, x): + return pack('>I', x) + + def get_xpub(self, bip32_path, xtype): + address_n = self.expand_path(bip32_path) + creating = False + node = self.get_public_node(address_n, creating).node + return serialize_xpub(xtype, node.chain_code, node.public_key, node.depth, self.i4b(node.fingerprint), self.i4b(node.child_num)) + + def toggle_passphrase(self): + if self.features.passphrase_protection: + self.msg = _("Confirm on your {} device to disable passphrases") + else: + self.msg = _("Confirm on your {} device to enable passphrases") + enabled = not self.features.passphrase_protection + self.apply_settings(use_passphrase=enabled) + + def change_label(self, label): + self.msg = _("Confirm the new label on your {} device") + self.apply_settings(label=label) + + def change_homescreen(self, homescreen): + self.msg = _("Confirm on your {} device to change your home screen") + self.apply_settings(homescreen=homescreen) + + def set_pin(self, remove): + if remove: + self.msg = _("Confirm on your {} device to disable PIN protection") + elif self.features.pin_protection: + self.msg = _("Confirm on your {} device to change your PIN") + else: + self.msg = _("Confirm on your {} device to set a PIN") + self.change_pin(remove) + + def clear_session(self): + '''Clear the session to force pin (and passphrase if enabled) + re-entry. Does not leak exceptions.''' + self.print_error("clear session:", self) + self.prevent_timeouts() + try: + super(TrezorClientBase, self).clear_session() + except BaseException as e: + # If the device was removed it has the same effect... + self.print_error("clear_session: ignoring error", str(e)) + + def get_public_node(self, address_n, creating): + self.creating_wallet = creating + return super(TrezorClientBase, self).get_public_node(address_n) + + def close(self): + '''Called when Our wallet was closed or the device removed.''' + self.print_error("closing client") + self.clear_session() + # Release the device + self.transport.close() + + def firmware_version(self): + f = self.features + return (f.major_version, f.minor_version, f.patch_version) + + def atleast_version(self, major, minor=0, patch=0): + return self.firmware_version() >= (major, minor, patch) + + def get_trezor_model(self): + """Returns '1' for Trezor One, 'T' for Trezor T.""" + return self.features.model + + @staticmethod + def wrapper(func): + '''Wrap methods to clear any message box they opened.''' + + def wrapped(self, *args, **kwargs): + try: + self.prevent_timeouts() + return func(self, *args, **kwargs) + finally: + self.used() + self.handler.finished() + self.creating_wallet = False + self.msg = None + + return wrapped + + @staticmethod + def wrap_methods(cls): + for method in ['apply_settings', 'change_pin', + 'get_address', 'get_public_node', + 'load_device_by_mnemonic', 'load_device_by_xprv', + 'recovery_device', 'reset_device', 'sign_message', + 'sign_tx', 'wipe_device']: + setattr(cls, method, cls.wrapper(getattr(cls, method))) diff --git a/electrum/plugins/trezor/cmdline.py b/electrum/plugins/trezor/cmdline.py @@ -0,0 +1,14 @@ +from electrum.plugin import hook +from .trezor import TrezorPlugin +from ..hw_wallet import CmdLineHandler + +class Plugin(TrezorPlugin): + handler = CmdLineHandler() + @hook + def init_keystore(self, keystore): + if not isinstance(keystore, self.keystore_class): + return + keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py @@ -0,0 +1,613 @@ +from functools import partial +import threading + +from PyQt5.Qt import Qt +from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton +from PyQt5.Qt import QVBoxLayout, QLabel + +from electrum.gui.qt.util import * +from electrum.i18n import _ +from electrum.plugin import hook, DeviceMgr +from electrum.util import PrintError, UserCancelled, bh2u +from electrum.wallet import Wallet, Standard_Wallet + +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase +from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, + RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX) + + +PASSPHRASE_HELP_SHORT =_( + "Passphrases allow you to access new wallets, each " + "hidden behind a particular case-sensitive passphrase.") +PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _( + "You need to create a separate Electrum wallet for each passphrase " + "you use as they each generate different addresses. Changing " + "your passphrase does not lose other wallets, each is still " + "accessible behind its own passphrase.") +RECOMMEND_PIN = _( + "You should enable PIN protection. Your PIN is the only protection " + "for your bitcoins if your device is lost or stolen.") +PASSPHRASE_NOT_PIN = _( + "If you forget a passphrase you will be unable to access any " + "bitcoins in the wallet behind it. A passphrase is not a PIN. " + "Only change this if you are sure you understand it.") +MATRIX_RECOVERY = _( + "Enter the recovery words by pressing the buttons according to what " + "the device shows on its display. You can also use your NUMPAD.\n" + "Press BACKSPACE to go back a choice or word.\n") + + +class MatrixDialog(WindowModalDialog): + + def __init__(self, parent): + super(MatrixDialog, self).__init__(parent) + self.setWindowTitle(_("Trezor Matrix Recovery")) + self.num = 9 + self.loop = QEventLoop() + + vbox = QVBoxLayout(self) + vbox.addWidget(WWLabel(MATRIX_RECOVERY)) + + grid = QGridLayout() + grid.setSpacing(0) + self.char_buttons = [] + for y in range(3): + for x in range(3): + button = QPushButton('?') + button.clicked.connect(partial(self.process_key, ord('1') + y * 3 + x)) + grid.addWidget(button, 3 - y, x) + self.char_buttons.append(button) + vbox.addLayout(grid) + + self.backspace_button = QPushButton("<=") + self.backspace_button.clicked.connect(partial(self.process_key, Qt.Key_Backspace)) + self.cancel_button = QPushButton(_("Cancel")) + self.cancel_button.clicked.connect(partial(self.process_key, Qt.Key_Escape)) + buttons = Buttons(self.backspace_button, self.cancel_button) + vbox.addSpacing(40) + vbox.addLayout(buttons) + self.refresh() + self.show() + + def refresh(self): + for y in range(3): + self.char_buttons[3 * y + 1].setEnabled(self.num == 9) + + def is_valid(self, key): + return key >= ord('1') and key <= ord('9') + + def process_key(self, key): + self.data = None + if key == Qt.Key_Backspace: + self.data = '\010' + elif key == Qt.Key_Escape: + self.data = 'x' + elif self.is_valid(key): + self.char_buttons[key - ord('1')].setFocus() + self.data = '%c' % key + if self.data: + self.loop.exit(0) + + def keyPressEvent(self, event): + self.process_key(event.key()) + if not self.data: + QDialog.keyPressEvent(self, event) + + def get_matrix(self, num): + self.num = num + self.refresh() + self.loop.exec_() + + +class QtHandler(QtHandlerBase): + + pin_signal = pyqtSignal(object) + matrix_signal = pyqtSignal(object) + close_matrix_dialog_signal = pyqtSignal() + + def __init__(self, win, pin_matrix_widget_class, device): + super(QtHandler, self).__init__(win, device) + self.pin_signal.connect(self.pin_dialog) + self.matrix_signal.connect(self.matrix_recovery_dialog) + self.close_matrix_dialog_signal.connect(self._close_matrix_dialog) + self.pin_matrix_widget_class = pin_matrix_widget_class + self.matrix_dialog = None + + def get_pin(self, msg): + self.done.clear() + self.pin_signal.emit(msg) + self.done.wait() + return self.response + + def get_matrix(self, msg): + self.done.clear() + self.matrix_signal.emit(msg) + self.done.wait() + data = self.matrix_dialog.data + if data == 'x': + self.close_matrix_dialog() + return data + + def _close_matrix_dialog(self): + if self.matrix_dialog: + self.matrix_dialog.accept() + self.matrix_dialog = None + + def close_matrix_dialog(self): + self.close_matrix_dialog_signal.emit() + + def pin_dialog(self, msg): + # Needed e.g. when resetting a device + self.clear_dialog() + dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN")) + matrix = self.pin_matrix_widget_class() + vbox = QVBoxLayout() + vbox.addWidget(QLabel(msg)) + vbox.addWidget(matrix) + vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) + dialog.setLayout(vbox) + dialog.exec_() + self.response = str(matrix.get_value()) + self.done.set() + + def matrix_recovery_dialog(self, msg): + if not self.matrix_dialog: + self.matrix_dialog = MatrixDialog(self.top_level_window()) + self.matrix_dialog.get_matrix(msg) + self.done.set() + + +class QtPlugin(QtPluginBase): + # Derived classes must provide the following class-static variables: + # icon_file + # pin_matrix_widget_class + + def create_handler(self, window): + return QtHandler(window, self.pin_matrix_widget_class(), self.device) + + @hook + def receive_menu(self, menu, addrs, wallet): + if len(addrs) != 1: + return + for keystore in wallet.get_keystores(): + if type(keystore) == self.keystore_class: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) + menu.addAction(_("Show on {}").format(self.device), show_address) + break + + def show_settings_dialog(self, window, keystore): + device_id = self.choose_device(window, keystore) + if device_id: + SettingsDialog(window, self, keystore, device_id).exec_() + + def request_trezor_init_settings(self, wizard, method, model): + vbox = QVBoxLayout() + next_enabled = True + label = QLabel(_("Enter a label to name your device:")) + name = QLineEdit() + hl = QHBoxLayout() + hl.addWidget(label) + hl.addWidget(name) + hl.addStretch(1) + vbox.addLayout(hl) + + def clean_text(widget): + text = widget.toPlainText().strip() + return ' '.join(text.split()) + + if method in [TIM_NEW, TIM_RECOVER]: + gb = QGroupBox() + hbox1 = QHBoxLayout() + gb.setLayout(hbox1) + vbox.addWidget(gb) + gb.setTitle(_("Select your seed length:")) + bg_numwords = QButtonGroup() + for i, count in enumerate([12, 18, 24]): + rb = QRadioButton(gb) + rb.setText(_("%d words") % count) + bg_numwords.addButton(rb) + bg_numwords.setId(rb, i) + hbox1.addWidget(rb) + rb.setChecked(True) + cb_pin = QCheckBox(_('Enable PIN protection')) + cb_pin.setChecked(True) + else: + text = QTextEdit() + text.setMaximumHeight(60) + if method == TIM_MNEMONIC: + msg = _("Enter your BIP39 mnemonic:") + else: + msg = _("Enter the master private key beginning with xprv:") + def set_enabled(): + from electrum.keystore import is_xprv + wizard.next_button.setEnabled(is_xprv(clean_text(text))) + text.textChanged.connect(set_enabled) + next_enabled = False + + vbox.addWidget(QLabel(msg)) + vbox.addWidget(text) + pin = QLineEdit() + pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) + pin.setMaximumWidth(100) + hbox_pin = QHBoxLayout() + hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) + hbox_pin.addWidget(pin) + hbox_pin.addStretch(1) + + if method in [TIM_NEW, TIM_RECOVER]: + vbox.addWidget(WWLabel(RECOMMEND_PIN)) + vbox.addWidget(cb_pin) + else: + vbox.addLayout(hbox_pin) + + passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + cb_phrase = QCheckBox(_('Enable passphrases')) + cb_phrase.setChecked(False) + vbox.addWidget(passphrase_msg) + vbox.addWidget(passphrase_warning) + vbox.addWidget(cb_phrase) + + # ask for recovery type (random word order OR matrix) + if method == TIM_RECOVER and not model == 'T': + gb_rectype = QGroupBox() + hbox_rectype = QHBoxLayout() + gb_rectype.setLayout(hbox_rectype) + vbox.addWidget(gb_rectype) + gb_rectype.setTitle(_("Select recovery type:")) + bg_rectype = QButtonGroup() + + rb1 = QRadioButton(gb_rectype) + rb1.setText(_('Scrambled words')) + bg_rectype.addButton(rb1) + bg_rectype.setId(rb1, RECOVERY_TYPE_SCRAMBLED_WORDS) + hbox_rectype.addWidget(rb1) + rb1.setChecked(True) + + rb2 = QRadioButton(gb_rectype) + rb2.setText(_('Matrix')) + bg_rectype.addButton(rb2) + bg_rectype.setId(rb2, RECOVERY_TYPE_MATRIX) + hbox_rectype.addWidget(rb2) + else: + bg_rectype = None + + wizard.exec_layout(vbox, next_enabled=next_enabled) + + if method in [TIM_NEW, TIM_RECOVER]: + item = bg_numwords.checkedId() + pin = cb_pin.isChecked() + recovery_type = bg_rectype.checkedId() if bg_rectype else None + else: + item = ' '.join(str(clean_text(text)).split()) + pin = str(pin.text()) + recovery_type = None + + return (item, name.text(), pin, cb_phrase.isChecked(), recovery_type) + + +class Plugin(TrezorPlugin, QtPlugin): + icon_unpaired = ":icons/trezor_unpaired.png" + icon_paired = ":icons/trezor.png" + + @classmethod + def pin_matrix_widget_class(self): + from trezorlib.qt.pinmatrix import PinMatrixWidget + return PinMatrixWidget + + +class SettingsDialog(WindowModalDialog): + '''This dialog doesn't require a device be paired with a wallet. + We want users to be able to wipe a device even if they've forgotten + their PIN.''' + + def __init__(self, window, plugin, keystore, device_id): + title = _("{} Settings").format(plugin.device) + super(SettingsDialog, self).__init__(window, title) + self.setMaximumWidth(540) + + devmgr = plugin.device_manager() + config = devmgr.config + handler = keystore.handler + thread = keystore.thread + hs_rows, hs_cols = (64, 128) + + def invoke_client(method, *args, **kw_args): + unpair_after = kw_args.pop('unpair_after', False) + + def task(): + client = devmgr.client_by_id(device_id) + if not client: + raise RuntimeError("Device not connected") + if method: + getattr(client, method)(*args, **kw_args) + if unpair_after: + devmgr.unpair_id(device_id) + return client.features + + thread.add(task, on_success=update) + + def update(features): + self.features = features + set_label_enabled() + if features.bootloader_hash: + bl_hash = bh2u(features.bootloader_hash) + bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) + else: + bl_hash = "N/A" + noyes = [_("No"), _("Yes")] + endis = [_("Enable Passphrases"), _("Disable Passphrases")] + disen = [_("Disabled"), _("Enabled")] + setchange = [_("Set a PIN"), _("Change PIN")] + + version = "%d.%d.%d" % (features.major_version, + features.minor_version, + features.patch_version) + + device_label.setText(features.label) + pin_set_label.setText(noyes[features.pin_protection]) + passphrases_label.setText(disen[features.passphrase_protection]) + bl_hash_label.setText(bl_hash) + label_edit.setText(features.label) + device_id_label.setText(features.device_id) + initialized_label.setText(noyes[features.initialized]) + version_label.setText(version) + clear_pin_button.setVisible(features.pin_protection) + clear_pin_warning.setVisible(features.pin_protection) + pin_button.setText(setchange[features.pin_protection]) + pin_msg.setVisible(not features.pin_protection) + passphrase_button.setText(endis[features.passphrase_protection]) + language_label.setText(features.language) + + def set_label_enabled(): + label_apply.setEnabled(label_edit.text() != self.features.label) + + def rename(): + invoke_client('change_label', label_edit.text()) + + def toggle_passphrase(): + title = _("Confirm Toggle Passphrase Protection") + currently_enabled = self.features.passphrase_protection + if currently_enabled: + msg = _("After disabling passphrases, you can only pair this " + "Electrum wallet if it had an empty passphrase. " + "If its passphrase was not empty, you will need to " + "create a new wallet with the install wizard. You " + "can use this wallet again at any time by re-enabling " + "passphrases and entering its passphrase.") + else: + msg = _("Your current Electrum wallet can only be used with " + "an empty passphrase. You must create a separate " + "wallet with the install wizard for other passphrases " + "as each one generates a new set of addresses.") + msg += "\n\n" + _("Are you sure you want to proceed?") + if not self.question(msg, title=title): + return + invoke_client('toggle_passphrase', unpair_after=currently_enabled) + + def change_homescreen(): + dialog = QFileDialog(self, _("Choose Homescreen")) + filename, __ = dialog.getOpenFileName() + if not filename: + return # user cancelled + + if filename.endswith('.toif'): + img = open(filename, 'rb').read() + if img[:8] != b'TOIf\x90\x00\x90\x00': + handler.show_error('File is not a TOIF file with size of 144x144') + return + else: + from PIL import Image # FIXME + im = Image.open(filename) + if im.size != (128, 64): + handler.show_error('Image must be 128 x 64 pixels') + return + im = im.convert('1') + pix = im.load() + img = bytearray(1024) + for j in range(64): + for i in range(128): + if pix[i, j]: + o = (i + j * 128) + img[o // 8] |= (1 << (7 - o % 8)) + img = bytes(img) + invoke_client('change_homescreen', img) + + def clear_homescreen(): + invoke_client('change_homescreen', b'\x00') + + def set_pin(): + invoke_client('set_pin', remove=False) + + def clear_pin(): + invoke_client('set_pin', remove=True) + + def wipe_device(): + wallet = window.wallet + if wallet and sum(wallet.get_balance()): + title = _("Confirm Device Wipe") + msg = _("Are you SURE you want to wipe the device?\n" + "Your wallet still has bitcoins in it!") + if not self.question(msg, title=title, + icon=QMessageBox.Critical): + return + invoke_client('wipe_device', unpair_after=True) + + def slider_moved(): + mins = timeout_slider.sliderPosition() + timeout_minutes.setText(_("%2d minutes") % mins) + + def slider_released(): + config.set_session_timeout(timeout_slider.sliderPosition() * 60) + + # Information tab + info_tab = QWidget() + info_layout = QVBoxLayout(info_tab) + info_glayout = QGridLayout() + info_glayout.setColumnStretch(2, 1) + device_label = QLabel() + pin_set_label = QLabel() + passphrases_label = QLabel() + version_label = QLabel() + device_id_label = QLabel() + bl_hash_label = QLabel() + bl_hash_label.setWordWrap(True) + language_label = QLabel() + initialized_label = QLabel() + rows = [ + (_("Device Label"), device_label), + (_("PIN set"), pin_set_label), + (_("Passphrases"), passphrases_label), + (_("Firmware Version"), version_label), + (_("Device ID"), device_id_label), + (_("Bootloader Hash"), bl_hash_label), + (_("Language"), language_label), + (_("Initialized"), initialized_label), + ] + for row_num, (label, widget) in enumerate(rows): + info_glayout.addWidget(QLabel(label), row_num, 0) + info_glayout.addWidget(widget, row_num, 1) + info_layout.addLayout(info_glayout) + + # Settings tab + settings_tab = QWidget() + settings_layout = QVBoxLayout(settings_tab) + settings_glayout = QGridLayout() + + # Settings tab - Label + label_msg = QLabel(_("Name this {}. If you have multiple devices " + "their labels help distinguish them.") + .format(plugin.device)) + label_msg.setWordWrap(True) + label_label = QLabel(_("Device Label")) + label_edit = QLineEdit() + label_edit.setMinimumWidth(150) + label_edit.setMaxLength(plugin.MAX_LABEL_LEN) + label_apply = QPushButton(_("Apply")) + label_apply.clicked.connect(rename) + label_edit.textChanged.connect(set_label_enabled) + settings_glayout.addWidget(label_label, 0, 0) + settings_glayout.addWidget(label_edit, 0, 1, 1, 2) + settings_glayout.addWidget(label_apply, 0, 3) + settings_glayout.addWidget(label_msg, 1, 1, 1, -1) + + # Settings tab - PIN + pin_label = QLabel(_("PIN Protection")) + pin_button = QPushButton() + pin_button.clicked.connect(set_pin) + settings_glayout.addWidget(pin_label, 2, 0) + settings_glayout.addWidget(pin_button, 2, 1) + pin_msg = QLabel(_("PIN protection is strongly recommended. " + "A PIN is your only protection against someone " + "stealing your bitcoins if they obtain physical " + "access to your {}.").format(plugin.device)) + pin_msg.setWordWrap(True) + pin_msg.setStyleSheet("color: red") + settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) + + # Settings tab - Homescreen + homescreen_label = QLabel(_("Homescreen")) + homescreen_change_button = QPushButton(_("Change...")) + homescreen_clear_button = QPushButton(_("Reset")) + homescreen_change_button.clicked.connect(change_homescreen) + try: + import PIL + except ImportError: + homescreen_change_button.setDisabled(True) + homescreen_change_button.setToolTip( + _("Required package 'PIL' is not available - Please install it or use the Trezor website instead.") + ) + homescreen_clear_button.clicked.connect(clear_homescreen) + homescreen_msg = QLabel(_("You can set the homescreen on your " + "device to personalize it. You must " + "choose a {} x {} monochrome black and " + "white image.").format(hs_rows, hs_cols)) + homescreen_msg.setWordWrap(True) + settings_glayout.addWidget(homescreen_label, 4, 0) + settings_glayout.addWidget(homescreen_change_button, 4, 1) + settings_glayout.addWidget(homescreen_clear_button, 4, 2) + settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1) + + # Settings tab - Session Timeout + timeout_label = QLabel(_("Session Timeout")) + timeout_minutes = QLabel() + timeout_slider = QSlider(Qt.Horizontal) + timeout_slider.setRange(1, 60) + timeout_slider.setSingleStep(1) + timeout_slider.setTickInterval(5) + timeout_slider.setTickPosition(QSlider.TicksBelow) + timeout_slider.setTracking(True) + timeout_msg = QLabel( + _("Clear the session after the specified period " + "of inactivity. Once a session has timed out, " + "your PIN and passphrase (if enabled) must be " + "re-entered to use the device.")) + timeout_msg.setWordWrap(True) + timeout_slider.setSliderPosition(config.get_session_timeout() // 60) + slider_moved() + timeout_slider.valueChanged.connect(slider_moved) + timeout_slider.sliderReleased.connect(slider_released) + settings_glayout.addWidget(timeout_label, 6, 0) + settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3) + settings_glayout.addWidget(timeout_minutes, 6, 4) + settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1) + settings_layout.addLayout(settings_glayout) + settings_layout.addStretch(1) + + # Advanced tab + advanced_tab = QWidget() + advanced_layout = QVBoxLayout(advanced_tab) + advanced_glayout = QGridLayout() + + # Advanced tab - clear PIN + clear_pin_button = QPushButton(_("Disable PIN")) + clear_pin_button.clicked.connect(clear_pin) + clear_pin_warning = QLabel( + _("If you disable your PIN, anyone with physical access to your " + "{} device can spend your bitcoins.").format(plugin.device)) + clear_pin_warning.setWordWrap(True) + clear_pin_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(clear_pin_button, 0, 2) + advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5) + + # Advanced tab - toggle passphrase protection + passphrase_button = QPushButton() + passphrase_button.clicked.connect(toggle_passphrase) + passphrase_msg = WWLabel(PASSPHRASE_HELP) + passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) + passphrase_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(passphrase_button, 3, 2) + advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5) + advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5) + + # Advanced tab - wipe device + wipe_device_button = QPushButton(_("Wipe Device")) + wipe_device_button.clicked.connect(wipe_device) + wipe_device_msg = QLabel( + _("Wipe the device, removing all data from it. The firmware " + "is left unchanged.")) + wipe_device_msg.setWordWrap(True) + wipe_device_warning = QLabel( + _("Only wipe a device if you have the recovery seed written down " + "and the device wallet(s) are empty, otherwise the bitcoins " + "will be lost forever.")) + wipe_device_warning.setWordWrap(True) + wipe_device_warning.setStyleSheet("color: red") + advanced_glayout.addWidget(wipe_device_button, 6, 2) + advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5) + advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5) + advanced_layout.addLayout(advanced_glayout) + advanced_layout.addStretch(1) + + tabs = QTabWidget(self) + tabs.addTab(info_tab, _("Information")) + tabs.addTab(settings_tab, _("Settings")) + tabs.addTab(advanced_tab, _("Advanced")) + dialog_vbox = QVBoxLayout(self) + dialog_vbox.addWidget(tabs) + dialog_vbox.addLayout(Buttons(CloseButton(self))) + + # Update information + invoke_client(None) diff --git a/electrum/plugins/trezor/transport.py b/electrum/plugins/trezor/transport.py @@ -0,0 +1,95 @@ +from electrum.util import PrintError + + +class TrezorTransport(PrintError): + + @staticmethod + def all_transports(): + """Reimplemented trezorlib.transport.all_transports so that we can + enable/disable specific transports. + """ + try: + # only to detect trezorlib version + from trezorlib.transport import all_transports + except ImportError: + # old trezorlib. compat for trezorlib < 0.9.2 + transports = [] + #try: + # from trezorlib.transport_bridge import BridgeTransport + # transports.append(BridgeTransport) + #except BaseException: + # pass + try: + from trezorlib.transport_hid import HidTransport + transports.append(HidTransport) + except BaseException: + pass + try: + from trezorlib.transport_udp import UdpTransport + transports.append(UdpTransport) + except BaseException: + pass + try: + from trezorlib.transport_webusb import WebUsbTransport + transports.append(WebUsbTransport) + except BaseException: + pass + else: + # new trezorlib. + transports = [] + #try: + # from trezorlib.transport.bridge import BridgeTransport + # transports.append(BridgeTransport) + #except BaseException: + # pass + try: + from trezorlib.transport.hid import HidTransport + transports.append(HidTransport) + except BaseException: + pass + try: + from trezorlib.transport.udp import UdpTransport + transports.append(UdpTransport) + except BaseException: + pass + try: + from trezorlib.transport.webusb import WebUsbTransport + transports.append(WebUsbTransport) + except BaseException: + pass + return transports + return transports + + def enumerate_devices(self): + """Just like trezorlib.transport.enumerate_devices, + but with exception catching, so that transports can fail separately. + """ + devices = [] + for transport in self.all_transports(): + try: + new_devices = transport.enumerate() + except BaseException as e: + self.print_error('enumerate failed for {}. error {}' + .format(transport.__name__, str(e))) + else: + devices.extend(new_devices) + return devices + + def get_transport(self, path=None): + """Reimplemented trezorlib.transport.get_transport, + (1) for old trezorlib + (2) to be able to disable specific transports + (3) to call our own enumerate_devices that catches exceptions + """ + if path is None: + try: + return self.enumerate_devices()[0] + except IndexError: + raise Exception("No TREZOR device found") from None + + def match_prefix(a, b): + return a.startswith(b) or b.startswith(a) + transports = [t for t in self.all_transports() if match_prefix(path, t.PATH_PREFIX)] + if transports: + return transports[0].find_by_path(path) + raise Exception("Unknown path prefix '%s'" % path) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py @@ -0,0 +1,516 @@ +from binascii import hexlify, unhexlify +import traceback +import sys + +from electrum.util import bfh, bh2u, versiontuple, UserCancelled +from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, deserialize_xpub, + TYPE_ADDRESS, TYPE_SCRIPT, is_address) +from electrum import constants +from electrum.i18n import _ +from electrum.plugin import BasePlugin, Device +from electrum.transaction import deserialize, Transaction +from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey, xtype_from_derivation +from electrum.base_wizard import ScriptTypeNotSupported + +from ..hw_wallet import HW_PluginBase +from ..hw_wallet.plugin import is_any_tx_output_on_change_branch + + +# TREZOR initialization methods +TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) +RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX = range(0, 2) + +# script "generation" +SCRIPT_GEN_LEGACY, SCRIPT_GEN_P2SH_SEGWIT, SCRIPT_GEN_NATIVE_SEGWIT = range(0, 3) + + +class TrezorKeyStore(Hardware_KeyStore): + hw_type = 'trezor' + device = 'TREZOR' + + def get_derivation(self): + return self.derivation + + def get_script_gen(self): + xtype = xtype_from_derivation(self.derivation) + if xtype in ('p2wpkh', 'p2wsh'): + return SCRIPT_GEN_NATIVE_SEGWIT + elif xtype in ('p2wpkh-p2sh', 'p2wsh-p2sh'): + return SCRIPT_GEN_P2SH_SEGWIT + else: + return SCRIPT_GEN_LEGACY + + def get_client(self, force_pair=True): + return self.plugin.get_client(self, force_pair) + + def decrypt_message(self, sequence, message, password): + raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + + def sign_message(self, sequence, message, password): + client = self.get_client() + address_path = self.get_derivation() + "/%d/%d"%sequence + address_n = client.expand_path(address_path) + msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) + return msg_sig.signature + + def sign_transaction(self, tx, password): + if tx.is_complete(): + return + # previous transactions used as inputs + prev_tx = {} + # path of the xpubs that are involved + xpub_path = {} + for txin in tx.inputs(): + pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) + tx_hash = txin['prevout_hash'] + if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): + raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + prev_tx[tx_hash] = txin['prev_tx'] + for x_pubkey in x_pubkeys: + if not is_xpubkey(x_pubkey): + continue + xpub, s = parse_xpubkey(x_pubkey) + if xpub == self.get_master_public_key(): + xpub_path[xpub] = self.get_derivation() + + self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) + + +class TrezorPlugin(HW_PluginBase): + # Derived classes provide: + # + # class-static variables: client_class, firmware_URL, handler_class, + # libraries_available, libraries_URL, minimum_firmware, + # wallet_class, types + + firmware_URL = 'https://wallet.trezor.io' + libraries_URL = 'https://github.com/trezor/python-trezor' + minimum_firmware = (1, 5, 2) + keystore_class = TrezorKeyStore + minimum_library = (0, 9, 0) + SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') + + MAX_LABEL_LEN = 32 + + def __init__(self, parent, config, name): + HW_PluginBase.__init__(self, parent, config, name) + + try: + # Minimal test if python-trezor is installed + import trezorlib + try: + library_version = trezorlib.__version__ + except AttributeError: + # python-trezor only introduced __version__ in 0.9.0 + library_version = 'unknown' + if library_version == 'unknown' or \ + versiontuple(library_version) < self.minimum_library: + self.libraries_available_message = ( + _("Library version for '{}' is too old.").format(name) + + '\nInstalled: {}, Needed: {}' + .format(library_version, self.minimum_library)) + self.print_stderr(self.libraries_available_message) + raise ImportError() + self.libraries_available = True + except ImportError: + self.libraries_available = False + return + + from . import client + from . import transport + import trezorlib.messages + self.client_class = client.TrezorClient + self.types = trezorlib.messages + self.DEVICE_IDS = ('TREZOR',) + + self.transport_handler = transport.TrezorTransport() + self.device_manager().register_enumerate_func(self.enumerate) + + def enumerate(self): + devices = self.transport_handler.enumerate_devices() + return [Device(d.get_path(), -1, d.get_path(), 'TREZOR', 0) for d in devices] + + def create_client(self, device, handler): + try: + self.print_error("connecting to device at", device.path) + transport = self.transport_handler.get_transport(device.path) + except BaseException as e: + self.print_error("cannot connect at", device.path, str(e)) + return None + + if not transport: + self.print_error("cannot connect at", device.path) + return + + self.print_error("connected to device at", device.path) + client = self.client_class(transport, handler, self) + + # Try a ping for device sanity + try: + client.ping('t') + except BaseException as e: + self.print_error("ping failed", str(e)) + return None + + if not client.atleast_version(*self.minimum_firmware): + msg = (_('Outdated {} firmware for device labelled {}. Please ' + 'download the updated firmware from {}') + .format(self.device, client.label(), self.firmware_URL)) + self.print_error(msg) + handler.show_error(msg) + return None + + return client + + def get_client(self, keystore, force_pair=True): + devmgr = self.device_manager() + handler = keystore.handler + with devmgr.hid_lock: + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) + # returns the client for a given keystore. can use xpub + if client: + client.used() + return client + + def get_coin_name(self): + return "Testnet" if constants.net.TESTNET else "Bitcoin" + + def initialize_device(self, device_id, wizard, handler): + # Initialization method + msg = _("Choose how you want to initialize your {}.\n\n" + "The first two methods are secure as no secret information " + "is entered into your computer.\n\n" + "For the last two methods you input secrets on your keyboard " + "and upload them to your {}, and so you should " + "only do those on a computer you know to be trustworthy " + "and free of malware." + ).format(self.device, self.device) + choices = [ + # Must be short as QT doesn't word-wrap radio button text + (TIM_NEW, _("Let the device generate a completely new seed randomly")), + (TIM_RECOVER, _("Recover from a seed you have previously written down")), + (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), + (TIM_PRIVKEY, _("Upload a master private key")) + ] + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + model = client.get_trezor_model() + def f(method): + import threading + settings = self.request_trezor_init_settings(wizard, method, model) + t = threading.Thread(target=self._initialize_device_safe, args=(settings, method, device_id, wizard, handler)) + t.setDaemon(True) + t.start() + exit_code = wizard.loop.exec_() + if exit_code != 0: + # this method (initialize_device) was called with the expectation + # of leaving the device in an initialized state when finishing. + # signal that this is not the case: + raise UserCancelled() + wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f) + + def _initialize_device_safe(self, settings, method, device_id, wizard, handler): + exit_code = 0 + try: + self._initialize_device(settings, method, device_id, wizard, handler) + except UserCancelled: + exit_code = 1 + except BaseException as e: + traceback.print_exc(file=sys.stderr) + handler.show_error(str(e)) + exit_code = 1 + finally: + wizard.loop.exit(exit_code) + + def _initialize_device(self, settings, method, device_id, wizard, handler): + item, label, pin_protection, passphrase_protection, recovery_type = settings + + if method == TIM_RECOVER and recovery_type == RECOVERY_TYPE_SCRAMBLED_WORDS: + handler.show_error(_( + "You will be asked to enter 24 words regardless of your " + "seed's actual length. If you enter a word incorrectly or " + "misspell it, you cannot change it or go back - you will need " + "to start again from the beginning.\n\nSo please enter " + "the words carefully!"), + blocking=True) + + language = 'english' + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + + if method == TIM_NEW: + strength = 64 * (item + 2) # 128, 192 or 256 + u2f_counter = 0 + skip_backup = False + client.reset_device(True, strength, passphrase_protection, + pin_protection, label, language, + u2f_counter, skip_backup) + elif method == TIM_RECOVER: + word_count = 6 * (item + 2) # 12, 18 or 24 + client.step = 0 + if recovery_type == RECOVERY_TYPE_SCRAMBLED_WORDS: + recovery_type_trezor = self.types.RecoveryDeviceType.ScrambledWords + else: + recovery_type_trezor = self.types.RecoveryDeviceType.Matrix + client.recovery_device(word_count, passphrase_protection, + pin_protection, label, language, + type=recovery_type_trezor) + if recovery_type == RECOVERY_TYPE_MATRIX: + handler.close_matrix_dialog() + elif method == TIM_MNEMONIC: + pin = pin_protection # It's the pin, not a boolean + client.load_device_by_mnemonic(str(item), pin, + passphrase_protection, + label, language) + else: + pin = pin_protection # It's the pin, not a boolean + client.load_device_by_xprv(item, pin, passphrase_protection, + label, language) + + def _make_node_path(self, xpub, address_n): + _, depth, fingerprint, child_num, chain_code, key = deserialize_xpub(xpub) + node = self.types.HDNodeType( + depth=depth, + fingerprint=int.from_bytes(fingerprint, 'big'), + child_num=int.from_bytes(child_num, 'big'), + chain_code=chain_code, + public_key=key, + ) + return self.types.HDNodePathType(node=node, address_n=address_n) + + def setup_device(self, device_info, wizard, purpose): + devmgr = self.device_manager() + device_id = device_info.device.id_ + client = devmgr.client_by_id(device_id) + if client is None: + raise Exception(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) + # fixme: we should use: client.handler = wizard + client.handler = self.create_handler(wizard) + if not device_info.initialized: + self.initialize_device(device_id, wizard, client.handler) + client.get_xpub('m', 'standard') + client.used() + + def get_xpub(self, device_id, derivation, xtype, wizard): + if xtype not in self.SUPPORTED_XTYPES: + raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + client.handler = wizard + xpub = client.get_xpub(derivation, xtype) + client.used() + return xpub + + def get_trezor_input_script_type(self, script_gen, is_multisig): + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + return self.types.InputScriptType.SPENDWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + return self.types.InputScriptType.SPENDP2SHWITNESS + else: + if is_multisig: + return self.types.InputScriptType.SPENDMULTISIG + else: + return self.types.InputScriptType.SPENDADDRESS + + def sign_transaction(self, keystore, tx, prev_tx, xpub_path): + self.prev_tx = prev_tx + self.xpub_path = xpub_path + client = self.get_client(keystore) + inputs = self.tx_inputs(tx, True, keystore.get_script_gen()) + outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.get_script_gen()) + signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0] + signatures = [(bh2u(x) + '01') for x in signatures] + tx.update_signatures(signatures) + + def show_address(self, wallet, address, keystore=None): + if keystore is None: + keystore = wallet.get_keystore() + if not self.show_address_helper(wallet, address, keystore): + return + client = self.get_client(keystore) + if not client.atleast_version(1, 3): + keystore.handler.show_error(_("Your device firmware is too old")) + return + change, index = wallet.get_address_index(address) + derivation = keystore.derivation + address_path = "%s/%d/%d"%(derivation, change, index) + address_n = client.expand_path(address_path) + xpubs = wallet.get_master_public_keys() + if len(xpubs) == 1: + script_gen = keystore.get_script_gen() + script_type = self.get_trezor_input_script_type(script_gen, is_multisig=False) + client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) + else: + def f(xpub): + return self._make_node_path(xpub, [change, index]) + pubkeys = wallet.get_public_keys(address) + # sort xpubs using the order of pubkeys + sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) + pubkeys = list(map(f, sorted_xpubs)) + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=[b''] * wallet.n, + m=wallet.m, + ) + script_gen = keystore.get_script_gen() + script_type = self.get_trezor_input_script_type(script_gen, is_multisig=True) + client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type) + + def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY): + inputs = [] + for txin in tx.inputs(): + txinputtype = self.types.TxInputType() + if txin['type'] == 'coinbase': + prev_hash = "\0"*32 + prev_index = 0xffffffff # signed int -1 + else: + if for_sig: + x_pubkeys = txin['x_pubkeys'] + if len(x_pubkeys) == 1: + x_pubkey = x_pubkeys[0] + xpub, s = parse_xpubkey(x_pubkey) + xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) + txinputtype._extend_address_n(xpub_n + s) + txinputtype.script_type = self.get_trezor_input_script_type(script_gen, is_multisig=False) + else: + def f(x_pubkey): + if is_xpubkey(x_pubkey): + xpub, s = parse_xpubkey(x_pubkey) + else: + xpub = xpub_from_pubkey(0, bfh(x_pubkey)) + s = [] + return self._make_node_path(xpub, s) + pubkeys = list(map(f, x_pubkeys)) + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))), + m=txin.get('num_sig'), + ) + script_type = self.get_trezor_input_script_type(script_gen, is_multisig=True) + txinputtype = self.types.TxInputType( + script_type=script_type, + multisig=multisig + ) + # find which key is mine + for x_pubkey in x_pubkeys: + if is_xpubkey(x_pubkey): + xpub, s = parse_xpubkey(x_pubkey) + if xpub in self.xpub_path: + xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) + txinputtype._extend_address_n(xpub_n + s) + break + + prev_hash = unhexlify(txin['prevout_hash']) + prev_index = txin['prevout_n'] + + if 'value' in txin: + txinputtype.amount = txin['value'] + txinputtype.prev_hash = prev_hash + txinputtype.prev_index = prev_index + + if txin.get('scriptSig') is not None: + script_sig = bfh(txin['scriptSig']) + txinputtype.script_sig = script_sig + + txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) + + inputs.append(txinputtype) + + return inputs + + def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY): + + def create_output_by_derivation(info): + index, xpubs, m = info + if len(xpubs) == 1: + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + script_type = self.types.OutputScriptType.PAYTOWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS + else: + script_type = self.types.OutputScriptType.PAYTOADDRESS + address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) + txoutputtype = self.types.TxOutputType( + amount=amount, + script_type=script_type, + address_n=address_n, + ) + else: + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + script_type = self.types.OutputScriptType.PAYTOWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS + else: + script_type = self.types.OutputScriptType.PAYTOMULTISIG + address_n = self.client_class.expand_path("/%d/%d" % index) + pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs] + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=[b''] * len(pubkeys), + m=m) + txoutputtype = self.types.TxOutputType( + multisig=multisig, + amount=amount, + address_n=self.client_class.expand_path(derivation + "/%d/%d" % index), + script_type=script_type) + return txoutputtype + + def create_output_by_address(): + txoutputtype = self.types.TxOutputType() + txoutputtype.amount = amount + if _type == TYPE_SCRIPT: + txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN + txoutputtype.op_return_data = address[2:] + elif _type == TYPE_ADDRESS: + txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS + txoutputtype.address = address + return txoutputtype + + outputs = [] + has_change = False + any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) + + for _type, address, amount in tx.outputs(): + use_create_by_derivation = False + + info = tx.output_info.get(address) + if info is not None and not has_change: + index, xpubs, m = info + on_change_branch = index[0] == 1 + # prioritise hiding outputs on the 'change' branch from user + # because no more than one change address allowed + # note: ^ restriction can be removed once we require fw + # that has https://github.com/trezor/trezor-mcu/pull/306 + if on_change_branch == any_output_on_change_branch: + use_create_by_derivation = True + has_change = True + + if use_create_by_derivation: + txoutputtype = create_output_by_derivation(info) + else: + txoutputtype = create_output_by_address() + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = self.types.TransactionType() + if tx is None: + # probably for segwit input and we don't need this prev txn + return t + d = deserialize(tx.raw) + t.version = d['version'] + t.lock_time = d['lockTime'] + inputs = self.tx_inputs(tx) + t._extend_inputs(inputs) + for vout in d['outputs']: + o = t._add_bin_outputs() + o.amount = vout['value'] + o.script_pubkey = bfh(vout['scriptPubKey']) + return t + + # This function is called from the TREZOR libraries (via tx_api) + def get_tx(self, tx_hash): + tx = self.prev_tx[tx_hash] + return self.electrum_tx_to_txtype(tx) diff --git a/electrum/plugins/trustedcoin/__init__.py b/electrum/plugins/trustedcoin/__init__.py @@ -0,0 +1,11 @@ +from electrum.i18n import _ + +fullname = _('Two Factor Authentication') +description = ''.join([ + _("This plugin adds two-factor authentication to your wallet."), '<br/>', + _("For more information, visit"), + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" +]) +requires_wallet_type = ['2fa'] +registers_wallet_type = '2fa' +available_for = ['qt', 'cmdline', 'kivy'] diff --git a/electrum/plugins/trustedcoin/cmdline.py b/electrum/plugins/trustedcoin/cmdline.py @@ -0,0 +1,45 @@ +#!/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.plugin import hook +from .trustedcoin import TrustedCoinPlugin + + +class Plugin(TrustedCoinPlugin): + + def prompt_user_for_otp(self, wallet, tx): + if not isinstance(wallet, self.wallet_class): + return + if not wallet.can_sign_without_server(): + self.print_error("twofactor:sign_tx") + auth_code = None + if wallet.keystores['x3/'].get_tx_derivations(tx): + msg = _('Please enter your Google Authenticator code:') + auth_code = int(input(msg)) + else: + self.print_error("twofactor: xpub3 not needed") + wallet.auth_code = auth_code + diff --git a/electrum/plugins/trustedcoin/kivy.py b/electrum/plugins/trustedcoin/kivy.py @@ -0,0 +1,110 @@ +#!/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 functools import partial +from threading import Thread +import re +from decimal import Decimal + +from kivy.clock import Clock + +from electrum.i18n import _ +from electrum.plugin import hook +from .trustedcoin import TrustedCoinPlugin, server, KIVY_DISCLAIMER, TrustedCoinException, ErrorConnectingServer + + + +class Plugin(TrustedCoinPlugin): + + disclaimer_msg = KIVY_DISCLAIMER + + def __init__(self, parent, config, name): + super().__init__(parent, config, name) + + @hook + def load_wallet(self, wallet, window): + if not isinstance(wallet, self.wallet_class): + return + self.start_request_thread(wallet) + + def go_online_dialog(self, wizard): + # we skip this step on android + wizard.run('accept_terms_of_use') + + def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): + from ...gui.kivy.uix.dialogs.label_dialog import LabelDialog + msg = _('Please enter your Google Authenticator code') + d = LabelDialog(msg, '', lambda otp: self.on_otp(wallet, tx, otp, on_success, on_failure)) + d.open() + + def on_otp(self, wallet, tx, otp, on_success, on_failure): + try: + wallet.on_otp(tx, otp) + except TrustedCoinException as e: + if e.status_code == 400: # invalid OTP + Clock.schedule_once(lambda dt: on_failure(_('Invalid one-time password.'))) + else: + Clock.schedule_once(lambda dt, bound_e=e: on_failure(_('Error') + ':\n' + str(bound_e))) + except Exception as e: + Clock.schedule_once(lambda dt, bound_e=e: on_failure(_('Error') + ':\n' + str(bound_e))) + else: + on_success(tx) + + def accept_terms_of_use(self, wizard): + def handle_error(msg, e): + wizard.show_error(msg + ':\n' + str(e)) + wizard.terminate() + try: + tos = server.get_terms_of_service() + except ErrorConnectingServer as e: + Clock.schedule_once(lambda dt, bound_e=e: handle_error(_('Error connecting to server'), bound_e)) + except Exception as e: + Clock.schedule_once(lambda dt, bound_e=e: handle_error(_('Error'), bound_e)) + else: + f = lambda x: self.read_email(wizard) + wizard.tos_dialog(tos=tos, run_next=f) + + def read_email(self, wizard): + f = lambda x: self.create_remote_key(x, wizard) + wizard.email_dialog(run_next=f) + + def request_otp_dialog(self, wizard, short_id, otp_secret, xpub3): + f = lambda otp, reset: self.check_otp(wizard, short_id, otp_secret, xpub3, otp, reset) + wizard.otp_dialog(otp_secret=otp_secret, run_next=f) + + @hook + def abort_send(self, window): + wallet = window.wallet + if not isinstance(wallet, self.wallet_class): + return + if wallet.can_sign_without_server(): + return + if wallet.billing_info is None: + self.start_request_thread(wallet) + Clock.schedule_once( + lambda dt: window.show_error(_('Requesting account info from TrustedCoin server...') + '\n' + + _('Please try again.'))) + return True + return False diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py @@ -0,0 +1,313 @@ +#!/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 functools import partial +import threading +from threading import Thread +import re +from decimal import Decimal + +from PyQt5.QtGui import * +from PyQt5.QtCore import * + +from electrum.gui.qt.util import * +from electrum.gui.qt.qrcodewidget import QRCodeWidget +from electrum.gui.qt.amountedit import AmountEdit +from electrum.gui.qt.main_window import StatusBarButton +from electrum.i18n import _ +from electrum.plugin import hook +from electrum.util import PrintError, is_valid_email +from .trustedcoin import TrustedCoinPlugin, server + + +class TOS(QTextEdit): + tos_signal = pyqtSignal() + error_signal = pyqtSignal(object) + + +class HandlerTwoFactor(QObject, PrintError): + + def __init__(self, plugin, window): + super().__init__() + self.plugin = plugin + self.window = window + + def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): + if not isinstance(wallet, self.plugin.wallet_class): + return + if wallet.can_sign_without_server(): + return + if not wallet.keystores['x3/'].get_tx_derivations(tx): + self.print_error("twofactor: xpub3 not needed") + return + window = self.window.top_level_window() + auth_code = self.plugin.auth_dialog(window) + try: + wallet.on_otp(tx, auth_code) + except: + on_failure(sys.exc_info()) + return + on_success(tx) + +class Plugin(TrustedCoinPlugin): + + def __init__(self, parent, config, name): + super().__init__(parent, config, name) + + @hook + def on_new_window(self, window): + wallet = window.wallet + if not isinstance(wallet, self.wallet_class): + return + wallet.handler_2fa = HandlerTwoFactor(self, window) + if wallet.can_sign_without_server(): + msg = ' '.join([ + _('This wallet was restored from seed, and it contains two master private keys.'), + _('Therefore, two-factor authentication is disabled.') + ]) + action = lambda: window.show_message(msg) + else: + action = partial(self.settings_dialog, window) + button = StatusBarButton(QIcon(":icons/trustedcoin-status.png"), + _("TrustedCoin"), action) + window.statusBar().addPermanentWidget(button) + self.start_request_thread(window.wallet) + + def auth_dialog(self, window): + d = WindowModalDialog(window, _("Authorization")) + vbox = QVBoxLayout(d) + pw = AmountEdit(None, is_int = True) + msg = _('Please enter your Google Authenticator code') + vbox.addWidget(QLabel(msg)) + grid = QGridLayout() + grid.setSpacing(8) + grid.addWidget(QLabel(_('Code')), 1, 0) + grid.addWidget(pw, 1, 1) + vbox.addLayout(grid) + msg = _('If you have lost your second factor, you need to restore your wallet from seed in order to request a new code.') + label = QLabel(msg) + label.setWordWrap(1) + vbox.addWidget(label) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + if not d.exec_(): + return + return pw.get_amount() + + def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): + wallet.handler_2fa.prompt_user_for_otp(wallet, tx, on_success, on_failure) + + def waiting_dialog(self, window, on_finished=None): + task = partial(self.request_billing_info, window.wallet) + return WaitingDialog(window, 'Getting billing information...', task, + on_finished) + + @hook + def abort_send(self, window): + wallet = window.wallet + if not isinstance(wallet, self.wallet_class): + return + if wallet.can_sign_without_server(): + return + if wallet.billing_info is None: + self.start_request_thread(wallet) + window.show_error(_('Requesting account info from TrustedCoin server...') + '\n' + + _('Please try again.')) + return True + return False + + def settings_dialog(self, window): + self.waiting_dialog(window, partial(self.show_settings_dialog, window)) + + def show_settings_dialog(self, window, success): + if not success: + window.show_message(_('Server not reachable.')) + return + + wallet = window.wallet + d = WindowModalDialog(window, _("TrustedCoin Information")) + d.setMinimumSize(500, 200) + vbox = QVBoxLayout(d) + hbox = QHBoxLayout() + + logo = QLabel() + logo.setPixmap(QPixmap(":icons/trustedcoin-status.png")) + msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '<br/>'\ + + _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" + label = QLabel(msg) + label.setOpenExternalLinks(1) + + hbox.addStretch(10) + hbox.addWidget(logo) + hbox.addStretch(10) + hbox.addWidget(label) + hbox.addStretch(10) + + vbox.addLayout(hbox) + vbox.addStretch(10) + + msg = _('TrustedCoin charges a small fee to co-sign transactions. The fee depends on how many prepaid transactions you buy. An extra output is added to your transaction every time you run out of prepaid transactions.') + '<br/>' + label = QLabel(msg) + label.setWordWrap(1) + vbox.addWidget(label) + + vbox.addStretch(10) + grid = QGridLayout() + vbox.addLayout(grid) + + price_per_tx = wallet.price_per_tx + n_prepay = wallet.num_prepay(self.config) + i = 0 + for k, v in sorted(price_per_tx.items()): + if k == 1: + continue + grid.addWidget(QLabel("Pay every %d transactions:"%k), i, 0) + grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1) + b = QRadioButton() + b.setChecked(k == n_prepay) + b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, True)) + grid.addWidget(b, i, 2) + i += 1 + + n = wallet.billing_info.get('tx_remaining', 0) + grid.addWidget(QLabel(_("Your wallet has {} prepaid transactions.").format(n)), i, 0) + vbox.addLayout(Buttons(CloseButton(d))) + d.exec_() + + def on_buy(self, window, k, v, d): + d.close() + if window.pluginsdialog: + window.pluginsdialog.close() + wallet = window.wallet + uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000) + wallet.is_billing = True + window.pay_to_URI(uri) + window.payto_e.setFrozen(True) + window.message_e.setFrozen(True) + window.amount_e.setFrozen(True) + + def go_online_dialog(self, wizard): + msg = [ + _("Your wallet file is: {}.").format(os.path.abspath(wizard.storage.path)), + _("You need to be online in order to complete the creation of " + "your wallet. If you generated your seed on an offline " + 'computer, click on "{}" to close this window, move your ' + "wallet file to an online computer, and reopen it with " + "Electrum.").format(_('Cancel')), + _('If you are online, click on "{}" to continue.').format(_('Next')) + ] + msg = '\n\n'.join(msg) + wizard.stack = [] + wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use')) + + def accept_terms_of_use(self, window): + vbox = QVBoxLayout() + vbox.addWidget(QLabel(_("Terms of Service"))) + + tos_e = TOS() + tos_e.setReadOnly(True) + vbox.addWidget(tos_e) + tos_received = False + + vbox.addWidget(QLabel(_("Please enter your e-mail address"))) + email_e = QLineEdit() + vbox.addWidget(email_e) + + next_button = window.next_button + prior_button_text = next_button.text() + next_button.setText(_('Accept')) + + def request_TOS(): + try: + tos = server.get_terms_of_service() + except Exception as e: + import traceback + traceback.print_exc(file=sys.stderr) + tos_e.error_signal.emit(_('Could not retrieve Terms of Service:') + + '\n' + str(e)) + return + self.TOS = tos + tos_e.tos_signal.emit() + + def on_result(): + tos_e.setText(self.TOS) + nonlocal tos_received + tos_received = True + set_enabled() + + def on_error(msg): + window.show_error(str(msg)) + window.terminate() + + def set_enabled(): + next_button.setEnabled(tos_received and is_valid_email(email_e.text())) + + tos_e.tos_signal.connect(on_result) + tos_e.error_signal.connect(on_error) + t = Thread(target=request_TOS) + t.setDaemon(True) + t.start() + email_e.textChanged.connect(set_enabled) + email_e.setFocus(True) + window.exec_layout(vbox, next_enabled=False) + next_button.setText(prior_button_text) + email = str(email_e.text()) + self.create_remote_key(email, window) + + def request_otp_dialog(self, window, short_id, otp_secret, xpub3): + vbox = QVBoxLayout() + if otp_secret is not None: + uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret) + l = QLabel("Please scan the following QR code in Google Authenticator. You may as well use the following key: %s"%otp_secret) + l.setWordWrap(True) + vbox.addWidget(l) + qrw = QRCodeWidget(uri) + vbox.addWidget(qrw, 1) + msg = _('Then, enter your Google Authenticator code:') + else: + label = QLabel( + "This wallet is already registered with TrustedCoin. " + "To finalize wallet creation, please enter your Google Authenticator Code. " + ) + label.setWordWrap(1) + vbox.addWidget(label) + msg = _('Google Authenticator code:') + hbox = QHBoxLayout() + hbox.addWidget(WWLabel(msg)) + pw = AmountEdit(None, is_int = True) + pw.setFocus(True) + pw.setMaximumWidth(50) + hbox.addWidget(pw) + vbox.addLayout(hbox) + cb_lost = QCheckBox(_("I have lost my Google Authenticator account")) + cb_lost.setToolTip(_("Check this box to request a new secret. You will need to retype your seed.")) + vbox.addWidget(cb_lost) + cb_lost.setVisible(otp_secret is None) + def set_enabled(): + b = True if cb_lost.isChecked() else len(pw.text()) == 6 + window.next_button.setEnabled(b) + pw.textChanged.connect(set_enabled) + cb_lost.toggled.connect(set_enabled) + window.exec_layout(vbox, next_enabled=False, raise_on_cancel=False) + self.check_otp(window, short_id, otp_secret, xpub3, pw.get_amount(), cb_lost.isChecked()) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py @@ -0,0 +1,672 @@ +#!/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 socket +import os +import requests +import json +import base64 +from urllib.parse import urljoin +from urllib.parse import quote + +from electrum import bitcoin, ecc, constants, keystore, version +from electrum.bitcoin import * +from electrum.mnemonic import Mnemonic +from electrum.wallet import Multisig_Wallet, Deterministic_Wallet +from electrum.i18n import _ +from electrum.plugin import BasePlugin, hook +from electrum.util import NotEnoughFunds +from electrum.storage import STO_EV_USER_PW + +# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server +def get_signing_xpub(): + if constants.net.TESTNET: + return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" + else: + return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" + +def get_billing_xpub(): + if constants.net.TESTNET: + return "tpubD6NzVbkrYhZ4X11EJFTJujsYbUmVASAYY7gXsEt4sL97AMBdypiH1E9ZVTpdXXEy3Kj9Eqd1UkxdGtvDt5z23DKsh6211CfNJo8bLLyem5r" + else: + return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" + +SEED_PREFIX = version.SEED_PREFIX_2FA + +DISCLAIMER = [ + _("Two-factor authentication is a service provided by TrustedCoin. " + "It uses a multi-signature wallet, where you own 2 of 3 keys. " + "The third key is stored on a remote server that signs transactions on " + "your behalf. To use this service, you will need a smartphone with " + "Google Authenticator installed."), + _("A small fee will be charged on each transaction that uses the " + "remote server. You may check and modify your billing preferences " + "once the installation is complete."), + _("Note that your coins are not locked in this service. You may withdraw " + "your funds at any time and at no cost, without the remote server, by " + "using the 'restore wallet' option with your wallet seed."), + _("The next step will generate the seed of your wallet. This seed will " + "NOT be saved in your computer, and it must be stored on paper. " + "To be safe from malware, you may want to do this on an offline " + "computer, and move your wallet later to an online computer."), +] + +KIVY_DISCLAIMER = [ + _("Two-factor authentication is a service provided by TrustedCoin. " + "To use it, you must have a separate device with Google Authenticator."), + _("This service uses a multi-signature wallet, where you own 2 of 3 keys. " + "The third key is stored on a remote server that signs transactions on " + "your behalf. A small fee will be charged on each transaction that uses the " + "remote server."), + _("Note that your coins are not locked in this service. You may withdraw " + "your funds at any time and at no cost, without the remote server, by " + "using the 'restore wallet' option with your wallet seed."), +] +RESTORE_MSG = _("Enter the seed for your 2-factor wallet:") + +class TrustedCoinException(Exception): + def __init__(self, message, status_code=0): + Exception.__init__(self, message) + self.status_code = status_code + + +class ErrorConnectingServer(Exception): + pass + + +class TrustedCoinCosignerClient(object): + def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/'): + self.base_url = base_url + self.debug = False + self.user_agent = user_agent + + def send_request(self, method, relative_url, data=None): + kwargs = {'headers': {}} + if self.user_agent: + kwargs['headers']['user-agent'] = self.user_agent + if method == 'get' and data: + kwargs['params'] = data + elif method == 'post' and data: + kwargs['data'] = json.dumps(data) + kwargs['headers']['content-type'] = 'application/json' + url = urljoin(self.base_url, relative_url) + if self.debug: + print('%s %s %s' % (method, url, data)) + try: + response = requests.request(method, url, **kwargs) + except Exception as e: + raise ErrorConnectingServer(e) + if self.debug: + print(response.text) + if response.status_code != 200: + message = str(response.text) + if response.headers.get('content-type') == 'application/json': + r = response.json() + if 'message' in r: + message = r['message'] + raise TrustedCoinException(message, response.status_code) + if response.headers.get('content-type') == 'application/json': + return response.json() + else: + return response.text + + def get_terms_of_service(self, billing_plan='electrum-per-tx-otp'): + """ + Returns the TOS for the given billing plan as a plain/text unicode string. + :param billing_plan: the plan to return the terms for + """ + payload = {'billing_plan': billing_plan} + return self.send_request('get', 'tos', payload) + + def create(self, xpubkey1, xpubkey2, email, billing_plan='electrum-per-tx-otp'): + """ + Creates a new cosigner resource. + :param xpubkey1: a bip32 extended public key (customarily the hot key) + :param xpubkey2: a bip32 extended public key (customarily the cold key) + :param email: a contact email + :param billing_plan: the billing plan for the cosigner + """ + payload = { + 'email': email, + 'xpubkey1': xpubkey1, + 'xpubkey2': xpubkey2, + 'billing_plan': billing_plan, + } + return self.send_request('post', 'cosigner', payload) + + def auth(self, id, otp): + """ + Attempt to authenticate for a particular cosigner. + :param id: the id of the cosigner + :param otp: the one time password + """ + payload = {'otp': otp} + return self.send_request('post', 'cosigner/%s/auth' % quote(id), payload) + + def get(self, id): + """ Get billing info """ + return self.send_request('get', 'cosigner/%s' % quote(id)) + + def get_challenge(self, id): + """ Get challenge to reset Google Auth secret """ + return self.send_request('get', 'cosigner/%s/otp_secret' % quote(id)) + + def reset_auth(self, id, challenge, signatures): + """ Reset Google Auth secret """ + payload = {'challenge':challenge, 'signatures':signatures} + return self.send_request('post', 'cosigner/%s/otp_secret' % quote(id), payload) + + def sign(self, id, transaction, otp): + """ + Attempt to authenticate for a particular cosigner. + :param id: the id of the cosigner + :param transaction: the hex encoded [partially signed] compact transaction to sign + :param otp: the one time password + """ + payload = { + 'otp': otp, + 'transaction': transaction + } + return self.send_request('post', 'cosigner/%s/sign' % quote(id), payload) + + def transfer_credit(self, id, recipient, otp, signature_callback): + """ + Transfer a cosigner's credits to another cosigner. + :param id: the id of the sending cosigner + :param recipient: the id of the recipient cosigner + :param otp: the one time password (of the sender) + :param signature_callback: a callback that signs a text message using xpubkey1/0/0 returning a compact sig + """ + payload = { + 'otp': otp, + 'recipient': recipient, + 'timestamp': int(time.time()), + + } + relative_url = 'cosigner/%s/transfer' % quote(id) + full_url = urljoin(self.base_url, relative_url) + headers = { + 'x-signature': signature_callback(full_url + '\n' + json.dumps(payload)) + } + return self.send_request('post', relative_url, payload, headers) + + +server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION) + +class Wallet_2fa(Multisig_Wallet): + + wallet_type = '2fa' + + def __init__(self, storage): + self.m, self.n = 2, 3 + Deterministic_Wallet.__init__(self, storage) + self.is_billing = False + self.billing_info = None + self._load_billing_addresses() + + def _load_billing_addresses(self): + billing_addresses = self.storage.get('trustedcoin_billing_addresses', {}) + self._billing_addresses = {} # index -> addr + # convert keys from str to int + for index, addr in list(billing_addresses.items()): + self._billing_addresses[int(index)] = addr + self._billing_addresses_set = set(self._billing_addresses.values()) # set of addrs + + def can_sign_without_server(self): + return not self.keystores['x2/'].is_watching_only() + + def get_user_id(self): + return get_user_id(self.storage) + + def min_prepay(self): + return min(self.price_per_tx.keys()) + + def num_prepay(self, config): + default = self.min_prepay() + n = config.get('trustedcoin_prepay', default) + if n not in self.price_per_tx: + n = default + return n + + def extra_fee(self, config): + if self.can_sign_without_server(): + return 0 + if self.billing_info is None: + self.plugin.start_request_thread(self) + return 0 + if self.billing_info.get('tx_remaining'): + return 0 + if self.is_billing: + return 0 + n = self.num_prepay(config) + price = int(self.price_per_tx[n]) + if price > 100000 * n: + raise Exception('too high trustedcoin fee ({} for {} txns)'.format(price, n)) + return price + + def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None, + change_addr=None, is_sweep=False): + mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction( + self, coins, o, config, fixed_fee, change_addr) + fee = self.extra_fee(config) if not is_sweep else 0 + if fee: + address = self.billing_info['billing_address'] + fee_output = (TYPE_ADDRESS, address, fee) + try: + tx = mk_tx(outputs + [fee_output]) + except NotEnoughFunds: + # TrustedCoin won't charge if the total inputs is + # lower than their fee + tx = mk_tx(outputs) + if tx.input_value() >= fee: + raise + self.print_error("not charging for this tx") + else: + tx = mk_tx(outputs) + return tx + + def on_otp(self, tx, otp): + if not otp: + self.print_error("sign_transaction: no auth code") + return + otp = int(otp) + long_user_id, short_id = self.get_user_id() + raw_tx = tx.serialize_to_network() + r = server.sign(short_id, raw_tx, otp) + if r: + raw_tx = r.get('transaction') + tx.update(raw_tx) + self.print_error("twofactor: is complete", tx.is_complete()) + # reset billing_info + self.billing_info = None + self.plugin.start_request_thread(self) + + def add_new_billing_address(self, billing_index: int, address: str): + saved_addr = self._billing_addresses.get(billing_index) + if saved_addr is not None: + if saved_addr == address: + return # already saved this address + else: + raise Exception('trustedcoin billing address inconsistency.. ' + 'for index {}, already saved {}, now got {}' + .format(billing_index, saved_addr, address)) + # do we have all prior indices? (are we synced?) + largest_index_we_have = max(self._billing_addresses) if self._billing_addresses else -1 + if largest_index_we_have + 1 < billing_index: # need to sync + for i in range(largest_index_we_have + 1, billing_index): + addr = make_billing_address(self, i) + self._billing_addresses[i] = addr + self._billing_addresses_set.add(addr) + # save this address; and persist to disk + self._billing_addresses[billing_index] = address + self._billing_addresses_set.add(address) + self.storage.put('trustedcoin_billing_addresses', self._billing_addresses) + # FIXME this often runs in a daemon thread, where storage.write will fail + self.storage.write() + + def is_billing_address(self, addr: str) -> bool: + return addr in self._billing_addresses_set + + +# Utility functions + +def get_user_id(storage): + def make_long_id(xpub_hot, xpub_cold): + return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold]))) + xpub1 = storage.get('x1/')['xpub'] + xpub2 = storage.get('x2/')['xpub'] + long_id = make_long_id(xpub1, xpub2) + short_id = hashlib.sha256(long_id).hexdigest() + return long_id, short_id + +def make_xpub(xpub, s): + version, _, _, _, c, cK = deserialize_xpub(xpub) + cK2, c2 = bitcoin._CKD_pub(cK, c, s) + return bitcoin.serialize_xpub(version, c2, cK2) + +def make_billing_address(wallet, num): + long_id, short_id = wallet.get_user_id() + xpub = make_xpub(get_billing_xpub(), long_id) + version, _, _, _, c, cK = deserialize_xpub(xpub) + cK, c = bitcoin.CKD_pub(cK, c, num) + return bitcoin.public_key_to_p2pkh(cK) + + +class TrustedCoinPlugin(BasePlugin): + wallet_class = Wallet_2fa + disclaimer_msg = DISCLAIMER + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.wallet_class.plugin = self + self.requesting = False + + @staticmethod + def is_valid_seed(seed): + return bitcoin.is_new_seed(seed, SEED_PREFIX) + + def is_available(self): + return True + + def is_enabled(self): + return True + + def can_user_disable(self): + return False + + @hook + def tc_sign_wrapper(self, wallet, tx, on_success, on_failure): + if not isinstance(wallet, self.wallet_class): + return + if tx.is_complete(): + return + if wallet.can_sign_without_server(): + return + if not wallet.keystores['x3/'].get_tx_derivations(tx): + self.print_error("twofactor: xpub3 not needed") + return + def wrapper(tx): + self.prompt_user_for_otp(wallet, tx, on_success, on_failure) + return wrapper + + @hook + def get_tx_extra_fee(self, wallet, tx): + if type(wallet) != Wallet_2fa: + return + for _type, addr, amount in tx.outputs(): + if _type == TYPE_ADDRESS and wallet.is_billing_address(addr): + return addr, amount + + def finish_requesting(func): + def f(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + finally: + self.requesting = False + return f + + @finish_requesting + def request_billing_info(self, wallet): + if wallet.can_sign_without_server(): + return + self.print_error("request billing info") + try: + billing_info = server.get(wallet.get_user_id()[1]) + except ErrorConnectingServer as e: + self.print_error('cannot connect to TrustedCoin server: {}'.format(e)) + return + billing_index = billing_info['billing_index'] + billing_address = make_billing_address(wallet, billing_index) + if billing_address != billing_info['billing_address']: + raise Exception('unexpected trustedcoin billing address: expected {}, received {}' + .format(billing_address, billing_info['billing_address'])) + wallet.add_new_billing_address(billing_index, billing_address) + wallet.billing_info = billing_info + wallet.price_per_tx = dict(billing_info['price_per_tx']) + wallet.price_per_tx.pop(1, None) + return True + + def start_request_thread(self, wallet): + from threading import Thread + if self.requesting is False: + self.requesting = True + t = Thread(target=self.request_billing_info, args=(wallet,)) + t.setDaemon(True) + t.start() + return t + + def make_seed(self): + return Mnemonic('english').make_seed(seed_type='2fa', num_bits=128) + + @hook + def do_clear(self, window): + window.wallet.is_billing = False + + def show_disclaimer(self, wizard): + wizard.set_icon(':icons/trustedcoin-wizard.png') + wizard.stack = [] + wizard.confirm_dialog(title='Disclaimer', message='\n\n'.join(self.disclaimer_msg), run_next = lambda x: wizard.run('choose_seed')) + + def choose_seed(self, wizard): + title = _('Create or restore') + message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') + choices = [ + ('create_seed', _('Create a new seed')), + ('restore_wallet', _('I already have a seed')), + ] + wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run) + + def create_seed(self, wizard): + seed = self.make_seed() + f = lambda x: wizard.request_passphrase(seed, x) + wizard.show_seed_dialog(run_next=f, seed_text=seed) + + @classmethod + def get_xkeys(self, seed, passphrase, derivation): + from electrum.mnemonic import Mnemonic + from electrum.keystore import bip32_root, bip32_private_derivation + bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase) + xprv, xpub = bip32_root(bip32_seed, 'standard') + xprv, xpub = bip32_private_derivation(xprv, "m/", derivation) + return xprv, xpub + + @classmethod + def xkeys_from_seed(self, seed, passphrase): + words = seed.split() + n = len(words) + # old version use long seed phrases + if n >= 20: + # note: pre-2.7 2fa seeds were typically 24-25 words, however they + # could probabilistically be arbitrarily shorter due to a bug. (see #3611) + # the probability of it being < 20 words is about 2^(-(256+12-19*11)) = 2^(-59) + if passphrase != '': + raise Exception('old 2fa seed cannot have passphrase') + xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/") + xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/") + elif n==12: + xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/") + xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/") + else: + raise Exception('unrecognized seed length: {} words'.format(n)) + return xprv1, xpub1, xprv2, xpub2 + + def create_keystore(self, wizard, seed, passphrase): + # this overloads the wizard's method + xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) + k1 = keystore.from_xprv(xprv1) + k2 = keystore.from_xpub(xpub2) + wizard.request_password(run_next=lambda pw, encrypt: self.on_password(wizard, pw, encrypt, k1, k2)) + + def on_password(self, wizard, password, encrypt_storage, k1, k2): + k1.update_password(None, password) + wizard.storage.set_keystore_encryption(bool(password)) + if encrypt_storage: + wizard.storage.set_password(password, enc_version=STO_EV_USER_PW) + wizard.storage.put('x1/', k1.dump()) + wizard.storage.put('x2/', k2.dump()) + wizard.storage.write() + self.go_online_dialog(wizard) + + def restore_wallet(self, wizard): + wizard.opt_bip39 = False + wizard.opt_ext = True + title = _("Restore two-factor Wallet") + f = lambda seed, is_bip39, is_ext: wizard.run('on_restore_seed', seed, is_ext) + wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed) + + def on_restore_seed(self, wizard, seed, is_ext): + f = lambda x: self.restore_choice(wizard, seed, x) + wizard.passphrase_dialog(run_next=f) if is_ext else f('') + + def restore_choice(self, wizard, seed, passphrase): + wizard.set_icon(':icons/trustedcoin-wizard.png') + wizard.stack = [] + title = _('Restore 2FA wallet') + msg = ' '.join([ + 'You are going to restore a wallet protected with two-factor authentication.', + 'Do you want to keep using two-factor authentication with this wallet,', + 'or do you want to disable it, and have two master private keys in your wallet?' + ]) + choices = [('keep', 'Keep'), ('disable', 'Disable')] + f = lambda x: self.on_choice(wizard, seed, passphrase, x) + wizard.choice_dialog(choices=choices, message=msg, title=title, run_next=f) + + def on_choice(self, wizard, seed, passphrase, x): + if x == 'disable': + f = lambda pw, encrypt: wizard.run('on_restore_pw', seed, passphrase, pw, encrypt) + wizard.request_password(run_next=f) + else: + self.create_keystore(wizard, seed, passphrase) + + def on_restore_pw(self, wizard, seed, passphrase, password, encrypt_storage): + storage = wizard.storage + xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) + k1 = keystore.from_xprv(xprv1) + k2 = keystore.from_xprv(xprv2) + k1.add_seed(seed) + k1.update_password(None, password) + k2.update_password(None, password) + storage.put('x1/', k1.dump()) + storage.put('x2/', k2.dump()) + long_user_id, short_id = get_user_id(storage) + xpub3 = make_xpub(get_signing_xpub(), long_user_id) + k3 = keystore.from_xpub(xpub3) + storage.put('x3/', k3.dump()) + + storage.set_keystore_encryption(bool(password)) + if encrypt_storage: + storage.set_password(password, enc_version=STO_EV_USER_PW) + + wizard.wallet = Wallet_2fa(storage) + wizard.create_addresses() + + + def create_remote_key(self, email, wizard): + xpub1 = wizard.storage.get('x1/')['xpub'] + xpub2 = wizard.storage.get('x2/')['xpub'] + # Generate third key deterministically. + long_user_id, short_id = get_user_id(wizard.storage) + xpub3 = make_xpub(get_signing_xpub(), long_user_id) + # secret must be sent by the server + try: + r = server.create(xpub1, xpub2, email) + except (socket.error, ErrorConnectingServer): + wizard.show_message('Server not reachable, aborting') + wizard.terminate() + return + except TrustedCoinException as e: + if e.status_code == 409: + r = None + else: + wizard.show_message(str(e)) + return + if r is None: + otp_secret = None + else: + otp_secret = r.get('otp_secret') + if not otp_secret: + wizard.show_message(_('Error')) + return + _xpub3 = r['xpubkey_cosigner'] + _id = r['id'] + if short_id != _id: + wizard.show_message("unexpected trustedcoin short_id: expected {}, received {}" + .format(short_id, _id)) + return + if xpub3 != _xpub3: + wizard.show_message("unexpected trustedcoin xpub3: expected {}, received {}" + .format(xpub3, _xpub3)) + return + self.request_otp_dialog(wizard, short_id, otp_secret, xpub3) + + def check_otp(self, wizard, short_id, otp_secret, xpub3, otp, reset): + if otp: + self.do_auth(wizard, short_id, otp, xpub3) + elif reset: + wizard.opt_bip39 = False + wizard.opt_ext = True + f = lambda seed, is_bip39, is_ext: wizard.run('on_reset_seed', short_id, seed, is_ext, xpub3) + wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed) + + def on_reset_seed(self, wizard, short_id, seed, is_ext, xpub3): + f = lambda passphrase: wizard.run('on_reset_auth', short_id, seed, passphrase, xpub3) + wizard.passphrase_dialog(run_next=f) if is_ext else f('') + + def do_auth(self, wizard, short_id, otp, xpub3): + try: + server.auth(short_id, otp) + except TrustedCoinException as e: + if e.status_code == 400: # invalid OTP + wizard.show_message(_('Invalid one-time password.')) + # ask again for otp + self.request_otp_dialog(wizard, short_id, None, xpub3) + else: + wizard.show_message(str(e)) + wizard.terminate() + except Exception as e: + wizard.show_message(str(e)) + wizard.terminate() + else: + k3 = keystore.from_xpub(xpub3) + wizard.storage.put('x3/', k3.dump()) + wizard.storage.put('use_trustedcoin', True) + wizard.storage.write() + wizard.wallet = Wallet_2fa(wizard.storage) + wizard.run('create_addresses') + + def on_reset_auth(self, wizard, short_id, seed, passphrase, xpub3): + xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) + if (wizard.storage.get('x1/')['xpub'] != xpub1 or + wizard.storage.get('x2/')['xpub'] != xpub2): + wizard.show_message(_('Incorrect seed')) + return + r = server.get_challenge(short_id) + challenge = r.get('challenge') + message = 'TRUSTEDCOIN CHALLENGE: ' + challenge + def f(xprv): + _, _, _, _, c, k = deserialize_xprv(xprv) + pk = bip32_private_key([0, 0], k, c) + key = ecc.ECPrivkey(pk) + sig = key.sign_message(message, True) + return base64.b64encode(sig).decode() + + signatures = [f(x) for x in [xprv1, xprv2]] + r = server.reset_auth(short_id, challenge, signatures) + new_secret = r.get('otp_secret') + if not new_secret: + wizard.show_message(_('Request rejected by server')) + return + self.request_otp_dialog(wizard, short_id, new_secret, xpub3) + + @hook + def get_action(self, storage): + if storage.get('wallet_type') != '2fa': + return + if not storage.get('x1/'): + return self, 'show_disclaimer' + if not storage.get('x2/'): + return self, 'show_disclaimer' + if not storage.get('x3/'): + return self, 'accept_terms_of_use' diff --git a/electrum/plugins/virtualkeyboard/__init__.py b/electrum/plugins/virtualkeyboard/__init__.py @@ -0,0 +1,5 @@ +from electrum.i18n import _ + +fullname = 'Virtual Keyboard' +description = '%s\n%s' % (_("Add an optional virtual keyboard to the password dialog."), _("Warning: do not use this if it makes you pick a weaker password.")) +available_for = ['qt'] diff --git a/electrum/plugins/virtualkeyboard/qt.py b/electrum/plugins/virtualkeyboard/qt.py @@ -0,0 +1,61 @@ +from PyQt5.QtGui import * +from PyQt5.QtWidgets import (QVBoxLayout, QGridLayout, QPushButton) +from electrum.plugin import BasePlugin, hook +from electrum.i18n import _ +import random + + +class Plugin(BasePlugin): + vkb = None + vkb_index = 0 + + @hook + def password_dialog(self, pw, grid, pos): + vkb_button = QPushButton(_("+")) + vkb_button.setFixedWidth(20) + vkb_button.clicked.connect(lambda: self.toggle_vkb(grid, pw)) + grid.addWidget(vkb_button, pos, 2) + self.kb_pos = 2 + self.vkb = None + + def toggle_vkb(self, grid, pw): + if self.vkb: + grid.removeItem(self.vkb) + self.vkb = self.virtual_keyboard(self.vkb_index, pw) + grid.addLayout(self.vkb, self.kb_pos, 0, 1, 3) + self.vkb_index += 1 + + def virtual_keyboard(self, i, pw): + i = i % 3 + if i == 0: + chars = 'abcdefghijklmnopqrstuvwxyz ' + elif i == 1: + chars = 'ABCDEFGHIJKLMNOPQRTSUVWXYZ ' + elif i == 2: + chars = '1234567890!?.,;:/%&()[]{}+-' + + n = len(chars) + s = [] + for i in range(n): + while True: + k = random.randint(0, n - 1) + if k not in s: + s.append(k) + break + + def add_target(t): + return lambda: pw.setText(str(pw.text()) + t) + + vbox = QVBoxLayout() + grid = QGridLayout() + grid.setSpacing(2) + for i in range(n): + l_button = QPushButton(chars[s[i]]) + l_button.setFixedWidth(25) + l_button.setFixedHeight(25) + l_button.clicked.connect(add_target(chars[s[i]])) + grid.addWidget(l_button, i // 6, i % 6) + + vbox.addLayout(grid) + + return vbox diff --git a/electrum/qrscanner.py b/electrum/qrscanner.py @@ -0,0 +1,88 @@ +#!/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 os +import sys +import ctypes + +if sys.platform == 'darwin': + name = 'libzbar.dylib' +elif sys.platform in ('windows', 'win32'): + name = 'libzbar-0.dll' +else: + name = 'libzbar.so.0' + +try: + libzbar = ctypes.cdll.LoadLibrary(name) +except BaseException: + libzbar = None + + +def scan_barcode(device='', timeout=-1, display=True, threaded=False, try_again=True): + if libzbar is None: + raise RuntimeError("Cannot start QR scanner; zbar not available.") + libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p + libzbar.zbar_processor_create.restype = ctypes.POINTER(ctypes.c_int) + libzbar.zbar_processor_get_results.restype = ctypes.POINTER(ctypes.c_int) + libzbar.zbar_symbol_set_first_symbol.restype = ctypes.POINTER(ctypes.c_int) + proc = libzbar.zbar_processor_create(threaded) + libzbar.zbar_processor_request_size(proc, 640, 480) + if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0: + if try_again: + # workaround for a bug in "ZBar for Windows" + # libzbar.zbar_processor_init always seem to fail the first time around + return scan_barcode(device, timeout, display, threaded, try_again=False) + raise RuntimeError("Can not start QR scanner; initialization failed.") + libzbar.zbar_processor_set_visible(proc) + if libzbar.zbar_process_one(proc, timeout): + symbols = libzbar.zbar_processor_get_results(proc) + else: + symbols = None + libzbar.zbar_processor_destroy(proc) + if symbols is None: + return + if not libzbar.zbar_symbol_set_get_size(symbols): + return + symbol = libzbar.zbar_symbol_set_first_symbol(symbols) + data = libzbar.zbar_symbol_get_data(symbol) + return data.decode('utf8') + +def _find_system_cameras(): + device_root = "/sys/class/video4linux" + devices = {} # Name -> device + if os.path.exists(device_root): + for device in os.listdir(device_root): + try: + with open(os.path.join(device_root, device, 'name')) as f: + name = f.read() + except IOError: + continue + name = name.strip('\n') + devices[name] = os.path.join("/dev", device) + return devices + + +if __name__ == "__main__": + print(scan_barcode()) diff --git a/electrum/ripemd.py b/electrum/ripemd.py @@ -0,0 +1,393 @@ +## ripemd.py - pure Python implementation of the RIPEMD-160 algorithm. +## Bjorn Edstrom <be@bjrn.se> 16 december 2007. +## +## Copyrights +## ========== +## +## This code is a derived from an implementation by Markus Friedl which is +## subject to the following license. This Python implementation is not +## subject to any other license. +## +##/* +## * Copyright (c) 2001 Markus Friedl. All rights reserved. +## * +## * Redistribution and use in source and binary forms, with or without +## * modification, are permitted provided that the following conditions +## * are met: +## * 1. Redistributions of source code must retain the above copyright +## * notice, this list of conditions and the following disclaimer. +## * 2. Redistributions in binary form must reproduce the above copyright +## * notice, this list of conditions and the following disclaimer in the +## * documentation and/or other materials provided with the distribution. +## * +## * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +## * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +## * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +## * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +## * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +## * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +## * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## */ +##/* +## * Preneel, Bosselaers, Dobbertin, "The Cryptographic Hash Function RIPEMD-160", +## * RSA Laboratories, CryptoBytes, Volume 3, Number 2, Autumn 1997, +## * ftp://ftp.rsasecurity.com/pub/cryptobytes/crypto3n2.pdf +## */ + +#block_size = 1 +digest_size = 20 +digestsize = 20 + +class RIPEMD160: + """Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed.""" + + def __init__(self, arg=None): + self.ctx = RMDContext() + if arg: + self.update(arg) + self.dig = None + + def update(self, arg): + """update(arg)""" + RMD160Update(self.ctx, arg, len(arg)) + self.dig = None + + def digest(self): + """digest()""" + if self.dig: + return self.dig + ctx = self.ctx.copy() + self.dig = RMD160Final(self.ctx) + self.ctx = ctx + return self.dig + + def hexdigest(self): + """hexdigest()""" + dig = self.digest() + hex_digest = '' + for d in dig: + hex_digest += '%02x' % d + return hex_digest + + def copy(self): + """copy()""" + import copy + return copy.deepcopy(self) + + + +def new(arg=None): + """Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed.""" + return RIPEMD160(arg) + + + +# +# Private. +# + +class RMDContext: + def __init__(self): + self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, + 0x10325476, 0xC3D2E1F0] # uint32 + self.count = 0 # uint64 + self.buffer = [0]*64 # uchar + def copy(self): + ctx = RMDContext() + ctx.state = self.state[:] + ctx.count = self.count + ctx.buffer = self.buffer[:] + return ctx + +K0 = 0x00000000 +K1 = 0x5A827999 +K2 = 0x6ED9EBA1 +K3 = 0x8F1BBCDC +K4 = 0xA953FD4E + +KK0 = 0x50A28BE6 +KK1 = 0x5C4DD124 +KK2 = 0x6D703EF3 +KK3 = 0x7A6D76E9 +KK4 = 0x00000000 + +def ROL(n, x): + return ((x << n) & 0xffffffff) | (x >> (32 - n)) + +def F0(x, y, z): + return x ^ y ^ z + +def F1(x, y, z): + return (x & y) | (((~x) % 0x100000000) & z) + +def F2(x, y, z): + return (x | ((~y) % 0x100000000)) ^ z + +def F3(x, y, z): + return (x & z) | (((~z) % 0x100000000) & y) + +def F4(x, y, z): + return x ^ (y | ((~z) % 0x100000000)) + +def R(a, b, c, d, e, Fj, Kj, sj, rj, X): + a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e + c = ROL(10, c) + return a % 0x100000000, c + +PADDING = [0x80] + [0]*63 + +import sys +import struct + +def RMD160Transform(state, block): #uint32 state[5], uchar block[64] + x = [0]*16 + if sys.byteorder == 'little': + x = struct.unpack('<16L', bytes([x for x in block[0:64]])) + else: + raise "Error!!" + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + #/* Round 1 */ + a, c = R(a, b, c, d, e, F0, K0, 11, 0, x); + e, b = R(e, a, b, c, d, F0, K0, 14, 1, x); + d, a = R(d, e, a, b, c, F0, K0, 15, 2, x); + c, e = R(c, d, e, a, b, F0, K0, 12, 3, x); + b, d = R(b, c, d, e, a, F0, K0, 5, 4, x); + a, c = R(a, b, c, d, e, F0, K0, 8, 5, x); + e, b = R(e, a, b, c, d, F0, K0, 7, 6, x); + d, a = R(d, e, a, b, c, F0, K0, 9, 7, x); + c, e = R(c, d, e, a, b, F0, K0, 11, 8, x); + b, d = R(b, c, d, e, a, F0, K0, 13, 9, x); + a, c = R(a, b, c, d, e, F0, K0, 14, 10, x); + e, b = R(e, a, b, c, d, F0, K0, 15, 11, x); + d, a = R(d, e, a, b, c, F0, K0, 6, 12, x); + c, e = R(c, d, e, a, b, F0, K0, 7, 13, x); + b, d = R(b, c, d, e, a, F0, K0, 9, 14, x); + a, c = R(a, b, c, d, e, F0, K0, 8, 15, x); #/* #15 */ + #/* Round 2 */ + e, b = R(e, a, b, c, d, F1, K1, 7, 7, x); + d, a = R(d, e, a, b, c, F1, K1, 6, 4, x); + c, e = R(c, d, e, a, b, F1, K1, 8, 13, x); + b, d = R(b, c, d, e, a, F1, K1, 13, 1, x); + a, c = R(a, b, c, d, e, F1, K1, 11, 10, x); + e, b = R(e, a, b, c, d, F1, K1, 9, 6, x); + d, a = R(d, e, a, b, c, F1, K1, 7, 15, x); + c, e = R(c, d, e, a, b, F1, K1, 15, 3, x); + b, d = R(b, c, d, e, a, F1, K1, 7, 12, x); + a, c = R(a, b, c, d, e, F1, K1, 12, 0, x); + e, b = R(e, a, b, c, d, F1, K1, 15, 9, x); + d, a = R(d, e, a, b, c, F1, K1, 9, 5, x); + c, e = R(c, d, e, a, b, F1, K1, 11, 2, x); + b, d = R(b, c, d, e, a, F1, K1, 7, 14, x); + a, c = R(a, b, c, d, e, F1, K1, 13, 11, x); + e, b = R(e, a, b, c, d, F1, K1, 12, 8, x); #/* #31 */ + #/* Round 3 */ + d, a = R(d, e, a, b, c, F2, K2, 11, 3, x); + c, e = R(c, d, e, a, b, F2, K2, 13, 10, x); + b, d = R(b, c, d, e, a, F2, K2, 6, 14, x); + a, c = R(a, b, c, d, e, F2, K2, 7, 4, x); + e, b = R(e, a, b, c, d, F2, K2, 14, 9, x); + d, a = R(d, e, a, b, c, F2, K2, 9, 15, x); + c, e = R(c, d, e, a, b, F2, K2, 13, 8, x); + b, d = R(b, c, d, e, a, F2, K2, 15, 1, x); + a, c = R(a, b, c, d, e, F2, K2, 14, 2, x); + e, b = R(e, a, b, c, d, F2, K2, 8, 7, x); + d, a = R(d, e, a, b, c, F2, K2, 13, 0, x); + c, e = R(c, d, e, a, b, F2, K2, 6, 6, x); + b, d = R(b, c, d, e, a, F2, K2, 5, 13, x); + a, c = R(a, b, c, d, e, F2, K2, 12, 11, x); + e, b = R(e, a, b, c, d, F2, K2, 7, 5, x); + d, a = R(d, e, a, b, c, F2, K2, 5, 12, x); #/* #47 */ + #/* Round 4 */ + c, e = R(c, d, e, a, b, F3, K3, 11, 1, x); + b, d = R(b, c, d, e, a, F3, K3, 12, 9, x); + a, c = R(a, b, c, d, e, F3, K3, 14, 11, x); + e, b = R(e, a, b, c, d, F3, K3, 15, 10, x); + d, a = R(d, e, a, b, c, F3, K3, 14, 0, x); + c, e = R(c, d, e, a, b, F3, K3, 15, 8, x); + b, d = R(b, c, d, e, a, F3, K3, 9, 12, x); + a, c = R(a, b, c, d, e, F3, K3, 8, 4, x); + e, b = R(e, a, b, c, d, F3, K3, 9, 13, x); + d, a = R(d, e, a, b, c, F3, K3, 14, 3, x); + c, e = R(c, d, e, a, b, F3, K3, 5, 7, x); + b, d = R(b, c, d, e, a, F3, K3, 6, 15, x); + a, c = R(a, b, c, d, e, F3, K3, 8, 14, x); + e, b = R(e, a, b, c, d, F3, K3, 6, 5, x); + d, a = R(d, e, a, b, c, F3, K3, 5, 6, x); + c, e = R(c, d, e, a, b, F3, K3, 12, 2, x); #/* #63 */ + #/* Round 5 */ + b, d = R(b, c, d, e, a, F4, K4, 9, 4, x); + a, c = R(a, b, c, d, e, F4, K4, 15, 0, x); + e, b = R(e, a, b, c, d, F4, K4, 5, 5, x); + d, a = R(d, e, a, b, c, F4, K4, 11, 9, x); + c, e = R(c, d, e, a, b, F4, K4, 6, 7, x); + b, d = R(b, c, d, e, a, F4, K4, 8, 12, x); + a, c = R(a, b, c, d, e, F4, K4, 13, 2, x); + e, b = R(e, a, b, c, d, F4, K4, 12, 10, x); + d, a = R(d, e, a, b, c, F4, K4, 5, 14, x); + c, e = R(c, d, e, a, b, F4, K4, 12, 1, x); + b, d = R(b, c, d, e, a, F4, K4, 13, 3, x); + a, c = R(a, b, c, d, e, F4, K4, 14, 8, x); + e, b = R(e, a, b, c, d, F4, K4, 11, 11, x); + d, a = R(d, e, a, b, c, F4, K4, 8, 6, x); + c, e = R(c, d, e, a, b, F4, K4, 5, 15, x); + b, d = R(b, c, d, e, a, F4, K4, 6, 13, x); #/* #79 */ + + aa = a; + bb = b; + cc = c; + dd = d; + ee = e; + + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + #/* Parallel round 1 */ + a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) + e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) + d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) + c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) + b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) + a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) + e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) + d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) + c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) + b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) + a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) + e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) + d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) + c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) + b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) + a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) #/* #15 */ + #/* Parallel round 2 */ + e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) + d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) + c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) + a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) + e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) + d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) + c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) + a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) + e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) + d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) + c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) + b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) + a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) + e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) #/* #31 */ + #/* Parallel round 3 */ + d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) + b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) + e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) + c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) + a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) + c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) + b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) + a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) + e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) #/* #47 */ + #/* Parallel round 4 */ + c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) + b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) + a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) + e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) + d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) + c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) + b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) + a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) + e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) + d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) + c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) + b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) + a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) + e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) + d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) + c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) #/* #63 */ + #/* Parallel round 5 */ + b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) + e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) + d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) + c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) + b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) + a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) + e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) + d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) + c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) + b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) + e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) + d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) + c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) + b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) #/* #79 */ + + t = (state[1] + cc + d) % 0x100000000; + state[1] = (state[2] + dd + e) % 0x100000000; + state[2] = (state[3] + ee + a) % 0x100000000; + state[3] = (state[4] + aa + b) % 0x100000000; + state[4] = (state[0] + bb + c) % 0x100000000; + state[0] = t % 0x100000000; + + pass + + +def RMD160Update(ctx, inp, inplen): + if type(inp) == str: + inp = [ord(i)&0xff for i in inp] + + have = (ctx.count // 8) % 64 + need = 64 - have + ctx.count += 8 * inplen + off = 0 + if inplen >= need: + if have: + for i in range(need): + ctx.buffer[have+i] = inp[i] + RMD160Transform(ctx.state, ctx.buffer) + off = need + have = 0 + while off + 64 <= inplen: + RMD160Transform(ctx.state, inp[off:]) #<--- + off += 64 + if off < inplen: + # memcpy(ctx->buffer + have, input+off, len-off); + for i in range(inplen - off): + ctx.buffer[have+i] = inp[off+i] + +def RMD160Final(ctx): + size = struct.pack("<Q", ctx.count) + padlen = 64 - ((ctx.count // 8) % 64) + if padlen < 1+8: + padlen += 64 + RMD160Update(ctx, PADDING, padlen-8) + RMD160Update(ctx, size, 8) + return struct.pack("<5L", *ctx.state) + + +assert '37f332f68db77bd9d7edd4969571ad671cf9dd3b' == \ + new(b'The quick brown fox jumps over the lazy dog').hexdigest() +assert '132072df690933835eb8b6ad0b77e7b6f14acad7' == \ + new(b'The quick brown fox jumps over the lazy cog').hexdigest() +assert '9c1185a5c5e9fc54612808977ee8f548b2258d31' == \ + new('').hexdigest() diff --git a/electrum/rsakey.py b/electrum/rsakey.py @@ -0,0 +1,542 @@ +#!/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. + +# This module uses functions from TLSLite (public domain) +# +# TLSLite Authors: +# Trevor Perrin +# Martin von Loewis - python 3 port +# Yngve Pettersen (ported by Paul Sokolovsky) - TLS 1.2 +# + +"""Pure-Python RSA implementation.""" + +import os +import math +import hashlib + +from .pem import * + + +def SHA1(x): + return hashlib.sha1(x).digest() + + +# ************************************************************************** +# PRNG Functions +# ************************************************************************** + +# Check that os.urandom works +import zlib +length = len(zlib.compress(os.urandom(1000))) +assert(length > 900) + +def getRandomBytes(howMany): + b = bytearray(os.urandom(howMany)) + assert(len(b) == howMany) + return b + +prngName = "os.urandom" + + +# ************************************************************************** +# Converter Functions +# ************************************************************************** + +def bytesToNumber(b): + total = 0 + multiplier = 1 + for count in range(len(b)-1, -1, -1): + byte = b[count] + total += multiplier * byte + multiplier *= 256 + return total + +def numberToByteArray(n, howManyBytes=None): + """Convert an integer into a bytearray, zero-pad to howManyBytes. + + The returned bytearray may be smaller than howManyBytes, but will + not be larger. The returned bytearray will contain a big-endian + encoding of the input integer (n). + """ + if howManyBytes == None: + howManyBytes = numBytes(n) + b = bytearray(howManyBytes) + for count in range(howManyBytes-1, -1, -1): + b[count] = int(n % 256) + n >>= 8 + return b + +def mpiToNumber(mpi): #mpi is an openssl-format bignum string + if (ord(mpi[4]) & 0x80) !=0: #Make sure this is a positive number + raise AssertionError() + b = bytearray(mpi[4:]) + return bytesToNumber(b) + +def numberToMPI(n): + b = numberToByteArray(n) + ext = 0 + #If the high-order bit is going to be set, + #add an extra byte of zeros + if (numBits(n) & 0x7)==0: + ext = 1 + length = numBytes(n) + ext + b = bytearray(4+ext) + b + b[0] = (length >> 24) & 0xFF + b[1] = (length >> 16) & 0xFF + b[2] = (length >> 8) & 0xFF + b[3] = length & 0xFF + return bytes(b) + + +# ************************************************************************** +# Misc. Utility Functions +# ************************************************************************** + +def numBits(n): + if n==0: + return 0 + s = "%x" % n + return ((len(s)-1)*4) + \ + {'0':0, '1':1, '2':2, '3':2, + '4':3, '5':3, '6':3, '7':3, + '8':4, '9':4, 'a':4, 'b':4, + 'c':4, 'd':4, 'e':4, 'f':4, + }[s[0]] + return int(math.floor(math.log(n, 2))+1) + +def numBytes(n): + if n==0: + return 0 + bits = numBits(n) + return int(math.ceil(bits / 8.0)) + +# ************************************************************************** +# Big Number Math +# ************************************************************************** + +def getRandomNumber(low, high): + if low >= high: + raise AssertionError() + howManyBits = numBits(high) + howManyBytes = numBytes(high) + lastBits = howManyBits % 8 + while 1: + bytes = getRandomBytes(howManyBytes) + if lastBits: + bytes[0] = bytes[0] % (1 << lastBits) + n = bytesToNumber(bytes) + if n >= low and n < high: + return n + +def gcd(a,b): + a, b = max(a,b), min(a,b) + while b: + a, b = b, a % b + return a + +def lcm(a, b): + return (a * b) // gcd(a, b) + +#Returns inverse of a mod b, zero if none +#Uses Extended Euclidean Algorithm +def invMod(a, b): + c, d = a, b + uc, ud = 1, 0 + while c != 0: + q = d // c + c, d = d-(q*c), c + uc, ud = ud - (q * uc), uc + if d == 1: + return ud % b + return 0 + + +def powMod(base, power, modulus): + if power < 0: + result = pow(base, power*-1, modulus) + result = invMod(result, modulus) + return result + else: + return pow(base, power, modulus) + +#Pre-calculate a sieve of the ~100 primes < 1000: +def makeSieve(n): + sieve = list(range(n)) + for count in range(2, int(math.sqrt(n))+1): + if sieve[count] == 0: + continue + x = sieve[count] * 2 + while x < len(sieve): + sieve[x] = 0 + x += sieve[count] + sieve = [x for x in sieve[2:] if x] + return sieve + +sieve = makeSieve(1000) + +def isPrime(n, iterations=5, display=False): + #Trial division with sieve + for x in sieve: + if x >= n: return True + if n % x == 0: return False + #Passed trial division, proceed to Rabin-Miller + #Rabin-Miller implemented per Ferguson & Schneier + #Compute s, t for Rabin-Miller + if display: print("*", end=' ') + s, t = n-1, 0 + while s % 2 == 0: + s, t = s//2, t+1 + #Repeat Rabin-Miller x times + a = 2 #Use 2 as a base for first iteration speedup, per HAC + for count in range(iterations): + v = powMod(a, s, n) + if v==1: + continue + i = 0 + while v != n-1: + if i == t-1: + return False + else: + v, i = powMod(v, 2, n), i+1 + a = getRandomNumber(2, n) + return True + +def getRandomPrime(bits, display=False): + if bits < 10: + raise AssertionError() + #The 1.5 ensures the 2 MSBs are set + #Thus, when used for p,q in RSA, n will have its MSB set + # + #Since 30 is lcm(2,3,5), we'll set our test numbers to + #29 % 30 and keep them there + low = ((2 ** (bits-1)) * 3) // 2 + high = 2 ** bits - 30 + p = getRandomNumber(low, high) + p += 29 - (p % 30) + while 1: + if display: print(".", end=' ') + p += 30 + if p >= high: + p = getRandomNumber(low, high) + p += 29 - (p % 30) + if isPrime(p, display=display): + return p + +#Unused at the moment... +def getRandomSafePrime(bits, display=False): + if bits < 10: + raise AssertionError() + #The 1.5 ensures the 2 MSBs are set + #Thus, when used for p,q in RSA, n will have its MSB set + # + #Since 30 is lcm(2,3,5), we'll set our test numbers to + #29 % 30 and keep them there + low = (2 ** (bits-2)) * 3//2 + high = (2 ** (bits-1)) - 30 + q = getRandomNumber(low, high) + q += 29 - (q % 30) + while 1: + if display: print(".", end=' ') + q += 30 + if (q >= high): + q = getRandomNumber(low, high) + q += 29 - (q % 30) + #Ideas from Tom Wu's SRP code + #Do trial division on p and q before Rabin-Miller + if isPrime(q, 0, display=display): + p = (2 * q) + 1 + if isPrime(p, display=display): + if isPrime(q, display=display): + return p + + +class RSAKey(object): + + def __init__(self, n=0, e=0, d=0, p=0, q=0, dP=0, dQ=0, qInv=0): + if (n and not e) or (e and not n): + raise AssertionError() + self.n = n + self.e = e + self.d = d + self.p = p + self.q = q + self.dP = dP + self.dQ = dQ + self.qInv = qInv + self.blinder = 0 + self.unblinder = 0 + + def __len__(self): + """Return the length of this key in bits. + + @rtype: int + """ + return numBits(self.n) + + def hasPrivateKey(self): + return self.d != 0 + + def hashAndSign(self, bytes): + """Hash and sign the passed-in bytes. + + This requires the key to have a private component. It performs + a PKCS1-SHA1 signature on the passed-in data. + + @type bytes: str or L{bytearray} of unsigned bytes + @param bytes: The value which will be hashed and signed. + + @rtype: L{bytearray} of unsigned bytes. + @return: A PKCS1-SHA1 signature on the passed-in data. + """ + hashBytes = SHA1(bytearray(bytes)) + prefixedHashBytes = self._addPKCS1SHA1Prefix(hashBytes) + sigBytes = self.sign(prefixedHashBytes) + return sigBytes + + def hashAndVerify(self, sigBytes, bytes): + """Hash and verify the passed-in bytes with the signature. + + This verifies a PKCS1-SHA1 signature on the passed-in data. + + @type sigBytes: L{bytearray} of unsigned bytes + @param sigBytes: A PKCS1-SHA1 signature. + + @type bytes: str or L{bytearray} of unsigned bytes + @param bytes: The value which will be hashed and verified. + + @rtype: bool + @return: Whether the signature matches the passed-in data. + """ + hashBytes = SHA1(bytearray(bytes)) + + # Try it with/without the embedded NULL + prefixedHashBytes1 = self._addPKCS1SHA1Prefix(hashBytes, False) + prefixedHashBytes2 = self._addPKCS1SHA1Prefix(hashBytes, True) + result1 = self.verify(sigBytes, prefixedHashBytes1) + result2 = self.verify(sigBytes, prefixedHashBytes2) + return (result1 or result2) + + def sign(self, bytes): + """Sign the passed-in bytes. + + This requires the key to have a private component. It performs + a PKCS1 signature on the passed-in data. + + @type bytes: L{bytearray} of unsigned bytes + @param bytes: The value which will be signed. + + @rtype: L{bytearray} of unsigned bytes. + @return: A PKCS1 signature on the passed-in data. + """ + if not self.hasPrivateKey(): + raise AssertionError() + paddedBytes = self._addPKCS1Padding(bytes, 1) + m = bytesToNumber(paddedBytes) + if m >= self.n: + raise ValueError() + c = self._rawPrivateKeyOp(m) + sigBytes = numberToByteArray(c, numBytes(self.n)) + return sigBytes + + def verify(self, sigBytes, bytes): + """Verify the passed-in bytes with the signature. + + This verifies a PKCS1 signature on the passed-in data. + + @type sigBytes: L{bytearray} of unsigned bytes + @param sigBytes: A PKCS1 signature. + + @type bytes: L{bytearray} of unsigned bytes + @param bytes: The value which will be verified. + + @rtype: bool + @return: Whether the signature matches the passed-in data. + """ + if len(sigBytes) != numBytes(self.n): + return False + paddedBytes = self._addPKCS1Padding(bytes, 1) + c = bytesToNumber(sigBytes) + if c >= self.n: + return False + m = self._rawPublicKeyOp(c) + checkBytes = numberToByteArray(m, numBytes(self.n)) + return checkBytes == paddedBytes + + def encrypt(self, bytes): + """Encrypt the passed-in bytes. + + This performs PKCS1 encryption of the passed-in data. + + @type bytes: L{bytearray} of unsigned bytes + @param bytes: The value which will be encrypted. + + @rtype: L{bytearray} of unsigned bytes. + @return: A PKCS1 encryption of the passed-in data. + """ + paddedBytes = self._addPKCS1Padding(bytes, 2) + m = bytesToNumber(paddedBytes) + if m >= self.n: + raise ValueError() + c = self._rawPublicKeyOp(m) + encBytes = numberToByteArray(c, numBytes(self.n)) + return encBytes + + def decrypt(self, encBytes): + """Decrypt the passed-in bytes. + + This requires the key to have a private component. It performs + PKCS1 decryption of the passed-in data. + + @type encBytes: L{bytearray} of unsigned bytes + @param encBytes: The value which will be decrypted. + + @rtype: L{bytearray} of unsigned bytes or None. + @return: A PKCS1 decryption of the passed-in data or None if + the data is not properly formatted. + """ + if not self.hasPrivateKey(): + raise AssertionError() + if len(encBytes) != numBytes(self.n): + return None + c = bytesToNumber(encBytes) + if c >= self.n: + return None + m = self._rawPrivateKeyOp(c) + decBytes = numberToByteArray(m, numBytes(self.n)) + #Check first two bytes + if decBytes[0] != 0 or decBytes[1] != 2: + return None + #Scan through for zero separator + for x in range(1, len(decBytes)-1): + if decBytes[x]== 0: + break + else: + return None + return decBytes[x+1:] #Return everything after the separator + + + + + # ************************************************************************** + # Helper Functions for RSA Keys + # ************************************************************************** + + def _addPKCS1SHA1Prefix(self, bytes, withNULL=True): + # There is a long history of confusion over whether the SHA1 + # algorithmIdentifier should be encoded with a NULL parameter or + # with the parameter omitted. While the original intention was + # apparently to omit it, many toolkits went the other way. TLS 1.2 + # specifies the NULL should be included, and this behavior is also + # mandated in recent versions of PKCS #1, and is what tlslite has + # always implemented. Anyways, verification code should probably + # accept both. However, nothing uses this code yet, so this is + # all fairly moot. + if not withNULL: + prefixBytes = bytearray(\ + [0x30,0x1f,0x30,0x07,0x06,0x05,0x2b,0x0e,0x03,0x02,0x1a,0x04,0x14]) + else: + prefixBytes = bytearray(\ + [0x30,0x21,0x30,0x09,0x06,0x05,0x2b,0x0e,0x03,0x02,0x1a,0x05,0x00,0x04,0x14]) + prefixedBytes = prefixBytes + bytes + return prefixedBytes + + def _addPKCS1Padding(self, bytes, blockType): + padLength = (numBytes(self.n) - (len(bytes)+3)) + if blockType == 1: #Signature padding + pad = [0xFF] * padLength + elif blockType == 2: #Encryption padding + pad = bytearray(0) + while len(pad) < padLength: + padBytes = getRandomBytes(padLength * 2) + pad = [b for b in padBytes if b != 0] + pad = pad[:padLength] + else: + raise AssertionError() + + padding = bytearray([0,blockType] + pad + [0]) + paddedBytes = padding + bytes + return paddedBytes + + + + + def _rawPrivateKeyOp(self, m): + #Create blinding values, on the first pass: + if not self.blinder: + self.unblinder = getRandomNumber(2, self.n) + self.blinder = powMod(invMod(self.unblinder, self.n), self.e, + self.n) + + #Blind the input + m = (m * self.blinder) % self.n + + #Perform the RSA operation + c = self._rawPrivateKeyOpHelper(m) + + #Unblind the output + c = (c * self.unblinder) % self.n + + #Update blinding values + self.blinder = (self.blinder * self.blinder) % self.n + self.unblinder = (self.unblinder * self.unblinder) % self.n + + #Return the output + return c + + + def _rawPrivateKeyOpHelper(self, m): + #Non-CRT version + #c = powMod(m, self.d, self.n) + + #CRT version (~3x faster) + s1 = powMod(m, self.dP, self.p) + s2 = powMod(m, self.dQ, self.q) + h = ((s1 - s2) * self.qInv) % self.p + c = s2 + self.q * h + return c + + def _rawPublicKeyOp(self, c): + m = powMod(c, self.e, self.n) + return m + + def acceptsPassword(self): + return False + + def generate(bits): + key = RSAKey() + p = getRandomPrime(bits//2, False) + q = getRandomPrime(bits//2, False) + t = lcm(p-1, q-1) + key.n = p * q + key.e = 65537 + key.d = invMod(key.e, t) + key.p = p + key.q = q + key.dP = key.d % (p-1) + key.dQ = key.d % (q-1) + key.qInv = invMod(q, p) + return key + generate = staticmethod(generate) diff --git a/electrum/scripts/bip70.py b/electrum/scripts/bip70.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# create a BIP70 payment request signed with a certificate + +import tlslite + +from electrum.transaction import Transaction +from electrum import paymentrequest +from electrum import paymentrequest_pb2 as pb2 + +chain_file = 'mychain.pem' +cert_file = 'mycert.pem' +amount = 1000000 +address = "18U5kpCAU4s8weFF8Ps5n8HAfpdUjDVF64" +memo = "blah" +out_file = "payreq" + + +with open(chain_file, 'r') as f: + chain = tlslite.X509CertChain() + chain.parsePemList(f.read()) + +certificates = pb2.X509Certificates() +certificates.certificate.extend(map(lambda x: str(x.bytes), chain.x509List)) + +with open(cert_file, 'r') as f: + rsakey = tlslite.utils.python_rsakey.Python_RSAKey.parsePEM(f.read()) + +script = Transaction.pay_script('address', address).decode('hex') + +pr_string = paymentrequest.make_payment_request(amount, script, memo, rsakey) + +with open(out_file,'wb') as f: + f.write(pr_string) + +print("Payment request was written to file '%s'"%out_file) diff --git a/electrum/scripts/block_headers.py b/electrum/scripts/block_headers.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +# A simple script that connects to a server and displays block headers + +import time +from .. import SimpleConfig, Network +from electrum.util import print_msg, json_encode + +# start network +c = SimpleConfig() +network = Network(c) +network.start() + +# wait until connected +while network.is_connecting(): + time.sleep(0.1) + +if not network.is_connected(): + print_msg("daemon is not connected") + sys.exit(1) + +# 2. send the subscription +callback = lambda response: print_msg(json_encode(response.get('result'))) +network.send([('server.version',["block_headers script", "1.2"])], callback) +network.subscribe_to_headers(callback) + +# 3. wait for results +while network.is_connected(): + time.sleep(1) diff --git a/electrum/scripts/estimate_fee.py b/electrum/scripts/estimate_fee.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +from . import util +import json +from electrum.network import filter_protocol +peers = filter_protocol(util.get_peers()) +results = util.send_request(peers, 'blockchain.estimatefee', [2]) +print(json.dumps(results, indent=4)) diff --git a/electrum/scripts/get_history.py b/electrum/scripts/get_history.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +import sys +from .. import Network +from electrum.util import json_encode, print_msg +from electrum import bitcoin + +try: + addr = sys.argv[1] +except Exception: + print("usage: get_history <bitcoin_address>") + sys.exit(1) + +n = Network() +n.start() +_hash = bitcoin.address_to_scripthash(addr) +h = n.get_history_for_scripthash(_hash) +print_msg(json_encode(h)) diff --git a/electrum/scripts/peers.py b/electrum/scripts/peers.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from . import util + +from electrum.network import filter_protocol +from electrum.blockchain import hash_header + +peers = util.get_peers() +peers = filter_protocol(peers, 's') + +results = util.send_request(peers, 'blockchain.headers.subscribe', []) + +for n,v in sorted(results.items(), key=lambda x:x[1].get('block_height')): + print("%60s"%n, v.get('block_height'), hash_header(v)) diff --git a/electrum/scripts/servers.py b/electrum/scripts/servers.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +from .. import set_verbosity +from electrum.network import filter_version +from . import util +import json +set_verbosity(False) + +servers = filter_version(util.get_peers()) +print(json.dumps(servers, sort_keys = True, indent = 4)) diff --git a/electrum/scripts/txradar.py b/electrum/scripts/txradar.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +from . import util +import sys +try: + tx = sys.argv[1] +except: + print("usage: txradar txid") + sys.exit(1) + +peers = util.get_peers() +results = util.send_request(peers, 'blockchain.transaction.get', [tx]) + +r1 = [] +r2 = [] + +for k, v in results.items(): + (r1 if v else r2).append(k) + +print("Received %d answers"%len(results)) +print("Propagation rate: %.1f percent" % (len(r1) *100./(len(r1)+ len(r2)))) diff --git a/electrum/scripts/util.py b/electrum/scripts/util.py @@ -0,0 +1,87 @@ +import select, time, queue +# import electrum +from .. import Connection, Interface, SimpleConfig + +from electrum.network import parse_servers +from collections import defaultdict + +# electrum.util.set_verbosity(1) +def get_interfaces(servers, timeout=10): + '''Returns a map of servers to connected interfaces. If any + connections fail or timeout, they will be missing from the map. + ''' + assert type(servers) is list + socket_queue = queue.Queue() + config = SimpleConfig() + connecting = {} + for server in servers: + if server not in connecting: + connecting[server] = Connection(server, socket_queue, config.path) + interfaces = {} + timeout = time.time() + timeout + count = 0 + while time.time() < timeout and count < len(servers): + try: + server, socket = socket_queue.get(True, 0.3) + except queue.Empty: + continue + if socket: + interfaces[server] = Interface(server, socket) + count += 1 + return interfaces + +def wait_on_interfaces(interfaces, timeout=10): + '''Return a map of servers to a list of (request, response) tuples. + Waits timeout seconds, or until each interface has a response''' + result = defaultdict(list) + timeout = time.time() + timeout + while len(result) < len(interfaces) and time.time() < timeout: + rin = [i for i in interfaces.values()] + win = [i for i in interfaces.values() if i.unsent_requests] + rout, wout, xout = select.select(rin, win, [], 1) + for interface in wout: + interface.send_requests() + for interface in rout: + responses = interface.get_responses() + if responses: + result[interface.server].extend(responses) + return result + +def get_peers(): + config = SimpleConfig() + peers = {} + # 1. get connected interfaces + server = config.get('server') + if server is None: + print("You need to set a secure server, for example (for mainnet): 'electrum setconfig server helicarrier.bauerj.eu:50002:s'") + return [] + interfaces = get_interfaces([server]) + if not interfaces: + print("No connection to", server) + return [] + # 2. get list of peers + interface = interfaces[server] + interface.queue_request('server.peers.subscribe', [], 0) + responses = wait_on_interfaces(interfaces).get(server) + if responses: + response = responses[0][1] # One response, (req, response) tuple + peers = parse_servers(response.get('result')) + return peers + + +def send_request(peers, method, params): + print("Contacting %d servers"%len(peers)) + interfaces = get_interfaces(peers) + print("%d servers could be reached" % len(interfaces)) + for peer in peers: + if not peer in interfaces: + print("Connection failed:", peer) + for msg_id, i in enumerate(interfaces.values()): + i.queue_request(method, params, msg_id) + responses = wait_on_interfaces(interfaces) + for peer in interfaces: + if not peer in responses: + print(peer, "did not answer") + results = dict(zip(responses.keys(), [t[0][1].get('result') for t in responses.values()])) + print("%d answers"%len(results)) + return results diff --git a/electrum/scripts/watch_address.py b/electrum/scripts/watch_address.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import sys +import time +from electrum import bitcoin +from .. import SimpleConfig, Network +from electrum.util import print_msg, json_encode + +try: + addr = sys.argv[1] +except Exception: + print("usage: watch_address <bitcoin_address>") + sys.exit(1) + +sh = bitcoin.address_to_scripthash(addr) + +# start network +c = SimpleConfig() +network = Network(c) +network.start() + +# wait until connected +while network.is_connecting(): + time.sleep(0.1) + +if not network.is_connected(): + print_msg("daemon is not connected") + sys.exit(1) + +# 2. send the subscription +callback = lambda response: print_msg(json_encode(response.get('result'))) +network.subscribe_to_address(addr, callback) + +# 3. wait for results +while network.is_connected(): + time.sleep(1) diff --git a/electrum/segwit_addr.py b/electrum/segwit_addr.py @@ -0,0 +1,122 @@ +# Copyright (c) 2017 Pieter Wuille +# +# 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. + +"""Reference implementation for Bech32 and segwit addresses.""" + + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 + + +def bech32_create_checksum(hrp, data): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + + +def bech32_decode(bech): + """Validate a Bech32 string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + if not bech32_verify_checksum(hrp, data): + return (None, None) + return (hrp, data[:-6]) + + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + assert decode(hrp, ret) is not (None, None) + return ret diff --git a/electrum/servers.json b/electrum/servers.json @@ -0,0 +1,304 @@ +{ + "207.154.223.80": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "4cii7ryno5j3axe4.onion": { + "pruning": "-", + "t": "50001", + "version": "1.2" + }, + "74.222.1.20": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "88.198.43.231": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "E-X.not.fyi": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "VPS.hsmiths.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "arihancckjge66iv.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "aspinall.io": { + "pruning": "-", + "s": "50002", + "version": "1.2" + }, + "bauerjda5hnedjam.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "bauerjhejlv6di7s.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "btc.asis.io": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "btc.cihar.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "btc.smsys.me": { + "pruning": "-", + "s": "995", + "version": "1.2" + }, + "daedalus.bauerj.eu": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "de.hamster.science": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "e.keff.org": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "elec.luggs.co": { + "pruning": "-", + "s": "443", + "version": "1.2" + }, + "electrum-server.ninja": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum.achow101.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum.cutie.ga": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum.hsmiths.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum.leblancnet.us": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum.meltingice.net": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum.nute.net": { + "pruning": "-", + "s": "50002", + "version": "1.2" + }, + "electrum.poorcoding.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum.qtornado.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum.vom-stausee.de": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrum0.snel.it": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrumx-core.1209k.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrumx.bot.nu": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrumx.nmdps.net": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrumx.westeurope.cloudapp.azure.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "electrumxhqdsmlu.onion": { + "pruning": "-", + "t": "50001", + "version": "1.2" + }, + "elx2018.mooo.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "helicarrier.bauerj.eu": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "hsmiths4fyqlw5xw.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "hsmiths5mjk6uijs.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "j5jfrdthqt5g25xz.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "kirsche.emzy.de": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "luggscoqbymhvnkp.onion": { + "pruning": "-", + "t": "80", + "version": "1.2" + }, + "ndnd.selfhost.eu": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "ndndword5lpb7eex.onion": { + "pruning": "-", + "t": "50001", + "version": "1.2" + }, + "node.arihanc.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "node.erratic.space": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "ozahtqwp25chjdjd.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "qtornadoklbgdyww.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "rbx.curalle.ovh": { + "pruning": "-", + "s": "50002", + "version": "1.2" + }, + "ruuxwv74pjxms3ws.onion": { + "pruning": "-", + "s": "10042", + "t": "50001", + "version": "1.2" + }, + "s7clinmo4cazmhul.onion": { + "pruning": "-", + "t": "50001", + "version": "1.2" + }, + "songbird.bauerj.eu": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + }, + "spv.48.org": { + "pruning": "-", + "s": "50002", + "t": "50003", + "version": "1.2" + }, + "tardis.bauerj.eu": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + } +} diff --git a/electrum/servers_regtest.json b/electrum/servers_regtest.json @@ -0,0 +1,8 @@ +{ + "127.0.0.1": { + "pruning": "-", + "s": "51002", + "t": "51001", + "version": "1.2" + } +} diff --git a/electrum/servers_testnet.json b/electrum/servers_testnet.json @@ -0,0 +1,31 @@ +{ + "electrumx.kekku.li": { + "pruning": "-", + "s": "51002", + "version": "1.2" + }, + "hsmithsxurybd7uh.onion": { + "pruning": "-", + "s": "53012", + "t": "53011", + "version": "1.2" + }, + "testnet.hsmiths.com": { + "pruning": "-", + "s": "53012", + "t": "53011", + "version": "1.2" + }, + "testnet.qtornado.com": { + "pruning": "-", + "s": "51002", + "t": "51001", + "version": "1.2" + }, + "testnet1.bauerj.eu": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.2" + } +} diff --git a/electrum/simple_config.py b/electrum/simple_config.py @@ -0,0 +1,552 @@ +import json +import threading +import time +import os +import stat +from decimal import Decimal +from typing import Union + +from copy import deepcopy + +from . import util +from .util import (user_dir, print_error, PrintError, make_dir, + NoDynamicFeeEstimates, format_fee_satoshis, quantize_feerate) +from .i18n import _ + +FEE_ETA_TARGETS = [25, 10, 5, 2] +FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000] + +# satoshi per kbyte +FEERATE_MAX_DYNAMIC = 1500000 +FEERATE_WARNING_HIGH_FEE = 600000 +FEERATE_FALLBACK_STATIC_FEE = 150000 +FEERATE_DEFAULT_RELAY = 1000 +FEERATE_STATIC_VALUES = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000] + + +config = None + + +def get_config(): + global config + return config + + +def set_config(c): + global config + config = c + + +FINAL_CONFIG_VERSION = 3 + + +class SimpleConfig(PrintError): + """ + The SimpleConfig class is responsible for handling operations involving + configuration files. + + There are two different sources of possible configuration values: + 1. Command line options. + 2. User configuration (in the user's config directory) + They are taken in order (1. overrides config options set in 2.) + """ + + def __init__(self, options=None, read_user_config_function=None, + read_user_dir_function=None): + + if options is None: + options = {} + + # This lock needs to be acquired for updating and reading the config in + # a thread-safe way. + self.lock = threading.RLock() + + self.mempool_fees = {} + self.fee_estimates = {} + self.fee_estimates_last_updated = {} + self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees + + # The following two functions are there for dependency injection when + # testing. + if read_user_config_function is None: + read_user_config_function = read_user_config + if read_user_dir_function is None: + self.user_dir = user_dir + else: + self.user_dir = read_user_dir_function + + # The command line options + self.cmdline_options = deepcopy(options) + # don't allow to be set on CLI: + self.cmdline_options.pop('config_version', None) + + # Set self.path and read the user config + self.user_config = {} # for self.get in electrum_path() + self.path = self.electrum_path() + self.user_config = read_user_config_function(self.path) + if not self.user_config: + # avoid new config getting upgraded + self.user_config = {'config_version': FINAL_CONFIG_VERSION} + + # config "upgrade" - CLI options + self.rename_config_keys( + self.cmdline_options, {'auto_cycle': 'auto_connect'}, True) + + # config upgrade - user config + if self.requires_upgrade(): + self.upgrade() + + # Make a singleton instance of 'self' + set_config(self) + + def electrum_path(self): + # Read electrum_path from command line + # Otherwise use the user's default data directory. + path = self.get('electrum_path') + if path is None: + path = self.user_dir() + + make_dir(path, allow_symlink=False) + if self.get('testnet'): + path = os.path.join(path, 'testnet') + make_dir(path, allow_symlink=False) + elif self.get('regtest'): + path = os.path.join(path, 'regtest') + make_dir(path, allow_symlink=False) + elif self.get('simnet'): + path = os.path.join(path, 'simnet') + make_dir(path, allow_symlink=False) + + self.print_error("electrum directory", path) + return path + + def rename_config_keys(self, config, keypairs, deprecation_warning=False): + """Migrate old key names to new ones""" + updated = False + for old_key, new_key in keypairs.items(): + if old_key in config: + if new_key not in config: + config[new_key] = config[old_key] + if deprecation_warning: + self.print_stderr('Note that the {} variable has been deprecated. ' + 'You should use {} instead.'.format(old_key, new_key)) + del config[old_key] + updated = True + return updated + + def set_key(self, key, value, save=True): + if not self.is_modifiable(key): + self.print_stderr("Warning: not changing config key '%s' set on the command line" % key) + return + self._set_key_in_user_config(key, value, save) + + def _set_key_in_user_config(self, key, value, save=True): + with self.lock: + if value is not None: + self.user_config[key] = value + else: + self.user_config.pop(key, None) + if save: + self.save_user_config() + + def get(self, key, default=None): + with self.lock: + out = self.cmdline_options.get(key) + if out is None: + out = self.user_config.get(key, default) + return out + + def requires_upgrade(self): + return self.get_config_version() < FINAL_CONFIG_VERSION + + def upgrade(self): + with self.lock: + self.print_error('upgrading config') + + self.convert_version_2() + self.convert_version_3() + + self.set_key('config_version', FINAL_CONFIG_VERSION, save=True) + + def convert_version_2(self): + if not self._is_upgrade_method_needed(1, 1): + return + + self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'}) + + try: + # change server string FROM host:port:proto TO host:port:s + server_str = self.user_config.get('server') + host, port, protocol = str(server_str).rsplit(':', 2) + assert protocol in ('s', 't') + int(port) # Throw if cannot be converted to int + server_str = '{}:{}:s'.format(host, port) + self._set_key_in_user_config('server', server_str) + except BaseException: + self._set_key_in_user_config('server', None) + + self.set_key('config_version', 2) + + def convert_version_3(self): + if not self._is_upgrade_method_needed(2, 2): + return + + base_unit = self.user_config.get('base_unit') + if isinstance(base_unit, str): + self._set_key_in_user_config('base_unit', None) + map_ = {'btc':8, 'mbtc':5, 'ubtc':2, 'bits':2, 'sat':0} + decimal_point = map_.get(base_unit.lower()) + self._set_key_in_user_config('decimal_point', decimal_point) + + self.set_key('config_version', 3) + + def _is_upgrade_method_needed(self, min_version, max_version): + cur_version = self.get_config_version() + if cur_version > max_version: + return False + elif cur_version < min_version: + raise Exception( + ('config upgrade: unexpected version %d (should be %d-%d)' + % (cur_version, min_version, max_version))) + else: + return True + + def get_config_version(self): + config_version = self.get('config_version', 1) + if config_version > FINAL_CONFIG_VERSION: + self.print_stderr('WARNING: config version ({}) is higher than ours ({})' + .format(config_version, FINAL_CONFIG_VERSION)) + return config_version + + def is_modifiable(self, key): + return key not in self.cmdline_options + + def save_user_config(self): + if not self.path: + return + path = os.path.join(self.path, "config") + s = json.dumps(self.user_config, indent=4, sort_keys=True) + try: + with open(path, "w", encoding='utf-8') as f: + f.write(s) + os.chmod(path, stat.S_IREAD | stat.S_IWRITE) + except FileNotFoundError: + # datadir probably deleted while running... + if os.path.exists(self.path): # or maybe not? + raise + + def get_wallet_path(self): + """Set the path of the wallet.""" + + # command line -w option + if self.get('wallet_path'): + return os.path.join(self.get('cwd'), self.get('wallet_path')) + + # path in config file + path = self.get('default_wallet_path') + if path and os.path.exists(path): + return path + + # default path + util.assert_datadir_available(self.path) + dirpath = os.path.join(self.path, "wallets") + make_dir(dirpath, allow_symlink=False) + + new_path = os.path.join(self.path, "wallets", "default_wallet") + + # default path in pre 1.9 versions + old_path = os.path.join(self.path, "electrum.dat") + if os.path.exists(old_path) and not os.path.exists(new_path): + os.rename(old_path, new_path) + + return new_path + + def remove_from_recently_open(self, filename): + recent = self.get('recently_open', []) + if filename in recent: + recent.remove(filename) + self.set_key('recently_open', recent) + + def set_session_timeout(self, seconds): + self.print_error("session timeout -> %d seconds" % seconds) + self.set_key('session_timeout', seconds) + + def get_session_timeout(self): + return self.get('session_timeout', 300) + + def open_last_wallet(self): + if self.get('wallet_path') is None: + last_wallet = self.get('gui_last_wallet') + if last_wallet is not None and os.path.exists(last_wallet): + self.cmdline_options['default_wallet_path'] = last_wallet + + def save_last_wallet(self, wallet): + if self.get('wallet_path') is None: + path = wallet.storage.path + self.set_key('gui_last_wallet', path) + + def impose_hard_limits_on_fee(func): + def get_fee_within_limits(self, *args, **kwargs): + fee = func(self, *args, **kwargs) + if fee is None: + return fee + fee = min(FEERATE_MAX_DYNAMIC, fee) + fee = max(FEERATE_DEFAULT_RELAY, fee) + return fee + return get_fee_within_limits + + @impose_hard_limits_on_fee + def eta_to_fee(self, slider_pos) -> Union[int, None]: + """Returns fee in sat/kbyte.""" + slider_pos = max(slider_pos, 0) + slider_pos = min(slider_pos, len(FEE_ETA_TARGETS)) + if slider_pos < len(FEE_ETA_TARGETS): + target_blocks = FEE_ETA_TARGETS[slider_pos] + fee = self.fee_estimates.get(target_blocks) + else: + fee = self.fee_estimates.get(2) + if fee is not None: + fee += fee/2 + fee = int(fee) + return fee + + def fee_to_depth(self, target_fee): + depth = 0 + for fee, s in self.mempool_fees: + depth += s + if fee <= target_fee: + break + else: + return 0 + return depth + + @impose_hard_limits_on_fee + def depth_to_fee(self, slider_pos) -> int: + """Returns fee in sat/kbyte.""" + target = self.depth_target(slider_pos) + depth = 0 + for fee, s in self.mempool_fees: + depth += s + if depth > target: + break + else: + return 0 + return fee * 1000 + + def depth_target(self, slider_pos): + slider_pos = max(slider_pos, 0) + slider_pos = min(slider_pos, len(FEE_DEPTH_TARGETS)-1) + return FEE_DEPTH_TARGETS[slider_pos] + + def eta_target(self, i): + if i == len(FEE_ETA_TARGETS): + return 1 + return FEE_ETA_TARGETS[i] + + def fee_to_eta(self, fee_per_kb): + import operator + l = list(self.fee_estimates.items()) + [(1, self.eta_to_fee(4))] + dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l) + min_target, min_value = min(dist, key=operator.itemgetter(1)) + if fee_per_kb < self.fee_estimates.get(25)/2: + min_target = -1 + return min_target + + def depth_tooltip(self, depth): + return "%.1f MB from tip"%(depth/1000000) + + def eta_tooltip(self, x): + if x < 0: + return _('Low fee') + elif x == 1: + return _('In the next block') + else: + return _('Within {} blocks').format(x) + + def get_fee_status(self): + dyn = self.is_dynfee() + mempool = self.use_mempool_fees() + pos = self.get_depth_level() if mempool else self.get_fee_level() + fee_rate = self.fee_per_kb() + target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate) + return tooltip + ' [%s]'%target if dyn else target + ' [Static]' + + def get_fee_text(self, pos, dyn, mempool, fee_rate): + """Returns (text, tooltip) where + text is what we target: static fee / num blocks to confirm in / mempool depth + tooltip is the corresponding estimate (e.g. num blocks for a static fee) + """ + if fee_rate is None: + rate_str = 'unknown' + else: + rate_str = format_fee_satoshis(fee_rate/1000) + ' sat/byte' + + if dyn: + if mempool: + depth = self.depth_target(pos) + text = self.depth_tooltip(depth) + else: + eta = self.eta_target(pos) + text = self.eta_tooltip(eta) + tooltip = rate_str + else: + text = rate_str + if mempool and self.has_fee_mempool(): + depth = self.fee_to_depth(fee_rate) + tooltip = self.depth_tooltip(depth) + elif not mempool and self.has_fee_etas(): + eta = self.fee_to_eta(fee_rate) + tooltip = self.eta_tooltip(eta) + else: + tooltip = '' + return text, tooltip + + def get_depth_level(self): + maxp = len(FEE_DEPTH_TARGETS) - 1 + return min(maxp, self.get('depth_level', 2)) + + def get_fee_level(self): + maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block" + return min(maxp, self.get('fee_level', 2)) + + def get_fee_slider(self, dyn, mempool): + if dyn: + if mempool: + pos = self.get_depth_level() + maxp = len(FEE_DEPTH_TARGETS) - 1 + fee_rate = self.depth_to_fee(pos) + else: + pos = self.get_fee_level() + maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block" + fee_rate = self.eta_to_fee(pos) + else: + fee_rate = self.fee_per_kb(dyn=False) + pos = self.static_fee_index(fee_rate) + maxp = 9 + return maxp, pos, fee_rate + + def static_fee(self, i): + return FEERATE_STATIC_VALUES[i] + + def static_fee_index(self, value): + if value is None: + raise TypeError('static fee cannot be None') + dist = list(map(lambda x: abs(x - value), FEERATE_STATIC_VALUES)) + return min(range(len(dist)), key=dist.__getitem__) + + def has_fee_etas(self): + return len(self.fee_estimates) == 4 + + def has_fee_mempool(self): + return bool(self.mempool_fees) + + def has_dynamic_fees_ready(self): + if self.use_mempool_fees(): + return self.has_fee_mempool() + else: + return self.has_fee_etas() + + def is_dynfee(self): + return bool(self.get('dynamic_fees', True)) + + def use_mempool_fees(self): + return bool(self.get('mempool_fees', False)) + + def _feerate_from_fractional_slider_position(self, fee_level: float, dyn: bool, + mempool: bool) -> Union[int, None]: + fee_level = max(fee_level, 0) + fee_level = min(fee_level, 1) + if dyn: + max_pos = (len(FEE_DEPTH_TARGETS) - 1) if mempool else len(FEE_ETA_TARGETS) + slider_pos = round(fee_level * max_pos) + fee_rate = self.depth_to_fee(slider_pos) if mempool else self.eta_to_fee(slider_pos) + else: + max_pos = len(FEERATE_STATIC_VALUES) - 1 + slider_pos = round(fee_level * max_pos) + fee_rate = FEERATE_STATIC_VALUES[slider_pos] + return fee_rate + + def fee_per_kb(self, dyn: bool=None, mempool: bool=None, fee_level: float=None) -> Union[int, None]: + """Returns sat/kvB fee to pay for a txn. + Note: might return None. + + fee_level: float between 0.0 and 1.0, representing fee slider position + """ + if dyn is None: + dyn = self.is_dynfee() + if mempool is None: + mempool = self.use_mempool_fees() + if fee_level is not None: + return self._feerate_from_fractional_slider_position(fee_level, dyn, mempool) + # there is no fee_level specified; will use config. + # note: 'depth_level' and 'fee_level' in config are integer slider positions, + # unlike fee_level here, which (when given) is a float in [0.0, 1.0] + if dyn: + if mempool: + fee_rate = self.depth_to_fee(self.get_depth_level()) + else: + fee_rate = self.eta_to_fee(self.get_fee_level()) + else: + fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE) + return fee_rate + + def fee_per_byte(self): + """Returns sat/vB fee to pay for a txn. + Note: might return None. + """ + fee_per_kb = self.fee_per_kb() + return fee_per_kb / 1000 if fee_per_kb is not None else None + + def estimate_fee(self, size): + fee_per_kb = self.fee_per_kb() + if fee_per_kb is None: + raise NoDynamicFeeEstimates() + return self.estimate_fee_for_feerate(fee_per_kb, size) + + @classmethod + def estimate_fee_for_feerate(cls, fee_per_kb, size): + fee_per_kb = Decimal(fee_per_kb) + fee_per_byte = fee_per_kb / 1000 + # to be consistent with what is displayed in the GUI, + # the calculation needs to use the same precision: + fee_per_byte = quantize_feerate(fee_per_byte) + return round(fee_per_byte * size) + + def update_fee_estimates(self, key, value): + self.fee_estimates[key] = value + self.fee_estimates_last_updated[key] = time.time() + + def is_fee_estimates_update_required(self): + """Checks time since last requested and updated fee estimates. + Returns True if an update should be requested. + """ + now = time.time() + return now - self.last_time_fee_estimates_requested > 60 + + def requested_fee_estimates(self): + self.last_time_fee_estimates_requested = time.time() + + def get_video_device(self): + device = self.get("video_device", "default") + if device == 'default': + device = '' + return device + + +def read_user_config(path): + """Parse and store the user config settings in electrum.conf into user_config[].""" + if not path: + return {} + config_path = os.path.join(path, "config") + if not os.path.exists(config_path): + return {} + try: + with open(config_path, "r", encoding='utf-8') as f: + data = f.read() + result = json.loads(data) + except: + print_error("Warning: Cannot read config file.", config_path) + return {} + if not type(result) is dict: + return {} + return result diff --git a/electrum/storage.py b/electrum/storage.py @@ -0,0 +1,645 @@ +#!/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 os +import ast +import threading +import json +import copy +import re +import stat +import pbkdf2, hmac, hashlib +import base64 +import zlib +from collections import defaultdict + +from . import util, bitcoin, ecc +from .util import PrintError, profiler, InvalidPassword, WalletFileException, bfh +from .plugin import run_hook, plugin_loaders +from .keystore import bip44_derivation + + +# seed_version is now used for the version of the wallet file + +OLD_SEED_VERSION = 4 # electrum versions < 2.0 +NEW_SEED_VERSION = 11 # electrum versions >= 2.0 +FINAL_SEED_VERSION = 17 # electrum >= 2.7 will set this to prevent + # old versions from overwriting new format + + + +def multisig_type(wallet_type): + '''If wallet_type is mofn multi-sig, return [m, n], + otherwise return None.''' + if not wallet_type: + return None + match = re.match('(\d+)of(\d+)', wallet_type) + if match: + match = [int(x) for x in match.group(1, 2)] + return match + +def get_derivation_used_for_hw_device_encryption(): + return ("m" + "/4541509'" # ascii 'ELE' as decimal ("BIP43 purpose") + "/1112098098'") # ascii 'BIE2' as decimal + +# storage encryption version +STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW = range(0, 3) + +class WalletStorage(PrintError): + + def __init__(self, path, manual_upgrades=False): + self.print_error("wallet path", path) + self.manual_upgrades = manual_upgrades + self.lock = threading.RLock() + self.data = {} + self.path = path + self.modified = False + self.pubkey = None + if self.file_exists(): + with open(self.path, "r", encoding='utf-8') as f: + self.raw = f.read() + self._encryption_version = self._init_encryption_version() + if not self.is_encrypted(): + self.load_data(self.raw) + else: + self._encryption_version = STO_EV_PLAINTEXT + # avoid new wallets getting 'upgraded' + self.put('seed_version', FINAL_SEED_VERSION) + + def load_data(self, s): + try: + self.data = json.loads(s) + except: + try: + d = ast.literal_eval(s) + labels = d.get('labels', {}) + except Exception as e: + raise IOError("Cannot read wallet file '%s'" % self.path) + self.data = {} + for key, value in d.items(): + try: + json.dumps(key) + json.dumps(value) + except: + self.print_error('Failed to convert label to json format', key) + continue + self.data[key] = value + + # check here if I need to load a plugin + t = self.get('wallet_type') + l = plugin_loaders.get(t) + if l: l() + + if not self.manual_upgrades: + if self.requires_split(): + raise WalletFileException("This wallet has multiple accounts and must be split") + if self.requires_upgrade(): + self.upgrade() + + def is_past_initial_decryption(self): + """Return if storage is in a usable state for normal operations. + + The value is True exactly + if encryption is disabled completely (self.is_encrypted() == False), + or if encryption is enabled but the contents have already been decrypted. + """ + return bool(self.data) + + def is_encrypted(self): + """Return if storage encryption is currently enabled.""" + return self.get_encryption_version() != STO_EV_PLAINTEXT + + def is_encrypted_with_user_pw(self): + return self.get_encryption_version() == STO_EV_USER_PW + + def is_encrypted_with_hw_device(self): + return self.get_encryption_version() == STO_EV_XPUB_PW + + def get_encryption_version(self): + """Return the version of encryption used for this storage. + + 0: plaintext / no encryption + + ECIES, private key derived from a password, + 1: password is provided by user + 2: password is derived from an xpub; used with hw wallets + """ + return self._encryption_version + + def _init_encryption_version(self): + try: + magic = base64.b64decode(self.raw)[0:4] + if magic == b'BIE1': + return STO_EV_USER_PW + elif magic == b'BIE2': + return STO_EV_XPUB_PW + else: + return STO_EV_PLAINTEXT + except: + return STO_EV_PLAINTEXT + + def file_exists(self): + return self.path and os.path.exists(self.path) + + @staticmethod + def get_eckey_from_password(password): + secret = pbkdf2.PBKDF2(password, '', iterations=1024, macmodule=hmac, digestmodule=hashlib.sha512).read(64) + ec_key = ecc.ECPrivkey.from_arbitrary_size_secret(secret) + return ec_key + + def _get_encryption_magic(self): + v = self._encryption_version + if v == STO_EV_USER_PW: + return b'BIE1' + elif v == STO_EV_XPUB_PW: + return b'BIE2' + else: + raise WalletFileException('no encryption magic for version: %s' % v) + + def decrypt(self, password): + ec_key = self.get_eckey_from_password(password) + if self.raw: + enc_magic = self._get_encryption_magic() + s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic)) + else: + s = None + self.pubkey = ec_key.get_public_key_hex() + s = s.decode('utf8') + self.load_data(s) + + def check_password(self, password): + """Raises an InvalidPassword exception on invalid password""" + if not self.is_encrypted(): + return + if self.pubkey and self.pubkey != self.get_eckey_from_password(password).get_public_key_hex(): + raise InvalidPassword() + + def set_keystore_encryption(self, enable): + self.put('use_encryption', enable) + + def set_password(self, password, enc_version=None): + """Set a password to be used for encrypting this storage.""" + if enc_version is None: + enc_version = self._encryption_version + if password and enc_version != STO_EV_PLAINTEXT: + ec_key = self.get_eckey_from_password(password) + self.pubkey = ec_key.get_public_key_hex() + self._encryption_version = enc_version + else: + self.pubkey = None + self._encryption_version = STO_EV_PLAINTEXT + # make sure next storage.write() saves changes + with self.lock: + self.modified = True + + def get(self, key, default=None): + with self.lock: + v = self.data.get(key) + if v is None: + v = default + else: + v = copy.deepcopy(v) + return v + + def put(self, key, value): + try: + json.dumps(key, cls=util.MyEncoder) + json.dumps(value, cls=util.MyEncoder) + except: + self.print_error("json error: cannot save", key) + return + with self.lock: + if value is not None: + if self.data.get(key) != value: + self.modified = True + self.data[key] = copy.deepcopy(value) + elif key in self.data: + self.modified = True + self.data.pop(key) + + @profiler + def write(self): + with self.lock: + self._write() + + def _write(self): + if threading.currentThread().isDaemon(): + self.print_error('warning: daemon thread cannot write wallet') + return + if not self.modified: + return + s = json.dumps(self.data, indent=4, sort_keys=True, cls=util.MyEncoder) + if self.pubkey: + s = bytes(s, 'utf8') + c = zlib.compress(s) + enc_magic = self._get_encryption_magic() + public_key = ecc.ECPubkey(bfh(self.pubkey)) + s = public_key.encrypt_message(c, enc_magic) + s = s.decode('utf8') + + temp_path = "%s.tmp.%s" % (self.path, os.getpid()) + with open(temp_path, "w", encoding='utf-8') as f: + f.write(s) + f.flush() + os.fsync(f.fileno()) + + mode = os.stat(self.path).st_mode if os.path.exists(self.path) else stat.S_IREAD | stat.S_IWRITE + # perform atomic write on POSIX systems + try: + os.rename(temp_path, self.path) + except: + os.remove(self.path) + os.rename(temp_path, self.path) + os.chmod(self.path, mode) + self.print_error("saved", self.path) + self.modified = False + + def requires_split(self): + d = self.get('accounts', {}) + return len(d) > 1 + + def split_accounts(storage): + result = [] + # backward compatibility with old wallets + d = storage.get('accounts', {}) + if len(d) < 2: + return + wallet_type = storage.get('wallet_type') + if wallet_type == 'old': + assert len(d) == 2 + storage1 = WalletStorage(storage.path + '.deterministic') + storage1.data = copy.deepcopy(storage.data) + storage1.put('accounts', {'0': d['0']}) + storage1.upgrade() + storage1.write() + storage2 = WalletStorage(storage.path + '.imported') + storage2.data = copy.deepcopy(storage.data) + storage2.put('accounts', {'/x': d['/x']}) + storage2.put('seed', None) + storage2.put('seed_version', None) + storage2.put('master_public_key', None) + storage2.put('wallet_type', 'imported') + storage2.upgrade() + storage2.write() + result = [storage1.path, storage2.path] + elif wallet_type in ['bip44', 'trezor', 'keepkey', 'ledger', 'btchip', 'digitalbitbox']: + mpk = storage.get('master_public_keys') + for k in d.keys(): + i = int(k) + x = d[k] + if x.get("pending"): + continue + xpub = mpk["x/%d'"%i] + new_path = storage.path + '.' + k + storage2 = WalletStorage(new_path) + storage2.data = copy.deepcopy(storage.data) + # save account, derivation and xpub at index 0 + storage2.put('accounts', {'0': x}) + storage2.put('master_public_keys', {"x/0'": xpub}) + storage2.put('derivation', bip44_derivation(k)) + storage2.upgrade() + storage2.write() + result.append(new_path) + else: + raise WalletFileException("This wallet has multiple accounts and must be split") + return result + + def requires_upgrade(self): + return self.file_exists() and self.get_seed_version() < FINAL_SEED_VERSION + + @profiler + def upgrade(self): + self.print_error('upgrading wallet format') + + self.convert_imported() + self.convert_wallet_type() + self.convert_account() + self.convert_version_13_b() + self.convert_version_14() + self.convert_version_15() + self.convert_version_16() + self.convert_version_17() + + self.put('seed_version', FINAL_SEED_VERSION) # just to be sure + self.write() + + def convert_wallet_type(self): + if not self._is_upgrade_method_needed(0, 13): + return + + wallet_type = self.get('wallet_type') + if wallet_type == 'btchip': wallet_type = 'ledger' + if self.get('keystore') or self.get('x1/') or wallet_type=='imported': + return False + assert not self.requires_split() + seed_version = self.get_seed_version() + seed = self.get('seed') + xpubs = self.get('master_public_keys') + xprvs = self.get('master_private_keys', {}) + mpk = self.get('master_public_key') + keypairs = self.get('keypairs') + key_type = self.get('key_type') + if seed_version == OLD_SEED_VERSION or wallet_type == 'old': + d = { + 'type': 'old', + 'seed': seed, + 'mpk': mpk, + } + self.put('wallet_type', 'standard') + self.put('keystore', d) + + elif key_type == 'imported': + d = { + 'type': 'imported', + 'keypairs': keypairs, + } + self.put('wallet_type', 'standard') + self.put('keystore', d) + + elif wallet_type in ['xpub', 'standard']: + xpub = xpubs["x/"] + xprv = xprvs.get("x/") + d = { + 'type': 'bip32', + 'xpub': xpub, + 'xprv': xprv, + 'seed': seed, + } + self.put('wallet_type', 'standard') + self.put('keystore', d) + + elif wallet_type in ['bip44']: + xpub = xpubs["x/0'"] + xprv = xprvs.get("x/0'") + d = { + 'type': 'bip32', + 'xpub': xpub, + 'xprv': xprv, + } + self.put('wallet_type', 'standard') + self.put('keystore', d) + + elif wallet_type in ['trezor', 'keepkey', 'ledger', 'digitalbitbox']: + xpub = xpubs["x/0'"] + derivation = self.get('derivation', bip44_derivation(0)) + d = { + 'type': 'hardware', + 'hw_type': wallet_type, + 'xpub': xpub, + 'derivation': derivation, + } + self.put('wallet_type', 'standard') + self.put('keystore', d) + + elif (wallet_type == '2fa') or multisig_type(wallet_type): + for key in xpubs.keys(): + d = { + 'type': 'bip32', + 'xpub': xpubs[key], + 'xprv': xprvs.get(key), + } + if key == 'x1/' and seed: + d['seed'] = seed + self.put(key, d) + else: + raise WalletFileException('Unable to tell wallet type. Is this even a wallet file?') + # remove junk + self.put('master_public_key', None) + self.put('master_public_keys', None) + self.put('master_private_keys', None) + self.put('derivation', None) + self.put('seed', None) + self.put('keypairs', None) + self.put('key_type', None) + + def convert_version_13_b(self): + # version 13 is ambiguous, and has an earlier and a later structure + if not self._is_upgrade_method_needed(0, 13): + return + + if self.get('wallet_type') == 'standard': + if self.get('keystore').get('type') == 'imported': + pubkeys = self.get('keystore').get('keypairs').keys() + d = {'change': []} + receiving_addresses = [] + for pubkey in pubkeys: + addr = bitcoin.pubkey_to_address('p2pkh', pubkey) + receiving_addresses.append(addr) + d['receiving'] = receiving_addresses + self.put('addresses', d) + self.put('pubkeys', None) + + self.put('seed_version', 13) + + def convert_version_14(self): + # convert imported wallets for 3.0 + if not self._is_upgrade_method_needed(13, 13): + return + + if self.get('wallet_type') =='imported': + addresses = self.get('addresses') + if type(addresses) is list: + addresses = dict([(x, None) for x in addresses]) + self.put('addresses', addresses) + elif self.get('wallet_type') == 'standard': + if self.get('keystore').get('type')=='imported': + addresses = set(self.get('addresses').get('receiving')) + pubkeys = self.get('keystore').get('keypairs').keys() + assert len(addresses) == len(pubkeys) + d = {} + for pubkey in pubkeys: + addr = bitcoin.pubkey_to_address('p2pkh', pubkey) + assert addr in addresses + d[addr] = { + 'pubkey': pubkey, + 'redeem_script': None, + 'type': 'p2pkh' + } + self.put('addresses', d) + self.put('pubkeys', None) + self.put('wallet_type', 'imported') + self.put('seed_version', 14) + + def convert_version_15(self): + if not self._is_upgrade_method_needed(14, 14): + return + if self.get('seed_type') == 'segwit': + # should not get here; get_seed_version should have caught this + raise Exception('unsupported derivation (development segwit, v14)') + self.put('seed_version', 15) + + def convert_version_16(self): + # fixes issue #3193 for Imported_Wallets with addresses + # also, previous versions allowed importing any garbage as an address + # which we now try to remove, see pr #3191 + if not self._is_upgrade_method_needed(15, 15): + return + + def remove_address(addr): + def remove_from_dict(dict_name): + d = self.get(dict_name, None) + if d is not None: + d.pop(addr, None) + self.put(dict_name, d) + + def remove_from_list(list_name): + lst = self.get(list_name, None) + if lst is not None: + s = set(lst) + s -= {addr} + self.put(list_name, list(s)) + + # note: we don't remove 'addr' from self.get('addresses') + remove_from_dict('addr_history') + remove_from_dict('labels') + remove_from_dict('payment_requests') + remove_from_list('frozen_addresses') + + if self.get('wallet_type') == 'imported': + addresses = self.get('addresses') + assert isinstance(addresses, dict) + addresses_new = dict() + for address, details in addresses.items(): + if not bitcoin.is_address(address): + remove_address(address) + continue + if details is None: + addresses_new[address] = {} + else: + addresses_new[address] = details + self.put('addresses', addresses_new) + + self.put('seed_version', 16) + + def convert_version_17(self): + # delete pruned_txo; construct spent_outpoints + if not self._is_upgrade_method_needed(16, 16): + return + + self.put('pruned_txo', None) + + from .transaction import Transaction + transactions = self.get('transactions', {}) # txid -> raw_tx + spent_outpoints = defaultdict(dict) + for txid, raw_tx in transactions.items(): + tx = Transaction(raw_tx) + for txin in tx.inputs(): + if txin['type'] == 'coinbase': + continue + prevout_hash = txin['prevout_hash'] + prevout_n = txin['prevout_n'] + spent_outpoints[prevout_hash][prevout_n] = txid + self.put('spent_outpoints', spent_outpoints) + + self.put('seed_version', 17) + + def convert_imported(self): + if not self._is_upgrade_method_needed(0, 13): + return + + # '/x' is the internal ID for imported accounts + d = self.get('accounts', {}).get('/x', {}).get('imported',{}) + if not d: + return False + addresses = [] + keypairs = {} + for addr, v in d.items(): + pubkey, privkey = v + if privkey: + keypairs[pubkey] = privkey + else: + addresses.append(addr) + if addresses and keypairs: + raise WalletFileException('mixed addresses and privkeys') + elif addresses: + self.put('addresses', addresses) + self.put('accounts', None) + elif keypairs: + self.put('wallet_type', 'standard') + self.put('key_type', 'imported') + self.put('keypairs', keypairs) + self.put('accounts', None) + else: + raise WalletFileException('no addresses or privkeys') + + def convert_account(self): + if not self._is_upgrade_method_needed(0, 13): + return + + self.put('accounts', None) + + def _is_upgrade_method_needed(self, min_version, max_version): + cur_version = self.get_seed_version() + if cur_version > max_version: + return False + elif cur_version < min_version: + raise WalletFileException( + 'storage upgrade: unexpected version {} (should be {}-{})' + .format(cur_version, min_version, max_version)) + else: + return True + + def get_action(self): + action = run_hook('get_action', self) + if self.file_exists() and self.requires_upgrade(): + if action: + raise WalletFileException('Incomplete wallet files cannot be upgraded.') + return 'upgrade_storage' + if action: + return action + if not self.file_exists(): + return 'new' + + def get_seed_version(self): + seed_version = self.get('seed_version') + if not seed_version: + seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION + if seed_version > FINAL_SEED_VERSION: + raise WalletFileException('This version of Electrum is too old to open this wallet.\n' + '(highest supported storage version: {}, version of this file: {})' + .format(FINAL_SEED_VERSION, seed_version)) + if seed_version==14 and self.get('seed_type') == 'segwit': + self.raise_unsupported_version(seed_version) + if seed_version >=12: + return seed_version + if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]: + self.raise_unsupported_version(seed_version) + return seed_version + + def raise_unsupported_version(self, seed_version): + msg = "Your wallet has an unsupported seed version." + msg += '\n\nWallet file: %s' % os.path.abspath(self.path) + if seed_version in [5, 7, 8, 9, 10, 14]: + msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version + if seed_version == 6: + # version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog + msg += '\n\nThis file was created because of a bug in version 1.9.8.' + if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None: + # pbkdf2 was not included with the binaries, and wallet creation aborted. + msg += "\nIt does not contain any keys, and can safely be removed." + else: + # creation was complete if electrum was run from source + msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet." + raise WalletFileException(msg) diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py @@ -0,0 +1,213 @@ +#!/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. +from threading import Lock +import hashlib + +# from .bitcoin import Hash, hash_encode +from .transaction import Transaction +from .util import ThreadJob, bh2u + + +class Synchronizer(ThreadJob): + '''The synchronizer keeps the wallet up-to-date with its set of + addresses and their transactions. It subscribes over the network + to wallet addresses, gets the wallet to generate new addresses + when necessary, requests the transaction history of any addresses + we don't have the full history of, and requests binary transaction + data of any transactions the wallet doesn't have. + + External interface: __init__() and add() member functions. + ''' + + def __init__(self, wallet, network): + self.wallet = wallet + self.network = network + self.new_addresses = set() + # Entries are (tx_hash, tx_height) tuples + self.requested_tx = {} + self.requested_histories = {} + self.requested_addrs = set() + self.lock = Lock() + + self.initialized = False + self.initialize() + + def parse_response(self, response): + if response.get('error'): + self.print_error("response error:", response) + return None, None + return response['params'], response['result'] + + def is_up_to_date(self): + return (not self.requested_tx and not self.requested_histories + and not self.requested_addrs) + + def release(self): + self.network.unsubscribe(self.on_address_status) + + def add(self, address): + '''This can be called from the proxy or GUI threads.''' + with self.lock: + self.new_addresses.add(address) + + def subscribe_to_addresses(self, addresses): + if addresses: + self.requested_addrs |= addresses + self.network.subscribe_to_addresses(addresses, self.on_address_status) + + def get_status(self, h): + if not h: + return None + status = '' + for tx_hash, height in h: + status += tx_hash + ':%d:' % height + return bh2u(hashlib.sha256(status.encode('ascii')).digest()) + + def on_address_status(self, response): + if self.wallet.synchronizer is None and self.initialized: + return # we have been killed, this was just an orphan callback + params, result = self.parse_response(response) + if not params: + return + addr = params[0] + history = self.wallet.history.get(addr, []) + if self.get_status(history) != result: + # note that at this point 'result' can be None; + # if we had a history for addr but now the server is telling us + # there is no history + if addr not in self.requested_histories: + self.requested_histories[addr] = result + self.network.request_address_history(addr, self.on_address_history) + # remove addr from list only after it is added to requested_histories + if addr in self.requested_addrs: # Notifications won't be in + self.requested_addrs.remove(addr) + + def on_address_history(self, response): + if self.wallet.synchronizer is None and self.initialized: + return # we have been killed, this was just an orphan callback + params, result = self.parse_response(response) + if not params: + return + addr = params[0] + try: + server_status = self.requested_histories[addr] + except KeyError: + # note: server_status can be None even if we asked for the history, + # so it is not sufficient to test that + self.print_error("receiving history (unsolicited)", addr, len(result)) + return + self.print_error("receiving history", addr, len(result)) + hashes = set(map(lambda item: item['tx_hash'], result)) + hist = list(map(lambda item: (item['tx_hash'], item['height']), result)) + # tx_fees + tx_fees = [(item['tx_hash'], item.get('fee')) for item in result] + tx_fees = dict(filter(lambda x:x[1] is not None, tx_fees)) + # Check that txids are unique + if len(hashes) != len(result): + self.print_error("error: server history has non-unique txids: %s"% addr) + # Check that the status corresponds to what was announced + elif self.get_status(hist) != server_status: + self.print_error("error: status mismatch: %s" % addr) + else: + # Store received history + self.wallet.receive_history_callback(addr, hist, tx_fees) + # Request transactions we don't have + self.request_missing_txs(hist) + # Remove request; this allows up_to_date to be True + self.requested_histories.pop(addr) + + def on_tx_response(self, response): + if self.wallet.synchronizer is None and self.initialized: + return # we have been killed, this was just an orphan callback + params, result = self.parse_response(response) + if not params: + return + tx_hash = params[0] + tx = Transaction(result) + try: + tx.deserialize() + except Exception: + self.print_msg("cannot deserialize transaction, skipping", tx_hash) + return + if tx_hash != tx.txid(): + self.print_error("received tx does not match expected txid ({} != {})" + .format(tx_hash, tx.txid())) + return + tx_height = self.requested_tx.pop(tx_hash) + self.wallet.receive_tx_callback(tx_hash, tx, tx_height) + self.print_error("received tx %s height: %d bytes: %d" % + (tx_hash, tx_height, len(tx.raw))) + # callbacks + self.network.trigger_callback('new_transaction', tx) + if not self.requested_tx: + self.network.trigger_callback('updated') + + def request_missing_txs(self, hist): + # "hist" is a list of [tx_hash, tx_height] lists + transaction_hashes = [] + for tx_hash, tx_height in hist: + if tx_hash in self.requested_tx: + continue + if tx_hash in self.wallet.transactions: + continue + transaction_hashes.append(tx_hash) + self.requested_tx[tx_hash] = tx_height + + self.network.get_transactions(transaction_hashes, self.on_tx_response) + + def initialize(self): + '''Check the initial state of the wallet. Subscribe to all its + addresses, and request any transactions in its address history + we don't have. + ''' + for history in self.wallet.history.values(): + # Old electrum servers returned ['*'] when all history for + # the address was pruned. This no longer happens but may + # remain in old wallets. + if history == ['*']: + continue + self.request_missing_txs(history) + + if self.requested_tx: + self.print_error("missing tx", self.requested_tx) + self.subscribe_to_addresses(set(self.wallet.get_addresses())) + self.initialized = True + + def run(self): + '''Called from the network proxy thread main loop.''' + # 1. Create new addresses + self.wallet.synchronize() + + # 2. Subscribe to new addresses + with self.lock: + addresses = self.new_addresses + self.new_addresses = set() + self.subscribe_to_addresses(addresses) + + # 3. Detect if situation has changed + up_to_date = self.is_up_to_date() + if up_to_date != self.wallet.is_up_to_date(): + self.wallet.set_up_to_date(up_to_date) + self.network.trigger_callback('updated') diff --git a/electrum/tests/__init__.py b/electrum/tests/__init__.py @@ -0,0 +1,38 @@ +import unittest +import threading + +from electrum import constants + + +# Set this locally to make the test suite run faster. +# If set, unit tests that would normally test functions with multiple implementations, +# will only be run once, using the fastest implementation. +# e.g. libsecp256k1 vs python-ecdsa. pycryptodomex vs pyaes. +FAST_TESTS = False + + +# some unit tests are modifying globals; sorry. +class SequentialTestCase(unittest.TestCase): + + test_lock = threading.Lock() + + def setUp(self): + super().setUp() + self.test_lock.acquire() + + def tearDown(self): + super().tearDown() + self.test_lock.release() + + +class TestCaseForTestnet(SequentialTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + constants.set_testnet() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + constants.set_mainnet() diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py @@ -0,0 +1,761 @@ +import base64 +import unittest +import sys + +from electrum.bitcoin import ( + public_key_to_p2pkh, + bip32_root, bip32_public_derivation, bip32_private_derivation, + Hash, address_from_private_key, + is_address, is_private_key, xpub_from_xprv, is_new_seed, is_old_seed, + var_int, op_push, address_to_script, + deserialize_privkey, serialize_privkey, is_segwit_address, + is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub, + xpub_type, is_xprv, is_bip32_derivation, seed_type, EncodeBase58Check, + script_num_to_hex, push_script, add_number_to_script, int_to_hex) +from electrum import ecc, crypto, constants +from electrum.ecc import number_to_string, string_to_number +from electrum.transaction import opcodes +from electrum.util import bfh, bh2u +from electrum.storage import WalletStorage +from electrum.keystore import xtype_from_derivation + +from electrum import ecc_fast + +from . import SequentialTestCase +from . import TestCaseForTestnet +from . import FAST_TESTS + + +try: + import ecdsa +except ImportError: + sys.exit("Error: python-ecdsa does not seem to be installed. Try 'sudo pip install ecdsa'") + + +def needs_test_with_all_ecc_implementations(func): + """Function decorator to run a unit test twice: + once when libsecp256k1 is not available, once when it is. + + NOTE: this is inherently sequential; + tests running in parallel would break things + """ + def run_test(*args, **kwargs): + if FAST_TESTS: # if set, only run tests once, using fastest implementation + func(*args, **kwargs) + return + ecc_fast.undo_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1() + try: + # first test without libsecp + func(*args, **kwargs) + finally: + # if libsecp is not available, we are done + if not ecc_fast._libsecp256k1: + return + ecc_fast.do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1() + # if libsecp is available, test again now + func(*args, **kwargs) + return run_test + + +def needs_test_with_all_aes_implementations(func): + """Function decorator to run a unit test twice: + once when pycryptodomex is not available, once when it is. + + NOTE: this is inherently sequential; + tests running in parallel would break things + """ + def run_test(*args, **kwargs): + if FAST_TESTS: # if set, only run tests once, using fastest implementation + func(*args, **kwargs) + return + _aes = crypto.AES + crypto.AES = None + try: + # first test without pycryptodomex + func(*args, **kwargs) + finally: + # if pycryptodomex is not available, we are done + if not _aes: + return + crypto.AES = _aes + # if pycryptodomex is available, test again now + func(*args, **kwargs) + return run_test + + +class Test_bitcoin(SequentialTestCase): + + def test_libsecp256k1_is_available(self): + # we want the unit testing framework to test with libsecp256k1 available. + self.assertTrue(bool(ecc_fast._libsecp256k1)) + + def test_pycryptodomex_is_available(self): + # we want the unit testing framework to test with pycryptodomex available. + self.assertTrue(bool(crypto.AES)) + + @needs_test_with_all_aes_implementations + @needs_test_with_all_ecc_implementations + def test_crypto(self): + for message in [b"Chancellor on brink of second bailout for banks", b'\xff'*512]: + self._do_test_crypto(message) + + def _do_test_crypto(self, message): + G = ecc.generator() + _r = G.order() + pvk = ecdsa.util.randrange(_r) + + Pub = pvk*G + pubkey_c = Pub.get_public_key_bytes(True) + #pubkey_u = point_to_ser(Pub,False) + addr_c = public_key_to_p2pkh(pubkey_c) + + #print "Private key ", '%064x'%pvk + eck = ecc.ECPrivkey(number_to_string(pvk,_r)) + + #print "Compressed public key ", pubkey_c.encode('hex') + enc = ecc.ECPubkey(pubkey_c).encrypt_message(message) + dec = eck.decrypt_message(enc) + self.assertEqual(message, dec) + + #print "Uncompressed public key", pubkey_u.encode('hex') + #enc2 = EC_KEY.encrypt_message(message, pubkey_u) + dec2 = eck.decrypt_message(enc) + self.assertEqual(message, dec2) + + signature = eck.sign_message(message, True) + #print signature + eck.verify_message_for_address(signature, message) + + @needs_test_with_all_ecc_implementations + def test_ecc_sanity(self): + G = ecc.generator() + n = G.order() + self.assertEqual(ecc.CURVE_ORDER, n) + inf = n * G + self.assertEqual(ecc.point_at_infinity(), inf) + self.assertTrue(inf.is_at_infinity()) + self.assertFalse(G.is_at_infinity()) + self.assertEqual(11 * G, 7 * G + 4 * G) + self.assertEqual((n + 2) * G, 2 * G) + self.assertEqual((n - 2) * G, -2 * G) + A = (n - 2) * G + B = (n - 1) * G + C = n * G + D = (n + 1) * G + self.assertFalse(A.is_at_infinity()) + self.assertFalse(B.is_at_infinity()) + self.assertTrue(C.is_at_infinity()) + self.assertTrue((C * 5).is_at_infinity()) + self.assertFalse(D.is_at_infinity()) + self.assertEqual(inf, C) + self.assertEqual(inf, A + 2 * G) + self.assertEqual(inf, D + (-1) * G) + self.assertNotEqual(A, B) + + @needs_test_with_all_ecc_implementations + def test_msg_signing(self): + msg1 = b'Chancellor on brink of second bailout for banks' + msg2 = b'Electrum' + + def sign_message_with_wif_privkey(wif_privkey, msg): + txin_type, privkey, compressed = deserialize_privkey(wif_privkey) + key = ecc.ECPrivkey(privkey) + return key.sign_message(msg, compressed) + + sig1 = sign_message_with_wif_privkey( + 'L1TnU2zbNaAqMoVh65Cyvmcjzbrj41Gs9iTLcWbpJCMynXuap6UN', msg1) + addr1 = '15hETetDmcXm1mM4sEf7U2KXC9hDHFMSzz' + sig2 = sign_message_with_wif_privkey( + '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', msg2) + addr2 = '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6' + + sig1_b64 = base64.b64encode(sig1) + sig2_b64 = base64.b64encode(sig2) + + self.assertEqual(sig1_b64, b'H/9jMOnj4MFbH3d7t4yCQ9i7DgZU/VZ278w3+ySv2F4yIsdqjsc5ng3kmN8OZAThgyfCZOQxZCWza9V5XzlVY0Y=') + self.assertEqual(sig2_b64, b'G84dmJ8TKIDKMT9qBRhpX2sNmR0y5t+POcYnFFJCs66lJmAs3T8A6Sbpx7KA6yTQ9djQMabwQXRrDomOkIKGn18=') + + self.assertTrue(ecc.verify_message_with_address(addr1, sig1, msg1)) + self.assertTrue(ecc.verify_message_with_address(addr2, sig2, msg2)) + + self.assertFalse(ecc.verify_message_with_address(addr1, b'wrong', msg1)) + self.assertFalse(ecc.verify_message_with_address(addr1, sig2, msg1)) + + @needs_test_with_all_aes_implementations + @needs_test_with_all_ecc_implementations + def test_decrypt_message(self): + key = WalletStorage.get_eckey_from_password('pw123') + self.assertEqual(b'me<(s_s)>age', key.decrypt_message(b'QklFMQMDFtgT3zWSQsa+Uie8H/WvfUjlu9UN9OJtTt3KlgKeSTi6SQfuhcg1uIz9hp3WIUOFGTLr4RNQBdjPNqzXwhkcPi2Xsbiw6UCNJncVPJ6QBg==')) + self.assertEqual(b'me<(s_s)>age', key.decrypt_message(b'QklFMQKXOXbylOQTSMGfo4MFRwivAxeEEkewWQrpdYTzjPhqjHcGBJwdIhB7DyRfRQihuXx1y0ZLLv7XxLzrILzkl/H4YUtZB4uWjuOAcmxQH4i/Og==')) + self.assertEqual(b'hey_there' * 100, key.decrypt_message(b'QklFMQLOOsabsXtGQH8edAa6VOUa5wX8/DXmxX9NyHoAx1a5bWgllayGRVPeI2bf0ZdWK0tfal0ap0ZIVKbd2eOJybqQkILqT6E1/Syzq0Zicyb/AA1eZNkcX5y4gzloxinw00ubCA8M7gcUjJpOqbnksATcJ5y2YYXcHMGGfGurWu6uJ/UyrNobRidWppRMW5yR9/6utyNvT6OHIolCMEf7qLcmtneoXEiz51hkRdZS7weNf9mGqSbz9a2NL3sdh1A0feHIjAZgcCKcAvksNUSauf0/FnIjzTyPRpjRDMeDC8Ci3sGiuO3cvpWJwhZfbjcS26KmBv2CHWXfRRNFYOInHZNIXWNAoBB47Il5bGSMd+uXiGr+SQ9tNvcu+BiJNmFbxYqg+oQ8dGAl1DtvY2wJVY8k7vO9BIWSpyIxfGw7EDifhc5vnOmGe016p6a01C3eVGxgl23UYMrP7+fpjOcPmTSF4rk5U5ljEN3MSYqlf1QEv0OqlI9q1TwTK02VBCjMTYxDHsnt04OjNBkNO8v5uJ4NR+UUDBEp433z53I59uawZ+dbk4v4ZExcl8EGmKm3Gzbal/iJ/F7KQuX2b/ySEhLOFVYFWxK73X1nBvCSK2mC2/8fCw8oI5pmvzJwQhcCKTdEIrz3MMvAHqtPScDUOjzhXxInQOCb3+UBj1PPIdqkYLvZss1TEaBwYZjLkVnK2MBj7BaqT6Rp6+5A/fippUKHsnB6eYMEPR2YgDmCHL+4twxHJG6UWdP3ybaKiiAPy2OHNP6PTZ0HrqHOSJzBSDD+Z8YpaRg29QX3UEWlqnSKaan0VYAsV1VeaN0XFX46/TWO0L5tjhYVXJJYGqo6tIQJymxATLFRF6AZaD1Mwd27IAL04WkmoQoXfO6OFfwdp/shudY/1gBkDBvGPICBPtnqkvhGF+ZF3IRkuPwiFWeXmwBxKHsRx/3+aJu32Ml9+za41zVk2viaxcGqwTc5KMexQFLAUwqhv+aIik7U+5qk/gEVSuRoVkihoweFzKolNF+BknH2oB4rZdPixag5Zje3DvgjsSFlOl69W/67t/Gs8htfSAaHlsB8vWRQr9+v/lxTbrAw+O0E+sYGoObQ4qQMyQshNZEHbpPg63eWiHtJJnrVBvOeIbIHzoLDnMDsWVWZSMzAQ1vhX1H5QLgSEbRlKSliVY03kDkh/Nk/KOn+B2q37Ialq4JcRoIYFGJ8AoYEAD0tRuTqFddIclE75HzwaNG7NyKW1plsa72ciOPwsPJsdd5F0qdSQ3OSKtooTn7uf6dXOc4lDkfrVYRlZ0PX')) + + @needs_test_with_all_aes_implementations + @needs_test_with_all_ecc_implementations + def test_encrypt_message(self): + key = WalletStorage.get_eckey_from_password('secret_password77') + msgs = [ + bytes([0] * 555), + b'cannot think of anything funny' + ] + for plaintext in msgs: + ciphertext1 = key.encrypt_message(plaintext) + ciphertext2 = key.encrypt_message(plaintext) + self.assertEqual(plaintext, key.decrypt_message(ciphertext1)) + self.assertEqual(plaintext, key.decrypt_message(ciphertext2)) + self.assertNotEqual(ciphertext1, ciphertext2) + + @needs_test_with_all_ecc_implementations + def test_sign_transaction(self): + eckey1 = ecc.ECPrivkey(bfh('7e1255fddb52db1729fc3ceb21a46f95b8d9fe94cc83425e936a6c5223bb679d')) + sig1 = eckey1.sign_transaction(bfh('5a548b12369a53faaa7e51b5081829474ebdd9c924b3a8230b69aa0be254cd94')) + self.assertEqual(bfh('3045022100902a288b98392254cd23c0e9a49ac6d7920f171b8249a48e484b998f1874a2010220723d844826828f092cf400cb210c4fa0b8cd1b9d1a7f21590e78e022ff6476b9'), sig1) + + eckey2 = ecc.ECPrivkey(bfh('c7ce8c1462c311eec24dff9e2532ac6241e50ae57e7d1833af21942136972f23')) + sig2 = eckey2.sign_transaction(bfh('642a2e66332f507c92bda910158dfe46fc10afbf72218764899d3af99a043fac')) + self.assertEqual(bfh('30440220618513f4cfc87dde798ce5febae7634c23e7b9254a1eabf486be820f6a7c2c4702204fef459393a2b931f949e63ced06888f35e286e446dc46feb24b5b5f81c6ed52'), sig2) + + @needs_test_with_all_aes_implementations + def test_aes_homomorphic(self): + """Make sure AES is homomorphic.""" + payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' + password = u'secret' + enc = crypto.pw_encode(payload, password) + dec = crypto.pw_decode(enc, password) + self.assertEqual(dec, payload) + + @needs_test_with_all_aes_implementations + def test_aes_encode_without_password(self): + """When not passed a password, pw_encode is noop on the payload.""" + payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' + enc = crypto.pw_encode(payload, None) + self.assertEqual(payload, enc) + + @needs_test_with_all_aes_implementations + def test_aes_deencode_without_password(self): + """When not passed a password, pw_decode is noop on the payload.""" + payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' + enc = crypto.pw_decode(payload, None) + self.assertEqual(payload, enc) + + @needs_test_with_all_aes_implementations + def test_aes_decode_with_invalid_password(self): + """pw_decode raises an Exception when supplied an invalid password.""" + payload = u"blah" + password = u"uber secret" + wrong_password = u"not the password" + enc = crypto.pw_encode(payload, password) + self.assertRaises(Exception, crypto.pw_decode, enc, wrong_password) + + def test_hash(self): + """Make sure the Hash function does sha256 twice""" + payload = u"test" + expected = b'\x95MZI\xfdp\xd9\xb8\xbc\xdb5\xd2R&x)\x95\x7f~\xf7\xfalt\xf8\x84\x19\xbd\xc5\xe8"\t\xf4' + + result = Hash(payload) + self.assertEqual(expected, result) + + def test_int_to_hex(self): + self.assertEqual('00', int_to_hex(0, 1)) + self.assertEqual('ff', int_to_hex(-1, 1)) + self.assertEqual('00000000', int_to_hex(0, 4)) + self.assertEqual('01000000', int_to_hex(1, 4)) + self.assertEqual('7f', int_to_hex(127, 1)) + self.assertEqual('7f00', int_to_hex(127, 2)) + self.assertEqual('80', int_to_hex(128, 1)) + self.assertEqual('80', int_to_hex(-128, 1)) + self.assertEqual('8000', int_to_hex(128, 2)) + self.assertEqual('ff', int_to_hex(255, 1)) + self.assertEqual('ff7f', int_to_hex(32767, 2)) + self.assertEqual('0080', int_to_hex(-32768, 2)) + self.assertEqual('ffff', int_to_hex(65535, 2)) + with self.assertRaises(OverflowError): int_to_hex(256, 1) + with self.assertRaises(OverflowError): int_to_hex(-129, 1) + with self.assertRaises(OverflowError): int_to_hex(-257, 1) + with self.assertRaises(OverflowError): int_to_hex(65536, 2) + with self.assertRaises(OverflowError): int_to_hex(-32769, 2) + + def test_var_int(self): + for i in range(0xfd): + self.assertEqual(var_int(i), "{:02x}".format(i) ) + + self.assertEqual(var_int(0xfd), "fdfd00") + self.assertEqual(var_int(0xfe), "fdfe00") + self.assertEqual(var_int(0xff), "fdff00") + self.assertEqual(var_int(0x1234), "fd3412") + self.assertEqual(var_int(0xffff), "fdffff") + self.assertEqual(var_int(0x10000), "fe00000100") + self.assertEqual(var_int(0x12345678), "fe78563412") + self.assertEqual(var_int(0xffffffff), "feffffffff") + self.assertEqual(var_int(0x100000000), "ff0000000001000000") + self.assertEqual(var_int(0x0123456789abcdef), "ffefcdab8967452301") + + def test_op_push(self): + self.assertEqual(op_push(0x00), '00') + self.assertEqual(op_push(0x12), '12') + self.assertEqual(op_push(0x4b), '4b') + self.assertEqual(op_push(0x4c), '4c4c') + self.assertEqual(op_push(0xfe), '4cfe') + self.assertEqual(op_push(0xff), '4cff') + self.assertEqual(op_push(0x100), '4d0001') + self.assertEqual(op_push(0x1234), '4d3412') + self.assertEqual(op_push(0xfffe), '4dfeff') + self.assertEqual(op_push(0xffff), '4dffff') + self.assertEqual(op_push(0x10000), '4e00000100') + self.assertEqual(op_push(0x12345678), '4e78563412') + + def test_script_num_to_hex(self): + # test vectors from https://github.com/btcsuite/btcd/blob/fdc2bc867bda6b351191b5872d2da8270df00d13/txscript/scriptnum.go#L77 + self.assertEqual(script_num_to_hex(127), '7f') + self.assertEqual(script_num_to_hex(-127), 'ff') + self.assertEqual(script_num_to_hex(128), '8000') + self.assertEqual(script_num_to_hex(-128), '8080') + self.assertEqual(script_num_to_hex(129), '8100') + self.assertEqual(script_num_to_hex(-129), '8180') + self.assertEqual(script_num_to_hex(256), '0001') + self.assertEqual(script_num_to_hex(-256), '0081') + self.assertEqual(script_num_to_hex(32767), 'ff7f') + self.assertEqual(script_num_to_hex(-32767), 'ffff') + self.assertEqual(script_num_to_hex(32768), '008000') + self.assertEqual(script_num_to_hex(-32768), '008080') + + def test_push_script(self): + # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#push-operators + self.assertEqual(push_script(''), bh2u(bytes([opcodes.OP_0]))) + self.assertEqual(push_script('07'), bh2u(bytes([opcodes.OP_7]))) + self.assertEqual(push_script('10'), bh2u(bytes([opcodes.OP_16]))) + self.assertEqual(push_script('81'), bh2u(bytes([opcodes.OP_1NEGATE]))) + self.assertEqual(push_script('11'), '0111') + self.assertEqual(push_script(75 * '42'), '4b' + 75 * '42') + self.assertEqual(push_script(76 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('4c' + 76 * '42'))) + self.assertEqual(push_script(100 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('64' + 100 * '42'))) + self.assertEqual(push_script(255 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('ff' + 255 * '42'))) + self.assertEqual(push_script(256 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA2]) + bfh('0001' + 256 * '42'))) + self.assertEqual(push_script(520 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA2]) + bfh('0802' + 520 * '42'))) + + def test_add_number_to_script(self): + # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#numbers + self.assertEqual(add_number_to_script(0), bytes([opcodes.OP_0])) + self.assertEqual(add_number_to_script(7), bytes([opcodes.OP_7])) + self.assertEqual(add_number_to_script(16), bytes([opcodes.OP_16])) + self.assertEqual(add_number_to_script(-1), bytes([opcodes.OP_1NEGATE])) + self.assertEqual(add_number_to_script(-127), bfh('01ff')) + self.assertEqual(add_number_to_script(-2), bfh('0182')) + self.assertEqual(add_number_to_script(17), bfh('0111')) + self.assertEqual(add_number_to_script(127), bfh('017f')) + self.assertEqual(add_number_to_script(-32767), bfh('02ffff')) + self.assertEqual(add_number_to_script(-128), bfh('028080')) + self.assertEqual(add_number_to_script(128), bfh('028000')) + self.assertEqual(add_number_to_script(32767), bfh('02ff7f')) + self.assertEqual(add_number_to_script(-8388607), bfh('03ffffff')) + self.assertEqual(add_number_to_script(-32768), bfh('03008080')) + self.assertEqual(add_number_to_script(32768), bfh('03008000')) + self.assertEqual(add_number_to_script(8388607), bfh('03ffff7f')) + self.assertEqual(add_number_to_script(-2147483647), bfh('04ffffffff')) + self.assertEqual(add_number_to_script(-8388608 ), bfh('0400008080')) + self.assertEqual(add_number_to_script(8388608), bfh('0400008000')) + self.assertEqual(add_number_to_script(2147483647), bfh('04ffffff7f')) + + def test_address_to_script(self): + # bech32 native segwit + # test vectors from BIP-0173 + self.assertEqual(address_to_script('BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4'), '0014751e76e8199196d454941c45d1b3a323f1433bd6') + self.assertEqual(address_to_script('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), '5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6') + self.assertEqual(address_to_script('BC1SW50QA3JX3S'), '6002751e') + self.assertEqual(address_to_script('bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), '5210751e76e8199196d454941c45d1b3a323') + + # base58 P2PKH + self.assertEqual(address_to_script('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), '76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac') + self.assertEqual(address_to_script('1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), '76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac') + + # base58 P2SH + self.assertEqual(address_to_script('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), 'a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487') + self.assertEqual(address_to_script('3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), 'a914f47c8954e421031ad04ecd8e7752c9479206b9d387') + + +class Test_bitcoin_testnet(TestCaseForTestnet): + + def test_address_to_script(self): + # bech32 native segwit + # test vectors from BIP-0173 + self.assertEqual(address_to_script('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7'), '00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262') + self.assertEqual(address_to_script('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy'), '0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433') + + # base58 P2PKH + self.assertEqual(address_to_script('mutXcGt1CJdkRvXuN2xoz2quAAQYQ59bRX'), '76a9149da64e300c5e4eb4aaffc9c2fd465348d5618ad488ac') + self.assertEqual(address_to_script('miqtaRTkU3U8rzwKbEHx3g8FSz8GJtPS3K'), '76a914247d2d5b6334bdfa2038e85b20fc15264f8e5d2788ac') + + # base58 P2SH + self.assertEqual(address_to_script('2N3LSvr3hv5EVdfcrxg2Yzecf3SRvqyBE4p'), 'a9146eae23d8c4a941316017946fc761a7a6c85561fb87') + self.assertEqual(address_to_script('2NE4ZdmxFmUgwu5wtfoN2gVniyMgRDYq1kk'), 'a914e4567743d378957cd2ee7072da74b1203c1a7a0b87') + + +class Test_xprv_xpub(SequentialTestCase): + + xprv_xpub = ( + # Taken from test vectors in https://en.bitcoin.it/wiki/BIP_0032_TestVectors + {'xprv': 'xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76', + 'xpub': 'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy', + 'xtype': 'standard'}, + {'xprv': 'yprvAJEYHeNEPcyBoQYM7sGCxDiNCTX65u4ANgZuSGTrKN5YCC9MP84SBayrgaMyZV7zvkHrr3HVPTK853s2SPk4EttPazBZBmz6QfDkXeE8Zr7', + 'xpub': 'ypub6XDth9u8DzXV1tcpDtoDKMf6kVMaVMn1juVWEesTshcX4zUVvfNgjPJLXrD9N7AdTLnbHFL64KmBn3SNaTe69iZYbYCqLCCNPZKbLz9niQ4', + 'xtype': 'p2wpkh-p2sh'}, + {'xprv': 'zprvAWgYBBk7JR8GkraNZJeEodAp2UR1VRWJTXyV1ywuUVs1awUgTiBS1ZTDtLA5F3MFDn1LZzu8dUpSKdT7ToDpvEG6PQu4bJs7zQY47Sd3sEZ', + 'xpub': 'zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8', + 'xtype': 'p2wpkh'}, + ) + + def _do_test_bip32(self, seed, sequence): + xprv, xpub = bip32_root(bfh(seed), 'standard') + self.assertEqual("m/", sequence[0:2]) + path = 'm' + sequence = sequence[2:] + for n in sequence.split('/'): + child_path = path + '/' + n + if n[-1] != "'": + xpub2 = bip32_public_derivation(xpub, path, child_path) + xprv, xpub = bip32_private_derivation(xprv, path, child_path) + if n[-1] != "'": + self.assertEqual(xpub, xpub2) + path = child_path + + return xpub, xprv + + @needs_test_with_all_ecc_implementations + def test_bip32(self): + # see https://en.bitcoin.it/wiki/BIP_0032_TestVectors + xpub, xprv = self._do_test_bip32("000102030405060708090a0b0c0d0e0f", "m/0'/1/2'/2/1000000000") + self.assertEqual("xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", xpub) + self.assertEqual("xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", xprv) + + xpub, xprv = self._do_test_bip32("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542","m/0/2147483647'/1/2147483646'/2") + self.assertEqual("xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", xpub) + self.assertEqual("xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", xprv) + + @needs_test_with_all_ecc_implementations + def test_xpub_from_xprv(self): + """We can derive the xpub key from a xprv.""" + for xprv_details in self.xprv_xpub: + result = xpub_from_xprv(xprv_details['xprv']) + self.assertEqual(result, xprv_details['xpub']) + + @needs_test_with_all_ecc_implementations + def test_is_xpub(self): + for xprv_details in self.xprv_xpub: + xpub = xprv_details['xpub'] + self.assertTrue(is_xpub(xpub)) + self.assertFalse(is_xpub('xpub1nval1d')) + self.assertFalse(is_xpub('xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG')) + + @needs_test_with_all_ecc_implementations + def test_xpub_type(self): + for xprv_details in self.xprv_xpub: + xpub = xprv_details['xpub'] + self.assertEqual(xprv_details['xtype'], xpub_type(xpub)) + + @needs_test_with_all_ecc_implementations + def test_is_xprv(self): + for xprv_details in self.xprv_xpub: + xprv = xprv_details['xprv'] + self.assertTrue(is_xprv(xprv)) + self.assertFalse(is_xprv('xprv1nval1d')) + self.assertFalse(is_xprv('xprv661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG')) + + def test_is_bip32_derivation(self): + self.assertTrue(is_bip32_derivation("m/0'/1")) + self.assertTrue(is_bip32_derivation("m/0'/0'")) + self.assertTrue(is_bip32_derivation("m/44'/0'/0'/0/0")) + self.assertTrue(is_bip32_derivation("m/49'/0'/0'/0/0")) + self.assertFalse(is_bip32_derivation("mmmmmm")) + self.assertFalse(is_bip32_derivation("n/")) + self.assertFalse(is_bip32_derivation("")) + self.assertFalse(is_bip32_derivation("m/q8462")) + + def test_xtype_from_derivation(self): + self.assertEqual('standard', xtype_from_derivation("m/44'")) + self.assertEqual('standard', xtype_from_derivation("m/44'/")) + self.assertEqual('standard', xtype_from_derivation("m/44'/0'/0'")) + self.assertEqual('standard', xtype_from_derivation("m/44'/5241'/221")) + self.assertEqual('standard', xtype_from_derivation("m/45'")) + self.assertEqual('standard', xtype_from_derivation("m/45'/56165/271'")) + self.assertEqual('p2wpkh-p2sh', xtype_from_derivation("m/49'")) + self.assertEqual('p2wpkh-p2sh', xtype_from_derivation("m/49'/134")) + self.assertEqual('p2wpkh', xtype_from_derivation("m/84'")) + self.assertEqual('p2wpkh', xtype_from_derivation("m/84'/112'/992/112/33'/0/2")) + self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/0'/0'/1'")) + self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/0'/0'/1'/52112/52'")) + self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/9'/2'/1'")) + self.assertEqual('p2wsh', xtype_from_derivation("m/48'/0'/0'/2'")) + self.assertEqual('p2wsh', xtype_from_derivation("m/48'/1'/0'/2'/77'/0")) + + def test_version_bytes(self): + xprv_headers_b58 = { + 'standard': 'xprv', + 'p2wpkh-p2sh': 'yprv', + 'p2wsh-p2sh': 'Yprv', + 'p2wpkh': 'zprv', + 'p2wsh': 'Zprv', + } + xpub_headers_b58 = { + 'standard': 'xpub', + 'p2wpkh-p2sh': 'ypub', + 'p2wsh-p2sh': 'Ypub', + 'p2wpkh': 'zpub', + 'p2wsh': 'Zpub', + } + for xtype, xkey_header_bytes in constants.net.XPRV_HEADERS.items(): + xkey_header_bytes = bfh("%08x" % xkey_header_bytes) + xkey_bytes = xkey_header_bytes + bytes([0] * 74) + xkey_b58 = EncodeBase58Check(xkey_bytes) + self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype])) + + xkey_bytes = xkey_header_bytes + bytes([255] * 74) + xkey_b58 = EncodeBase58Check(xkey_bytes) + self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype])) + + for xtype, xkey_header_bytes in constants.net.XPUB_HEADERS.items(): + xkey_header_bytes = bfh("%08x" % xkey_header_bytes) + xkey_bytes = xkey_header_bytes + bytes([0] * 74) + xkey_b58 = EncodeBase58Check(xkey_bytes) + self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) + + xkey_bytes = xkey_header_bytes + bytes([255] * 74) + xkey_b58 = EncodeBase58Check(xkey_bytes) + self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) + + +class Test_xprv_xpub_testnet(TestCaseForTestnet): + + def test_version_bytes(self): + xprv_headers_b58 = { + 'standard': 'tprv', + 'p2wpkh-p2sh': 'uprv', + 'p2wsh-p2sh': 'Uprv', + 'p2wpkh': 'vprv', + 'p2wsh': 'Vprv', + } + xpub_headers_b58 = { + 'standard': 'tpub', + 'p2wpkh-p2sh': 'upub', + 'p2wsh-p2sh': 'Upub', + 'p2wpkh': 'vpub', + 'p2wsh': 'Vpub', + } + for xtype, xkey_header_bytes in constants.net.XPRV_HEADERS.items(): + xkey_header_bytes = bfh("%08x" % xkey_header_bytes) + xkey_bytes = xkey_header_bytes + bytes([0] * 74) + xkey_b58 = EncodeBase58Check(xkey_bytes) + self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype])) + + xkey_bytes = xkey_header_bytes + bytes([255] * 74) + xkey_b58 = EncodeBase58Check(xkey_bytes) + self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype])) + + for xtype, xkey_header_bytes in constants.net.XPUB_HEADERS.items(): + xkey_header_bytes = bfh("%08x" % xkey_header_bytes) + xkey_bytes = xkey_header_bytes + bytes([0] * 74) + xkey_b58 = EncodeBase58Check(xkey_bytes) + self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) + + xkey_bytes = xkey_header_bytes + bytes([255] * 74) + xkey_b58 = EncodeBase58Check(xkey_bytes) + self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) + + +class Test_keyImport(SequentialTestCase): + + priv_pub_addr = ( + {'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6', + 'exported_privkey': 'p2pkh:KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6', + 'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997', + 'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR', + 'minikey' : False, + 'txin_type': 'p2pkh', + 'compressed': True, + 'addr_encoding': 'base58', + 'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'}, + {'priv': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD', + 'exported_privkey': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD', + 'pub': '0352d78b4b37e0f6d4e164423436f2925fa57817467178eca550a88f2821973c41', + 'address': '1GXgZ5Qi6gmXTHVSpUPZLy4Ci2nbfb3ZNb', + 'minikey': False, + 'txin_type': 'p2pkh', + 'compressed': True, + 'addr_encoding': 'base58', + 'scripthash': 'a9b2a76fc196c553b352186dfcca81fcf323a721cd8431328f8e9d54216818c1'}, + {'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', + 'exported_privkey': 'p2pkh:5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', + 'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f', + 'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6', + 'minikey': False, + 'txin_type': 'p2pkh', + 'compressed': False, + 'addr_encoding': 'base58', + 'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'}, + {'priv': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN', + 'exported_privkey': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN', + 'pub': '048f0431b0776e8210376c81280011c2b68be43194cb00bd47b7e9aa66284b713ce09556cde3fee606051a07613f3c159ef3953b8927c96ae3dae94a6ba4182e0e', + 'address': '147kiRHHm9fqeMQSgqf4k35XzuWLP9fmmS', + 'minikey': False, + 'txin_type': 'p2pkh', + 'compressed': False, + 'addr_encoding': 'base58', + 'scripthash': '6dd2e07ad2de9ba8eec4bbe8467eb53f8845acff0d9e6f5627391acc22ff62df'}, + {'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz', + 'exported_privkey': 'p2wpkh-p2sh:Kz9XebiCXL2BZzhYJViiHDzn5iup1povWV8aqstzWU4sz1K5nVva', + 'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81', + 'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7', + 'minikey': False, + 'txin_type': 'p2wpkh-p2sh', + 'compressed': True, + 'addr_encoding': 'base58', + 'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'}, + {'priv': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW', + 'exported_privkey': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW', + 'pub': '0229da20a15b3363b2c28e3c5093c180b56c439df0b968a970366bb1f38435361e', + 'address': '3C79goMwT7zSTjXnPoCg6VFGAnUpZAkyus', + 'minikey': False, + 'txin_type': 'p2wpkh-p2sh', + 'compressed': True, + 'addr_encoding': 'base58', + 'scripthash': '714bf6bfe1083e69539f40d4c7a7dca85d187471b35642e55f20d7e866494cf7'}, + {'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj', + 'exported_privkey': 'p2wpkh:Kz6SuyPM5VktY5dr2d2YqdVgBA6LCWkiHqXJaC3BzxnMPSUuYzmF', + 'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b', + 'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue', + 'minikey': False, + 'txin_type': 'p2wpkh', + 'compressed': True, + 'addr_encoding': 'bech32', + 'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'}, + {'priv': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo', + 'exported_privkey': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo', + 'pub': '038c57657171c1f73e34d5b3971d05867d50221ad94980f7e87cbc2344425e6a1e', + 'address': 'bc1qpakeeg4d9ydyjxd8paqrw4xy9htsg532xzxn50', + 'minikey': False, + 'txin_type': 'p2wpkh', + 'compressed': True, + 'addr_encoding': 'bech32', + 'scripthash': '242f02adde84ebb2a7dd778b2f3a81b3826f111da4d8960d826d7a4b816cb261'}, + # from http://bitscan.com/articles/security/spotlight-on-mini-private-keys + {'priv': 'SzavMBLoXU6kDrqtUVmffv', + 'exported_privkey': 'p2pkh:5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF', + 'pub': '04588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9f88ff2a00d7e752d44cbe16e1ebcf0890b76ec7c78886109dee76ccfc8445424', + 'address': '1CC3X2gu58d6wXUWMffpuzN9JAfTUWu4Kj', + 'minikey': True, + 'txin_type': 'p2pkh', + 'compressed': False, # this is actually ambiguous... issue #2748 + 'addr_encoding': 'base58', + 'scripthash': '5b07ddfde826f5125ee823900749103cea37808038ecead5505a766a07c34445'}, + ) + + @needs_test_with_all_ecc_implementations + def test_public_key_from_private_key(self): + for priv_details in self.priv_pub_addr: + txin_type, privkey, compressed = deserialize_privkey(priv_details['priv']) + result = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) + self.assertEqual(priv_details['pub'], result) + self.assertEqual(priv_details['txin_type'], txin_type) + self.assertEqual(priv_details['compressed'], compressed) + + @needs_test_with_all_ecc_implementations + def test_address_from_private_key(self): + for priv_details in self.priv_pub_addr: + addr2 = address_from_private_key(priv_details['priv']) + self.assertEqual(priv_details['address'], addr2) + + @needs_test_with_all_ecc_implementations + def test_is_valid_address(self): + for priv_details in self.priv_pub_addr: + addr = priv_details['address'] + self.assertFalse(is_address(priv_details['priv'])) + self.assertFalse(is_address(priv_details['pub'])) + self.assertTrue(is_address(addr)) + + is_enc_b58 = priv_details['addr_encoding'] == 'base58' + self.assertEqual(is_enc_b58, is_b58_address(addr)) + + is_enc_bech32 = priv_details['addr_encoding'] == 'bech32' + self.assertEqual(is_enc_bech32, is_segwit_address(addr)) + + self.assertFalse(is_address("not an address")) + + @needs_test_with_all_ecc_implementations + def test_is_private_key(self): + for priv_details in self.priv_pub_addr: + self.assertTrue(is_private_key(priv_details['priv'])) + self.assertTrue(is_private_key(priv_details['exported_privkey'])) + self.assertFalse(is_private_key(priv_details['pub'])) + self.assertFalse(is_private_key(priv_details['address'])) + self.assertFalse(is_private_key("not a privkey")) + + @needs_test_with_all_ecc_implementations + def test_serialize_privkey(self): + for priv_details in self.priv_pub_addr: + txin_type, privkey, compressed = deserialize_privkey(priv_details['priv']) + priv2 = serialize_privkey(privkey, compressed, txin_type) + self.assertEqual(priv_details['exported_privkey'], priv2) + + @needs_test_with_all_ecc_implementations + def test_address_to_scripthash(self): + for priv_details in self.priv_pub_addr: + sh = address_to_scripthash(priv_details['address']) + self.assertEqual(priv_details['scripthash'], sh) + + @needs_test_with_all_ecc_implementations + def test_is_minikey(self): + for priv_details in self.priv_pub_addr: + minikey = priv_details['minikey'] + priv = priv_details['priv'] + self.assertEqual(minikey, is_minikey(priv)) + + @needs_test_with_all_ecc_implementations + def test_is_compressed(self): + for priv_details in self.priv_pub_addr: + self.assertEqual(priv_details['compressed'], + is_compressed(priv_details['priv'])) + + +class Test_seeds(SequentialTestCase): + """ Test old and new seeds. """ + + mnemonics = { + ('cell dumb heartbeat north boom tease ship baby bright kingdom rare squeeze', 'old'), + ('cell dumb heartbeat north boom tease ' * 4, 'old'), + ('cell dumb heartbeat north boom tease ship baby bright kingdom rare badword', ''), + ('cElL DuMb hEaRtBeAt nOrTh bOoM TeAsE ShIp bAbY BrIgHt kInGdOm rArE SqUeEzE', 'old'), + (' cElL DuMb hEaRtBeAt nOrTh bOoM TeAsE ShIp bAbY BrIgHt kInGdOm rArE SqUeEzE ', 'old'), + # below seed is actually 'invalid old' as it maps to 33 hex chars + ('hurry idiot prefer sunset mention mist jaw inhale impossible kingdom rare squeeze', 'old'), + ('cram swing cover prefer miss modify ritual silly deliver chunk behind inform able', 'standard'), + ('cram swing cover prefer miss modify ritual silly deliver chunk behind inform', ''), + ('ostrich security deer aunt climb inner alpha arm mutual marble solid task', 'standard'), + ('OSTRICH SECURITY DEER AUNT CLIMB INNER ALPHA ARM MUTUAL MARBLE SOLID TASK', 'standard'), + (' oStRiCh sEcUrItY DeEr aUnT ClImB InNeR AlPhA ArM MuTuAl mArBlE SoLiD TaSk ', 'standard'), + ('x8', 'standard'), + ('science dawn member doll dutch real can brick knife deny drive list', '2fa'), + ('science dawn member doll dutch real ca brick knife deny drive list', ''), + (' sCience dawn member doll Dutch rEAl can brick knife deny drive lisT', '2fa'), + ('frost pig brisk excite novel report camera enlist axis nation novel desert', 'segwit'), + (' fRoSt pig brisk excIte novel rePort CamEra enlist axis nation nOVeL dEsert ', 'segwit'), + ('9dk', 'segwit'), + } + + def test_new_seed(self): + seed = "cram swing cover prefer miss modify ritual silly deliver chunk behind inform able" + self.assertTrue(is_new_seed(seed)) + + seed = "cram swing cover prefer miss modify ritual silly deliver chunk behind inform" + self.assertFalse(is_new_seed(seed)) + + def test_old_seed(self): + self.assertTrue(is_old_seed(" ".join(["like"] * 12))) + self.assertFalse(is_old_seed(" ".join(["like"] * 18))) + self.assertTrue(is_old_seed(" ".join(["like"] * 24))) + self.assertFalse(is_old_seed("not a seed")) + + self.assertTrue(is_old_seed("0123456789ABCDEF" * 2)) + self.assertTrue(is_old_seed("0123456789ABCDEF" * 4)) + + def test_seed_type(self): + for seed_words, _type in self.mnemonics: + self.assertEqual(_type, seed_type(seed_words), msg=seed_words) diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py @@ -0,0 +1,33 @@ +import unittest +from decimal import Decimal + +from electrum.commands import Commands + + +class TestCommands(unittest.TestCase): + + def test_setconfig_non_auth_number(self): + self.assertEqual(7777, Commands._setconfig_normalize_value('rpcport', "7777")) + self.assertEqual(7777, Commands._setconfig_normalize_value('rpcport', '7777')) + self.assertAlmostEqual(Decimal(2.3), Commands._setconfig_normalize_value('somekey', '2.3')) + + def test_setconfig_non_auth_number_as_string(self): + self.assertEqual("7777", Commands._setconfig_normalize_value('somekey', "'7777'")) + + def test_setconfig_non_auth_boolean(self): + self.assertEqual(True, Commands._setconfig_normalize_value('show_console_tab', "true")) + self.assertEqual(True, Commands._setconfig_normalize_value('show_console_tab', "True")) + + def test_setconfig_non_auth_list(self): + self.assertEqual(['file:///var/www/', 'https://electrum.org'], + Commands._setconfig_normalize_value('url_rewrite', "['file:///var/www/','https://electrum.org']")) + self.assertEqual(['file:///var/www/', 'https://electrum.org'], + Commands._setconfig_normalize_value('url_rewrite', '["file:///var/www/","https://electrum.org"]')) + + def test_setconfig_auth(self): + self.assertEqual("7777", Commands._setconfig_normalize_value('rpcuser', "7777")) + self.assertEqual("7777", Commands._setconfig_normalize_value('rpcuser', '7777')) + self.assertEqual("7777", Commands._setconfig_normalize_value('rpcpassword', '7777')) + self.assertEqual("2asd", Commands._setconfig_normalize_value('rpcpassword', '2asd')) + self.assertEqual("['file:///var/www/','https://electrum.org']", + Commands._setconfig_normalize_value('rpcpassword', "['file:///var/www/','https://electrum.org']")) diff --git a/electrum/tests/test_dnssec.py b/electrum/tests/test_dnssec.py @@ -0,0 +1,41 @@ +import dns + +from electrum import dnssec + +from . import SequentialTestCase +from .test_bitcoin import needs_test_with_all_ecc_implementations + + +class TestDnsSec(SequentialTestCase): + + @needs_test_with_all_ecc_implementations + def test_python_validate_rrsig_ecdsa(self): + rrset = dns.rrset.from_text("getmonero.org.", 3599, 1, 48, + "257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0d xCjjnopKl+GqJxpVXckHAeF+KkxLbxIL fDLUT0rAK9iUzy1L53eKGQ==", + "256 3 13 koPbw9wmYZ7ggcjnQ6ayHyhHaDNMYELK TqT+qRGrZpWSccr/lBcrm10Z1PuQHB3A zhii+sb0PYFkH1ruxLhe5g==") + rrsig = dns.rdtypes.ANY.RRSIG.RRSIG.from_text(1, 46, + dns.tokenizer.Tokenizer("DNSKEY 13 2 3600 20180612115508 20180413115508 2371 getmonero.org. SSjtP2jCtXPukps7E3kum709xq2TH6Lt Ur32UhE7WKwSUfLTZ4EAoD5g22mi1fpB GDGb30kCMndDVjnHAEBDWw==")) + keys = {dns.name.Name([b'getmonero', b'org', b'']): rrset} + origin = None + now = 1527185178.7842247 + + # 'None' means it is valid + self.assertEqual(None, dnssec.python_validate_rrsig(rrset, rrsig, keys, origin, now)) + + def test_python_validate_rrsig_rsa(self): + rrset = dns.rrset.from_text("getmonero.org.", 12698, 1, 43, + "2371 13 2 3b7f818a879ecb9931dae983d4529afedeb53993759d8080735083f954d40bc8") + rrsig = dns.rdtypes.ANY.RRSIG.RRSIG.from_text(1, 46, + dns.tokenizer.Tokenizer("DS 7 2 86400 20180609010045 20180519000045 1862 org. SgdGsY4BAm7c3qpwzVLy3ua4orvrsJQO 0rUQDDrrXR6lElnbF+AS0gEEfdZfDv11 65AuNil/+kT2Qh/ExgstvhWQ88XdDnHB ouvRMf9pg3p/q5Otet/StRzf33SMPgC1 zLzkfkSBCjJkwVmwde8saGnjdcW522ra Ge/6JcsryRw=")) + + rrset2 = dns.rrset.from_text("org.", 866, 1, 48, + "256 3 7 AwEAAXxsMmN/JgpEE9Y4uFNRJm7Q9GBw mEYUCsCxuKlgBU9WrQEFRrvAeMamUBeX 4SE8s3V/TEk/TgGmPPp0pMkKD7mseluK 6Ard2HZ6O3nPAzL4i8py/UDRUmYNSCxw fdfjUWRmcB9H+NKWMsJoDhAkLFqg5HS7 f0j4Vb99Wac24Fk7", + "256 3 7 AwEAAcLdAPt3vn/ND00zZlyTx7OBko+9 YeCrSl2eGuEXjef0Lqf0tKGikoHwnmTH tT8J/aGqkZImLMVByJbknE0wKDnbvbKD oTQxPwUQZLH6k3sTdsPKESKDSBSc6VFM q35gx6CeuRYZ9KkGWiUsKqJhXPo6tyJF CBxfaNQQyrzBnv4/", + "257 3 7 AwEAAZTjbIO5kIpxWUtyXc8avsKyHIIZ +LjC2Dv8naO+Tz6X2fqzDC1bdq7HlZwt kaqTkMVVJ+8gE9FIreGJ4c8G1GdbjQgb P1OyYIG7OHTc4hv5T2NlyWr6k6QFz98Q 4zwFIGTFVvwBhmrMDYsOTtXakK6QwHov A1+83BsUACxlidpwB0hQacbD6x+I2RCD zYuTzj64Jv0/9XsX6AYV3ebcgn4hL1jI R2eJYyXlrAoWxdzxcW//5yeL5RVWuhRx ejmnSVnCuxkfS4AQ485KH2tpdbWcCopL JZs6tw8q3jWcpTGzdh/v3xdYfNpQNcPI mFlxAun3BtORPA2r8ti6MNoJEHU=", + "257 3 7 AwEAAcMnWBKLuvG/LwnPVykcmpvnntwx fshHlHRhlY0F3oz8AMcuF8gw9McCw+Bo C2YxWaiTpNPuxjSNhUlBtcJmcdkz3/r7 PIn0oDf14ept1Y9pdPh8SbIBIWx50ZPf VRlj8oQXv2Y6yKiQik7bi3MT37zMRU2k w2oy3cgrsGAzGN4s/C6SFYon5N1Q2O4h GDbeOq538kATOy0GFELjuauV9guX/431 msYu4Rgb5lLuQ3Mx5FSIxXpI/RaAn2mh M4nEZ/5IeRPKZVGydcuLBS8GZlxW4qbb 8MgRZ8bwMg0pqWRHmhirGmJIt3UuzvN1 pSFBfX7ysI9PPhSnwXCNDXk0kk0=") + keys = {dns.name.Name([b'org', b'']): rrset2} + origin = None + now = 1527191953.6527798 + + # 'None' means it is valid + self.assertEqual(None, dnssec.python_validate_rrsig(rrset, rrsig, keys, origin, now)) diff --git a/electrum/tests/test_interface.py b/electrum/tests/test_interface.py @@ -0,0 +1,28 @@ +import unittest + +from electrum import interface + +from . import SequentialTestCase + + +class TestInterface(SequentialTestCase): + + def test_match_host_name(self): + self.assertTrue(interface._match_hostname('asd.fgh.com', 'asd.fgh.com')) + self.assertFalse(interface._match_hostname('asd.fgh.com', 'asd.zxc.com')) + self.assertTrue(interface._match_hostname('asd.fgh.com', '*.fgh.com')) + self.assertFalse(interface._match_hostname('asd.fgh.com', '*fgh.com')) + self.assertFalse(interface._match_hostname('asd.fgh.com', '*.zxc.com')) + + def test_check_host_name(self): + i = interface.TcpConnection(server=':1:', queue=None, config_path=None) + + self.assertFalse(i.check_host_name(None, None)) + self.assertFalse(i.check_host_name( + peercert={'subjectAltName': []}, name='')) + self.assertTrue(i.check_host_name( + peercert={'subjectAltName': [('DNS', 'foo.bar.com')]}, + name='foo.bar.com')) + self.assertTrue(i.check_host_name( + peercert={'subject': [('commonName', 'foo.bar.com')]}, + name='foo.bar.com')) diff --git a/electrum/tests/test_mnemonic.py b/electrum/tests/test_mnemonic.py @@ -0,0 +1,42 @@ +import unittest +from electrum import keystore +from electrum import mnemonic +from electrum import old_mnemonic +from electrum.util import bh2u + +from . import SequentialTestCase + + +class Test_NewMnemonic(SequentialTestCase): + + def test_to_seed(self): + seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic='foobar', passphrase='none') + self.assertEqual(bh2u(seed), + '741b72fd15effece6bfe5a26a52184f66811bd2be363190e07a42cca442b1a5b' + 'b22b3ad0eb338197287e6d314866c7fba863ac65d3f156087a5052ebc7157fce') + + def test_random_seeds(self): + iters = 10 + m = mnemonic.Mnemonic(lang='en') + for _ in range(iters): + seed = m.make_seed() + i = m.mnemonic_decode(seed) + self.assertEqual(m.mnemonic_encode(i), seed) + + +class Test_OldMnemonic(SequentialTestCase): + + def test(self): + seed = '8edad31a95e7d59f8837667510d75a4d' + result = old_mnemonic.mn_encode(seed) + words = 'hardly point goal hallway patience key stone difference ready caught listen fact' + self.assertEqual(result, words.split()) + self.assertEqual(old_mnemonic.mn_decode(result), seed) + +class Test_BIP39Checksum(SequentialTestCase): + + def test(self): + mnemonic = u'gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog' + is_checksum_valid, is_wordlist_valid = keystore.bip39_is_checksum_valid(mnemonic) + self.assertTrue(is_wordlist_valid) + self.assertTrue(is_checksum_valid) diff --git a/electrum/tests/test_simple_config.py b/electrum/tests/test_simple_config.py @@ -0,0 +1,149 @@ +import ast +import sys +import os +import unittest +import tempfile +import shutil + +from io import StringIO +from electrum.simple_config import (SimpleConfig, read_user_config) + +from . import SequentialTestCase + + +class Test_SimpleConfig(SequentialTestCase): + + def setUp(self): + super(Test_SimpleConfig, self).setUp() + # make sure "read_user_config" and "user_dir" return a temporary directory. + self.electrum_dir = tempfile.mkdtemp() + # Do the same for the user dir to avoid overwriting the real configuration + # for development machines with electrum installed :) + self.user_dir = tempfile.mkdtemp() + + self.options = {"electrum_path": self.electrum_dir} + self._saved_stdout = sys.stdout + self._stdout_buffer = StringIO() + sys.stdout = self._stdout_buffer + + def tearDown(self): + super(Test_SimpleConfig, self).tearDown() + # Remove the temporary directory after each test (to make sure we don't + # pollute /tmp for nothing. + shutil.rmtree(self.electrum_dir) + shutil.rmtree(self.user_dir) + + # Restore the "real" stdout + sys.stdout = self._saved_stdout + + def test_simple_config_key_rename(self): + """auto_cycle was renamed auto_connect""" + fake_read_user = lambda _: {"auto_cycle": True} + read_user_dir = lambda : self.user_dir + config = SimpleConfig(options=self.options, + read_user_config_function=fake_read_user, + read_user_dir_function=read_user_dir) + self.assertEqual(config.get("auto_connect"), True) + self.assertEqual(config.get("auto_cycle"), None) + fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True} + config = SimpleConfig(options=self.options, + read_user_config_function=fake_read_user, + read_user_dir_function=read_user_dir) + self.assertEqual(config.get("auto_connect"), False) + self.assertEqual(config.get("auto_cycle"), None) + + def test_simple_config_command_line_overrides_everything(self): + """Options passed by command line override all other configuration + sources""" + fake_read_user = lambda _: {"electrum_path": "b"} + read_user_dir = lambda : self.user_dir + config = SimpleConfig(options=self.options, + read_user_config_function=fake_read_user, + read_user_dir_function=read_user_dir) + self.assertEqual(self.options.get("electrum_path"), + config.get("electrum_path")) + + def test_simple_config_user_config_is_used_if_others_arent_specified(self): + """If no system-wide configuration and no command-line options are + specified, the user configuration is used instead.""" + fake_read_user = lambda _: {"electrum_path": self.electrum_dir} + read_user_dir = lambda : self.user_dir + config = SimpleConfig(options={}, + read_user_config_function=fake_read_user, + read_user_dir_function=read_user_dir) + self.assertEqual(self.options.get("electrum_path"), + config.get("electrum_path")) + + def test_cannot_set_options_passed_by_command_line(self): + fake_read_user = lambda _: {"electrum_path": "b"} + read_user_dir = lambda : self.user_dir + config = SimpleConfig(options=self.options, + read_user_config_function=fake_read_user, + read_user_dir_function=read_user_dir) + config.set_key("electrum_path", "c") + self.assertEqual(self.options.get("electrum_path"), + config.get("electrum_path")) + + def test_can_set_options_set_in_user_config(self): + another_path = tempfile.mkdtemp() + fake_read_user = lambda _: {"electrum_path": self.electrum_dir} + read_user_dir = lambda : self.user_dir + config = SimpleConfig(options={}, + read_user_config_function=fake_read_user, + read_user_dir_function=read_user_dir) + config.set_key("electrum_path", another_path) + self.assertEqual(another_path, config.get("electrum_path")) + + def test_user_config_is_not_written_with_read_only_config(self): + """The user config does not contain command-line options when saved.""" + fake_read_user = lambda _: {"something": "a"} + read_user_dir = lambda : self.user_dir + self.options.update({"something": "c"}) + config = SimpleConfig(options=self.options, + read_user_config_function=fake_read_user, + read_user_dir_function=read_user_dir) + config.save_user_config() + contents = None + with open(os.path.join(self.electrum_dir, "config"), "r") as f: + contents = f.read() + result = ast.literal_eval(contents) + result.pop('config_version', None) + self.assertEqual({"something": "a"}, result) + + +class TestUserConfig(SequentialTestCase): + + def setUp(self): + super(TestUserConfig, self).setUp() + self._saved_stdout = sys.stdout + self._stdout_buffer = StringIO() + sys.stdout = self._stdout_buffer + + self.user_dir = tempfile.mkdtemp() + + def tearDown(self): + super(TestUserConfig, self).tearDown() + shutil.rmtree(self.user_dir) + sys.stdout = self._saved_stdout + + def test_no_path_means_no_result(self): + result = read_user_config(None) + self.assertEqual({}, result) + + def test_path_without_config_file(self): + """We pass a path but if does not contain a "config" file.""" + result = read_user_config(self.user_dir) + self.assertEqual({}, result) + + def test_path_with_reprd_object(self): + + class something(object): + pass + + thefile = os.path.join(self.user_dir, "config") + payload = something() + with open(thefile, "w") as f: + f.write(repr(payload)) + + result = read_user_config(self.user_dir) + self.assertEqual({}, result) diff --git a/electrum/tests/test_storage_upgrade.py b/electrum/tests/test_storage_upgrade.py @@ -0,0 +1,301 @@ +import shutil +import tempfile + +from electrum.storage import WalletStorage +from electrum.wallet import Wallet + +from .test_wallet import WalletTestCase + +from . import SequentialTestCase + + +# TODO add other wallet types: 2fa, xpub-only +# TODO hw wallet with client version 2.6.x (single-, and multiacc) +class TestStorageUpgrade(WalletTestCase): + + def test_upgrade_from_client_1_9_8_seeded(self): + wallet_str = "{'addr_history':{'177hEYTccmuYH8u68pYfaLteTxwJrVgvJj':[],'15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc':[],'1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf':[],'1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs':[],'1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC':[],'1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm':[],'1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj':[],'1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa':[]},'accounts_expanded':{},'master_public_key':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb','use_encryption':False,'seed':'2605aafe50a45bdf2eb155302437e678','accounts':{0:{0:['1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC','1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj','177hEYTccmuYH8u68pYfaLteTxwJrVgvJj','1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm','15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc'],1:['1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs','1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa','1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf']}},'seed_version':4}" + self._upgrade_storage(wallet_str) + + # TODO pre-2.0 mixed wallets are not split currently + #def test_upgrade_from_client_1_9_8_mixed(self): + # wallet_str = "{'addr_history':{'15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc':[],'177hEYTccmuYH8u68pYfaLteTxwJrVgvJj':[],'1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC':[],'1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm':[],'1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj':[],'1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf':[],'1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs':[],'1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa':[]},'accounts_expanded':{},'master_public_key':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb','use_encryption':False,'seed':'2605aafe50a45bdf2eb155302437e678','accounts':{0:{0:['1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC','1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj','177hEYTccmuYH8u68pYfaLteTxwJrVgvJj','1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm','15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc'],1:['1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs','1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa','1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf'],'mpk':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb'}},'imported_keys':{'15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA':'5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq','1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6':'L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U','1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr':'L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM'},'seed_version':4}" + # self._upgrade_storage(wallet_str, accounts=2) + + def test_upgrade_from_client_2_0_4_seeded(self): + wallet_str = '{"accounts":{"0":{"change":["03d8e267e8de7769b52a8727585b3c44b4e148b86b2c90e3393f78a75bd6aab83f","03f09b3562bec870b4eb8626c20d449ee85ef17ea896a6a82b454e092eef91b296","02df953880df9284715e8199254edcf3708c635adc92a90dbf97fbd64d1eb88a36"],"receiving":["02cd4d73d5e335dafbf5c9338f88ceea3d7511ab0f9b8910745ac940ff40913a30","0243ed44278a178101e0fb14d36b68e6e13d00fe3434edb56e4504ea6f5db2e467","0367c0aa3681ec3635078f79f8c78aa339f19e38d9e1c9e2853e30e66ade02cac3","0237d0fe142cff9d254a3bdd3254f0d5f72676b0099ba799764a993a0d0ba80111","020a899fd417527b3929c8f625c93b45392244bab69ff91b582ed131977d5cd91e","039e84264920c716909b88700ef380336612f48237b70179d0b523784de28101f7","03125452df109a51be51fe21e71c3a4b0bba900c9c0b8d29b4ee2927b51f570848","0291fa554217090bab96eeff63e1c6fdec37358ed597d18fa32c60c02a48878c8c","030b6354a4365bab55e86269fb76241fd69716f02090ead389e1fce13d474aa569","023dcba431d8887ab63595f0df1e978e4a5f1c3aac6670e43d03956448a229f740","0332a61cbe04fe027033369ce7569b860c24462878bdd8c0332c22a3f5fdcc1790","021249480422d93dba2aafcd4575e6f630c4e3a2a832dd8a15f884e1052b6836e4","02516e91dede15d3a15dd648591bb92e107b3a53d5bc34b286ab389ce1af3130aa","02e1da3dddd81fa6e4895816da9d4b8ab076d6ea8034b1175169c0f247f002f4cf","0390ef1e3fdbe137767f8b5abad0088b105eee8c39e075305545d405be3154757a","03fca30eb33c6e1ffa071d204ccae3060680856ae9b93f31f13dd11455e67ee85d","034f6efdbbe1bfa06b32db97f16ff3a0dd6cf92769e8d9795c465ff76d2fbcb794","021e2901009954f23d2bf3429d4a531c8ca3f68e9598687ef816f20da08ff53848","02d3ccf598939ff7919ee23d828d229f85e3e58842582bf054491c59c8b974aa6e","03a1daffa39f42c1aaae24b859773a170905c6ee8a6dab8c1bfbfc93f09b88f4db"],"xpub":"xpub661MyMwAqRbcFsrzES8RWNiD7RxDqT4p8NjvTY9mLi8xdphQ9x1TiY8GnqCpQx4LqJBdcGeXrsAa2b2G7ZcjJcest9wHcqYfTqXmQja6vfV"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K3PnX8QbR9EmUZQ7jRzLxm9pKf9k9nNbym2NFcQhDAjonwZ39jtWLYp6qk5UHotj13p2y7w1ZhhvvyV5eCcaPUrKofs9CXQ9"},"master_public_keys":{"x/":"xpub661MyMwAqRbcFsrzES8RWNiD7RxDqT4p8NjvTY9mLi8xdphQ9x1TiY8GnqCpQx4LqJBdcGeXrsAa2b2G7ZcjJcest9wHcqYfTqXmQja6vfV"},"seed":"seven direct thunder glare prevent please fatal blush buzz artefact gate vendor above","seed_version":11,"use_encryption":false,"wallet_type":"standard"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_0_4_importedkeys(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"use_encryption":false,"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_0_4_watchaddresses(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_0_4_trezor_singleacc(self): + wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_0_4_trezor_multiacc(self): + wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]]},"labels":{"0":"Main account","1":"acc1"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str, accounts=2) + + def test_upgrade_from_client_2_0_4_multisig(self): + wallet_str = '{"accounts":{"0":{"change":[["03c3a8549f35d7842192e7e00afa25ef1c779d05f1c891ba7c30de968fb29e3e78","02e191e105bccf1b4562d216684632b9ec22c87e1457b537eb27516afa75c56831"],["03793397f02b3bd3d0f6f0dafc7d42b9701234a269805d89efbbc2181683368e4b","02153705b8e4df41dc9d58bc0360c79a9209b3fc289ec54118f0b149d5a3b3546d"],["02511e8cfb39c8ce1c790f26bcab68ba5d5f79845ec1c6a92b0ac9f331648d866a","02c29c1ea70e23d866204a11ec8d8ecd70d6f51f58dd8722824cacb1985d4d1870"]],"receiving":[["0283ce4f0f12811e1b27438a3edb784aeb600ca7f4769c9c49c3e704e216421d3e","03a1bbada7401cade3b25a23e354186c772e2ba4ac0d9c0447627f7d540eb9891d"],["0286b45a0bcaa215716cbc59a22b6b1910f9ebad5884f26f55c2bb38943ee8fdb6","02799680336c6bd19005588fad12256223cb8a416649d60ea5d164860c0872b931"],["039e2bf377709e41bba49fb1f3f873b9b87d50ae3b574604cd9b96402211ea1f36","02ef9ceaaf754ba46f015e1d704f1a06157cc4441da0cfaf096563b22ec225ca5f"],["025220baaca5bff1a5ffbf4d36e9fcc6f5d05f4af750ef29f6d88d9b5f95fef79a","02350c81bebfa3a894df69302a6601175731d443948a12d8ec7860981988e3803e"],["028fd6411534d722b625482659de54dd609f5b5c935ae8885ca24bfd3266210527","03b9c7780575f17e64f9dfd5947945b1dbdb65aecef562ac076335fd7aa09844e4"],["0353066065985ec06dbef33e7a081d9240023891a51c4e9eda7b3eb1b4af165e04","028c3fa7622e4c8bac07a2c549885a045532e67a934ca10e20729d0fdfe3a75339"],["02253b4eabf2834af86b409d5ca8e671de9a75c3937bff2dac9521c377ca195668","02d5e83c445684eb502049f48e621e1ca16e07e5dc4013c84d661379635f58877b"],["030d38e4c7a5c7c9551adcace3b70dcaa02bf841febd6dc308f3abd7b7bf2bdc49","0375a0b50cd7f3af51550207a766c5db326b2294f5a4b456a90190e4fbeb720d97"],["0327280215ba4a0d8c404085c4f6091906a9e1ada7ce4202a640ac701446095954","037cd9b5e6664d28a61e01626056cdb7e008815b365c8b65fa50ac44d6c1ad126e"],["02f80a80146674da828fc67a062d1ab47fb0714cf40ec5c517ee23ea71d3033474","03fd8ab9bc9458b87e0b7b2a46ea6b46de0a5f6ecaf1a204579698bfa881ff93ce"],["034965bd56c6ca97e0e5ffa79cdc1f15772fa625b76da84cc8adb1707e2e101775","033e13cb19d930025bfc801b829e64d12934f9f19df718f4ea6160a4fb61320a9c"],["034de271009a06d733de22601c3d3c6fe8b3ec5a44f49094ac002dc1c90a3b096d","023f0b2f653c0fdbdc292040fee363ceaa5828cfd8e012abcf6cd9bad2eaa3dc72"],["022aec8931c5b17bdcdd6637db34718db6f267cb0a55a611eb6602e15deb6ed4df","021de5d4bbb73b6dfab2c0df6970862b08130902ff3160f31681f34aecf39721f6"],["02a0e3b52293ec73f89174ff6e5082fcfebc45f2fdd9cfe12a6981aa120a7c1fa7","0371d41b5f18e8e1990043c1e52f998937bc7e81b8ace4ddfc5cd0d029e4c81894"],["030bc1cbe4d750067254510148e3af9bc84925cdd17db3b54d9bbf4a409b83719a","0371c4800364a8a32bfbda7ea7724c1f5bdbd794df8a6080a3bd3b52c52cf32402"],["0318c5cd5f19ff037e3dec3ce5ac1a48026f5a58c4129271b12ae22f8542bcd718","03b5c70db71d520d04f810742e7a5f42d810e94ec6cbf4b48fa6dd7b4d425e76c1"],["0213f68b86a8c4a0840fa88d9a06904c59292ec50172813b8cca62768f3b708811","0353037209eb400ba7fcfa9f296a8b2745e1bbcbfb28c4adebf74de2e0e6a58c00"],["028decff8a7f5a7982402d95b050fbc9958e449f154990bbfe0f553a1d4882fd03","025ecd14812876e885d8f54cab30d1c2a8ae6c6ed0847e96abd65a3700148d94e2"],["0267f8dab8fdc1df4231414f31cfeb58ce96f3471ba78328cd429263d151c81fed","03e0d01df1fd9e958a7324d29afefbc76793a40447a2625c494355c577727d69ba"],["03de3c4d173b27cdfdd8e56fbf3cd6ee8729b94209c20e5558ddd7a76281a37e2e","0218ccb595d7fa559f0bae1ea76d19526980b027fb9be009b6b486d8f8eb0e00d5"]],"xpub":"xpub661MyMwAqRbcFUEYv1psxyPnjiHhTYe85AwFRs5jShbpgrfQ9UXBmxantqgGT3oAVLiHDYoR3ruT3xRGcxsmBMJxyg94FGcxF86QnzYDc6e","xpub2":"xpub661MyMwAqRbcGFd5DccFn4YW2HEdPhVZ2NEBAn416bvDFBi8HN5udmB6DkWpuXFtXaXZdq9UvMoiHxaauk6R1CZgKUR8vpng4LoudP4YVXA"}},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2zA5ozHsbqT4BgTD45vGhx1edUg7tN4qp4LFbwCwEAGK3ZVaBaCRQnuy7AJ7qbPGxKiynNtGd7CzjBXEV4mEwStnPo98Xve"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFUEYv1psxyPnjiHhTYe85AwFRs5jShbpgrfQ9UXBmxantqgGT3oAVLiHDYoR3ruT3xRGcxsmBMJxyg94FGcxF86QnzYDc6e","x2/":"xpub661MyMwAqRbcGFd5DccFn4YW2HEdPhVZ2NEBAn416bvDFBi8HN5udmB6DkWpuXFtXaXZdq9UvMoiHxaauk6R1CZgKUR8vpng4LoudP4YVXA"},"seed":"start accuse bounce inhale crucial infant october radar enforce stage dumb spot account","seed_version":11,"use_encryption":false,"wallet_type":"2of2"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_1_1_seeded(self): + wallet_str = '{"accounts":{"0":{"change":["03cbd39265f007d39045ccab5833e1ae16c357f9d35e67099d8e41940bf63ec330","03c94e9590d9bcd579caae15d062053e2820fe2a405c153dd4dca4618b7172ea6f","028a875b6f7e56f8cba66a1cec5dc1dfca9df79b7c92702d0a551c6c1b49d0f59b"],"receiving":["02fa100994f912df3e9538c244856828531f84e707f4d9eccfdd312c2e3ef7cf10","02fe230740aa27ace4f4b2e8b330cd57792051acf03652ae1622704d7eb7d4e5e4","03e3f65a991f417d69a732e040090c8c2f18baf09c3a9dc8aa465949aeb0b3271f","0382aa34a9cb568b14ebae35e69b3be6462d9ed8f30d48e0a6983e5af74fa441d3","03dfd8638e751e48fd42bf020874f49fbb5f54e96eff67d72eeeda3aa2f84f01c6","033904139de555bdf978e45931702c27837312ed726736eeff340ca6e0a439d232","03c6ca845d5bd9055f8889edcd53506cf714ac1042d9e059db630ec7e1af34133d","030b3bafc8a4ff8822951d4983f65b9bc43552c8181937188ba8c26e4c1d1be3ab","03828c371d3984ca5a248997a3e096ce21f9aeeb2f2a16457784b92a55e2aef288","033f42b4fbc434a587f6c6a0d10ac401f831a77c9e68453502a50fe278b6d9265c","0384e2c23268e2eb88c674c860519217af42fd6816273b299f0a6c39ddcc05bfa2","0257c60adde9edca8c14b6dd804004abc66bac17cc2acbb0490fcab8793289b921","02e2a67b1618a3a449f45296ea72a8fa9d8be6c58759d11d038c2fe034981efa73","02a9ef53a502b3a38c2849b130e2b20de9e89b023274463ea1a706ed92719724eb","037fc8802a11ba7ef06682908c24bcaedca1e2240111a1dd229bf713e2aa1d65a1","03ea0685fbd134545869234d1f219fff951bc3ec9e3e7e41d8b90283cd3f445470","0296bbe06cdee522b6ee654cc3592fce1795e9ff4dc0e2e2dea8acaf6d2d6b953b","036beac563bc85f9bc479a15d1937ea8e2c20637825a134c01d257d43addab217a","03389a4a6139de61a2e0e966b07d7b25b0c5f3721bf6fdcad20e7ae11974425bd9","026cffa2321319433518d75520c3a852542e0fa8b95e2cf4af92932a7c48ee9dbd"],"xpub":"xpub661MyMwAqRbcGDxKhL5YS1kaB5B7q8H6xPZwCrgZ1iE2XXaiUeqD9MFEYRAuX7UNfdAED9yhAZdCB4ZS8dFrGDVU3x9ZK8uej8u8Pa2DLMq"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K3jsrbJYY4soqd3LdRfZFbAeLQUGwTNh3ejFZw7WxbYvkhAmPM88Swt1JwFX6DVGjPXeUcGcqa1XFuJPeiQaC9wiZ16PTKgQ"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGDxKhL5YS1kaB5B7q8H6xPZwCrgZ1iE2XXaiUeqD9MFEYRAuX7UNfdAED9yhAZdCB4ZS8dFrGDVU3x9ZK8uej8u8Pa2DLMq"},"pruned_txo":{},"seed":"flat toe story egg tide casino leave liquid strike cat busy knife absorb","seed_version":11,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_1_1_importedkeys(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_1_1_watchaddresses(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_1_1_trezor_singleacc(self): + wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_1_1_trezor_multiacc(self): + wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str, accounts=2) + + def test_upgrade_from_client_2_1_1_multisig(self): + wallet_str = '{"accounts":{"0":{"change":[["03b5ca15f87baa1bb9d2508a9cf7cb596915a2749a6932bd71a5f353d72e2ff51e","03069d12bb7dc9fe7b8dab9ab2c7828173a4a4a5bacb10b9004854aef2ada2e440"],["036d7aeef82d50520f7d30d20a6b58a5e61c40949af4c147a105a8724478ba6339","021208a4a6c76934fbc2eed72a4a71713a5a093fb203ec3197edd1e4be8d9fb342"],["03ee5bd2bc7f9800b85f6f0a3fe8c23c797fa90d832f0332dfc72532e298dce54e","03474b76f33036673e1df73800b06d2df4b3617768c2b6a4f8a7f7d17c2b08cec3"]],"receiving":[["0288d4cc7e83b7028b8d2197c4efb490cb3dd248ee8683c715d9c59eb1884b2696","02c8ffee4ef168237f4a303dfe4957e328a8163c827cbe8ad07dcc24304b343869"],["022770e608e45981a31bad39a747a827ff4ce1eb28348fbe29ab776bdbf39346b4","03ebd247971aced7e2f49c495658ac5c32f764ebc4df5d033505e665f8d3f87b56"],["0256ede358326a99878d9de6c2c6a156548c266195fecea7906ddbb170da740f8d","02a500e7438d672c374713a9179fef03cbf075dd4c854566d6d9f4d899c01a4cf4"],["03fe2f59f10f6703bd3a43d0ae665ab72fb8b73b14f3a389b92e735e825fffdbe9","0255dd91624ba62481e432b9575729757b046501b8310b1dee915df6c4472f7979"],["0262c7c02f83196f6e3b9dd29e1bcad4834891b69ece12f628eea4379af6e701f8","0319ce2894fdf42bc87d45167a64b24ee2acdb5d45b6e4aadce4154a1479c8c58a"],["03bfb9ca9edab6650a908ffdcc0514f784aaccac466ba26c15340bc89a158d0b4c","03bcce80eed7b494f793b38b55cc25ae62e462ec7bf4d8ff6e4d583e8d04a4ac6d"],["0301dc9a41a44189e40c786048a0b6c13cc8865f3674fdf8e6cb2ab041eb71c0c7","020ded564880e7298068cf1498efcfb0f2306c6003e3de09f89030477ff7d02e18"],["03baffd970ecba170c31f48a95694a1063d14c834ccf2fdce0df46c3b81ab8edfb","0243ec650fc7c6642f7fb3b98e1df62f8b28b2e8722e79ccb271badba3545e8fc2"],["024be204a4bd321a727fb4a427189ae2f761f2a2c9898e9c37072e8a01026736d4","0239dc233c3e9e7c32287fdd7932c248650a36d8ab033875d272281297fadf292a"],["02197190b214c0215511d17e54e3e82cbe09f08e5ba2fb47aeafe01d8a88a8cb25","034a13cf01e26e9aa574f9ba37e75f6df260958154b0f6425e0242eacd5a3979c5"],["0226660fce4351019be974959b6b7dcf18d5aa280c6315af362ab60374b5283746","0304e49d2337a529ed8a647eceb555cd82e7e2546073568e30254530a61c174100"],["0324bb7d892dbe30930eb8de4b021f6d5d7e7da0c4ac9e3b95e1a2c684258d5d6c","02487aa272f0d3a86358064e080daf209ee501654e083f0917ad2aff3bbeb43424"],["03678b52056416da4baa8d51dca8eea534e38bd1d9328c8d01d5774c7107a0f9c1","0331deff043d709fc8171e08625a9adffba1bb614417b589a206c3a80eff86eddd"],["023a94d91c08c8c574199bc16e12789630c97cb990aeb5a54d938ff3c86786aabf","02d139837e34858f733e7e1b7d61b51d2730c57c274ed644ab80aff6e9e2fdef73"],["032f92dc11020035cd16995cfdc4bc6bef92bc4a06eb70c43474e6f7a782c9c0e1","0307d2c32713f010a0d0186e47670c6e46d7a7e623026f9ed99eb27cdae2ae4b49"],["02f66a91a024628d6f6969af2ed9ded087a88e9be86e4b3e5830868643244ec1ae","02f2a83ebb1fbbd04e59a93284e35320c74347176c0592512411a15efa7bf5fa44"],["03585bae6f04f2d3f927d79321b819cccf2bcd1d28d616aac9407c6c13d590dfbd","021f48f02b485b9b3223fca4fbc4dd823a8151053b8640b3766c37dfa99ba78006"],["02b28e2d6f1ac3fde4b34c938e83c0ef0d85fd540d8c33b33a109f4ebbc4a36a4d","030a25a960e28e751a95d3c0167fad496f9ec4bc307637c69b3bd6682930532736"],["03782c0dee8d279c547d26853e31d90bc7d098e16015c2cc334f2cc2a2964f2118","021fe4d6392dba40f1aa35fa9ec3ebfde710423f036482f6a5b3c47d0e149dfe47"],["0379b464b4f9cced0c71ee66c4fca1e61190bac9a6294242aabd4108f6a986a029","030a5802c5997ebae590147cb5eeba1690455c5d2a87306345586e808167072b50"]],"xpub":"xpub661MyMwAqRbcErzzVC45mcZaZM7gpxh4iwfsQVuyTma3qpWuRi9ZRdL8ACqu25LP2jssmKmpEbnGohH9XnoZ1etW3TKaiy5dWnUuiN6LvD9","xpub2":"xpub661MyMwAqRbcH4DqLo2tRYzSnnqjXk21aqNz3oAuYkr66YxucWgc2X8oLdS2KLPSKqrfZwStQYEpUp5jVxQfTBmEwtw3JaPRf6mq6JLD3Qr"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2NvXPAX5QUcr1KHCRVyDMikGc7WMuS34y2BktAqJsq1eJvk7JWroKM8PdGa2FHWiTpAvH9nj6BkQos5XhJU5mfS12tdtBYy"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcErzzVC45mcZaZM7gpxh4iwfsQVuyTma3qpWuRi9ZRdL8ACqu25LP2jssmKmpEbnGohH9XnoZ1etW3TKaiy5dWnUuiN6LvD9","x2/":"xpub661MyMwAqRbcH4DqLo2tRYzSnnqjXk21aqNz3oAuYkr66YxucWgc2X8oLdS2KLPSKqrfZwStQYEpUp5jVxQfTBmEwtw3JaPRf6mq6JLD3Qr"},"pruned_txo":{},"seed":"snack oxygen clock very envelope staff table bus sense fiscal cereal pilot abuse","seed_version":11,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_2_0_seeded(self): + wallet_str = '{"accounts":{"0":{"change":["038f4bae4a901fe5f2a30a06a09681fff6678e8efda4e881f71dcdc0fdb36dd1b8","032c628bec66fe98c3921b4fea6f18d241e6b23f4baf9e56c78b7a5262cd4cc412","0232b68a11cde50a49fb3155fe2c9e9cf7aa9f4bcb0f51c3963b13c997e40de40d"],"receiving":["0237246e68c6916c43c7c5aca1031df0c442439b80ceda07eaf72645a0597ed6aa","03f35bee973012909d839c9999137b7f2f3296c02791764da3f55561425bb1d53c","02fdbe9f95e2279045e6ef5f04172c6fe9476ba09d70aa0a8483347bfc10dee65e","026bc52dc91445594bb639c7a996d682ac74a4564381874b9d36cc5feea103d7a4","0319182796c6377447234eeee9fe62ce6b25b83a9c46965d9a02c579a23f9fa57a","02e23d202a45515ce509c8b9548a251de3ad8e64c92b24bb74b354c8d4d0dc85af","0307d7ccb51aa6860606bcbe008acc1aae5b53d19d0752a20a327b6ec164399b52","038a2362fde711e1a4b9c5f8fe1090a0a38aec3643c0c3d69b00660b213dc4bfb8","0396255ef7b75e5d8ffc18d01b9012a98141ee5458a68cde8b25c492c569a22ab8","02c7edf03d215b7d3478fb26e9375d541440f4a8b5c562c0eb98fab6215dbea731","024286902b95da3daf6ffb571d5465537dae5b4e00139e6465e440d6a26892158e","03aa0d3fa1fe190a24e14d6aabd9c163c7fe70707b00f7e0f9fa6b4d3a4e441149","03995d433093a2ae9dc305fe8664f6ab9143b2f7eaf6f31bc5fefdacb183699808","033c5da7c4c7a3479ddb569fecbcbb8725867370746c04ff5d2a84d1706607bbab","036a097331c285c83c4dab7d454170b60a94d8d9daa152b0af6af81dbd7f0cc440","033ed002ddf99c1e21cb8468d0f5512d71466ac5ba4003b33d71a181e3a696e3c5","02a6a0f30d1a341063a57a0549a3d16d9487b1d4e0d4bffadabdc62d1ad1a43f8f","02dcae71fc2e31013cf12ad78f9e16672eeb7c75e536f4f7d36adb54f9682884eb","028ef32bc57b95697dacdb29b724e3d0fa860ffdc33c295962b680d31b23232090","0314afd1ac2a4bf324d6e73f466a60f511d59088843f93c895507e7af1ccdb5a3b"],"xpub":"xpub661MyMwAqRbcEuc5dCRqgPpGX2bKStk4g2cbZ96SSmKsQmLUrhaQEtrfnBMsXSUSyKWuCtjLiZ8zXrxcNeC2LR8gnZPrQJdmUEeofS2yuux"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2RXcXAtqKFsXxzkq3S2DJogzkkgptRntXy1LKAG9h6YBvw8JjSUogF1UNneyYgS5uYshMBemqr41XsC7bTr8Fjx1uAyLbPC"},"master_public_keys":{"x/":"xpub661MyMwAqRbcEuc5dCRqgPpGX2bKStk4g2cbZ96SSmKsQmLUrhaQEtrfnBMsXSUSyKWuCtjLiZ8zXrxcNeC2LR8gnZPrQJdmUEeofS2yuux"},"pruned_txo":{},"seed":"agree tongue gas total hollow clip wasp slender dolphin rebel ozone omit achieve","seed_version":11,"stored_height":0,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_2_0_importedkeys(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":489714,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_2_0_watchaddresses(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_2_0_trezor_singleacc(self): + wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_2_0_trezor_multiacc(self): + wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490006,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str, accounts=2) + + def test_upgrade_from_client_2_2_0_multisig(self): + wallet_str = '{"accounts":{"0":{"change":[["037ba2d9d7446d54f1b46c902427e58a4b63915745de40f31db52e95e2eb8c559c","03aab9d4cb98fec92e1a9fc93b93f439b30cdb47cb3fae113779d0d26e85ceca7b"],["036c6cb5ed99f4d3c8d2dd594c0a791e266a443d57a51c3c7320e0e90cf040dad0","03f777561f36c795911e1e42b3b4babe473bcce32463eb9340b48d86fded8a226a"],["03de4acea515b1b3b6a2b574d08539ced475f86fdf00b43bff16ec43f6f8efc8b7","036ebfdd8ba75c94e0cb1819ecba464d04a77bab11c8fc2b7e90dd952092c01f0e"]],"receiving":[["03e768d9de027e4edaf0685abb240dde9af1188f5b5d2aa08773b0083972bdec74","0280eccb8edec0e6de521abba3831f51900e9d0655c59cddf054b72a70b520ddae"],["02f9c0b7e8fe426a45540027abca63c27109db47b5c86886b99db63450444bb460","03cb5cdcc26b0aa326bc895fcc38b63416880cdc404efbeab3ff14f849e4f4bd63"],["024d6267b9348a64f057b8e094649de36e45da586ef8ca5ecb7137f6294f6fd9e3","034c14b014eb28abfeaa0676b195bde158ab9b4c3806428e587a8a3c3c0f2d38bb"],["02bc3d5456aa836e9a155296be6a464dfa45eb2164dd0691c53c8a7a05b2cb7c42","03a374129009d7e407a5f185f74100554937c118faf3bbe4fe1cac31547f46effa"],["024808c2d17387cd6d466d13b278f76d4d04a7d31734f0708a8baf20ae8c363f9a","02e18dfc7f5ea9e8b6afe0853a9aba55861208b32f22c81aa4be0e6aee7951963d"],["0331bef7adca60ae484a12cc3c4b788d4296e0b52500731bf5dff1b935973d4768","025774c45aeac2ae87b7a67e79517ffb8264bdf1b56905a76e7e7579f875cbed55"],["020566e7351b4bfe6c0d7bda3af24267245a856af653dd00c482555f305b71a8e3","036545f66ad2fe95eeb0ec1feb501d552773e0910ec6056d6b827bc0bb970a1ecc"],["038dc34e68a49d2205f4934b739e510dca95961d0f8ab6f6cd9279d68048cfd93b","03810c50d1e2ff0e39179788e8506784bc214768884f6f71dc4323f6c29e25c888"],["035059ff052ab044fd807905067ec79b19177edcf1b1b969051dc0e6957b1e1eab","03d790376a0144860017bea5b5f2f0a9f184a55623e9a1e8f3670bf6aba273f4fb"],["02bb730d880b90e421d9ac97313b3c0eec6b12a8c778388d52a188af7dc026db43","030ae3ae865b805c3c11668b46ec4f324d50f6b5fbc2bb3a9ae0ddc4aea0d1487a"],["0306eeb93a37b7dcbb5c20146cfd3036e9a16e5b35ecfe77261a6e257ee0a7b178","03fb49f5f1d843ca6d62cee86fd4f79b6cc861f692e54576a9c937fdff13714be9"],["03f4c358e03bd234055c1873e77f451bea6b54167d36c005abeb704550fbe7bee1","03fc36f11d726fd4321f99177a0fff9b924ec6905d581a16436417d2ea884d3c80"],["024d68322a93f2924d6a0290ebe7481e29215f1c182bd8fdeb514ade8563321c87","02aa5502de7b402e064dfebc28cb09316a0f90eec333104c981f571b8bc69279e2"],["03cbda5b33a72be05b0e50ef7a9872e28d82d5a883e78a73703f53e40a5184f7a5","02ebf10a631436aa0fdef9c61e1f7d645aa149c67d3cb8d94d673eb3a994c36f86"],["0285891a0f1212efff208baf289fd6316f08615bee06c0b9385cc0baad60ebc08a","0356a6c4291f26a5b0c798f3d0b9837d065a50c9af7708f928c540017f150c40b6"],["02403988346d00e9b949a230647edbe5c03ce36b06c4c64da774a13aca0f49ce92","02717944f0bb32067fb0f858f7a7b422984c33d42fd5de9a055d00c33b72731426"],["02161a510f42bcc7cdd24e7541a0bdbcac08b1c63b491df1974c6d5cd977d57750","03006d73c0ab9fdd8867690d9282031995cfd094b5bdc3ff66f3832c5b8a9ca7f9"],["03d80ea710e1af299f1079dd528d6cdc5797faa310bafa90ca7c45ea44d5ba64f3","02b29e1170d6bec16ace70536565f1dff1480cba2a7545cfec7b522568a6ab5c38"],["02c3f6e8dea3cace7aab89d8258751827cb5791424c71fa82ae30192251ca11a28","02a43d2d952e1f3fb58c56dadabb39cf5ed437c566f504a79f2ade243abd2c9139"],["0308e96e38eb89ca5abaa6776a1968a1cbb33197ec91d40bb44bede61cb11a517f","034d0545444e5a5410872a3384cedd3fb198a8211bb391107e8e2c0b0b67932b20"]],"xpub":"xpub661MyMwAqRbcFCKg479EAwb6KLrQNcFSQKNjQRJpRFSiFRnp87cpntXkDUEvRtFTEARirm9584ML8sLBkF3gDBcyYgknnxCCrBMwPDDMQwC","xpub2":"xpub661MyMwAqRbcFaEDoCANCiY9dhXvA8GgXFSLXYADmxmatLidGTxnVL6vuoFAMg9ugX8MTKjZPiP9uUPXusUji11LnWWLCw8Lzgx7pM5sg1s"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2iFCx5cDooeMmK1uy9Xb36T8c2uCruujNdTfaaJaF6DGNDcDKkX1U4V1XiEcvCqoNsQhMQUnp8ZvMgxDBDErtMACo2HtGgQ"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFCKg479EAwb6KLrQNcFSQKNjQRJpRFSiFRnp87cpntXkDUEvRtFTEARirm9584ML8sLBkF3gDBcyYgknnxCCrBMwPDDMQwC","x2/":"xpub661MyMwAqRbcFaEDoCANCiY9dhXvA8GgXFSLXYADmxmatLidGTxnVL6vuoFAMg9ugX8MTKjZPiP9uUPXusUji11LnWWLCw8Lzgx7pM5sg1s"},"pruned_txo":{},"seed":"such duck column calm verb sock used message army suffer humble olive abstract","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_3_2_seeded(self): + wallet_str = '{"accounts":{"0":{"change":["03b37d18c0c52da686e8fd3cc5d242e62036ac2b38f101439227f9e15b46f88c42","026f946e309e64dcb4e62b00a12aee9ee14d26989880e690d8c307f45385958875","03c75552e48d1d44f966fb9cfe483b9479cc882edcf81e2faf92fba27c7bbecbc1","020965e9f1468ebda183fea500856c7e2afcc0ccdc3da9ccafc7548658d35d1fb3","03da778470ee52e0e22b34505a7cc4a154e67de67175e609a6466db4833a4623ed","0243f6bbb6fea8e0da750645b18973bc4bd107c224d136f26c7219aab6359c2705"],"receiving":["0376bf85c1bf8960947fe575adc0a3f3ba08f6172336a1099793efd0483b19e089","03f0fe0412a3710a5a8a1c2e01fe6065b7a902f1ccbf38cd7669806423860ad111","03eacb81482ba01a741b5ee8d52bb6e48647107ef9a638ca9a7b09f6d98964a456","03c8b598f6153a87fc37f693a148a7c1d32df30597404e6a162b3b5198d0f2ba33","03fefef3ee4f918e9cd3e56501018bcededc48090b33c15bf1a4c3155c8059610a","0390562881078a8b0d54d773d6134091e2da43c8a97f4f3088a92ca64d21fcf549","0366a0977bb35903390e6b86bbb6faa818e603954042e98fe954a4b8d81d815311","025d176af6047d959cfdd9842f35d31837034dd4269324dc771c698d28ad9ae3d6","02667adce009891ee872612f31cd23c5e94604567140b81d0eae847f5539c906d6","03de40832017ba85e8131c2af31079ab25a72646d28c8d2b6a39c98c4d1253ae2f","02854c17fdef156b1681f494dfc7a10c6a8033d0c577b287947b72ecada6e6386b","0283ff8f775ba77038f787b9bf667f538f186f861b003833600065b4ad8fd84362","03b0a4e9a6ffecd955bd0e2b169113b544a7cba1688dca6fce204552403dc28391","02445465cf40603506dbe7fa853bc1aae0d79ca90e57b6a7af6ffc1341c4ca8e2d","0220ea678e2541f809da75552c07f9e64863a254029446d6270e433a4434be2bd7","02640e87aab83bd84fe964eac72657b34d5ad924026f8d2222557c56580607808e","020fa9a0c3b335c6cdc6588b14c596dfae242547dd68e5c6bce6a9347152ff4021","03f7f052076dc35483c91033edef2cc93b54fb054fe3b36546800fa1a76b1d321a","030fd12243e1ffe1fc6ec3cdb7e020a467d3146d55d52af915552f2481a91657cd","02dd1a2becbc344a297b104e4bb41f7de4f5fcff1f3244e4bb124fbb6a70b5eb18"],"xpub":"xpub661MyMwAqRbcEnd8FGgkz7V8iJZ2FvDcg669i7NSS7h7nmq5k5WeHohNqosRSjx9CKiRxMgTidPWA5SJYsjrXhr1azR3boubNp24gZHUeY4"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2JYf9F9kcyYQAGiXrTVmJsAYuixpsnA8uyVwCYCPk1NtzYuNmeLRLKcMYb3UoPgTocYsHsAje3mSjX4jp3Ci17VhuESjsBU"},"master_public_keys":{"x/":"xpub661MyMwAqRbcEnd8FGgkz7V8iJZ2FvDcg669i7NSS7h7nmq5k5WeHohNqosRSjx9CKiRxMgTidPWA5SJYsjrXhr1azR3boubNp24gZHUeY4"},"pruned_txo":{},"seed":"scheme grape nephew hen song purity pizza syrup must dentist bright grit accuse","seed_version":11,"stored_height":0,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_3_2_importedkeys(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":489715,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_3_2_watchaddresses(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_3_2_trezor_singleacc(self): + wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_3_2_trezor_multiacc(self): + wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490008,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str, accounts=2) + + def test_upgrade_from_client_2_3_2_multisig(self): + wallet_str = '{"accounts":{"0":{"change":[["03083942fe75c1345833faa4d31a635e088ca173047ddd6ef5b7f1395892ef339d","03c02f486ed1f0e6d1aefbdea293c8cb44b34a3c719849c45e52ef397e6540bbda"],["0326d9adb5488c6aba8238e26c6185f4d2f1b072673e33fb6b495d62dc800ff988","023634ebe9d7448af227be5c85e030656b353df81c7cf9d23bc2c7403b9af7509b"],["0223728d8dd019e2bd2156754c2136049a3d2a39bf2cb65965945f4c598fdb6db6","037b6d4df2dde500789f79aa2549e8a6cb421035cda485581f7851175e0c95d00e"],["03c47ade02def712ebbf142028d304971bec99ca53be8e668e9cf15ff0ef186e19","02e212ad25880f2c9be7dfd1966e4b6ae8b3ea40e09d482378b942ca2e716397b0"],["03dab42b0eaee6b0e0d982fbf03364b378f39a1b3a80e980460ae96930a10bff6c","02baf8778e83fbad7148f3860ce059b3d27002c323eab5957693fb8e529f2d757f"],["02fc3019e886b0ce171242ddedb5f8dcde87d80ad9f707edb8e6db66a4389bea49","0241b4e9394698af006814acf09bf301f79d6feb2e1831a7bc3e8097311b1a96dd"]],"receiving":[["023e2bf49bc40aeed95cb1697d8542354df8572a8f93f5abe1bcec917778cc9fc6","03cf4e80c4bf3779e402b85f268ada2384932651cc41e324e51fc69d6af55ae593"],["02d9ba257aa3aba2517bb889d1d5a2e435d10c9352b2330600decab8c8082db242","03de9e91769733f6943483167602dd3d439e34b7078186066af8e90ec58076c2a7"],["02ccdd5b486cefa658af0c49d85aefa3ab62f808335ffcd4b8d4197a3c50ab073c","03e80dbbd0fb93d01d6446d0af1c18c16d26bdbb2538d8bf7f2f68ce95ba857667"],["031605867287fe3b1fee55e07b2f513792374bb5baf30f316970c5bc095651a789","02c0802b96cee67d6acec5266eb3b491c303cea009d57a6bb7aee83cc602206ad5"],["037d07d30dec97da4ea09d568f96f0eb6cd86d02781a7adff16c1647e1bcd23260","03d856a53bc90be84810ce94c8aac0791c9a63379fd61790c11dae926647aa4eec"],["028887f2d54ffefc98e5a605c83bedba79367c2a4fe11b98ec6582896ffad79216","0259dab6dafe52306fe6e3686f27a36e0650c99789bb19cbcd0907db00957030a9"],["039d83064dd37681eaf7babe333b210685ba9fe63627e2f2d525c1fb9c4d84d772","03381011299678d6b72ff82d6a47ed414b9e35fcf97fc391b3ff1607fb0bf18617"],["03ace6ceb95c93a446ae9ff5211385433c9bbf5785d52b4899e80623586f354004","0369de6b20b87219b3a56ea8007c33091f090698301b89dd6132cf6ef24b7889a0"],["031ec2b1d53da6a162138fb8f4a1ec27d62c45c13dddecebbd55ad8a5d05397382","02417a3320e15c2a5f0345ac927a10d7218883170a9e64837e629d14f8f3de7c78"],["02b85c8b2f33b6a8a882c383368be8e0a91491ea57595b6a690f01041be5bef4fb","0383ad57c7899284e9497e9dccb1de5bf8559b87157f13fee5677dcf2fbeb7b782"],["03eaa9e3ea81b2fa6e636373d860c0014e67ac6363c9284e465384986c2ec77ee2","03b1bd0d6355d99e8cab6d177f10f05eb8ddd3e762871f176d78a79f14ae037826"],["03ecd1b458e7c2b71a6542f8e64c750358c1421542ffe7630cc3ecc6866d379dfe","02d5c5432ca5e4243430f73a69c180c23bda8c7c269d7b824a4463e3ac58850984"],["028098ae6e772460047cdd6694230dcfc44da8ceabcae0624225f2452be7ae26c4","02add86858446c8a59ed3132264a8141292cd4ece6653bf3605895cceb00ba30b9"],["02f580882255cda6fae954294164b26f2c4b6b2744c0930daaa7a9953275f2f410","02c09c5e369910d84057637157bdf1fb721387bb2867c3c2adb2d91711498bbe5e"],["025e628f78c95135669ab8b9178f4396b0b513cbeae9ca631ba5e5e8321a4a05bc","03476f35b4defcc67334a0ff2ce700fb55df39b0f7f4ff993907e21091f6a29a31"],["026fa6f3214dce2ad2325dae3cd8d6728ce62af1903e308797ff071129fe111eca","03d07eb26749caceca56ffe77d9837aaf2f657c028bd3575724b7e2f1a8b3261a5"],["03894311c920ef03295c3f1c8851f5dc9c77e903943940820b084953a0a92efcc3","0368b0b3774f9de81b9f10e884d819ccf22b3c0ed507d12ce2a13efc36d06cdc17"],["024f8a61c23aa4a13a3a9eb9519ed3ec734f54c5e71d55f1805e873c31a125c467","039e9c6708767bd563fcdca049c4d8a1acab4a051d4f804ae31b5e9de07942570f"],["038f9b8f4b9fe6af5ced879a16bb6d56d81831f11987d23b32716ca4331f6cbabf","035453374f020646f6eda9528543ec0363923a3b7bbb40bc9db34740245d0132e7"],["02e30cd68ae23b3b3239d4e98745660b08d7ce30f2f6296647af977268a23b6c86","02ee5e33d164f0ad6b63f0c412734c1960507286ad675a343df9f0479d21a86ecc"]],"xpub":"xpub661MyMwAqRbcGAPwDuNBFdPguAcMFDrUFznD8RsCFkjQqtMPE66H5CDpecMJ9giZ1GVuZUpxhX4nFh1R3fzzr4hjDoxDSHymXVXQa7H1TjG","xpub2":"xpub661MyMwAqRbcFMKuZtmYryCNiNvHAki74TizX3b6dxaREtjLMoqnLJbd1zQKjWwKEThmB4VRtwePAWHNk9G5nHvAEvMHDYemERPQ7bMjQE3"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3gKU7sqAtVSxM8mrqm8ctmrcL3TahRCRy62EgYn2XPuLoJAGbBGvL4ArbPoAay5jo7L1UbBv15SsmrSKdTQSgDE351WSkm6"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcGAPwDuNBFdPguAcMFDrUFznD8RsCFkjQqtMPE66H5CDpecMJ9giZ1GVuZUpxhX4nFh1R3fzzr4hjDoxDSHymXVXQa7H1TjG","x2/":"xpub661MyMwAqRbcFMKuZtmYryCNiNvHAki74TizX3b6dxaREtjLMoqnLJbd1zQKjWwKEThmB4VRtwePAWHNk9G5nHvAEvMHDYemERPQ7bMjQE3"},"pruned_txo":{},"seed":"brick huge enforce behave cabin cram okay friend sketch actor casual barrel abuse","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_4_3_seeded(self): + wallet_str = '{"accounts":{"0":{"change":["02707eb483e51d859b52605756aee6773ea74c148d415709467f0b2a965cd78648","0321cddfb60d7ac41fdf866b75e4ad0b85cc478a3a84dc2e8db17d9a2b9f61c3b5","0368b237dea621f6e1d580a264580380da95126e46c7324b601c403339e25a6de9","02334d75548225b421f556e39f50425da8b8a36960cce564db8001f7508fef49f6","02990b264de812802743a378e7846338411c3afab895cff35fb24a430fa6b43733","02bc3b39ca00a777e95d89f773428bad5051272b0df582f52eb8d6ebb5bb849383"],"receiving":["0286c9d9b59daa3845b2d96ce13ac0312baebaf318251bac6d634bcac5ff815d9d","0220b65829b3a030972be34559c4bb1fc91f8dfd7e1703ddb43da9aa28aa224864","02fe34b26938c29faee00d8d704eae92b7c97d487825892290309073dc85ae5374","03ea255ae2ba7169802543cf7af135783f4fca91924fd0285bdbe386d78a0ab87e","027115aeea786e2745812f2ec2ae8fee3d038d96c9556b1324ac50c913b83a9e6a","03627439bb701352e35d0cf8e00617d8e9bf329697e430b0a5d999370097e025b4","034120249c6b15d051525156845aefaa83988adf9ed1dd18b796217dcf9824b617","02dfeb0c89eee66026d7650ee618c2172551f97fdd9ed249e696c54734d26e39a3","037e031bb4e51beb5c739ba6ab64aa696e85457ea63cc56698b7d9b731fd1e8e61","0302ea6818525492adc5ed8cfd2966efd704915199559fe1c06d6651fd36533012","0349394140560d685d455595f697d17b44e832ec453b5a2f02a3f5ed66205f3d30","036815bf2437df00440b15cfa7123544648cf266247989e82540d6b1cae1589892","02f98568e8f0f4b780f005e538a7452a60b2c06a5d2e3a23fa26d88459d118ef56","02e36ccb8b05a2762a08f60541d1a5a136afd6a73119eea8c7c377cc8b07eb2e2f","031566539feb6f0a212cca2604906b1c1f5cfc5bf5d5206e0c695e37ef3a141fd2","025754e770bedeef6f4e932fa231b858b49d28183e1be6da23e597c67dd7785f19","03a29961f5fb9c197cffe743081a761442a3cf9ded0be2fa07ab67023a74c08d28","023184c1995a9f51af566c9c0b4da92d7fd4a5c59ff93c34a323e94671ddbe414a","029efdb15d3aec708b3af2aee34a9157ff731bec94e4f19f634ab43d3101e47bd8","03e16b13fe6bb9aa6dc4e331e19ab4d3d291a2670b97e6040e87a7c7309b243af9"],"xpub":"xpub661MyMwAqRbcF1KGEGXxFTupKQHTTUan1qZMTp4yUxiwF2uRRum7u1TCnaJRjaSBW4d42Fwfi6xfLvfRfgtDixekGDWK9CPWguR7YzXKKeV"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2XEo8EzwtKy5mNSy41rvecdkfRfMvdBxNEaGtNSsMD8iwHsc91UxKtSrDHXex53NkMRRDwnm4PmqS7N35K8BR1KCD2qm5iE"},"master_public_keys":{"x/":"xpub661MyMwAqRbcF1KGEGXxFTupKQHTTUan1qZMTp4yUxiwF2uRRum7u1TCnaJRjaSBW4d42Fwfi6xfLvfRfgtDixekGDWK9CPWguR7YzXKKeV"},"seed":"smart fish version ocean category disagree hospital mystery survey chef kid latin about","seed_version":11,"use_encryption":false,"wallet_type":"standard"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_4_3_importedkeys(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"stored_height":477636,"use_encryption":false,"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_4_3_watchaddresses(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_4_3_trezor_singleacc(self): + wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":485855,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_4_3_trezor_multiacc(self): + wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]]},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490009,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' + self._upgrade_storage(wallet_str, accounts=2) + + def test_upgrade_from_client_2_4_3_multisig(self): + wallet_str = '{"accounts":{"0":{"change":[["03467a8bae231aff83aa01999ee4d3834894969df7a3b0753e23ae7a3aae089f6b","02180c539980494b4e59edbda5e5340be2f5fbf07e7c3898b0488950dda04f3476"],["03d8e18a428837e707f35d8e2da106da2e291b8acbf40ca0e7bf1ac102cda1de11","03fad368e3eb468a7fe721805c89f4405581854a58dcef7205a0ab9b903fd39c23"],["0331c9414d3eee5bee3c2dcab911537376148752af83471bf3b623c184562815d9","02dcd25d2752a6303f3a8366fae2d62a9ff46519d70da96380232fc9818ee7029e"],["03bb18a304533086e85782870413688eabef6a444a620bf679f77095b9d06f5a16","02f089ed84b0f7b6cb0547741a18517f2e67d7b5d4d4dd050490345831ce2aef9e"],["02dc6ebde88fdfeb2bcd69fce5c5c76db6409652c347d766b91671e37d0747e423","038086a75e36ac0d6e321b581464ea863ab0be9c77098b01d9bc8561391ed0c695"],["02a0b30b12f0c4417a4bef03cb64aa55e4de52326cf9ebe0714613b7375d48a22e","02c149adda912e8dc060e3bbe4020c96cff1a32e0c95098b2573e67b330e714df0"]],"m":2,"receiving":[["0254281a737060e919b071cb58cc16a3865e36ea65d08a7a50ba2e10b80ff326d5","0257421fa90b0f0bc75b67dd54ffa61dc421d583f307c58c48b719dd59078023e4"],["03854ce9bbc7813d535099658bcc6c671a2c25a269fdb044ee0ed5deb95da0d7e0","025379ca82313dde797e5aa3f222dddf0f7223cb271f79ecce2c8178bea3e33c62"],["03ae6ad5ffc75d71adc2ab87e3adc63fa8696a8656e1135adb5ae88ddb6d39089f","025ed8821f8b37aef69b1aabf89e4e405f09206c330c78e94206b21139ddafcc4f"],["033ea4d8b88d36d14a52983ae30d486254af2dfa1c7f8e04bc9d8e34b3ffe4b32a","02b441a3e47a338d89027755b81724219362b8d9b66142d32fcb91c9c7829d8c9f"],["029195704b9bbc3014452bbf07baa7bf6277dfefd9721aea8438f2671ba57b898b","022264503140f99b41c0269666ab6d16b2dad72865dbd2bf6153d45f5d11978e4d"],["037e3caa2d151123821dff34fd8a76ac0d56fa97c41127e9b330a115bf12d76674","02a4ae28e2011537de4cce0c47af4ac0484b38d408befcb731c3d752922fcd3c5b"],["02226853ca32e72b4771ccc47c0aae27c65ed0d25c525c1f673b913b97dca46cc5","027a9c855fc4e6b3f8495e77347a1e03c0298c6a86bd5a89800195bd445ae3e3bd"],["02890f7eee0766d2dde92f3146cd461ae0fa9caf07e1f3559d023a20349bae5e44","0380249f30829b3656c32064ddf657311159cecb36f9dbbf8e50e3d7279b70c57e"],["02ab9613fd5a67a3fdf6b6241d757ce92b2640d9d436e968742cb7c4ec4bb3e6e9","0204b29cc980b18dfb3a4f9ca6796c6be3e0aee2462719b4a787e31c8c5d79c8cf"],["029103b50ecc0cc818c1c97e8acb8ce3e1d86f67e49f60c8496683f15e753c3eed","0247abb2c5e4cde22eb59a203557c0bbe87e9c449e6c2973e693ac14d0d9cf3f28"],["02817c935c971e6e318ba9e25402df26ca016a4e532459be5841c2d83a5aa8a967","03331fe3a2e4aa3e2dc1d8d4afc5a88c57350806b905e593b5876c6b9cef71fd4d"],["03023c6797af5c9c3d7db2fbeb9d7236601fe5438036200f2f59d9b997d29ec123","023b1084f008cf2e9632967095958bb0bbd59e60a0537e6003d780c7ebccb2d4f5"],["0245e0bdebe483fef984e4e023eb34641e65909cd566eb6bd6c0bce592296265a1","0363bad4b477d551f46b19afcc10decf6a4c1200becb5b22c032c62e6d90b373b8"],["0379ba2f8c5e8e5e3f358615d230348fe8d7855ef9c0e1cf97aac4ec09dfe690aa","02ecda86ff40b286a3faadf9a5b361ab7a5beb50426296a8c0e3d222f404ae4380"],["02e090227c22efa7f60f290408ce9f779e27b39d4acec216111cc3a8b9594ab451","02144954ddabb55abcfe49ea703a4e909ab86db2f971a2e85fc006dffbdf85af52"],["025dc4bd1c4809470b5a14cf741519ad7f5f2ccd331b42e0afd2ce182cdf25f82d","03d292524190af850665c2255a785d66c59fea2b502d4037bb31fdde10ad9b043f"],["027e7c549f613ae9ba1d806c8c8256f870e1c7912e3e91cbb326d61fb20ac3a096","03fbbf15ee2b49878c022d0b30478b6a3acb61f24af6754b3f8bcb4d2e71968099"],["02c188eaf5391e52fdcd66f8522df5ae996e20c524577ac9ffa7a9a9af54508f7c","03fe28f1ea4a0f708fa2539988758efd5144a128cc12aed28285e4483382a6636a"],["03bea51abacd82d971f1ef2af58dcbd1b46cdfa5a3a107af526edf40ca3097b78d","02267d2c8d43034d03219bb5bc0af842fb08f028111fc363ec43ab3b631134228a"],["03c3a0ecdbf8f0a162434b0db53b3b51ce02886cbc20c52e19a42b5f681dac6ffb","02d1ede70e7b1520a6ccabd91488af24049f1f1cf2661c07d8d87aee31d5aec7c9"]],"xpubs":["xpub661MyMwAqRbcFafkG2opdo3ou3zUEpFK3eKpWWYkdA5kfvootPkZzqvUV1rtLYRLdUxvXBZApzZpwyR2mXBd1hRtnc4LoaLTQWDFcPKnKiQ","xpub661MyMwAqRbcFrxPbuWkHdMeaZMjb4jKpm51RHxQ3czEDmyK3Qa3Z43niVzVjFyhJs6SrdXgQg56DHMDcC94a7MCtn9Pwh2bafhHGJbLWeH"]}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3NsvVsyjvVQv2XXFBc1UTY9QcuYnVHTFLyeAVsFo1FjJsBk48XK16jZLqRs1B5Sa6SCqYdA2XFvB9riBca2GyGccYGKKP6t"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFrxPbuWkHdMeaZMjb4jKpm51RHxQ3czEDmyK3Qa3Z43niVzVjFyhJs6SrdXgQg56DHMDcC94a7MCtn9Pwh2bafhHGJbLWeH","x2/":"xpub661MyMwAqRbcFafkG2opdo3ou3zUEpFK3eKpWWYkdA5kfvootPkZzqvUV1rtLYRLdUxvXBZApzZpwyR2mXBd1hRtnc4LoaLTQWDFcPKnKiQ"},"pruned_txo":{},"seed":"angry work entry banana taste climb script fold level rate organ edge account","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_5_4_seeded(self): + wallet_str = '{"accounts":{"0":{"change":["0253e61683b66ebf5a4916334adf1409ffe031016717868c9600d313e87538e745","021762e47578385ecedc03c7055da1713971c82df242920e7079afaf153cc37570","0303a8d6a35956c228aa95a17aab3dee0bca255e8b4f7e8155b23acef15cf4a974","02e881bc60018f9a6c566e2eb081a670f48d89b4a6615466788a4e2ce20246d4c6","02f0090e29817ef64c17f27bf6cdebc1222f7e11d7112073f45708e8d218340777","035b9c53b85fd0c2b434682675ac862bfcc7c5bb6993aee8e542f01d96ff485d67"],"receiving":["024fbc610bd51391794c40a7e04b0e4d4adeb6b0c0cc84ac0b3dad90544e428c47","024a2832afb0a366b149b6a64b648f0df0d28c15caa77f7bbf62881111d6915fe9","028cd24716179906bee99851a9062c6055ec298a3956b74631e30f5239a50cb328","039761647d7584ba83386a27875fe3d7715043c2817f4baca91e7a0c81d164d73d","02606fc2f0ce90edc495a617329b3c5c5cc46e36d36e6c66015b1615137278eabd","02191cc2986e33554e7b155f9eddcc3904fdba43a5a3638499d3b7b5452692b740","024b5bf755b2f65cab1f7e5505febc1db8b91781e5aac352902e79bc96ad7d9ad0","0309816cb047402b84133f4f3c5e56c215e860204513278beef54a87254e44c14a","03f53d34337c12ddb94950b1fee9e4a9cf06ad591db66194871d31a17ec7b59ac7","0325ede4b08073d7f288741c2c577878919fd5d832a9e6e04c9eac5563ae13aa83","02eca43081b04f68d6c8b81781acd59e5b8d2ba44dba195369afc40790fd9edef7","029a8ca96c64d3a98345be1594208908f2be5e6af6bcc6ff3681f271e75fcf232e","02fbe0804980750163a216cc91cfe86e907addf0e80797a8ea5067977eb4897c1b","0344f32fc1ee8b2eb08f419325529f495d77a3b5ea683bbce7a44178705ab59302","021dd62bdf18256bd5316ce3cbcca58785378058a41ba2d1c58f4cc76449b3c424","035e61cdbdb4306e58a816a19ad92c7ca3a392b67ac6d7257646868ffe512068c5","0326a4db82f21787d0246f8144abe6cda124383b7d93a6536f36c05af530ea262a","02b352a27a8f9c57b8e5c89b357ba9d0b5cb18bf623509b34cd881fcf8b89a819a","02a59188edef1ed29c158a0adb970588d2031cfe53e72e83d35b7e8dd0c0c77525","02e8b9e42a54d072c8887542c405f6c99cfabf41bdde639944b44ba7408837afd1"],"xpub":"xpub661MyMwAqRbcGh7ywNf1BYoFCs8mht2YnvkMYUJTazrAWbnbvkrisvSvrKGjRTDtw324xzprbDgphsmPv2pB6K5Sux3YNHC8pnJANCBY6vG"}},"accounts_expanded":{},"addr_history":{"12LXoVHUnAXn6BVBpshjwd7sSTwp5nsd7W":[],"12iXPYBErR6ZMESB9Nv74S4pVxdGMNLiW2":[],"13jmb5Vc2qh29tPhg637BwCJN7hStGWYXE":[],"14dHBBbwFVC7niSCqrb5HCHRK5K8rrgaW6":[],"14xsHuYGs4gKpRK3deuYwhMBTAwUeu2dpB":[],"15MpWMUasNVPTpzC5hK2AuVFwQ3AHd8fkv":[],"17nmvao3F84ebPrcv1LUxPUSS94U9EvCUt":[],"17yotEc8oUgJVQUnkjZSQjcqqZEbFFnXx8":[],"1A3c1rCCS2MYYobffyUHwPqkqE5ZpvG8Um":[],"1AtCzmcth79q6HgeyDnM3NLfr29hBHcfcg":[],"1AufJhUsMbqwbLK9JzUGQ9tTwphCQiVCwD":[],"1B77DkhJ8qHcwPQC2c1HyuNcYu5TzxxaJ7":[],"1D4bgjc4MDtEPWNTVfqG5bAodVu3D1Gjft":[],"1DefMPXdeCSQC5ieu8kR7hNGAXykNzWXpm":[],"1E673RESY1SvTWwUr5hQ1E7dGiRiSgkYFP":[],"1Ex6hnmpgp3FQrpR5aYvp9zpXemFiH7vky":[],"1FH2iAc5YgJKj1KcpJ1djuW3wJ2GbQezAv":[],"1GpjShJMGrLQGP6nZFDEswU7qUUgJbNRKi":[],"1H4BtV4Grfq2azQgHSNziN7MViQMDR9wxd":[],"1HnWq29dPuDRA7gx9HQLySGdwGWiNx4UP1":[],"1LMuebyhm8vnuw5qX3tqU2BhbacegeaFuE":[],"1LTJK8ffwJzRaNR5dDEKqJt6T8b4oVbaZx":[],"1LtXYvRr4j1WpLLA398nbmKhzhqq4abKi8":[],"1NfsUmibBxnuA3ir8GJvPUtY5czuiCfuYK":[],"1Q3cZjzADnnx5pcc1NN2ekJjLijNjXMXfr":[],"1okpBWorqo5WsBf5KmocsfhBCEDhNstW2":[]},"master_private_keys":{"x/":"xprv9s21ZrQH143K4D3WqM7zpQrWeqJHJRJhRhpkk5tr2fKBdoTTPDYUL88T12Ad9RHwViugcMbngkMDY626vD5syaFDoUB2cpLeraBaHvZHWFn"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGh7ywNf1BYoFCs8mht2YnvkMYUJTazrAWbnbvkrisvSvrKGjRTDtw324xzprbDgphsmPv2pB6K5Sux3YNHC8pnJANCBY6vG"},"pruned_txo":{},"seed":"tent alien genius panic stage below spoon swap merge hammer gorilla squeeze ability","seed_version":11,"stored_height":489715,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard","winpos-qt":[100,100,840,400]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_5_4_importedkeys(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported","winpos-qt":[595,261,840,400]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_5_4_watchaddresses(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported","winpos-qt":[406,393,840,400]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_5_4_trezor_singleacc(self): + wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"addr_history":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":490046,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor","winpos-qt":[522,328,840,400]}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_5_4_trezor_multiacc(self): + wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12bBPWWDwvtXrR9ntSgaQ7AnGyVJr16m5q":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"13853om3ye5c8x6K1LfT3uCWEnG14Z82ML":[],"13BGVmizH8fk3qNm1biNZxAaQY3vPwurjZ":[],"13Tvp2DLQFpUxvc7JxAD3TXfAUWvjhwUiL":[],"15EQcTGzduGXSaRihKy1FY99EQQco8k2UW":[],"15paDwtQ33jJmJhjoBJhpWYGJDFCZppEF9":[],"17X8K766zBYLTjSNvHB9hA6SWRPMTcT556":[],"17zSo4aveNaE5DiTmwNZtxrJmS5ymzvwqj":[],"19BRVkUFfrAcxW9poaBSEUA2yv7SwN3SXh":[],"19gPT2mb9FQCiiPdAmMAaberShzNRiAtTB":[],"1A3vopoUcrWn7JbiAzGZactQz8HbnC1MoD":[],"1D1bn2Jzcx4D2GXbxzrJ1GwP4eNq98Q948":[],"1DvytpRGLJujPtSLYTRABzpy2r6hKJBYQd":[],"1EGg2acXNhJfv1bU3ixrbrmgxFtAUWpdY":[],"1Ev3S9YWxS7KWT8kyLmEuKV5sexNKcMUKV":[],"1FfpRnukxbfBnoudWvw9sdmc86YbVs7eGb":[],"1GBxNE82WLgd38CzoFTEkz6QS9EwLj1ym7":[],"1JFDe97zENNUiKeizcFUHss13vS2AcrVdE":[],"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ":[],"1JQqX3yg6VYxL6unuRArDQaBZYo3ktSCCP":[],"1JUbrr4grE71ZgWNqm9z9ZHHJDcCzFYM4V":[],"1JuHUVbYfBLDUhTHx5tkDDyDbCnMsF8C9w":[],"1KZu7p244ETkdB5turRP4vhG2QJskARYWS":[],"1LE7jioE7y24m3MMZayRKpvdCy2Dz2LQae":[],"1LVr2pTU7LPQu8o8DqsxcGrvwu5rZADxfi":[],"1LmugnVryiuMbgdUAv3LucnRMLvqg8AstU":[],"1MPN5vptDZCXc11fZjpW1pvAgUZ5Ksh3ky":[]},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490009,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor","winpos-qt":[757,469,840,400]}''' + self._upgrade_storage(wallet_str, accounts=2) + + def test_upgrade_from_client_2_5_4_multisig(self): + wallet_str = '{"accounts":{"0":{"change":[["02a63209b49df0bb98d8a262e9891fe266ffdce4be09d5e1ffaf269a10d7e7a17c","02a074035006ed8ee8f200859c004c073b687140f7d40bd333cdbbe43bad1e50bc"],["0280e2367142669e08e27fb9fd476076a7f34f596e130af761aef54ec54954a64d","02719a66c59f76c36921cf7b330fca7aaa4d863ee367828e7d89cd2f1aad98c3ac"],["0332083e80df509d3bd8a06538ca20030086c9ed3313300f7313ed98421482020f","032f336744f53843d8a007990fa909e35e42e1e32460fae2e0fc1aef7c2cff2180"],["03fe014e5816497f9e27d26ce3ae8d374edadec410227b2351e9e65eb4c5d32ab7","0226edd8c3af9e339631145fd8a9f6d321fdc52fe0dc8e30503541c348399dd52a"],["03e6717b18d7cbe264c6f5d0ad80f915163f6f6c08c121ac144a7664b95aedfdf3","03d69a074eba3bc2c1c7b1f6f85822be39aee20341923e406c2b445c255545394a"],["023112f87a5b9b2eadc73b8d5657c137b50609cd83f128d130172a0ed9e3fea9bc","029a81fd5ba57a2c2c6cfbcb34f369d87af8759b66364d5411eddd28e8a65f67fa"]],"m":2,"receiving":[["03c35c3da2c864ee3192a847ffd3f67fa59c095d8c2c0f182ed9556308ec37231e","03cfcb6d1774bfd916bd261232645f6c765da3401bf794ab74e84a6931d8318786"],["03973c83f84a4cf5d7b21d1e8b29d6cbd4cb40d7460166835cd1e1fd2418cfcf2e","03596801e66976959ac1bdb4025d65a412d95d320ed9d1280ac3e89b041e663cf4"],["02b78ac89bfdf90559f24313d7393af272092827efc33ba3a0d716ee8b75fd08ff","038e21fae8a033459e15a700551c1980131eb555bbb8b23774f8851aa10dcac6b8"],["0288e9695bb24f336421d5dcf16efb799e7d1f8284413fe08e9569588bc116567e","027123ba3314f77a8eb8bb57ba1015dd6d61b709420f6a3320ba4571b728ef2d91"],["0312e1483f7f558aef1a14728cc125bb4ee5cff0e7fa916ba8edd25e3ebceb05e9","02dad92a9893ad95d3be5ebc40828cef080e4317e3a47af732127c3fee41451356"],["03a694e428a74d37194edc9e231e68399767fdb38a20eca7b72caf81b7414916a8","03129a0cef4ed428031972050f00682974b3d9f30a571dc3917377595923ac41d8"],["026ed41491a6d0fb3507f3ca7de7fb2fbfdfb28463ae2b91f2ab782830d8d5b32c","03211b3c30c41d54734b3f13b8c9354dac238d82d012839ee0199b2493d7e7b6fc"],["03480e87ffa55a96596be0af1d97bca86987741eb5809675952a854d59f5e8adc2","0215f04df467d411e2a9ed8883a21860071ab721314503019a10ed30e225e522e7"],["0389fce63841e9231d5890b1a0c19479f8f40f4f463ef8e54ef306641abe545ac8","02396961d498c2dcb3c7081b50c5a4df15fda31300285a4c779a59c9abc98ea20d"],["03d4a3053e9e08dc21a334106b5f7d9ac93e42c9251ceb136b83f1a614925eb1fb","025533963c22b4f5fbfe75e6ee5ad7ee1c7bff113155a7695a408049e0b16f1c52"],["038a07c8d2024b9118651474bd881527e8b9eb85fc90fdcb04c1e38688d498de4b","03164b188eb06a3ea96039047d0db1c8f9be34bfd454e35471b1c2f429acd40afb"],["0214070cd393f39c062ce1e982a8225e5548dbbbd654aeba6d36bfcc7a685c7b12","029c6a9fb61705cc39bef34b09c684a362d4862b16a3b0b39ca4f94d75cd72290c"],["027b3497f72f581fea0a678bc20482b6fc7b4b507f7263d588001d73fdf5fe314e","021b80b159d19b6978a41c2a6bf7d3448bc73001885f933f7854f450b5873091f3"],["0303e9d76e4fe7336397c760f6fdfd5fb7500f83e491efb604fa2442db6e1da417","03a8d1b22a73d4c181aecd8cfe8bb2ee30c5dd386249d2a5a3b071b7a25b9da73a"],["0298e472b74832af856fb68eed02ff00a235fd0424d833bc305613e9f44087d0ee","03bb9bc2e4aaa9b022b35c8d122dfccb6c28ae8f0996a8fb4a021af8ec96a7beaf"],["02e933a4afb354500da03373514247e1be12e67cc4683e0cb82f508878cc3cc048","02c07a57b071bc449a95dd80308e53b26e4ebf4d523f620eecb17f96ae3aa814e9"],["03f73476951078b3ccc549bc7e6362797aaaacb1ea0edc81404b4d16cb321255a3","03b3a825fb9fc497e568fba69f70e2c3dcdc793637e242fce578546fcbd33cb312"],["03bbdf99fddeea64a96bbb9d1e6d7ced571c9c7757045dcbd8c40137125b017dc5","03aedf4452afefb1c3da25e698f621cb3a3a0130aa299488e018b93a45b5e6c21d"],["03b85891edb147d43c0a5935a20d6bbf8d32c542bfecccf3ae0158b65bd639b34e","03b34713c636a1c103b82d6cec917d442c59522ddc5a60bf7412266dd9790e7760"],["028ddf53b85f6c01122a96bd6c181ee17ca222ee9eca85bdeeb25c4b5315005e3b","02f4821995bfd5d0adb7a78d6e3a967ac72ace9d9a4f9392aff2711533893e017b"]],"xpubs":["xpub661MyMwAqRbcGHtCYBSGGVgMSihroMkuyE25GPyzfQvS2vSFG7SgJYf7rtXJjMh7srBJj8WddLtjapHnUQLwJ7kxsy5HiNZnGvF9pm2du7b","xpub661MyMwAqRbcEdd7bzA86LbhMoTv8NeyqcNP5z1Tiz9ajCRQDzdeXHw3h5ucDNGWr6mPFCZBcBE31VNKyR3vWM7WEeisu5m4VsCyuA6H8fp"]}},"accounts_expanded":{},"addr_history":{"32JvbwfEGJwZHGm3nwYiXyfsnGCb3L8hMX":[],"32pWy5sKkQsjyDz45tog47cA8vQyzC3UUZ":[],"334yqX1WtS6mY2vize7znTaL64HspwVkGF":[],"33GY9w6a4XmLAWxNgNFFRXTTRxbu3Nz8ip":[],"33geBcyW8Bw53EgAv3qwMVkVnvxZWj5J1X":[],"35BneogkCNxSiSN1YLmhKLP8giDbGkZiTX":[],"37U4J5b9B7rQnQXYstMoQnb6i9aWpptnLi":[],"37gqbHdbrCcGyrNF21AiDkofVCie5LpFmQ":[],"37t1Q5R92co4by2aagtLcqdWTDEzFuAuwZ":[],"37z3ruAHCxnzeJeLz96ZpkbwS3CLbtXtPc":[],"39qePsKaeviFEMC6CWX37DqaQda4jA2E6A":[],"3A5eratrDWu4SqsoHpuqswNsQmp9k8TXR2":[],"3B1N3PG5dNPYsTAuHFbVfkwXeZqqNS1CuP":[],"3BABbvd3eAuwiqJwppm54dJauKnRUieQU8":[],"3CAsH7BJnNT4kmwrbG8XZMMwW6ue8w4auJ":[],"3CX2GLCTfpFHSgAmbGRmuDKGHMbWY8tCp7":[],"3CrLUTVHuG1Y3swny9YDmkfJ89iHHU93NB":[],"3CxRa6yAQ2N2rpDHyUTaViGG4XVASAqwAN":[],"3DLTrsdYabso7QpxoLSW5ZFjLxBwrLEqqW":[],"3GG3APgrdDCTmC9tTwWu3sNV9aAnpFcddA":[],"3JDWpTxnsKoKut9WdG4k933qmPE5iJ8hRR":[],"3LdHoahj7rHRrQVe38D4iN43ySBpW5HQRZ":[],"3Lt56BqiJwZ1um1FtXJXzbY5uk32GVBa8K":[],"3MM9417myjN7ubMDkaK1wQ9RbjEc1zHCRH":[],"3NTivFVXva4DCjPmsf5p5Gt1dmuV39qD2v":[],"3QCwtjMywMtT3Vg6BwS146LcQjJnZPAPHZ":[]},"master_private_keys":{"x1/":"xprv9s21ZrQH143K29YeVxd7jCexomdRiuw8UPSnHbbrAecbrQ6FgTKPyVcZqp2256L5DSTdb8UepPVaDwJecswTrEhdyZiaNGERJpfzWV5FcN5"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcEdd7bzA86LbhMoTv8NeyqcNP5z1Tiz9ajCRQDzdeXHw3h5ucDNGWr6mPFCZBcBE31VNKyR3vWM7WEeisu5m4VsCyuA6H8fp","x2/":"xpub661MyMwAqRbcGHtCYBSGGVgMSihroMkuyE25GPyzfQvS2vSFG7SgJYf7rtXJjMh7srBJj8WddLtjapHnUQLwJ7kxsy5HiNZnGvF9pm2du7b"},"pruned_txo":{},"seed":"park dash merit trend life field acid wrap dinosaur kit bar hotel abuse","seed_version":11,"stored_height":490034,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2","winpos-qt":[564,329,840,400]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_6_4_seeded(self): + wallet_str = '{"accounts":{"0":{"change":["03236a8ce6fd3d343358f92d3686b33fd6e7301bf9f635e94c21825780ab79c93d","0393e39f6b4a3651013fca3352b89f1ae31751d4268603f1423c71ff79cbb453a1","033d9722ecf50846527037295736708b20857b4dd7032fc02317f9780d6715e8ff","03f1d56d2ade1daae5706ea945cab2af719060a955c8ad78153693d8d08ed6b456","029260d935322dd3188c3c6b03a7b82e174f11ca7b4d332521740c842c34649137","0266e8431b49f129b892273ab4c8834a19c6432d5ed0a72f6e88be8c629c731ede"],"receiving":["0350f41cfac3fa92310bb4f36e4c9d45ec39f227a0c6e7555748dff17e7a127f67","02f997d3ed0e460961cdfa91dec4fa09f6a7217b2b14c91ed71d208375914782ba","029a498e2457744c02f4786ac5f0887619505c1dae99de24cf500407089d523414","03b15b06044de7935a0c1486566f0459f5e66c627b57d2cda14b418e8b9017aca1","026e9c73bdf2160630720baa3da2611b6e34044ad52519614d264fbf4adc5c229a","0205184703b5a8df9ae622ea0e8326134cbeb92e1f252698bc617c9598aff395a1","02af55f9af0e46631cb7fde6d1df6715dc6018df51c2370932507e3d6d41c19eec","0374e0c89aa4ecf1816f374f6de8750b9c6648d67fe0316a887a132c608af5e7c0","0321bb62f5b5c393aa82750c5512703e39f4824f4c487d1dc130f690360c0e5847","0338ea6ebb2ed80445f64b2094b290c81d0e085e6000367eb64b1dc5049f11c2e9","020c3371a9fd283977699c44a205621dea8abfc8ebc52692a590c60e22202fa49b","0395555e4646f94b10af7d9bc57e1816895ad2deddef9d93242d6d342cea3d753b","02ffa4495d020d17b54da83eaf8fbe489d81995577021ade3a340a39f5a0e2d45c","030f0e16b2d55c3b40b64835f87ab923d58bcdbb1195fadc2f05b6714d9331e837","02f70041fc4b1155785784a7c23f35d5d6490e300a7dd5b7053f88135fc1f14dfd","03b39508c6f9c7b8c3fb8a1b91e61a0850c3ac76ccd1a53fbc5b853a94979cffa8","03b02aa869aa14b0ec03c4935cc12f221c3f204f44d64146d468e07370c040bfe7","02b7d246a721e150aaf0e0e60a30ad562a32ef76a450101f3f772fef4d92b212d9","037cd5271b31466a75321d7c9e16f995fd0a2b320989c14bee82e161c83c714321","03d4ad77e15be312b29987630734d27ca6e9ee418faa6a8d6a50581eca40662829"],"xpub":"xpub661MyMwAqRbcGwHDovebbFy19vHfW2Cqtyf2TaJkAwhFWsLYfHHYcCnM7smpvntxJP1YMVT5triFbWiCGXPRPhqdCxFumA77MuQB1CeWHpE"}},"accounts_expanded":{},"addr_history":{"12qKnKuhCZ1Q9XBi1N6SnxYEUtb5XZXuY5":[],"1321ddunxShHmF4cjh3v5yqR7uatvSNndK":[],"13Ji3kGWn9qxLcWGhd46xjV6hg8SRw8x2P":[],"145q5ZDXuFi6v9dA2t8HyD8ysorfb81NRt":[],"14gB2wLy2DMkBVtuU6HHP3kQYNFYPzAguU":[],"16VGRwtZwp4yapQN5fS8CprK6mmnEicCEj":[],"16ahKVzCviRi24rwkoKgiSVSkvRNiQudE1":[],"16wjKZ1CWAMEzSR4UxQTWqXRm9jcJ9Dbuf":[],"18ReWGJBq1XkJaPAirVdT6RqDskcFeD5Ho":[],"1A1ECMMJU4NicWNwfMBn3XJriB4WHAcPUC":[],"1Bvxbfc2wXB8z8kyz2uyKw2Ps8JeGQM9FP":[],"1EDWUz4kPq8ZbCdQq8rLhFc3qSZ6Fpt1TD":[],"1EsvTarawMm5BfF44hpRtE4GfZFfZZ1JG3":[],"1JgaekD2ETMJm6oRNnwTWRK9ZxXeUcbi18":[],"1KHdLodsSWj1LrrD9d1RbApfqzpxRs5sxu":[],"1KgGwpKhruHWpMNtrpRExDWLLk5qHCHBdg":[],"1LFf8d3XD9atZvMVMAiq9ygaeZbphbKzSo":[],"1N3XncDQsWE2qff1EVyQEmR6JLLzD3mEL7":[],"1NUtLcVQNmY5TJCieM1cUmBmv18AafY1vq":[],"1NYFsm7PpneT65byRtm8niyvtzKsbEeuXA":[],"1NvEcSvfCe8LPvPkK4ZxhjzaUncTPqe9jX":[],"1PV8xdkYKxeMpnzeeA4eYEpL24j1G9ApV2":[],"1PdiGtznaW1mok6ETffeRvPP5f4ekBRAfq":[],"1QApNe4DtK7HAbJrn5kYkYxZMt86U5ChSb":[],"1QnH7F6RBXFe7LtszQ6KTRUPkQKRtXTnm":[],"1ekukhMNSWCfnRsmpkuTRuLMbz6cstkrq":[]},"master_private_keys":{"x/":"xprv9s21ZrQH143K4TCkhu7bE82GbtTB6ZUzXkjRfBu8ccAGe51Q7jyJ4QTsGbWxpHxnatKeYV7Ad83m7KC81THBm2xmyxA1q8BuuRXSGnmhhR8"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGwHDovebbFy19vHfW2Cqtyf2TaJkAwhFWsLYfHHYcCnM7smpvntxJP1YMVT5triFbWiCGXPRPhqdCxFumA77MuQB1CeWHpE"},"pruned_txo":{},"seed":"heart cabbage scout rely square census satoshi home purpose legal replace move able","seed_version":11,"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard","winpos-qt":[582,394,840,400]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_6_4_importedkeys(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported","winpos-qt":[510,338,840,400]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_6_4_watchaddresses(self): + wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported","winpos-qt":[582,425,840,400]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_6_4_multisig(self): + wallet_str = '{"accounts":{"0":{"change":[["03d0bcdc86a64cc2024c84853e88985f6f30d3dc3f219b432680c338a3996a89ed","024f326d48aa0a62310590b10522b69d250a2439544aa4dc496f7ba6351e6ebbfe"],["03c0416928528a9aaaee558590447ee63fd33fa497deebefcf363b1af90d867762","03db7de16cd6f3dcd0329a088382652bc3e6b21ee1a732dd9655e192c887ed88a7"],["0291790656844c9d9c24daa344c0b426089eadd3952935c58ce6efe00ef1369828","02c2a5493893643102f77f91cba709f11aaab3e247863311d6fc3d3fc82624c3cc"],["023dc976bd1410a7e9f34c230051db58a3f487763f00df1f529b10f55ee85b931c","036c318a7530eedf3584fd8b24c4024656508e35057a0e7654f21e89e121d0bd30"],["02c8820711b39272e9730a1c5c5c78fe39a642b8097f8724b2592cc987017680ce","0380e3ebe0ea075e33acb3f796ad6548fde86d37c62fe8e4f6ab5d2073c1bb1d43"],["0369a32ddd213677a0509c85af514537d5ee04c68114da3bc720faeb3adb45e6f8","0370e85ac01af5e3fd5a5c3969c8bca3e4fc24efb9f82d34d5790e718a507cecb6"]],"m":2,"receiving":[["0207739a9ff4a643e1d4adb03736ec43d13ec897bdff76b40a25d3a16e19e464aa","02372ea4a291aeb1fadb26f36976348fc169fc70514797e53b789a87c9b27cc568"],["0248ae7671882ec87dd6bacf7eb2ff078558456cf5753952cddb5dde08f471f3d6","035bac54828b383545d7b70824a8be2f2d9584f656bfdc680298a38e9383ed9e51"],["02cb99ba41dfbd510cd25491c12bd0875fe8155b5a6694ab781b42bd949252ff26","03b520feba42149947f8b2bbc7e8c03f9376521f20ac7b7f122dd44ab27309d7c6"],["0395902d5ebb4905edd7c4aedecf17be0675a2ffeb27d85af25451659c05cc5198","02b4a01d4bd25cadcbf49900005e8d5060ed9cdc35eb33f2cd65cc45cc7ebc00c5"],["02f9d06c136f05acc94e4572399f17238bb56fa15271e3cb816ae7bb9be24b00b6","035516437612574b2b563929c49308911651205e7cebb621940742e570518f1c50"],["0376a7de3abaee6631bd4441658987c27e0c7eee2190a86d44841ae718a014ee43","03cb702364ffd59cb92b2e2128c18d8a5a255be2b95eb950641c5f17a5a900eecb"],["03240c5e868ecb02c4879ae5f5bad809439fdbd2825769d75be188e34f6e533a67","026b0d05784e4b4c8193443ce60bea162eee4d99f9dfa94a53ae3bc046a8574eeb"],["02d087cccb7dc457074aa9decc04de5a080757493c6aa12fa5d7d3d389cfdb5b8e","0293ab7d0d8bbb2d433e7521a1100a08d75a32a02be941f731d5809b22d86edb33"],["03d1b83ab13c5b35701129bed42c1f1fbe86dd503181ad66af3f4fb729f46a277e","0382ec5e920bc5c60afa6775952760668af42b67d36d369cd0e9acc17e6d0a930d"],["03f1737db45f3a42aebd813776f179d5724fce9985e715feb54d836020b8517bfe","0287a9dfb8ee2adab81ef98d52acd27c25f558d2a888539f7d583ef8c00c34d6dc"],["038eb8804e433023324c1d439cd5fbbd641ca85eadcfc5a8b038cb833a755dac21","0361a7c80f0d9483c416bc63d62506c3c8d34f6233b6d100bb43b6fe8ec39388b9"],["0336437ada4cd35bec65469afce298fe49e846085949d93ef59bf77e1a1d804e4a","0321898ed89df11fcfb1be44bb326e4bb3272464f000a9e51fb21d25548619d377"],["0260f0e59d6a80c49314d5b5b857d1df64d474aba48a37c95322292786397f3dc6","03acd6c9aeac54c9510304c2c97b7e206bbf5320c1e268a2757d400356a30c627b"],["0373dc423d6ee57fac3b9de5e2b87cf36c21f2469f17f32f5496e9e7454598ba8e","031ddc1f40c8b8bf68117e790e2d18675b57166e9521dff1da44ba368be76555b3"],["031878b39bc6e35b33ceac396b429babd02d15632e4a926be0220ccbd710c7d7b9","025a71cc5009ae07e3e991f78212e99dd5be7adf941766d011197f331ce8c1bed0"],["032d3b42ed4913a134145f004cf105b66ae97a9914c35fb73d37170d37271acfcd","0322adeb83151937ddcd32d5bf2d3ed07c245811d0f7152716f82120f21fb25426"],["0312759ff0441c59cb477b5ec1b22e76a794cd821c13b8900d72e34e9848f088c2","02d868626604046887d128388e86c595483085f86a395d68920e244013b544ef3b"],["038c4d5f49ab08be619d4fed7161c339ea37317f92d36d4b3487f7934794b79df4","03f4afb40ae7f4a886f9b469a81168ad549ad341390ff91ebf043c4e4bfa05ecc1"],["02378b36e9f84ba387f0605a738288c159a5c277bbea2ea70191ade359bc597dbb","029fd6f0ee075a08308c0ccda7ace4ad9107573d2def988c2e207ac1d69df13355"],["02cfecde7f415b0931fc1ec06055ff127e9c3bec82af5e3affb15191bf995ffc1a","02abb7481504173a7aa1b9860915ef62d09a323425f680d71746be6516f0bb4acf"]],"xpubs":["xpub661MyMwAqRbcF4mZnFnBRYGBaiD9aQRp9w2jaPUrDg3Eery5gywV7eFMzQKmNyY1W4m4fUwsinMw1tFhMNEZ9KjNtkUSBHPXdcXBwCg5ctV","xpub661MyMwAqRbcGHU5H41miJ2wXBLYYk4psK7pB5pWyxK6m5EARwLrKtmpnMzP52qGsKZEtjJCyohVEaZTFXbohjVdfpDFifgMBT82EvkFpsW"]}},"accounts_expanded":{},"addr_history":{"329Ju5tiAr4vHZExAT4KydYEkfKiHraY2N":[],"32HJ13iTVh3sCWyXzipcGb1e78ZxcHrQ7v":[],"32cAdiAapUzNVRYXmDud5J5vEDcGsPHjD8":[],"33fKLmoCo8oFfeV987P6KrNTghSHjJM251":[],"34cE6ZcgXvHEyKbEP2Jpz5C3aEWhvPoPG2":[],"36xsnTKKBojYRHEApVR6bCFbDLp9oqNAxU":[],"372PG6D3chr8tWF3J811dKSpPS84MPU6SE":[],"378nVF8daT4r3jfX1ebKRheUVZX5zaa9wd":[],"392ZtXKp2THrk5VtbandXxFLB8yr2g14aA":[],"39cCrU3Zz3SsHiQUDiyPS1Qd5ZL3Rh1GhQ":[],"3A2cRoBdem5tdRjq514Pp7ZvaxydgZiaNG":[],"3Ceoi3MKdh2xiziHDAzmriwjDx4dvxxLzm":[],"3FcXdG8mh1YeQCYVib8Aw7zwnKpComimLH":[],"3J4b31yAbQkKhejSW7Qz54qNJDEy3t9uSe":[],"3JpJrSxE1GP1X5h82zvLA2TbMZ8nUsGW6z":[],"3K1dzpbcop1MotuqyFQyEuXbvQehaKnGVM":[],"3L8Us8SN22Hj6GnZPRCLaowA1ZtbptXxxL":[],"3LANyoJyShQ8w55tvopoGiZ2BTVjLfChiP":[],"3LoJGQdXTzVaDYudUguP4jNJYy4gNDaRpN":[],"3MD8jVH7Crp5ucFomDnWqB6kQrEQ9VF5xv":[],"3ME8DemkFJSn2tHS23yuk2WfaMP86rd3s7":[],"3MFNr17oSZpFtH16hGPgXz2em2hJkd3SZn":[],"3QHRTYnW2HWCWoeisVcy3xsAFC5xb6UYAK":[],"3QKwygVezHFBthudRUh8V7wwtWjZk3whpB":[],"3QNPY3dznFwRv6VMcKgmn8FGJdsuSRRjco":[],"3QNwwD8dp6kvS8Fys4ZxVJYZAwCXdXQBKo":[]},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3oPcB2UmMA6Cy9W49HLyW6CDNhQuRcn7tGu1tQ2bn6TLw8HFWbu5oP38Z2fFCo5Q4n3fog4DTqywYqfSDWhYbDgVD1TGZoP"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcGHU5H41miJ2wXBLYYk4psK7pB5pWyxK6m5EARwLrKtmpnMzP52qGsKZEtjJCyohVEaZTFXbohjVdfpDFifgMBT82EvkFpsW","x2/":"xpub661MyMwAqRbcF4mZnFnBRYGBaiD9aQRp9w2jaPUrDg3Eery5gywV7eFMzQKmNyY1W4m4fUwsinMw1tFhMNEZ9KjNtkUSBHPXdcXBwCg5ctV"},"pruned_txo":{},"seed":"turkey weapon legend tower style multiply tomorrow wet like frame leave cash achieve","seed_version":11,"stored_height":490035,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2","winpos-qt":[610,418,840,400]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_7_18_seeded(self): + wallet_str = '{"addr_history":{"12nzqpb4vxiFmcvypswSWK1f4cvGwhYAE8":[],"13sapXcP5Wq25PiXh5Zr9mLhyjdfrppWyi":[],"14EzC5y5eFCXg4T7cH4hXoivzysEpGXBTM":[],"15PUQBi2eEzprCZrS8dkfXuoNv8TuqwoBm":[],"16NvXzjxHbiNAULoRRTBjSmecMgF87FAtb":[],"16oyPjLM4R96aZCnSHqBBkDMgbE2ehDWFe":[],"1BfhL8ZPcaZkXTZKASQYcFJsPfXNwCwVMV":[],"1Bn3vun14mDWBDkx4PvK2SyWK1nqB9MSmM":[],"1BrCEnhf763JhVNcZsjGcNmmisBfRkrdcn":[],"1BvXCwXAdaSTES4ENALv3Tw6TJcZbMzu5o":[],"1C2vzgDyPqtvzFRYUgavoLvk3KGujkUUjg":[],"1CN22zUHuX5SxGTmGvPTa2X6qiCJZjDUAW":[],"1CUT9Su42c4MFxrfbrouoniuhVuvRjsKYS":[],"1DLaXDPng4wWXW7AdDG3cLkuKXgEUpjFHq":[],"1DTLcXN6xPUVXP1ZQmt2heXe2KHDSdvRNv":[],"1F1zYJag8yXVnDgGGy7waQT3Sdyp7wLZm3":[],"1Fim67c46NHTcSUu329uF8brTmkoiz6Ej8":[],"1Go6JcgkfZuA7fyQFKuLddee9hzpo31uvL":[],"1J6mhetXo9Eokq7NGjwbKnHryxUCpgbCDn":[],"1K9sFmS7qM2P5JpVGQhHMqQgAnNiujS5jZ":[],"1KBdFn9tGPYEqXnHyJAHxBfCQFF9v3mq95":[],"1LRWRLWHE2pdMviVeTeJBa8nFbUTWSCvrg":[],"1LpXAktoSKbRx7QFkyb2KkSNJXSGLtTg9T":[],"1LtxCQLTqD1q5Q5BReP932t5D7pKx5wiap":[],"1MX5AS3pA5jBhmg4DDuDQEuNhPGS4cGU4F":[],"1Pz9bYFMeqZkXahx9yPjXtJwL69zB3xCp2":[]},"keystore":{"seed":"giraffe tuition frog desk airport rural since dizzy regular victory mind coconut","type":"bip32","xprv":"xprv9s21ZrQH143K28Jvnpm7hU3xPt18neaDpcpoMKTyi9ewNRg6puJ2RAE5gZNPQ73bbmU9WsagxLQ3a6i2t1M9W289HY9Q5sEzFsLaYq3ZQf3","xpub":"xpub661MyMwAqRbcEcPPtrJ84bzgwuqdC7J5BqkQ9hsbGVBvFE1FNScGxxYZXpC9ncowEe7EZVbAerSypw3wCjrmLmsHeG3RzySw5iEJhAfZaZT"},"pruned_txo":{},"pubkeys":{"change":["033e860b0823ed2bf143594b07031d9d95d35f6e4ad6093ddc3071b8d2760f133f","03f51e8798a1a46266dee899bada3e1517a7a57a8402deeef30300a8918c81889a","0308168b05810f62e3d08c61e3c545ccbdce9af603adbdf23dcc366c47f1c5634c","03d7eddff48be72310347efa93f6022ac261cc33ee0704cdad7b6e376e9f90f574","0287e34a1d3fd51efdc83f946f2060f13065e39e587c347b65a579b95ef2307d45","02df34e258a320a11590eca5f0cb0246110399de28186011e8398ce99dd806854a"],"receiving":["031082ff400cbe517cc2ae37492a6811d129b8fb0a8c6bd083313f234e221527ae","03fac4d7402c0d8b290423a05e09a323b51afebd4b5917964ba115f48ab280ef07","03c0a8c4ab604634256d3cfa350c4b6ca294a4374193055195a46626a6adea920f","03b0bc3112231a9bea6f5382f4324f23b4e2deb5f01a90b0fe006b816367e43958","03a59c08c8e2d66523c888416e89fa1aaec679f7043aa5a9145925c7a80568e752","0346fefc07ab2f38b16c8d979a8ffe05bc9f31dd33291b4130797fa7d78f6e4a35","025eb34724546b3c6db2ee8b59fbc4731bafadac5df51bd9bbb20b456d550ef56e","02b79c26e2eac48401d8a278c63eec84dc5bef7a71fa7ce01a6e333902495272e2","03a3a212462a2b12dc33a89a3e85684f3a02a647db3d7eaae18c029a6277c4f8ac","02d13fc5b57c4d057accf42cc918912221c528907a1474b2c6e1b9ca24c9655c1a","023c87c3ca86f25c282d9e6b8583b0856a4888f46666b413622d72baad90a25221","030710e320e9911ebfc89a6b377a5c2e5ae0ab16b9a3df54baa9dbd3eb710bf03c","03406b5199d34be50725db2fcd440e487d13d1f7611e604db81bb06cdd9077ffa5","0378139461735db84ff4d838eb408b9c124e556cfb6bac571ed6b2d0ec671abd0c","030538379532c476f664d8795c0d8e5d29aea924d964c685ea5c2343087f055a82","02d1b93fa37b824b4842c46ef36e5c50aadbac024a6f066b482be382bec6b41e5a","02d64e92d12666cde831eb21e00079ecfc3c4f64728415cc38f899aca32f1a5558","0347480bf4d321f5dce2fcd496598fbdce19825de6ed5b06f602d66de7155ac1c0","03242e3dfd8c4b6947b0fbb0b314620c0c3758600bb842f0848f991e9a2520a81c","021acadf6300cb7f2cca11c6e1c7e59e3cf923a786f6371c3b85dd6f8b65c68470"]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[709,314,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_7_18_importedkeys(self): + wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"pubkeys":{"change":[],"receiving":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2"]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[420,312,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_7_18_watchaddresses(self): + wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[553,402,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_7_18_trezor_singleacc(self): + wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"pubkeys":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"]},"seed_version":13,"stored_height":490013,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[631,410,840,405]}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_7_18_multisig(self): + wallet_str = '{"addr_history":{"32WKXQ6BWtGJDVTpdcUMhtRZWzgk5eKnhD":[],"33rvo2pxaccCV7jLwvth36sdLkdEqhM8B8":[],"347kG9dzt2M1ZPTa2zzcmVrAE75LuZs9A2":[],"34BBeAVEe5AM6xkRebddFG8JH6Vx1M5hHH":[],"34MAGbxxCHPX8ASfKsyNkzpqPEUTZ5i1Kx":[],"36uNpoPSgUhN5Cc1wRQyL77aD1RL3a9X6f":[],"384xygkfYsSuXN478zhN4jmNcky1bPo7Cq":[],"39GBGaGpp1ePBsjjaw8NmbZNZkMzhfmZ3W":[],"3BRhw13g9ShGcuHbHExxtFfvhjrxiSiA7J":[],"3BboKZc2VgjKVxoC5gndLGpwEkPJuQrZah":[],"3C3gKJ2UQNNHY2SG4h43zRS1faSLhnqQEr":[],"3CEY1V5WvCTxjHEPG5BY4eXpcYhakTvULJ":[],"3DJyQ94H9g18PR6hfzZNxwwdU6773JaYHd":[],"3Djb7sWog5ANggPWHm4xT5JiTrTSCmVQ8N":[],"3EfgjpUeJBhp3DcgP9wz3EhHNdkCbiJe2L":[],"3FWgjvaL8xN6ne19WCEeD5xxryyKAQ5tn1":[],"3H4ZtDFovXxwWXCpRo8mrCczjTrtbT6eYL":[],"3HvnjPzpaE3VGWwGTALZBguT8p9fyAcfHS":[],"3JGuY9EpzuZkDLR7vVGhqK7zmX9jhYEfmD":[],"3JvrP4gpCUeQzqgPyDt2XePXn3kpqFTo9i":[],"3K3TVvsfo52gdwz7gk84hfP77gRmpc3hkf":[],"3K5uh5viV4Dac267Q3eNurQQBnpEbYck5G":[],"3KaoWE1m3QrtvxTQLFfvNs8gwQH8kQDpFM":[],"3Koo71MC4wBfiDKTsck7qCrRjtGx2SwZqT":[],"3L8XBt8KxwqNX1vJprp6C9YfNW4hkYrC6d":[],"3QmZjxPwcsHZgVUR2gQ6wdbGJBbFro8KLJ":[]},"pruned_txo":{},"pubkeys":{"change":[["031bfbbfb36b5e526bf4d94bfc59f170177b2c821f7d4d4c0e1ee945467fe031a0","03c4664d68e3948e2017c5c55f7c1aec72c1c15686b07875b0f20d5f856ebeb703"],["03c515314e4b695a809d3ba08c20bef00397a0e2df729eaf17b8e082825395e06b","032391d8ab8cad902e503492f1051129cee42dc389231d3cdba60541d70e163244"],["035934f55c09ecec3e8f2aa72407ee7ba3c2f077be08b92a27bc4e81b5e27643fe","0332b121ed13753a1f573feaf4d0a94bf5dd1839b94018844a30490dd501f5f5fb"],["02b1367f7f07cbe1ef2c75ac83845c173770e42518da20efde3239bf988dbff5ac","03f3a8b9033b3545fbe47cab10a6f42c51393ed6e525371e864109f0865a0af43c"],["02e7c25f25ecc17969a664d5225c37ec76184a8843f7a94655f5ed34b97c52445d","030ae4304923e6d8d6cd67324fa4c8bc44827918da24a05f9240df7c91c8e8db8f"],["02deb653a1d54372dbc8656fe0a461d91bcaec18add290ccaa742bdaefdb9ec69b","023c1384f90273e3fc8bc551e71ace8f34831d4a364e56a6e778cd802b7f7965a6"]],"receiving":[["02d978f23dc1493db4daf066201f25092d91d60c4b749ca438186764e6d80e6aa1","02912a8c05d16800589579f08263734957797d8e4bc32ad7411472d3625fd51f10"],["024a4b4f2553d7f4cc2229922387aad70e5944a5266b2feb15f453cedbb5859b13","03f8c6751ee93a0f4afb7b2263982b849b3d4d13c2e30b3f8318908ad148274b4b"],["03cd88a88aabc4b833b4631f4ffb4b9dc4a0845bb7bc3309fab0764d6aa08c4f25","03568901b1f3fb8db05dd5c2092afc90671c3eb8a34b03f08bcfb6b20adf98f1cd"],["030530ffe2e4a41312a41f708febab4408ca8e431ce382c1eedb837901839b550d","024d53412197fc609a6ca6997c6634771862f2808c155723fac03ea89a5379fdcc"],["02de503d2081b523087ca195dbae55bafb27031a918a1cfedbd2c4c0da7d519902","03f4a27a98e41bddb7543bf81a9c53313bf9cfb2c2ebdb6bf96551221d8aecb01a"],["03504bc595ac0d947299759871bfdcf46bcdd8a0590c44a78b8b69f1b152019418","0291f188301773dbc7c1d12e88e3aa86e6d4a88185a896f02852141e10e7e986ab"],["0389c3ab262b7994d2202e163632a264f49dd5f78517e01c9210b6d0a29f524cd4","034bdfa9cc0c6896cb9488329d14903cfe60a2879771c5568adfc452f8dba1b2cb"],["02c55a517c162aae2cb5b36eef78b51aa15040e7293033a5b55ba299e375da297d","027273faf29e922d95987a09c2554229becb857a68112bd139409eb111e7cdb45e"],["02401e62d645dc64d43f77ba1f360b529a4c644ed3fc15b35932edafbaf741e844","02c44cbffc13cb53134354acd18c54c59fa78ec61307e147fa0f6f536fb030a675"],["02194a538f37b388b2b138f73a37d7fbb9a3e62f6b5a00bad2420650adc4fb44d9","03e5cc15d47fcdcf815baa0e15227bc5e6bd8af6cae6add71f724e95bc29714ce5"],["037ebf7b2029c8ea0c1861f98e0952c544a38b9e7caebbf514ff58683063cd0e78","022850577856c810dead8d3d44f28a3b71aaf21cdc682db1beb8056408b1d57d52"],["02aea7537611754fdafd98f341c5a6827f8301eaf98f5710c02f17a07a8938a30e","032fa37659a8365fdae3b293a855c5a692faca687b0875e9720219f9adf4bdb6c2"],["0224b0b8d200238495c58e1bc83afd2b57f9dbb79f9a1fdb40747bebb51542c8d3","03b88cd2502e62b69185b989abb786a57de27431ece4eabb26c934848d8426cbd6"],["032802b0be2a00a1e28e1e29cfd2ad79d36ef936a0ef1c834b0bbe55c1b2673bff","032669b2d80f9110e49d49480acf696b74ecca28c21e7d9c1dd2743104c54a0b13"],["03fcfa90eac92950dd66058bbef0feb153e05a114af94b6843d15200ef7cf9ea4a","023246268fbe8b9a023d9a3fa413f666853bbf92c4c0af47731fdded51751e0c3a"],["020cf5fffe70b174e242f6193930d352c54109578024677c1a13ffce5e1f9e6a29","03cb996663b9c895c3e04689f0cf1473974023fa0d59416be2a0b01ccdaa3cc484"],["03467e4fff9b33c73b0140393bde3b35a3f804bce79eccf9c53a1f76c59b7452bd","03251c2a041e953c8007d9ee838569d6be9eacfbf65857e875d87c32a8123036d8"],["02192e19803bfa6f55748aada33f778f0ebb22a1c573e5e49cba14b6a431ef1c37","02224ce74f1ee47ba6eaaf75618ce2d4768a041a553ee5eb60b38895f3f6de11dc"],["032679be8a73fa5f72d438d6963857bd9e49aef6134041ca950c70b017c0c7d44f","025a8463f1c68e85753bd2d37a640ab586d8259f21024f6173aeed15a23ad4287b"],["03ab0355c95480f0157ae48126f893a6d434aa1341ad04c71517b104f3eda08d3d","02ba4aadba99ae8dc60515b15a087e8763496fcf4026f5a637d684d0d0f8a5f76c"]]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[523,230,840,405],"x1/":{"seed":"pudding sell evoke crystal try order supply chase fine drive nurse double","type":"bip32","xprv":"xprv9s21ZrQH143K2MK5erSSgeaPA1H7gENYS6grakohkaK2M4tzqo6XAjLoRPcBRW9NbGNpaZN3pdoSKLeiQJwmqdSi3GJWZLnK1Txpbn3zinV","xpub":"xpub661MyMwAqRbcEqPYksyT3nX7i37c5h6PoKcTP9DKJur1DsE9PLQmiXfHGe8RmN538Pj8t3qUQcZXCMrkS5z1uWJ6jf9EptAFbC4Z2nKaEQE"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcGYXvLgWjW91feK49GajmPdEarB3Ny8JDduUhzTcEThc8Xs1GyqMR4S7xPHvSq4sbDEFzQh3hjJJFEksUzvnjYnap5RX9o4j"}}' + self._upgrade_storage(wallet_str) + + # seed_version 13 is ambiguous + # client 2.7.18 created wallets with an earlier "v13" structure + # client 2.8.3 created wallets with a later "v13" structure + # client 2.8.3 did not do a proper clean-slate upgrade + # the wallet here was created in 2.7.18 with a couple privkeys imported + # then opened in 2.8.3, after which a few other new privkeys were imported + # it's in some sense in an "inconsistent" state + def test_upgrade_from_client_2_8_3_importedkeys_flawed_previous_upgrade_from_2_7_18(self): + wallet_str = '{"addr_history":{"15VBrfYwoXvDWyXHq1myxDv4h36qUmCHcE":[],"179vRrzjT9k7k5oCNCx6eodYCaLKPy9UQn":[],"18o6WCBWdAaM5kjKnyEL4HysoT324rvJu7":[],"1A9F6ZEqmfKeuLeEq5eWFxajgiJfGCc7ar":[],"1BTjGNUmeMSPBTuXTdwD3DLyCugAZaFb7w":[],"1CjW4KM38acCRw3spiFKiZsj7xmmQqqwd8":[],"1EaDNLPwHRraX1N3ecPWJ2mm7NRgdtvpCj":[],"1PYtQBkjXHQX6YtMzEgehN638o784pK3ce":[],"1yT2T4ha3i1GZoK2iP8EpcgSNG34R2ufM":[]},"addresses":{"change":[],"receiving":["1PYtQBkjXHQX6YtMzEgehN638o784pK3ce","1yT2T4ha3i1GZoK2iP8EpcgSNG34R2ufM","1CjW4KM38acCRw3spiFKiZsj7xmmQqqwd8","1A9F6ZEqmfKeuLeEq5eWFxajgiJfGCc7ar","18o6WCBWdAaM5kjKnyEL4HysoT324rvJu7","1EaDNLPwHRraX1N3ecPWJ2mm7NRgdtvpCj","179vRrzjT9k7k5oCNCx6eodYCaLKPy9UQn","1BTjGNUmeMSPBTuXTdwD3DLyCugAZaFb7w","15VBrfYwoXvDWyXHq1myxDv4h36qUmCHcE"]},"keystore":{"keypairs":{"0206b77fd06f212ad7d85f4a054c231ba4e7894b1773dcbb449671ee54618ff5e9":"L52LWS2hB5ev9JYiisFewJH9Q16U7yYcSNt3M8UKLmL5p1q3v2H2","028cda4a0f03cbcbc695d9cac0858081fd5458acfd29564127d329553245afca42":"KzRhkN9Psm9BobcPx3X3VykVA8yhCBrVvE4tTyq6NE283sL6uvYG","02ba4117a24d7e38ae14c429fce0d521aa1fb6bb97558a13f1ef2bc0a476a1741f":"KySXfvidmMBf8iw6m3R9WtdfKcQPWXenwMZtpno5XpfLMNHH8PMn","031bb44462038b97010624a8f8cb15a10fd0d277f12aba3ccf5ce0d36fc6df3112":"KxmcmCvNrZFgy2jyz9W353XbMwCYWHzYTQVzbaDfZM4FLxemgmKh","0339081c4a0ce22c01aa78a5d025e7a109100d1a35ef0f8f06a0d4c5f9ffefc042":"L53Ks569m3H1dRzua3nGzBE3AaEV8dMvBoHDeSJGnWEDeL775mJ5","0339ea71aba2805238e636c2f1b3e5a8308b1dbdbb335787c51f2f6bf3f6218643":"KwHDUpfvnSC58bs3nGy7YpducXkbmo6UUHrydBHy6sT1mRJcVvBo","04e7dc460c87267cf0958d6904d9cd99a4af0d64d61858636aec7a02e5f9a578d27c1329d5ddc45a937130ed4a59e4147cb4907724321baa6a976f9972a17f79ba":"5JECca5E7r1eNgME7NsPdE29XiVCVwXSzEihnhAQXuMdsJ4VL8S","04e9ad0bf70c51c06c2459961175c47cfec59d58ebef4ffcd9836904ef11230afce03ab5eaac5958b538382195b5aea9bf057c0486079869bb72ef9c958f33f1ed":"5Jt9rGLWgxoJUo4eoYEECskLmRA4BkZqHPHg7DdghKBaWarKuxW","04f8cbd67830ab37138c92898a64a4edf836a60aa5b36956547788bd205c635d6a3056fa6a079961384ae336e737d4c45835821c8915dbc5e18a7def88df83946b":"5KRjCNThRDP8aQTJ3Hq9HUSVNRNUB2e69xwLfMUsrXYLXT7U8b9"},"type":"imported"},"pruned_txo":{},"pubkeys":{"change":[],"receiving":["04e9ad0bf70c51c06c2459961175c47cfec59d58ebef4ffcd9836904ef11230afce03ab5eaac5958b538382195b5aea9bf057c0486079869bb72ef9c958f33f1ed","0339081c4a0ce22c01aa78a5d025e7a109100d1a35ef0f8f06a0d4c5f9ffefc042","0339ea71aba2805238e636c2f1b3e5a8308b1dbdbb335787c51f2f6bf3f6218643","02ba4117a24d7e38ae14c429fce0d521aa1fb6bb97558a13f1ef2bc0a476a1741f","028cda4a0f03cbcbc695d9cac0858081fd5458acfd29564127d329553245afca42","04e7dc460c87267cf0958d6904d9cd99a4af0d64d61858636aec7a02e5f9a578d27c1329d5ddc45a937130ed4a59e4147cb4907724321baa6a976f9972a17f79ba","04f8cbd67830ab37138c92898a64a4edf836a60aa5b36956547788bd205c635d6a3056fa6a079961384ae336e737d4c45835821c8915dbc5e18a7def88df83946b"]},"seed_version":13,"stored_height":492756,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_8_3_seeded(self): + wallet_str = '{"addr_history":{"13sNgoAhqDUTB3YSzWYcKKvP2EczG5JGmt":[],"14C6nXs2GRaK3o5U5e8dJSpVRCoqTsyAkJ":[],"14fH7oRM4bqJtJkgJEynTShcUXQwdxH6mw":[],"16FECc7nP2wor1ijXKihGofUoCkoJnq6XR":[],"16cMJC5ZAtPnvLQBzfHm9YR9GoDxUseMEk":[],"17CbQhK3gutqgWt2iLX69ZeSCvw8yFxPLz":[],"17jEaAyekE8BHPvPmkJqFUh1v1GSi6ywoV":[],"19F5SjaWYVCKMPWR8q1Freo4RGChSmFztL":[],"19snysSPZEbgjmeMtuT7qDMTLH2fa7zrWW":[],"1AFgvLGNHP3nZDNrZ4R2BZKnbwDVAEUP4q":[],"1AwWgUbjQfRhKVLKm1o7qfpXnqeN3cu7Ms":[],"1B4FU2WEd2NQzd2MkWBLHw87uJhBxoVghh":[],"1BEBouVJFihDmEQMTAv4bNV2Q7dZh5iJzv":[],"1BdB7ahc8TSR9RJDmWgGSgsWji2BgzcVvC":[],"1DGhQ1up6dMieEwFdsQQFHRriyyR59rYVq":[],"1HBAAqFVndXBcWdWQNYVYSDK9kdUu8ZRU3":[],"1HMrRJkTayNRBZdXZKVb7oLZKj24Pq65T6":[],"1HiB2QCfNem8b4cJaZ2Rt9T4BbUCPXvTpT":[],"1HkbtbyocwHWjKBmzKmq8szv3cFgSGy7dL":[],"1K5CWjgZEYcKTsJWeQrH6NcMPzFUAikD8z":[],"1KMDUXdqpthH1XZU4q5kdSoMZmCW9yDMcN":[],"1KmHNiNmeS7tWRLYTFDMrTbKR6TERYicst":[],"1NQwmHYdxU1pFTTWyptn8vPW1hsSWJBRTn":[],"1NuPofeK8yNEjtVAu9Rc2pKS9kw8YWUatL":[],"1Q3eTNJWTnfxPkUJXQkeCqPh1cBQjjEXFn":[],"1QEuVTdenchPn9naMhakYx8QwGUXE6JYp":[]},"addresses":{"change":["1K5CWjgZEYcKTsJWeQrH6NcMPzFUAikD8z","19snysSPZEbgjmeMtuT7qDMTLH2fa7zrWW","1DGhQ1up6dMieEwFdsQQFHRriyyR59rYVq","17CbQhK3gutqgWt2iLX69ZeSCvw8yFxPLz","1Q3eTNJWTnfxPkUJXQkeCqPh1cBQjjEXFn","17jEaAyekE8BHPvPmkJqFUh1v1GSi6ywoV"],"receiving":["1KMDUXdqpthH1XZU4q5kdSoMZmCW9yDMcN","1HkbtbyocwHWjKBmzKmq8szv3cFgSGy7dL","1HiB2QCfNem8b4cJaZ2Rt9T4BbUCPXvTpT","14fH7oRM4bqJtJkgJEynTShcUXQwdxH6mw","1NuPofeK8yNEjtVAu9Rc2pKS9kw8YWUatL","16FECc7nP2wor1ijXKihGofUoCkoJnq6XR","19F5SjaWYVCKMPWR8q1Freo4RGChSmFztL","1NQwmHYdxU1pFTTWyptn8vPW1hsSWJBRTn","1HBAAqFVndXBcWdWQNYVYSDK9kdUu8ZRU3","1B4FU2WEd2NQzd2MkWBLHw87uJhBxoVghh","1HMrRJkTayNRBZdXZKVb7oLZKj24Pq65T6","1KmHNiNmeS7tWRLYTFDMrTbKR6TERYicst","1BdB7ahc8TSR9RJDmWgGSgsWji2BgzcVvC","14C6nXs2GRaK3o5U5e8dJSpVRCoqTsyAkJ","1AFgvLGNHP3nZDNrZ4R2BZKnbwDVAEUP4q","13sNgoAhqDUTB3YSzWYcKKvP2EczG5JGmt","1AwWgUbjQfRhKVLKm1o7qfpXnqeN3cu7Ms","1QEuVTdenchPn9naMhakYx8QwGUXE6JYp","1BEBouVJFihDmEQMTAv4bNV2Q7dZh5iJzv","16cMJC5ZAtPnvLQBzfHm9YR9GoDxUseMEk"]},"keystore":{"seed":"novel clay width echo swing blanket absorb salute asset under ginger final","type":"bip32","xprv":"xprv9s21ZrQH143K2jfFF6ektPj6zCCsDGGjQxhD2FQ21j6yrA1piWWEjch2kf1smzB2rzm8rPkdJuHf3vsKqMX9ogtE2A7JF49qVUHrgtjRymM","xpub":"xpub661MyMwAqRbcFDjiM8BmFXfqYE3McizanBcopdoda4dxixLyG3pVHR1WbwgjLo9RL882KRfpfpxh7a7zXPogDdR4xj9TpJWJGsbwaodLSKe"},"pruned_txo":{},"seed_type":"standard","seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_8_3_importedkeys(self): + wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_8_3_watchaddresses(self): + wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[535,380,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_8_3_trezor_singleacc(self): + wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"addresses":{"change":["1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ","14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM","1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG","15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6","1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL","1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs"],"receiving":["1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu","18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw","17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH","12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC","15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ","1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid","1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz","1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj","146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz","1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC","1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo","1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb","1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe","1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv","1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp","15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S","1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX","1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp","1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk","1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD"]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[744,390,840,405]}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_8_3_multisig(self): + wallet_str = '{"addr_history":{"32Qk6Q7XYD2v3et9g5fA97ky8XRAJNDZCS":[],"339axnadPaQg3ngChNBKap2dndUWrSwjk6":[],"34FG8qzA6UYLxrnkpVkM9mrGYix3ZyePJZ":[],"35CR3h2dFF3EkRX5yK47NGuF2FcLtJvpUM":[],"35zrocLBQbHfEqysgv2v5z3RH7BRGQzSMJ":[],"36uBJPkgiQwav23ybewbgkQ2zEzJDY2EX1":[],"37nSiBvGXm1PNYseymaJn5ERcU4mSMueYc":[],"39r4XCmfU4J3N98YQ8Fwvm8VN1Fukfj7QW":[],"3BDqFoYMxyy7nWCpRChYV6YCGh9qnWDmav":[],"3CGCLSHU8ZjeXv6oukJ3eAQN4fqEQ7wuyX":[],"3DCNnfh7oWLsnS3p5QdWfW3hvcFF8qAPFq":[],"3DPheE9uany9ET2qBnWF1wh3zDtptGP6Ts":[],"3EeNJHgSYVJPxYR2NaYv2M2ZnXkPRWSHQh":[],"3FWZ7pJPxZhGr8p6HNr9LLsHA8sABcP7cF":[],"3FZbzEF9HdRqzif2cKUFnwW9AFTJcibjVK":[],"3GEhQHTrWykC6Jfu923qtpxJECsEGVdhUc":[],"3HJ95uxwW6rMoEhYgUfcgpd3ExU3fjkfNb":[],"3HbdMVgKRqadNiHRNGizUCyTQYpJ1aXFav":[],"3J6xRF9d16QNsvoXkYkeTwTU8L5N3Y8f7c":[],"3JBbS3GvhvoLgtLcuMvHCtqjE7dnbpTMkz":[],"3KNWZasWDBuVzzp5Y5cbEgjeYn3NKHZKso":[],"3KQ5tTEbkQSkKiccKFDPrhLnBjSMey6CQM":[],"3KrFHcAzNJYjukGDDZm2HeV5Mok4NGQaD6":[],"3LNZbX9wenL3bLxJTQnPidSvVt3EtDrnUg":[],"3LzjAqqfiN8w4TSiW8Up7bKLD5CicBUC3a":[],"3Nro51wauHugv72NMtY9pmLnwX3FXWU1eE":[]},"addresses":{"change":["34FG8qzA6UYLxrnkpVkM9mrGYix3ZyePJZ","3LzjAqqfiN8w4TSiW8Up7bKLD5CicBUC3a","3GEhQHTrWykC6Jfu923qtpxJECsEGVdhUc","3Nro51wauHugv72NMtY9pmLnwX3FXWU1eE","3JBbS3GvhvoLgtLcuMvHCtqjE7dnbpTMkz","3CGCLSHU8ZjeXv6oukJ3eAQN4fqEQ7wuyX"],"receiving":["35zrocLBQbHfEqysgv2v5z3RH7BRGQzSMJ","3FWZ7pJPxZhGr8p6HNr9LLsHA8sABcP7cF","3DPheE9uany9ET2qBnWF1wh3zDtptGP6Ts","3HbdMVgKRqadNiHRNGizUCyTQYpJ1aXFav","3KQ5tTEbkQSkKiccKFDPrhLnBjSMey6CQM","35CR3h2dFF3EkRX5yK47NGuF2FcLtJvpUM","3HJ95uxwW6rMoEhYgUfcgpd3ExU3fjkfNb","3FZbzEF9HdRqzif2cKUFnwW9AFTJcibjVK","39r4XCmfU4J3N98YQ8Fwvm8VN1Fukfj7QW","3LNZbX9wenL3bLxJTQnPidSvVt3EtDrnUg","32Qk6Q7XYD2v3et9g5fA97ky8XRAJNDZCS","339axnadPaQg3ngChNBKap2dndUWrSwjk6","3EeNJHgSYVJPxYR2NaYv2M2ZnXkPRWSHQh","3BDqFoYMxyy7nWCpRChYV6YCGh9qnWDmav","3DCNnfh7oWLsnS3p5QdWfW3hvcFF8qAPFq","3KNWZasWDBuVzzp5Y5cbEgjeYn3NKHZKso","37nSiBvGXm1PNYseymaJn5ERcU4mSMueYc","3KrFHcAzNJYjukGDDZm2HeV5Mok4NGQaD6","36uBJPkgiQwav23ybewbgkQ2zEzJDY2EX1","3J6xRF9d16QNsvoXkYkeTwTU8L5N3Y8f7c"]},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[671,238,840,405],"x1/":{"seed":"property play install hill hunt follow trash comic pulse consider canyon limit","type":"bip32","xprv":"xprv9s21ZrQH143K46tCjDh5i4H9eSJpnMrYyLUbVZheTbNjiamdxPiffMEYLgxuYsMFokFrNEZ6S6z5wSXXszXaCVQWf6jzZvn14uYZhsnM9Sb","xpub":"xpub661MyMwAqRbcGaxfqFE65CDtCU9KBpaQLZQCHx7G1vuibP6nVw2vD9Z2Bz2DsH43bDZGXjmcvx2TD9wq3CmmFcoT96RCiDd1wMSUB2UH7Gu"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcEncvVc1zrPFZSKe7iAP1LTRhzxuXpmztu1kTtnfj8XNFzzmGH1X1gcGxczBZ3MmYKkxXgZKJCsNXXdasNaQJKJE4KcUjn1L"}}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_9_3_seeded(self): + wallet_str = '{"addr_history":{"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes":[],"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1":[],"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB":[],"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c":[],"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz":[],"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA":[],"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV":[],"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z":[],"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv":[],"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B":[],"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz":[],"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G":[],"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq":[],"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d":[],"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs":[],"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado":[],"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z":[],"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52":[],"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP":[],"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv":[],"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb":[],"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ":[],"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G":[],"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN":[],"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J":[],"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt":[]},"addresses":{"change":["1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP","1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z","15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV","1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq","19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G","1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb"],"receiving":["14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA","13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB","19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz","1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv","1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt","13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c","1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ","12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes","12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1","14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz","1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN","17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z","1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado","18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv","1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G","18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B","1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d","1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs","1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52","1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J"]},"keystore":{"seed":"cereal wise two govern top pet frog nut rule sketch bundle logic","type":"bip32","xprv":"xprv9s21ZrQH143K29XjRjUs6MnDB9wXjXbJP2kG1fnRk8zjdDYWqVkQYUqaDtgZp5zPSrH5PZQJs8sU25HrUgT1WdgsPU8GbifKurtMYg37d4v","xpub":"xpub661MyMwAqRbcEdcCXm1sTViwjBn28zK9kFfrp4C3JUXiW1sfP34f6HA45B9yr7EH5XGzWuTfMTdqpt9XPrVQVUdgiYb5NW9m8ij1FSZgGBF"},"pruned_txo":{},"seed_type":"standard","seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[619,310,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_9_3_importedkeys(self): + wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_9_3_watchaddresses(self): + wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":490039,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[499,386,840,405]}' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_9_3_trezor_singleacc(self): + wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"addresses":{"change":["1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ","14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM","1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG","15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6","1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL","1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs"],"receiving":["1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu","18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw","17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH","12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC","15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ","1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid","1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz","1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj","146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz","1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC","1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo","1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb","1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe","1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv","1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp","15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S","1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX","1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp","1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk","1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD"]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"seed_version":13,"stored_height":490014,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[753,486,840,405]}''' + self._upgrade_storage(wallet_str) + + def test_upgrade_from_client_2_9_3_multisig(self): + wallet_str = '{"addr_history":{"31uiqKhw4PQSmZWnCkqpeh6moB8B1jXEt3":[],"32PBjkXmwRoEQt8HBZcAEUbNwaHw5dR5fe":[],"33FQMD675LMRLZDLYLK7QV6TMYA1uYW1sw":[],"33MQEs6TCgxmAJhZvUEXYr6gCkEoEYzUfm":[],"33vuhs2Wor9Xkax66ucDkscPcU6nQHw8LA":[],"35tbMt1qBGmy5RNcsdGZJgs7XVbf5gEgPs":[],"36zhHEtGA33NjHJdxCMjY6DLeU2qxhiLUE":[],"37rZuTsieKVpRXshwrY8qvFBn6me42mYr5":[],"38A2KDXYRmRKZRRCGgazrj19i22kDr8d4V":[],"38GZH5GhxLKi5so9Aka6orY2EDZkvaXdxm":[],"3AEtxrCwiYv5Y5CRmHn1c5nZnV3Hpfh5BM":[],"3AaHWprY1MytygvQVDLp6i63e9o5CwMSN5":[],"3DAD19hHXNxAfZjCtUbWjZVxw1fxQqCbY7":[],"3GK4CBbgwumoeR9wxJjr1QnfnYhGUEzHhN":[],"3H18xmkyX3XAb5MwucqKpEhTnh3qz8V4Mn":[],"3JhkakvHAyFvukJ3cyaVgiyaqjYNo2gmsS":[],"3JtA4x1AKW4BR5YAEeLR5D157Nd92NHArC":[],"3KQosfGFGsUniyqsidE2Y4Bz1y4iZUkGW6":[],"3KXe1z2Lfk22zL6ggQJLpHZfc9dKxYV95p":[],"3KZiENj4VHdUycv9UDts4ojVRsaMk8LC5c":[],"3KeTKHJbkZN1QVkvKnHRqYDYP7UXsUu6va":[],"3L5aZKtDKSd65wPLMRooNtWHkKd5Mz6E3i":[],"3LAPqjqW4C2Se9HNziUhNaJQS46X1r9p3M":[],"3P3JJPoyNFussuyxkDbnYevYim5XnPGmwZ":[],"3PgNdMYSaPRymskby885DgKoTeA1uZr6Gi":[],"3Pm7DaUzaDMxy2mW5WzHp1sE9hVWEpdf7J":[]},"addresses":{"change":["31uiqKhw4PQSmZWnCkqpeh6moB8B1jXEt3","3JhkakvHAyFvukJ3cyaVgiyaqjYNo2gmsS","3GK4CBbgwumoeR9wxJjr1QnfnYhGUEzHhN","3LAPqjqW4C2Se9HNziUhNaJQS46X1r9p3M","33MQEs6TCgxmAJhZvUEXYr6gCkEoEYzUfm","3AEtxrCwiYv5Y5CRmHn1c5nZnV3Hpfh5BM"],"receiving":["3P3JJPoyNFussuyxkDbnYevYim5XnPGmwZ","33FQMD675LMRLZDLYLK7QV6TMYA1uYW1sw","3DAD19hHXNxAfZjCtUbWjZVxw1fxQqCbY7","3AaHWprY1MytygvQVDLp6i63e9o5CwMSN5","3H18xmkyX3XAb5MwucqKpEhTnh3qz8V4Mn","36zhHEtGA33NjHJdxCMjY6DLeU2qxhiLUE","37rZuTsieKVpRXshwrY8qvFBn6me42mYr5","38A2KDXYRmRKZRRCGgazrj19i22kDr8d4V","38GZH5GhxLKi5so9Aka6orY2EDZkvaXdxm","33vuhs2Wor9Xkax66ucDkscPcU6nQHw8LA","3L5aZKtDKSd65wPLMRooNtWHkKd5Mz6E3i","3KXe1z2Lfk22zL6ggQJLpHZfc9dKxYV95p","3KQosfGFGsUniyqsidE2Y4Bz1y4iZUkGW6","3KZiENj4VHdUycv9UDts4ojVRsaMk8LC5c","32PBjkXmwRoEQt8HBZcAEUbNwaHw5dR5fe","3KeTKHJbkZN1QVkvKnHRqYDYP7UXsUu6va","3JtA4x1AKW4BR5YAEeLR5D157Nd92NHArC","3PgNdMYSaPRymskby885DgKoTeA1uZr6Gi","3Pm7DaUzaDMxy2mW5WzHp1sE9hVWEpdf7J","35tbMt1qBGmy5RNcsdGZJgs7XVbf5gEgPs"]},"pruned_txo":{},"seed_version":13,"stored_height":485855,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[617,227,840,405],"x1/":{"seed":"speed cruise market wasp ability alarm hold essay grass coconut tissue recipe","type":"bip32","xprv":"xprv9s21ZrQH143K48ig2wcAuZoEKaYdNRaShKFR3hLrgwsNW13QYRhXH6gAG1khxim6dw2RtAzF8RWbQxr1vvWUJFfEu2SJZhYbv6pfreMpuLB","xpub":"xpub661MyMwAqRbcGco98y9BGhjxscP7mtJJ4YB1r5kUFHQMNoNZ5y1mptze7J37JypkbrmBdnqTvSNzxL7cE1FrHg16qoj9S12MUpiYxVbTKQV"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcGrCDZaVs9VC7Z6579tsGvpqyDYZEHKg2MXoDkxhrWoukqvwDPXKdxVkYA6Hv9XHLETptfZfNpcJZmsUThdXXkTNGoBjQv1o"}}' + self._upgrade_storage(wallet_str) + +########## + + @classmethod + def setUpClass(cls): + super().setUpClass() + from electrum.plugin import Plugins + from electrum.simple_config import SimpleConfig + + cls.electrum_path = tempfile.mkdtemp() + config = SimpleConfig({'electrum_path': cls.electrum_path}) + + gui_name = 'cmdline' + # TODO it's probably wasteful to load all plugins... only need Trezor + Plugins(config, True, gui_name) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + shutil.rmtree(cls.electrum_path) + + def _upgrade_storage(self, wallet_json, accounts=1): + storage = self._load_storage_from_json_string(wallet_json, manual_upgrades=True) + + if accounts == 1: + self.assertFalse(storage.requires_split()) + if storage.requires_upgrade(): + storage.upgrade() + self._sanity_check_upgraded_storage(storage) + else: + self.assertTrue(storage.requires_split()) + new_paths = storage.split_accounts() + self.assertEqual(accounts, len(new_paths)) + for new_path in new_paths: + new_storage = WalletStorage(new_path, manual_upgrades=False) + self._sanity_check_upgraded_storage(new_storage) + + def _sanity_check_upgraded_storage(self, storage): + self.assertFalse(storage.requires_split()) + self.assertFalse(storage.requires_upgrade()) + w = Wallet(storage) + + def _load_storage_from_json_string(self, wallet_json, manual_upgrades=True): + with open(self.wallet_path, "w") as f: + f.write(wallet_json) + storage = WalletStorage(self.wallet_path, manual_upgrades=manual_upgrades) + return storage diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py @@ -0,0 +1,813 @@ +import unittest + +from electrum import transaction +from electrum.bitcoin import TYPE_ADDRESS +from electrum.keystore import xpubkey_to_address +from electrum.util import bh2u, bfh + +from . import SequentialTestCase, TestCaseForTestnet +from .test_bitcoin import needs_test_with_all_ecc_implementations + +unsigned_blob = '45505446ff0001000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000005701ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' +signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' +v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700" +signed_segwit_blob = "01000000000101b66d722484f2db63e827ebf41d02684fed0c6550e85015a6c9d41ef216a8a6f00000000000fdffffff0280c3c90100000000160014b65ce60857f7e7892b983851c2a8e3526d09e4ab64bac30400000000160014c478ebbc0ab2097706a98e10db7cf101839931c4024730440220789c7d47f876638c58d98733c30ae9821c8fa82b470285dcdf6db5994210bf9f02204163418bbc44af701212ad42d884cc613f3d3d831d2d0cc886f767cca6e0235e012103083a6dc250816d771faa60737bfe78b23ad619f6b458e0a1f1688e3a0605e79c00000000" + +signed_blob_signatures = ['3046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501', ] + +class TestBCDataStream(SequentialTestCase): + + def test_compact_size(self): + s = transaction.BCDataStream() + values = [0, 1, 252, 253, 2**16-1, 2**16, 2**32-1, 2**32, 2**64-1] + for v in values: + s.write_compact_size(v) + + with self.assertRaises(transaction.SerializationError): + s.write_compact_size(-1) + + self.assertEqual(bh2u(s.input), + '0001fcfdfd00fdfffffe00000100feffffffffff0000000001000000ffffffffffffffffff') + for v in values: + self.assertEqual(s.read_compact_size(), v) + + with self.assertRaises(transaction.SerializationError): + s.read_compact_size() + + def test_string(self): + s = transaction.BCDataStream() + with self.assertRaises(transaction.SerializationError): + s.read_string() + + msgs = ['Hello', ' ', 'World', '', '!'] + for msg in msgs: + s.write_string(msg) + for msg in msgs: + self.assertEqual(s.read_string(), msg) + + with self.assertRaises(transaction.SerializationError): + s.read_string() + + def test_bytes(self): + s = transaction.BCDataStream() + s.write(b'foobar') + self.assertEqual(s.read_bytes(3), b'foo') + self.assertEqual(s.read_bytes(2), b'ba') + self.assertEqual(s.read_bytes(4), b'r') + self.assertEqual(s.read_bytes(1), b'') + +class TestTransaction(SequentialTestCase): + + @needs_test_with_all_ecc_implementations + def test_tx_unsigned(self): + expected = { + 'inputs': [{ + 'type': 'p2pkh', + 'address': '1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD', + 'num_sig': 1, + 'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a', + 'prevout_n': 0, + 'pubkeys': ['02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6'], + 'scriptSig': '01ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000', + 'sequence': 4294967295, + 'signatures': [None], + 'x_pubkeys': ['ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000']}], + 'lockTime': 0, + 'outputs': [{ + 'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', + 'prevout_n': 0, + 'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac', + 'type': TYPE_ADDRESS, + 'value': 1000000}], + 'partial': True, + 'segwit_ser': False, + 'version': 1, + } + tx = transaction.Transaction(unsigned_blob) + self.assertEqual(tx.deserialize(), expected) + self.assertEqual(tx.deserialize(), None) + + self.assertEqual(tx.as_dict(), {'hex': unsigned_blob, 'complete': False, 'final': True}) + self.assertEqual(tx.get_outputs(), [('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', 1000000)]) + self.assertEqual(tx.get_output_addresses(), ['14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs']) + + self.assertTrue(tx.has_address('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs')) + self.assertTrue(tx.has_address('1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD')) + self.assertFalse(tx.has_address('1CQj15y1N7LDHp7wTt28eoD1QhHgFgxECH')) + + self.assertEqual(tx.serialize(), unsigned_blob) + + tx.update_signatures(signed_blob_signatures) + self.assertEqual(tx.raw, signed_blob) + + tx.update(unsigned_blob) + tx.raw = None + blob = str(tx) + self.assertEqual(transaction.deserialize(blob), expected) + + @needs_test_with_all_ecc_implementations + def test_tx_signed(self): + expected = { + 'inputs': [{'address': None, + 'num_sig': 0, + 'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a', + 'prevout_n': 0, + 'scriptSig': '493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6', + 'sequence': 4294967295, + 'type': 'unknown'}], + 'lockTime': 0, + 'outputs': [{ + 'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', + 'prevout_n': 0, + 'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac', + 'type': TYPE_ADDRESS, + 'value': 1000000}], + 'partial': False, + 'segwit_ser': False, + 'version': 1 + } + tx = transaction.Transaction(signed_blob) + self.assertEqual(tx.deserialize(), expected) + self.assertEqual(tx.deserialize(), None) + self.assertEqual(tx.as_dict(), {'hex': signed_blob, 'complete': True, 'final': True}) + + self.assertEqual(tx.serialize(), signed_blob) + + tx.update_signatures(signed_blob_signatures) + + self.assertEqual(tx.estimated_total_size(), 193) + self.assertEqual(tx.estimated_base_size(), 193) + self.assertEqual(tx.estimated_witness_size(), 0) + self.assertEqual(tx.estimated_weight(), 772) + self.assertEqual(tx.estimated_size(), 193) + + def test_estimated_output_size(self): + estimated_output_size = transaction.Transaction.estimated_output_size + self.assertEqual(estimated_output_size('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), 34) + self.assertEqual(estimated_output_size('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), 32) + self.assertEqual(estimated_output_size('bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af'), 31) + self.assertEqual(estimated_output_size('bc1qnvks7gfdu72de8qv6q6rhkkzu70fqz4wpjzuxjf6aydsx7wxfwcqnlxuv3'), 43) + + # TODO other tests for segwit tx + def test_tx_signed_segwit(self): + tx = transaction.Transaction(signed_segwit_blob) + + self.assertEqual(tx.estimated_total_size(), 222) + self.assertEqual(tx.estimated_base_size(), 113) + self.assertEqual(tx.estimated_witness_size(), 109) + self.assertEqual(tx.estimated_weight(), 561) + self.assertEqual(tx.estimated_size(), 141) + + def test_errors(self): + with self.assertRaises(TypeError): + transaction.Transaction.pay_script(output_type=None, addr='') + + with self.assertRaises(BaseException): + xpubkey_to_address('') + + def test_parse_xpub(self): + res = xpubkey_to_address('fe4e13b0f311a55b8a5db9a32e959da9f011b131019d4cebe6141b9e2c93edcbfc0954c358b062a9f94111548e50bde5847a3096b8b7872dcffadb0e9579b9017b01000200') + self.assertEqual(res, ('04ee98d63800824486a1cf5b4376f2f574d86e0a3009a6448105703453f3368e8e1d8d090aaecdd626a45cc49876709a3bbb6dc96a4311b3cac03e225df5f63dfc', '19h943e4diLc68GXW7G75QNe2KWuMu7BaJ')) + + def test_version_field(self): + tx = transaction.Transaction(v2_blob) + self.assertEqual(tx.txid(), "b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe") + + def test_get_address_from_output_script(self): + # the inverse of this test is in test_bitcoin: test_address_to_script + addr_from_script = lambda script: transaction.get_address_from_output_script(bfh(script)) + ADDR = transaction.TYPE_ADDRESS + + # bech32 native segwit + # test vectors from BIP-0173 + self.assertEqual((ADDR, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6')) + self.assertEqual((ADDR, 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')) + self.assertEqual((ADDR, 'bc1sw50qa3jx3s'), addr_from_script('6002751e')) + self.assertEqual((ADDR, 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), addr_from_script('5210751e76e8199196d454941c45d1b3a323')) + + # base58 p2pkh + self.assertEqual((ADDR, '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac')) + self.assertEqual((ADDR, '1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac')) + + # base58 p2sh + self.assertEqual((ADDR, '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487')) + self.assertEqual((ADDR, '3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387')) + +##### + + def _run_naive_tests_on_tx(self, raw_tx, txid): + tx = transaction.Transaction(raw_tx) + self.assertEqual(txid, tx.txid()) + self.assertEqual(raw_tx, tx.serialize()) + self.assertTrue(tx.estimated_size() >= 0) + + def test_txid_coinbase_to_p2pk(self): + raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4103400d0302ef02062f503253482f522cfabe6d6dd90d39663d10f8fd25ec88338295d4c6ce1c90d4aeb368d8bdbadcc1da3b635801000000000000000474073e03ffffffff013c25cf2d01000000434104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac00000000' + txid = 'dbaf14e1c476e76ea05a8b71921a46d6b06f0a950f17c5f9f1a03b8fae467f10' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_coinbase_to_p2pkh(self): + raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff25033ca0030400001256124d696e656420627920425443204775696c640800000d41000007daffffffff01c00d1298000000001976a91427a1f12771de5cc3b73941664b2537c15316be4388ac00000000' + txid = '4328f9311c6defd9ae1bd7f4516b62acf64b361eb39dfcf09d9925c5fd5c61e8' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_segwit_coinbase_to_p2pk(self): + raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502cd010101ffffffff0240be402500000000232103f4e686cdfc96f375e7c338c40c9b85f4011bb843a3e62e46a1de424ef87e9385ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000' + txid = 'fb5a57c24e640a6d8d831eb6e41505f3d54363c507da3733b098d820e3803301' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_segwit_coinbase_to_p2pkh(self): + raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502c3010101ffffffff0240be4025000000001976a9141ea896d897483e0eb33dd6423f4a07970d0a0a2788ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000' + txid = 'ed3d100577477d799107eba97e76770b3efa253c7200e9abfb43da5d2b33513e' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_segwit_coinbase_to_p2sh(self): + raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff050214030101ffffffff02902f50090000000017a914ba582096f8647ca4195f55c8ef7e7e6e120e88b1870000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000' + txid = 'e28ee5866ec0535fe5efac5ad350cbf4960ed981b471a0c4a6baad1d8168d3d7' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2pk_to_p2pkh(self): + raw_tx = '010000000118231a31d2df84f884ced6af11dc24306319577d4d7c340124a7e2dd9c314077000000004847304402200b6c45891aed48937241907bc3e3868ee4c792819821fcde33311e5a3da4789a02205021b59692b652a01f5f009bd481acac2f647a7d9c076d71d85869763337882e01fdffffff016c95052a010000001976a9149c4891e7791da9e622532c97f43863768264faaf88ac00000000' + txid = '90ba90a5b115106d26663fce6c6215b8699c5d4b2672dd30756115f3337dddf9' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2pk_to_p2sh(self): + raw_tx = '0100000001e4643183d6497823576d17ac2439fb97eba24be8137f312e10fcc16483bb2d070000000048473044022032bbf0394dfe3b004075e3cbb3ea7071b9184547e27f8f73f967c4b3f6a21fa4022073edd5ae8b7b638f25872a7a308bb53a848baa9b9cc70af45fcf3c683d36a55301fdffffff011821814a0000000017a9143c640bc28a346749c09615b50211cb051faff00f8700000000' + txid = '172bdf5a690b874385b98d7ab6f6af807356f03a26033c6a65ab79b4ac2085b5' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2pk_to_p2wpkh(self): + raw_tx = '01000000015e5e2bf15f5793fdfd01e0ccd380033797ed2d4dba9498426ca84904176c26610000000049483045022100c77aff69f7ab4bb148f9bccffc5a87ee893c4f7f7f96c97ba98d2887a0f632b9022046367bdb683d58fa5b2e43cfc8a9c6d57724a27e03583942d8e7b9afbfeea5ab01fdffffff017289824a00000000160014460fc70f208bffa9abf3ae4abbd2f629d9cdcf5900000000' + txid = 'ca554b1014952f900aa8cf6e7ab02137a6fdcf933ad6a218de3891a2ef0c350d' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2pkh_to_p2pkh(self): + raw_tx = '0100000001f9dd7d33f315617530dd72264b5d9c69b815626cce3f66266d1015b1a590ba90000000006a4730440220699bfee3d280a499daf4af5593e8750b54fef0557f3c9f717bfa909493a84f60022057718eec7985b7796bb8630bf6ea2e9bf2892ac21bd6ab8f741a008537139ffe012103b4289890b40590447b57f773b5843bf0400e9cead08be225fac587b3c2a8e973fdffffff01ec24052a010000001976a914ce9ff3d15ed5f3a3d94b583b12796d063879b11588ac00000000' + txid = '24737c68f53d4b519939119ed83b2a8d44d716d7f3ca98bcecc0fbb92c2085ce' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2pkh_to_p2sh(self): + raw_tx = '010000000195232c30f6611b9f2f82ec63f5b443b132219c425e1824584411f3d16a7a54bc000000006b4830450221009f39ac457dc8ff316e5cc03161c9eff6212d8694ccb88d801dbb32e85d8ed100022074230bb05e99b85a6a50d2b71e7bf04d80be3f1d014ea038f93943abd79421d101210317be0f7e5478e087453b9b5111bdad586038720f16ac9658fd16217ffd7e5785fdffffff0200e40b540200000017a914d81df3751b9e7dca920678cc19cac8d7ec9010b08718dfd63c2c0000001976a914303c42b63569ff5b390a2016ff44651cd84c7c8988acc7010000' + txid = '155e4740fa59f374abb4e133b87247dccc3afc233cb97c2bf2b46bba3094aedc' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2pkh_to_p2wpkh(self): + raw_tx = '0100000001ce85202cb9fbc0ecbc98caf3d716d7448d2a3bd89e113999514b3df5687c7324000000006b483045022100adab7b6cb1179079c9dfc0021f4db0346730b7c16555fcc4363059dcdd95f653022028bcb816f4fb98615fb8f4b18af3ad3708e2d72f94a6466cc2736055860422cf012102a16a25148dd692462a691796db0a4a5531bcca970a04107bf184a2c9f7fd8b12fdffffff012eb6042a010000001600147d0170de18eecbe84648979d52b666dddee0b47400000000' + txid = 'ed29e100499e2a3a64a2b0cb3a68655b9acd690d29690fa541be530462bf3d3c' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2sh_to_p2pkh(self): + raw_tx = '01000000000101f9823f87af35d158e7dc81a67011f4e511e3f6cab07ac108e524b0ff8b950b39000000002322002041f0237866eb72e4a75cd6faf5ccd738703193907d883aa7b3a8169c636706a9fdffffff020065cd1d000000001976a9148150cd6cf729e7e262699875fec1f760b0aab3cc88acc46f9a3b0000000017a91433ccd0f95a7b9d8eef68be40bb59c64d6e14d87287040047304402205ca97126a5956c2deaa956a2006d79a348775d727074a04b71d9c18eb5e5525402207b9353497af15881100a2786adab56c8930c02d46cc1a8b55496c06e22d3459b01483045022100b4fa898057927c2d920ae79bca752dda58202ea8617d3e6ed96cbd5d1c0eb2fc02200824c0e742d1b4d643cec439444f5d8779c18d4f42c2c87cce24044a3babf2df0147522102db78786b3c214826bd27010e3c663b02d67144499611ee3f2461c633eb8f1247210377082028c124098b59a5a1e0ea7fd3ebca72d59c793aecfeedd004304bac15cd52aec9010000' + txid = '17e1d498ba82503e3bfa81ac4897a57e33f3d36b41bcf4765ba604466c478986' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2sh_to_p2sh(self): + raw_tx = '01000000000101b58520acb479ab656a3c03263af0567380aff6b67a8db98543870b695adf2b170000000017160014cfd2b9f7ed9d4d4429ed6946dbb3315f75e85f14fdffffff020065cd1d0000000017a91485f5681bec38f9f07ae9790d7f27c2bb90b5b63c87106ab32c0000000017a914ff402e164dfce874435641ae9ac41fc6fb14c4e18702483045022100b3d1c89c7c92151ed1df78815924569446782776b6a2c170ca5d74c5dd1ad9b102201d7bab1974fd2aa66546dd15c1f1e276d787453cec31b55a2bd97b050abf20140121024a1742ece86df3dbce4717c228cf51e625030cef7f5e6dde33a4fffdd17569eac7010000' + txid = 'ead0e7abfb24ddbcd6b89d704d7a6091e43804a458baa930adf6f1cb5b6b42f7' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2sh_to_p2wpkh(self): + raw_tx = '010000000001018689476c4604a65b76f4bc416bd3f3337ea59748ac81fa3b3e5082ba98d4e1170100000023220020ae40340707f9726c0f453c3d47c96e7f3b7b4b85608eb3668b69bbef9c7ab374fdffffff0218b2cc1d0000000017a914f2fdd81e606ff2ab804d7bb46bf8838a711c277b870065cd1d0000000016001496ad8959c1f0382984ecc4da61c118b4c8751e5104004730440220387b9e7d402fbcada9ba55a27a8d0563eafa9904ebd2f8f7e3d86e4b45bc0ec202205f37fa0e2bf8cbd384f804562651d7c6f69adce5db4c1a5b9103250a47f73e6b01473044022074903f4dd4fd6b32289be909eb5109924740daa55e79be6dbd728687683f9afa02205d934d981ca12cbec450611ca81dc4127f8da5e07dd63d41049380502de3f15401475221025c3810b37147105106cef970f9b91d3735819dee4882d515c1187dbd0b8f0c792103e007c492323084f1c103beff255836408af89bb9ae7f2fcf60502c28ff4b0c9152aeca010000' + txid = '6f294c84cbd0241650931b4c1be3dfb2f175d682c7a9538b30b173e1083deed3' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2wpkh_to_p2pkh(self): + raw_tx = '0100000000010197e6bf4a70bc118e3a8d9842ed80422e335679dfc29b5ba0f9123f6a5863b8470000000000fdffffff02402bca7f130000001600146f579c953d9e7e7719f2baa20bde22eb5f24119200e87648170000001976a9140cd8fa5fd81c3acf33f93efd179b388de8dd693388ac0247304402204ff33b3ea8fb270f62409bfc257457ca5eb1fec5e4d3a7c11aa487207e131d4d022032726b998e338e5245746716e5cd0b40d32b69d1535c3d841f049d98a5d819b1012102dc3ce3220363aff579eb2c45c973e8b186a829c987c3caea77c61975666e7d1bc8010000' + txid = 'c721ed35767a3a209b688e68e3bb136a72d2b631fe81c56be8bdbb948c343dbc' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2wpkh_to_p2sh(self): + raw_tx = '010000000001013c3dbf620453be41a50f69290d69cd9a5b65683acbb0a2643a2a9e4900e129ed0000000000fdffffff02002f68590000000017a914c7c4dcd0ddf70f15c6df13b4a4d56e9f13c49b2787a0429cd000000000160014e514e3ecf89731e7853e4f3a20983484c569d3910247304402205368cc548209303db5a8f2ebc282bd0f7af0d080ce0f7637758587f94d3971fb0220098cec5752554758bc5fa4de332b980d5e0054a807541581dc5e4de3ed29647501210233717cd73d95acfdf6bd72c4fb5df27cd6bd69ce947daa3f4a442183a97877efc8010000' + txid = '390b958bffb024e508c17ab0caf6e311e5f41170a681dce758d135af873f82f9' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_p2wpkh_to_p2wpkh(self): + raw_tx = '010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000' + txid = '51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_input_p2wsh_p2sh_not_multisig(self): + raw_tx = '0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000' + txid = 'e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d' + self._run_naive_tests_on_tx(raw_tx, txid) + + # input: p2sh, not multisig + def test_txid_regression_issue_3899(self): + raw_tx = '0100000004328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c010000000b0009630330472d5fae685bffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c020000000b0009630359646d5fae6858ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c030000000b000963034bd4715fae6854ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c040000000b000963036de8705fae6860ffffffff0130750000000000001976a914b5abca61d20f9062fb1fdbb880d9d93bac36675188ac00000000' + txid = 'f570d5d1e965ee61bcc7005f8fefb1d3abbed9d7ddbe035e2a68fa07e5fc4a0d' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_negative_version_num(self): + raw_tx = 'f0b47b9a01ecf5e5c3bbf2cf1f71ecdc7f708b0b222432e914b394e24aad1494a42990ddfc000000008b483045022100852744642305a99ad74354e9495bf43a1f96ded470c256cd32e129290f1fa191022030c11d294af6a61b3da6ed2c0c296251d21d113cfd71ec11126517034b0dcb70014104a0fe6e4a600f859a0932f701d3af8e0ecd4be886d91045f06a5a6b931b95873aea1df61da281ba29cadb560dad4fc047cf47b4f7f2570da4c0b810b3dfa7e500ffffffff0240420f00000000001976a9147eeacb8a9265cd68c92806611f704fc55a21e1f588ac05f00d00000000001976a914eb3bd8ccd3ba6f1570f844b59ba3e0a667024a6a88acff7f0000' + txid = 'c659729a7fea5071361c2c1a68551ca2bf77679b27086cc415adeeb03852e369' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_regression_issue_4333(self): + raw_tx = '0100000001a300499298b3f03200c05d1a15aa111a33c769aff6fb355c6bf52ebdb58ca37100000000171600756161616161616161616161616161616161616151fdffffff01c40900000000000017a914001975d5f07f3391674416c1fcd67fd511d257ff871bc71300' + txid = '9b9f39e314662a7433aadaa5c94a2f1e24c7e7bf55fc9e1f83abd72be933eb95' + self._run_naive_tests_on_tx(raw_tx, txid) + + +# these transactions are from Bitcoin Core unit tests ---> +# https://github.com/bitcoin/bitcoin/blob/11376b5583a283772c82f6d32d0007cdbf5b8ef0/src/test/data/tx_valid.json + + def test_txid_bitcoin_core_0001(self): + raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' + txid = '23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0002(self): + raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' + txid = 'fcabc409d8e685da28536e1e5ccc91264d755cd4c57ed4cae3dbaa4d3b93e8ed' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0003(self): + raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' + txid = 'c9aa95f2c48175fdb70b34c23f1c3fc44f869b073a6f79b1343fbce30c3cb575' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0004(self): + raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' + txid = 'da94fda32b55deb40c3ed92e135d69df7efc4ee6665e0beb07ef500f407c9fd2' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0005(self): + raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' + txid = 'f76f897b206e4f78d60fe40f2ccb542184cfadc34354d3bb9bdc30cc2f432b86' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0006(self): + raw_tx = '01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d00000000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac00000000' + txid = 'c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0007(self): + raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000' + txid = 'e41ffe19dff3cbedb413a2ca3fbbcd05cb7fd7397ffa65052f8928aa9c700092' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0008(self): + raw_tx = '01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000' + txid = 'f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0009(self): + raw_tx = '01000000020002000000000000000000000000000000000000000000000000000000000000000000000151ffffffff0001000000000000000000000000000000000000000000000000000000000000000000006b483045022100c9cdd08798a28af9d1baf44a6c77bcc7e279f47dc487c8c899911bc48feaffcc0220503c5c50ae3998a733263c5c0f7061b483e2b56c4c41b456e7d2f5a78a74c077032102d5c25adb51b61339d2b05315791e21bbe80ea470a49db0135720983c905aace0ffffffff010000000000000000015100000000' + txid = 'b56471690c3ff4f7946174e51df68b47455a0d29344c351377d712e6d00eabe5' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0010(self): + raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000' + txid = '99517e5b47533453cc7daa332180f578be68b80370ecfe84dbfff7f19d791da4' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0011(self): + raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100c66c9cdf4c43609586d15424c54707156e316d88b0a1534c9e6b0d4f311406310221009c0fe51dbc9c4ab7cc25d3fdbeccf6679fe6827f08edf2b4a9f16ee3eb0e438a0123210338e8034509af564c62644c07691942e0c056752008a173c89f60ab2a88ac2ebfacffffffff010000000000000000015100000000' + txid = 'ab097537b528871b9b64cb79a769ae13c3c3cd477cc9dddeebe657eabd7fdcea' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0012(self): + raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010040075af0750700015100000000' + txid = '4d163e00f1966e9a1eab8f9374c3e37f4deb4857c247270e25f7d79a999d2dc9' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0013(self): + raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510000000000000000015100000000' + txid = '9fe2ef9dde70e15d78894a4800b7df3bbfb1addb9a6f7d7c204492fdb6ee6cc4' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0014(self): + raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025151ffffffff010000000000000000015100000000' + txid = '99d3825137602e577aeaf6a2e3c9620fd0e605323dc5265da4a570593be791d4' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0015(self): + raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6451515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151ffffffff010000000000000000015100000000' + txid = 'c0d67409923040cc766bbea12e4c9154393abef706db065ac2e07d91a9ba4f84' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0016(self): + raw_tx = '010000000200010000000000000000000000000000000000000000000000000000000000000000000049483045022100d180fd2eb9140aeb4210c9204d3f358766eb53842b2a9473db687fa24b12a3cc022079781799cd4f038b85135bbe49ec2b57f306b2bb17101b17f71f000fcab2b6fb01ffffffff0002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000' + txid = 'c610d85d3d5fdf5046be7f123db8a0890cee846ee58de8a44667cfd1ab6b8666' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0017(self): + raw_tx = '01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df101010000000002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000' + txid = 'a647a7b3328d2c698bfa1ee2dd4e5e05a6cea972e764ccb9bd29ea43817ca64f' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0018(self): + raw_tx = '010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c59da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbea097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf950650802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e18217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a3788dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9cfdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa34b26f62db4a88ac00000000' + txid = 'afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0019(self): + raw_tx = '01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe0000483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa0148304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f4014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000' + txid = 'f4b05f978689c89000f729cae187dcfbe64c9819af67a4f05c0b4d59e717d64d' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0020(self): + raw_tx = '0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000' + txid = 'cc60b1f899ec0a69b7c3f25ddf32c4524096a9c5b01cbd84c6d0312a0c478984' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0021(self): + raw_tx = '01000000012c651178faca83be0b81c8c1375c4b0ad38d53c8fe1b1c4255f5e795c25792220000000049483045022100d6044562284ac76c985018fc4a90127847708c9edb280996c507b28babdc4b2a02203d74eca3f1a4d1eea7ff77b528fde6d5dc324ec2dbfdb964ba885f643b9704cd01ffffffff010100000000000000232102c2410f8891ae918cab4ffc4bb4a3b0881be67c7a1e7faa8b5acf9ab8932ec30cac00000000' + txid = '1edc7f214659d52c731e2016d258701911bd62a0422f72f6c87a1bc8dd3f8667' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0022(self): + raw_tx = '0100000001f725ea148d92096a79b1709611e06e94c63c4ef61cbae2d9b906388efd3ca99c000000000100ffffffff0101000000000000002321028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02ac00000000' + txid = '018adb7133fde63add9149a2161802a1bcf4bdf12c39334e880c073480eda2ff' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0023(self): + raw_tx = '0100000001be599efaa4148474053c2fa031c7262398913f1dc1d9ec201fd44078ed004e44000000004900473044022022b29706cb2ed9ef0cb3c97b72677ca2dfd7b4160f7b4beb3ba806aa856c401502202d1e52582412eba2ed474f1f437a427640306fd3838725fab173ade7fe4eae4a01ffffffff010100000000000000232103ac4bba7e7ca3e873eea49e08132ad30c7f03640b6539e9b59903cf14fd016bbbac00000000' + txid = '1464caf48c708a6cc19a296944ded9bb7f719c9858986d2501cf35068b9ce5a2' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0024(self): + raw_tx = '010000000112b66d5e8c7d224059e946749508efea9d66bf8d0c83630f080cf30be8bb6ae100000000490047304402206ffe3f14caf38ad5c1544428e99da76ffa5455675ec8d9780fac215ca17953520220779502985e194d84baa36b9bd40a0dbd981163fa191eb884ae83fc5bd1c86b1101ffffffff010100000000000000232103905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9fac00000000' + txid = '1fb73fbfc947d52f5d80ba23b67c06a232ad83fdd49d1c0a657602f03fbe8f7a' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0025(self): + raw_tx = '0100000001b0ef70cc644e0d37407e387e73bfad598d852a5aa6d691d72b2913cebff4bceb000000004a00473044022068cd4851fc7f9a892ab910df7a24e616f293bcb5c5fbdfbc304a194b26b60fba022078e6da13d8cb881a22939b952c24f88b97afd06b4c47a47d7f804c9a352a6d6d0100ffffffff0101000000000000002321033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194ac00000000' + txid = '24cecfce0fa880b09c9b4a66c5134499d1b09c01cc5728cd182638bea070e6ab' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0026(self): + raw_tx = '0100000001c188aa82f268fcf08ba18950f263654a3ea6931dabc8bf3ed1d4d42aaed74cba000000004b0000483045022100940378576e069aca261a6b26fb38344e4497ca6751bb10905c76bb689f4222b002204833806b014c26fd801727b792b1260003c55710f87c5adbd7a9cb57446dbc9801ffffffff0101000000000000002321037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1ac00000000' + txid = '9eaa819e386d6a54256c9283da50c230f3d8cd5376d75c4dcc945afdeb157dd7' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0027(self): + raw_tx = '01000000012432b60dc72cebc1a27ce0969c0989c895bdd9e62e8234839117f8fc32d17fbc000000004a493046022100a576b52051962c25e642c0fd3d77ee6c92487048e5d90818bcf5b51abaccd7900221008204f8fb121be4ec3b24483b1f92d89b1b0548513a134e345c5442e86e8617a501ffffffff010000000000000000016a00000000' + txid = '46224764c7870f95b58f155bce1e38d4da8e99d42dbb632d0dd7c07e092ee5aa' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0028(self): + raw_tx = '01000000014710b0e7cf9f8930de259bdc4b84aa5dfb9437b665a3e3a21ff26e0bf994e183000000004a493046022100a166121a61b4eeb19d8f922b978ff6ab58ead8a5a5552bf9be73dc9c156873ea02210092ad9bc43ee647da4f6652c320800debcf08ec20a094a0aaf085f63ecb37a17201ffffffff010000000000000000016a00000000' + txid = '8d66836045db9f2d7b3a75212c5e6325f70603ee27c8333a3bce5bf670d9582e' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0029(self): + raw_tx = '01000000015ebaa001d8e4ec7a88703a3bcf69d98c874bca6299cca0f191512bf2a7826832000000004948304502203bf754d1c6732fbf87c5dcd81258aefd30f2060d7bd8ac4a5696f7927091dad1022100f5bcb726c4cf5ed0ed34cc13dadeedf628ae1045b7cb34421bc60b89f4cecae701ffffffff010000000000000000016a00000000' + txid = 'aab7ef280abbb9cc6fbaf524d2645c3daf4fcca2b3f53370e618d9cedf65f1f8' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0030(self): + raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a900000000924830450221009c0a27f886a1d8cb87f6f595fbc3163d28f7a81ec3c4b252ee7f3ac77fd13ffa02203caa8dfa09713c8c4d7ef575c75ed97812072405d932bd11e6a1593a98b679370148304502201e3861ef39a526406bad1e20ecad06be7375ad40ddb582c9be42d26c3a0d7b240221009d0a3985e96522e59635d19cc4448547477396ce0ef17a58e7d74c3ef464292301ffffffff010000000000000000016a00000000' + txid = '6327783a064d4e350c454ad5cd90201aedf65b1fc524e73709c52f0163739190' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0031(self): + raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0100ffffffff010000000000000000016a00000000' + txid = '892464645599cc3c2d165adcc612e5f982a200dfaa3e11e9ce1d228027f46880' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0032(self): + raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510151ffffffff010000000000000000016a00000000' + txid = '578db8c6c404fec22c4a8afeaf32df0e7b767c4dda3478e0471575846419e8fc' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0033(self): + raw_tx = '0100000001e0be9e32f1f89c3d916c4f21e55cdcd096741b895cc76ac353e6023a05f4f7cc00000000d86149304602210086e5f736a2c3622ebb62bd9d93d8e5d76508b98be922b97160edc3dcca6d8c47022100b23c312ac232a4473f19d2aeb95ab7bdf2b65518911a0d72d50e38b5dd31dc820121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac4730440220508fa761865c8abd81244a168392876ee1d94e8ed83897066b5e2df2400dad24022043f5ee7538e87e9c6aef7ef55133d3e51da7cc522830a9c4d736977a76ef755c0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000' + txid = '974f5148a0946f9985e75a240bb24c573adbbdc25d61e7b016cdbb0a5355049f' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0034(self): + raw_tx = '01000000013c6f30f99a5161e75a2ce4bca488300ca0c6112bde67f0807fe983feeff0c91001000000e608646561646265656675ab61493046022100ce18d384221a731c993939015e3d1bcebafb16e8c0b5b5d14097ec8177ae6f28022100bcab227af90bab33c3fe0a9abfee03ba976ee25dc6ce542526e9b2e56e14b7f10121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac493046022100c3b93edcc0fd6250eb32f2dd8a0bba1754b0f6c3be8ed4100ed582f3db73eba2022100bf75b5bd2eff4d6bf2bda2e34a40fcc07d4aa3cf862ceaa77b47b81eff829f9a01ab21038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000' + txid = 'b0097ec81df231893a212657bf5fe5a13b2bff8b28c0042aca6fc4159f79661b' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0035(self): + raw_tx = '01000000016f3dbe2ca96fa217e94b1017860be49f20820dea5c91bdcb103b0049d5eb566000000000fd1d0147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac47304402203757e937ba807e4a5da8534c17f9d121176056406a6465054bdd260457515c1a02200f02eccf1bec0f3a0d65df37889143c2e88ab7acec61a7b6f5aa264139141a2b0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000' + txid = 'feeba255656c80c14db595736c1c7955c8c0a497622ec96e3f2238fbdd43a7c9' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0036(self): + raw_tx = '01000000012139c555ccb81ee5b1e87477840991ef7b386bc3ab946b6b682a04a621006b5a01000000fdb40148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f2204148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390175ac4830450220646b72c35beeec51f4d5bc1cbae01863825750d7f490864af354e6ea4f625e9c022100f04b98432df3a9641719dbced53393022e7249fb59db993af1118539830aab870148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a580039017521038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000' + txid = 'a0c984fc820e57ddba97f8098fa640c8a7eb3fe2f583923da886b7660f505e1e' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0037(self): + raw_tx = '0100000002f9cbafc519425637ba4227f8d0a0b7160b4e65168193d5af39747891de98b5b5000000006b4830450221008dd619c563e527c47d9bd53534a770b102e40faa87f61433580e04e271ef2f960220029886434e18122b53d5decd25f1f4acb2480659fea20aabd856987ba3c3907e0121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffff42e7988254800876b69f24676b3e0205b77be476512ca4d970707dd5c60598ab00000000fd260100483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a53034930460221008431bdfa72bc67f9d41fe72e94c88fb8f359ffa30b33c72c121c5a877d922e1002210089ef5fc22dd8bfc6bf9ffdb01a9862d27687d424d1fefbab9e9c7176844a187a014c9052483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c7153aeffffffff01a08601000000000017a914d8dacdadb7462ae15cd906f1878706d0da8660e68700000000' + txid = '5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0038(self): + raw_tx = '0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000' + txid = 'ded7ff51d89a4e1ec48162aee5a96447214d93dfb3837946af2301a28f65dbea' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0039(self): + raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000' + txid = '3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0040(self): + raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d' + txid = 'abd62b4627d8d9b2d95fcfd8c87e37d2790637ce47d28018e3aece63c1d62649' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0041(self): + raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d' + txid = '58b6de8413603b7f556270bf48caedcf17772e7105f5419f6a80be0df0b470da' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0042(self): + raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff' + txid = '5f99c0abf511294d76cbe144d86b77238a03e086974bc7a8ea0bdb2c681a0324' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0043(self): + raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000' + txid = '25d35877eaba19497710666473c50d5527d38503e3521107a3fc532b74cd7453' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0044(self): + raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff' + txid = '1b9aef851895b93c62c29fbd6ca4d45803f4007eff266e2f96ff11e9b6ef197b' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0045(self): + raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000' + txid = '3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0046(self): + raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000' + txid = 'f53761038a728b1f17272539380d96e93f999218f8dcb04a8469b523445cd0fd' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0047(self): + raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000' + txid = 'd193f0f32fceaf07bb25c897c8f99ca6f69a52f6274ca64efc2a2e180cb97fc1' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0048(self): + raw_tx = '010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000' + txid = '50a1e0e6a134a564efa078e3bd088e7e8777c2c0aec10a752fd8706470103b89' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0049(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000' + txid = 'e2207d1aaf6b74e5d98c2fa326d2dc803b56b30a3f90ce779fa5edb762f38755' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0050(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000' + txid = 'f335864f7c12ec7946d2c123deb91eb978574b647af125a414262380c7fbd55c' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0051(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000' + txid = 'd1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0052(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000' + txid = '3a13e1b6371c545147173cc4055f0ed73686a9f73f092352fb4b39ca27d360e6' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0053(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000' + txid = 'bffda23e40766d292b0510a1b556453c558980c70c94ab158d8286b3413e220d' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0054(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000' + txid = '01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0055(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000' + txid = 'f6d2359c5de2d904e10517d23e7c8210cca71076071bbf46de9fbd5f6233dbf1' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0056(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000' + txid = '19c2b7377229dae7aa3e50142a32fd37cef7171a01682f536e9ffa80c186f6c9' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0057(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000' + txid = 'c9dda3a24cc8a5acb153d1085ecd2fecf6f87083122f8cdecc515b1148d4c40d' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0058(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000' + txid = 'd1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0059(self): + raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000' + txid = '01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0060(self): + raw_tx = '02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2010000000100000000000000000000000000' + txid = '4b5e0aae1251a9dc66b4d5f483f1879bf518ea5e1765abc5a9f2084b43ed1ea7' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0061(self): + raw_tx = '0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000' + txid = '5f16eb3ca4581e2dfb46a28140a4ee15f85e4e1c032947da8b93549b53c105f5' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0062(self): + raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000' + txid = 'b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0063(self): + raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000' + txid = 'b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0064(self): + raw_tx = '01000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000' + txid = 'fee125c6cd142083fabd0187b1dd1f94c66c89ec6e6ef6da1374881c0c19aece' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0065(self): + raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000023220020ff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000' + txid = '5f32557914351fee5f89ddee6c8983d476491d29e601d854e3927299e50450da' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0066(self): + raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff05540b0000000000000151d0070000000000000151840300000000000001513c0f00000000000001512c010000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71000000000000' + txid = '07dfa2da3d67c8a2b9f7bd31862161f7b497829d5da90a88ba0f1a905e7a43f7' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0067(self): + raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0068(self): + raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff0484030000000000000151d0070000000000000151540b0000000000000151c800000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = 'f92bb6e4f3ff89172f23ef647f74c13951b665848009abb5862cdf7a0412415a' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0069(self): + raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0070(self): + raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff04b60300000000000001519e070000000000000151860b00000000000001009600000000000000015100000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = 'e657e25fc9f2b33842681613402759222a58cf7dd504d6cdc0b69a0b8c2e7dcb' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0071(self): + raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0072(self): + raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = '4ede5e22992d43d42ccdf6553fb46e448aa1065ba36423f979605c1e5ab496b8' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0073(self): + raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0074(self): + raw_tx = '01000000000103000100000000000000000000000000000000000000000000000000000000000000000000000200000000010000000000000000000000000000000000000000000000000000000000000100000000ffffffff000100000000000000000000000000000000000000000000000000000000000002000000000200000003e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = 'cfe9f4b19f52b8366860aec0d2b5815e329299b2e9890d477edd7f1182be7ac8' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0075(self): + raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = 'aee8f4865ca40fa77ff2040c0d7de683bea048b103d42ca406dc07dd29d539cb' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0076(self): + raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0077(self): + raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0078(self): + raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd08020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000' + txid = 'd93ab9e12d7c29d2adc13d5cdf619d53eec1f36eb6612f55af52be7ba0448e97' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0079(self): + raw_tx = '0100000000010c00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff0001000000000000000000000000000000000000000000000000000000000000020000006a473044022026c2e65b33fcd03b2a3b0f25030f0244bd23cc45ae4dec0f48ae62255b1998a00220463aa3982b718d593a6b9e0044513fd67a5009c2fdccc59992cffc2b167889f4012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000030000006a4730440220008bd8382911218dcb4c9f2e75bf5c5c3635f2f2df49b36994fde85b0be21a1a02205a539ef10fb4c778b522c1be852352ea06c67ab74200977c722b0bc68972575a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000040000006b483045022100d9436c32ff065127d71e1a20e319e4fe0a103ba0272743dbd8580be4659ab5d302203fd62571ee1fe790b182d078ecfd092a509eac112bea558d122974ef9cc012c7012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000050000006a47304402200e2c149b114ec546015c13b2b464bbcb0cdc5872e6775787527af6cbc4830b6c02207e9396c6979fb15a9a2b96ca08a633866eaf20dc0ff3c03e512c1d5a1654f148012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000060000006b483045022100b20e70d897dc15420bccb5e0d3e208d27bdd676af109abbd3f88dbdb7721e6d6022005836e663173fbdfe069f54cde3c2decd3d0ea84378092a5d9d85ec8642e8a41012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff00010000000000000000000000000000000000000000000000000000000000000700000000ffffffff00010000000000000000000000000000000000000000000000000000000000000800000000ffffffff00010000000000000000000000000000000000000000000000000000000000000900000000ffffffff00010000000000000000000000000000000000000000000000000000000000000a00000000ffffffff00010000000000000000000000000000000000000000000000000000000000000b0000006a47304402206639c6e05e3b9d2675a7f3876286bdf7584fe2bbd15e0ce52dd4e02c0092cdc60220757d60b0a61fc95ada79d23746744c72bac1545a75ff6c2c7cdb6ae04e7e9592012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0ce8030000000000000151e9030000000000000151ea030000000000000151eb030000000000000151ec030000000000000151ed030000000000000151ee030000000000000151ef030000000000000151f0030000000000000151f1030000000000000151f2030000000000000151f30300000000000001510248304502210082219a54f61bf126bfc3fa068c6e33831222d1d7138c6faa9d33ca87fd4202d6022063f9902519624254d7c2c8ea7ba2d66ae975e4e229ae38043973ec707d5d4a83012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022017fb58502475848c1b09f162cb1688d0920ff7f142bed0ef904da2ccc88b168f02201798afa61850c65e77889cbcd648a5703b487895517c88f85cdd18b021ee246a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000000247304402202830b7926e488da75782c81a54cd281720890d1af064629ebf2e31bf9f5435f30220089afaa8b455bbeb7d9b9c3fe1ed37d07685ade8455c76472cda424d93e4074a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022026326fcdae9207b596c2b05921dbac11d81040c4d40378513670f19d9f4af893022034ecd7a282c0163b89aaa62c22ec202cef4736c58cd251649bad0d8139bcbf55012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71024730440220214978daeb2f38cd426ee6e2f44131a33d6b191af1c216247f1dd7d74c16d84a02205fdc05529b0bc0c430b4d5987264d9d075351c4f4484c16e91662e90a72aab24012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402204a6e9f199dc9672cf2ff8094aaa784363be1eb62b679f7ff2df361124f1dca3302205eeb11f70fab5355c9c8ad1a0700ea355d315e334822fa182227e9815308ee8f012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' + txid = 'b83579db5246aa34255642768167132a0c3d2932b186cd8fb9f5490460a0bf91' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0080(self): + raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000' + txid = '2b1e44fff489d09091e5e20f9a01bbc0e8d80f0662e629fd10709cdb4922a874' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0081(self): + raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff01d00700000000000001510003483045022100e078de4e96a0e05dcdc0a414124dd8475782b5f3f0ed3f607919e9a5eeeb22bf02201de309b3a3109adb3de8074b3610d4cf454c49b61247a2779a0bcbf31c889333032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc711976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac00000000' + txid = '60ebb1dd0b598e20dd0dd462ef6723dd49f8f803b6a2492926012360119cfdd7' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0082(self): + raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff02e8030000000000000151e90300000000000001510247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000' + txid = 'ed0c7f4163e275f3f77064f471eac861d01fdf55d03aa6858ebd3781f70bf003' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0083(self): + raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff02e9030000000000000151e80300000000000001510248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000' + txid = 'f531ddf5ce141e1c8a7fdfc85cc634e5ff686f446a5cf7483e9dbe076b844862' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0084(self): + raw_tx = '01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000' + txid = '98229b70948f1c17851a541f1fe532bf02c408267fecf6d7e174c359ae870654' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0085(self): + raw_tx = '01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000' + txid = '570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0086(self): + raw_tx = '01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000' + txid = 'e0b8142f587aaa322ca32abce469e90eda187f3851043cc4f2a0fff8c13fc84e' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0087(self): + raw_tx = '0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000' + txid = 'b9ecf72df06b8f98f8b63748d1aded5ffc1a1186f8a302e63cf94f6250e29f4d' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0088(self): + raw_tx = '0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000' + txid = '27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0089(self): + raw_tx = '010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000' + txid = '22d020638e3b7e1f2f9a63124ac76f5e333c74387862e3675f64b25e960d3641' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0090(self): + raw_tx = '0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9781d66b61fb5a7ef00ac5ad5bc6ffc78be7b44a566e3c87870e1079368df4c4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000' + txid = '2862bc0c69d2af55da7284d1b16a7cddc03971b77e5a97939cca7631add83bf5' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0091(self): + raw_tx = '01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601522102cd74a2809ffeeed0092bc124fd79836706e41f048db3f6ae9df8708cefb83a1c2102e615999372426e46fd107b76eaf007156a507584aa2cc21de9eee3bdbd26d36c4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000' + txid = '1aebf0c98f01381765a8c33d688f8903e4d01120589ac92b78f1185dc1f4119c' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_bitcoin_core_0092(self): + raw_tx = '010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960101022102966f109c54e85d3aee8321301136cedeb9fc710fdef58a9de8a73942f8e567c021034ffc99dd9a79dd3cb31e2ab3e0b09e0e67db41ac068c625cd1f491576016c84e9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000' + txid = '45d17fb7db86162b2b6ca29fa4e163acf0ef0b54110e49b819bda1f948d423a3' + self._run_naive_tests_on_tx(raw_tx, txid) + +# txns from Bitcoin Core ends <--- + + +class TestTransactionTestnet(TestCaseForTestnet): + + def _run_naive_tests_on_tx(self, raw_tx, txid): + tx = transaction.Transaction(raw_tx) + self.assertEqual(txid, tx.txid()) + self.assertEqual(raw_tx, tx.serialize()) + self.assertTrue(tx.estimated_size() >= 0) + +# partial txns using our partial format ---> + # NOTE: our partial format contains xpubs, and xpubs have version bytes, + # and version bytes encode the network as well; so these are network-sensitive! + + def test_txid_partial_segwit_p2wpkh(self): + raw_tx = '45505446ff000100000000010115a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff02f6fd1200000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600140f9de573bc679d040e763d13f0250bd03e625f6ffeffffffff9095ab000000000000000201ff53ff045f1cf6014af5fa07800000002fa3f450ba41799b9b62642979505817783a9b6c656dc11cd0bb4fa362096808026adc616c25a4d0a877d1741eb1db9cef65c15118bd7d5f31bf65f319edda81840100c8000f391400' + txid = '63ff7e99d85d8e33f683e6ec84574bdf8f5111078a5fe900893e019f9a7f95c3' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_partial_segwit_p2wpkh_p2sh_simple(self): + raw_tx = '45505446ff0001000000000101d0d23a6fbddb21cc664cb81cca96715baa4d6dbe5b7b9bcc6632f1005a7b0b840100000017160014a78a91261e71a681b6312cd184b14503a21f856afdffffff0134410f000000000017a914d6514ca17ecc31952c990daf96e307fbc58529cd87feffffffff40420f000000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c30000020011391400' + txid = '2739f2e7fde9b8ec73fce4aee53722cc7683312d1321ded073284c51fadf44df' + self._run_naive_tests_on_tx(raw_tx, txid) + + def test_txid_partial_segwit_p2wpkh_p2sh_mixed_outputs(self): + raw_tx = '45505446ff00010000000001011dcac788f24b84d771b60c44e1f9b6b83429e50f06e1472d47241922164013b00100000017160014801d28ca6e2bde551112031b6cb75de34f10851ffdffffff0440420f00000000001600140f9de573bc679d040e763d13f0250bd03e625f6fc0c62d000000000017a9142899f6484e477233ce60072fc185ef4c1f2c654487809698000000000017a914d40f85ba3c8fa0f3615bcfa5d6603e36dfc613ef87712d19040000000017a914e38c0cffde769cb65e72cda1c234052ae8d2254187feffffffff6ad1ee040000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c301000c000f391400' + txid = 'ba5c88e07a4025a39ad3b85247cbd4f556a70d6312b18e04513c7cec9d45d6ac' + self._run_naive_tests_on_tx(raw_tx, txid) + +# end partial txns <--- + + +class NetworkMock(object): + + def __init__(self, unspent): + self.unspent = unspent + + def synchronous_send(self, arg): + return self.unspent diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py @@ -0,0 +1,109 @@ +import unittest +from electrum.util import format_satoshis, parse_URI + +from . import SequentialTestCase + + +class TestUtil(SequentialTestCase): + + def test_format_satoshis(self): + result = format_satoshis(1234) + expected = "0.00001234" + self.assertEqual(expected, result) + + def test_format_satoshis_negative(self): + result = format_satoshis(-1234) + expected = "-0.00001234" + self.assertEqual(expected, result) + + def test_format_fee(self): + result = format_satoshis(1700/1000, 0, 0) + expected = "1.7" + self.assertEqual(expected, result) + + def test_format_fee_precision(self): + result = format_satoshis(1666/1000, 0, 0, precision=6) + expected = "1.666" + self.assertEqual(expected, result) + + result = format_satoshis(1666/1000, 0, 0, precision=1) + expected = "1.7" + self.assertEqual(expected, result) + + def test_format_satoshis_whitespaces(self): + result = format_satoshis(12340, whitespaces=True) + expected = " 0.0001234 " + self.assertEqual(expected, result) + + result = format_satoshis(1234, whitespaces=True) + expected = " 0.00001234" + self.assertEqual(expected, result) + + def test_format_satoshis_whitespaces_negative(self): + result = format_satoshis(-12340, whitespaces=True) + expected = " -0.0001234 " + self.assertEqual(expected, result) + + result = format_satoshis(-1234, whitespaces=True) + expected = " -0.00001234" + self.assertEqual(expected, result) + + def test_format_satoshis_diff_positive(self): + result = format_satoshis(1234, is_diff=True) + expected = "+0.00001234" + self.assertEqual(expected, result) + + def test_format_satoshis_diff_negative(self): + result = format_satoshis(-1234, is_diff=True) + expected = "-0.00001234" + self.assertEqual(expected, result) + + def _do_test_parse_URI(self, uri, expected): + result = parse_URI(uri) + self.assertEqual(expected, result) + + def test_parse_URI_address(self): + self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', + {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma'}) + + def test_parse_URI_only_address(self): + self._do_test_parse_URI('15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', + {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma'}) + + + def test_parse_URI_address_label(self): + self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?label=electrum%20test', + {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'label': 'electrum test'}) + + def test_parse_URI_address_message(self): + self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?message=electrum%20test', + {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'message': 'electrum test', 'memo': 'electrum test'}) + + def test_parse_URI_address_amount(self): + self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003', + {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'amount': 30000}) + + def test_parse_URI_address_request_url(self): + self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?r=http://domain.tld/page?h%3D2a8628fc2fbe', + {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'r': 'http://domain.tld/page?h=2a8628fc2fbe'}) + + def test_parse_URI_ignore_args(self): + self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?test=test', + {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'test': 'test'}) + + def test_parse_URI_multiple_args(self): + self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.00004&label=electrum-test&message=electrum%20test&test=none&r=http://domain.tld/page', + {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'amount': 4000, 'label': 'electrum-test', 'message': u'electrum test', 'memo': u'electrum test', 'r': 'http://domain.tld/page', 'test': 'none'}) + + def test_parse_URI_no_address_request_url(self): + self._do_test_parse_URI('bitcoin:?r=http://domain.tld/page?h%3D2a8628fc2fbe', + {'r': 'http://domain.tld/page?h=2a8628fc2fbe'}) + + def test_parse_URI_invalid_address(self): + self.assertRaises(BaseException, parse_URI, 'bitcoin:invalidaddress') + + def test_parse_URI_invalid(self): + self.assertRaises(BaseException, parse_URI, 'notbitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma') + + def test_parse_URI_parameter_polution(self): + self.assertRaises(Exception, parse_URI, 'bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003&label=test&amount=30.0') diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py @@ -0,0 +1,71 @@ +import shutil +import tempfile +import sys +import unittest +import os +import json + +from io import StringIO +from electrum.storage import WalletStorage, FINAL_SEED_VERSION + +from . import SequentialTestCase + + +class FakeSynchronizer(object): + + def __init__(self): + self.store = [] + + def add(self, address): + self.store.append(address) + + +class WalletTestCase(SequentialTestCase): + + def setUp(self): + super(WalletTestCase, self).setUp() + self.user_dir = tempfile.mkdtemp() + + self.wallet_path = os.path.join(self.user_dir, "somewallet") + + self._saved_stdout = sys.stdout + self._stdout_buffer = StringIO() + sys.stdout = self._stdout_buffer + + def tearDown(self): + super(WalletTestCase, self).tearDown() + shutil.rmtree(self.user_dir) + # Restore the "real" stdout + sys.stdout = self._saved_stdout + + +class TestWalletStorage(WalletTestCase): + + def test_read_dictionary_from_file(self): + + some_dict = {"a":"b", "c":"d"} + contents = json.dumps(some_dict) + with open(self.wallet_path, "w") as f: + contents = f.write(contents) + + storage = WalletStorage(self.wallet_path, manual_upgrades=True) + self.assertEqual("b", storage.get("a")) + self.assertEqual("d", storage.get("c")) + + def test_write_dictionary_to_file(self): + + storage = WalletStorage(self.wallet_path) + + some_dict = { + u"a": u"b", + u"c": u"d", + u"seed_version": FINAL_SEED_VERSION} + + for key, value in some_dict.items(): + storage.put(key, value) + storage.write() + + contents = "" + with open(self.wallet_path, "r") as f: + contents = f.read() + self.assertEqual(some_dict, json.loads(contents)) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py @@ -0,0 +1,1603 @@ +import unittest +from unittest import mock +import shutil +import tempfile +from typing import Sequence + +from electrum import storage, bitcoin, keystore, constants +from electrum import Transaction +from electrum import SimpleConfig +from electrum.wallet import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT, sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet +from electrum.util import bfh, bh2u + +from electrum.plugins.trustedcoin import trustedcoin + +from . import TestCaseForTestnet +from . import SequentialTestCase +from .test_bitcoin import needs_test_with_all_ecc_implementations + + +class WalletIntegrityHelper: + + gap_limit = 1 # make tests run faster + + @classmethod + def check_seeded_keystore_sanity(cls, test_obj, ks): + test_obj.assertTrue(ks.is_deterministic()) + test_obj.assertFalse(ks.is_watching_only()) + test_obj.assertFalse(ks.can_import()) + test_obj.assertTrue(ks.has_seed()) + + @classmethod + def check_xpub_keystore_sanity(cls, test_obj, ks): + test_obj.assertTrue(ks.is_deterministic()) + test_obj.assertTrue(ks.is_watching_only()) + test_obj.assertFalse(ks.can_import()) + test_obj.assertFalse(ks.has_seed()) + + @classmethod + def create_standard_wallet(cls, ks, gap_limit=None): + store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') + store.put('keystore', ks.dump()) + store.put('gap_limit', gap_limit or cls.gap_limit) + w = Standard_Wallet(store) + w.synchronize() + return w + + @classmethod + def create_imported_wallet(cls, privkeys=False): + store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') + if privkeys: + k = keystore.Imported_KeyStore({}) + store.put('keystore', k.dump()) + w = Imported_Wallet(store) + return w + + @classmethod + def create_multisig_wallet(cls, keystores: Sequence, multisig_type: str, gap_limit=None): + """Creates a multisig wallet.""" + store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') + for i, ks in enumerate(keystores): + cosigner_index = i + 1 + store.put('x%d/' % cosigner_index, ks.dump()) + store.put('wallet_type', multisig_type) + store.put('gap_limit', gap_limit or cls.gap_limit) + w = Multisig_Wallet(store) + w.synchronize() + return w + + +# TODO passphrase/seed_extension +class TestWalletKeystoreAddressIntegrityForMainnet(SequentialTestCase): + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_electrum_seed_standard(self, mock_write): + seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song' + self.assertEqual(bitcoin.seed_type(seed_words), 'standard') + + ks = keystore.from_seed(seed_words, '', False) + + WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks) + self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) + + self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K32jECVM729vWgGq4mUDJCk1ozqAStTphzQtCTuoFmFafNoG1g55iCnBTXUzz3zWnDb5CVLGiFvmaZjuazHDL8a81cPQ8KL6') + self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U') + + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(w.txin_type, 'p2pkh') + + self.assertEqual(w.get_receiving_addresses()[0], '1NNkttn1YvVGdqBW4PR6zvc3Zx3H5owKRf') + self.assertEqual(w.get_change_addresses()[0], '1KSezYMhAJMWqFbVFB2JshYg69UpmEXR4D') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_electrum_seed_segwit(self, mock_write): + seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' + self.assertEqual(bitcoin.seed_type(seed_words), 'segwit') + + ks = keystore.from_seed(seed_words, '', False) + + WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks) + self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) + + self.assertEqual(ks.xprv, 'zprvAZswDvNeJeha8qZ8g7efN3FXYVJLaEUsE9TW6qXDEbVe74AZ75c2sZFZXPNFzxnhChDQ89oC8C5AjWwHmH1HeRKE1c4kKBQAmjUDdKDUZw2') + self.assertEqual(ks.xpub, 'zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ') + + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(w.txin_type, 'p2wpkh') + + self.assertEqual(w.get_receiving_addresses()[0], 'bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af') + self.assertEqual(w.get_change_addresses()[0], 'bc1qdy94n2q5qcp0kg7v9yzwe6wvfkhnvyzje7nx2p') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_electrum_seed_old(self, mock_write): + seed_words = 'powerful random nobody notice nothing important anyway look away hidden message over' + self.assertEqual(bitcoin.seed_type(seed_words), 'old') + + ks = keystore.from_seed(seed_words, '', False) + + WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks) + self.assertTrue(isinstance(ks, keystore.Old_KeyStore)) + + self.assertEqual(ks.mpk, 'e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3') + + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(w.txin_type, 'p2pkh') + + self.assertEqual(w.get_receiving_addresses()[0], '1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo') + self.assertEqual(w.get_change_addresses()[0], '1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_electrum_seed_2fa(self, mock_write): + seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove' + self.assertEqual(bitcoin.seed_type(seed_words), '2fa') + + xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '') + + ks1 = keystore.from_xprv(xprv1) + self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) + self.assertEqual(ks1.xprv, 'xprv9uraXy9F3HP7i8QDqwNTBiD8Jf4bPD4Epif8cS8qbUbgeidUesyZpKmzfcSeHutsGfFnjgih7kzwTB5UQVRNB5LoXaNc8pFusKYx3KVVvYR') + self.assertEqual(ks1.xpub, 'xpub68qvwUg8sewQvcUgwxuTYr9rrgu5nfn6BwajQpYT9p8fXWxdCRHpN86UWruWJAD1ede8Sv8ERrTa22Gyc4SBfm7zFpcyoVWVBKCVwnw6s1J') + self.assertEqual(ks1.xpub, xpub1) + + ks2 = keystore.from_xprv(xprv2) + self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) + self.assertEqual(ks2.xprv, 'xprv9uraXy9F3HP7kKSiRAvLV7Nrjj7YzspDys7dvGLLu4tLZT49CEBxPWp88dHhVxvZ69SHrPQMUCWjj4Ka2z9kNvs1HAeEf3extGGeSWqEVqf') + self.assertEqual(ks2.xpub, 'xpub68qvwUg8sewQxoXBXCTLrFKbHkx3QLY5M63EiejxTQRKSFPHjmWCwK8byvZMM2wZNYA3SmxXoma3M1zxhGESHZwtB7SwrxRgKXAG8dCD2eS') + self.assertEqual(ks2.xpub, xpub2) + + long_user_id, short_id = trustedcoin.get_user_id( + {'x1/': {'xpub': xpub1}, + 'x2/': {'xpub': xpub2}}) + xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(), long_user_id) + ks3 = keystore.from_xpub(xpub3) + WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3) + self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore)) + + w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3') + self.assertEqual(w.txin_type, 'p2sh') + + self.assertEqual(w.get_receiving_addresses()[0], '35L8XmCDoEBKeaWRjvmZvoZvhp8BXMMMPV') + self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bip39_seed_bip44_standard(self, mock_write): + seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' + self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) + + ks = keystore.from_bip39_seed(seed_words, '', "m/44'/0'/0'") + + self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) + + self.assertEqual(ks.xprv, 'xprv9zGLcNEb3cHUKizLVBz6RYeE9bEZAVPjH2pD1DEzCnPcsemWc3d3xTao8sfhfUmDLMq6e3RcEMEvJG1Et8dvfL8DV4h7mwm9J6AJsW9WXQD') + self.assertEqual(ks.xpub, 'xpub6DFh1smUsyqmYD4obDX6ngaxhd53Zx7aeFjoobebm7vbkT6f9awJWFuGzBT9FQJEWFBL7UyhMXtYzRcwDuVbcxtv9Ce2W9eMm4KXLdvdbjv') + + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(w.txin_type, 'p2pkh') + + self.assertEqual(w.get_receiving_addresses()[0], '16j7Dqk3Z9DdTdBtHcCVLaNQy9MTgywUUo') + self.assertEqual(w.get_change_addresses()[0], '1GG5bVeWgAp5XW7JLCphse14QaC4qiHyWn') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bip39_seed_bip49_p2sh_segwit(self, mock_write): + seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' + self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) + + ks = keystore.from_bip39_seed(seed_words, '', "m/49'/0'/0'") + + self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) + + self.assertEqual(ks.xprv, 'yprvAJEYHeNEPcyBoQYM7sGCxDiNCTX65u4ANgZuSGTrKN5YCC9MP84SBayrgaMyZV7zvkHrr3HVPTK853s2SPk4EttPazBZBmz6QfDkXeE8Zr7') + self.assertEqual(ks.xpub, 'ypub6XDth9u8DzXV1tcpDtoDKMf6kVMaVMn1juVWEesTshcX4zUVvfNgjPJLXrD9N7AdTLnbHFL64KmBn3SNaTe69iZYbYCqLCCNPZKbLz9niQ4') + + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(w.txin_type, 'p2wpkh-p2sh') + + self.assertEqual(w.get_receiving_addresses()[0], '35ohQTdNykjkF1Mn9nAVEFjupyAtsPAK1W') + self.assertEqual(w.get_change_addresses()[0], '3KaBTcviBLEJajTEMstsA2GWjYoPzPK7Y7') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bip39_seed_bip84_native_segwit(self, mock_write): + # test case from bip84 + seed_words = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' + self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) + + ks = keystore.from_bip39_seed(seed_words, '', "m/84'/0'/0'") + + self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) + + self.assertEqual(ks.xprv, 'zprvAdG4iTXWBoARxkkzNpNh8r6Qag3irQB8PzEMkAFeTRXxHpbF9z4QgEvBRmfvqWvGp42t42nvgGpNgYSJA9iefm1yYNZKEm7z6qUWCroSQnE') + self.assertEqual(ks.xpub, 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs') + + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(w.txin_type, 'p2wpkh') + + self.assertEqual(w.get_receiving_addresses()[0], 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu') + self.assertEqual(w.get_change_addresses()[0], 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_electrum_multisig_seed_standard(self, mock_write): + seed_words = 'blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure' + self.assertEqual(bitcoin.seed_type(seed_words), 'standard') + + ks1 = keystore.from_seed(seed_words, '', True) + WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks1) + self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) + self.assertEqual(ks1.xprv, 'xprv9s21ZrQH143K3t9vo23J3hajRbzvkRLJ6Y1zFrUFAfU3t8oooMPfb7f87cn5KntgqZs5nipZkCiBFo5ZtaSD2eDo7j7CMuFV8Zu6GYLTpY6') + self.assertEqual(ks1.xpub, 'xpub661MyMwAqRbcGNEPu3aJQqXTydqR9t49Tkwb4Esrj112kw8xLthv8uybxvaki4Ygt9xiwZUQGeFTG7T2TUzR3eA4Zp3aq5RXsABHFBUrq4c') + + # electrum seed: ghost into match ivory badge robot record tackle radar elbow traffic loud + ks2 = keystore.from_xpub('xpub661MyMwAqRbcGfCPEkkyo5WmcrhTq8mi3xuBS7VEZ3LYvsgY1cCFDbenT33bdD12axvrmXhuX3xkAbKci3yZY9ZEk8vhLic7KNhLjqdh5ec') + WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) + self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) + + w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') + self.assertEqual(w.txin_type, 'p2sh') + + self.assertEqual(w.get_receiving_addresses()[0], '32ji3QkAgXNz6oFoRfakyD3ys1XXiERQYN') + self.assertEqual(w.get_change_addresses()[0], '36XWwEHrrVCLnhjK5MrVVGmUHghr9oWTN1') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_electrum_multisig_seed_segwit(self, mock_write): + seed_words = 'snow nest raise royal more walk demise rotate smooth spirit canyon gun' + self.assertEqual(bitcoin.seed_type(seed_words), 'segwit') + + ks1 = keystore.from_seed(seed_words, '', True) + WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks1) + self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) + self.assertEqual(ks1.xprv, 'ZprvAjxLRqPiDfPDxXrm8JvcoCGRAW6xUtktucG6AMtdzaEbTEJN8qcECvujfhtDU3jLJ9g3Dr3Gz5m1ypfMs8iSUh62gWyHZ73bYLRWyeHf6y4') + self.assertEqual(ks1.xpub, 'Zpub6xwgqLvc42wXB1wEELTdALD9iXwStMUkGqBgxkJFYumaL2dWgNvUkjEDWyDFZD3fZuDWDzd1KQJ4NwVHS7hs6H6QkpNYSShfNiUZsgMdtNg') + + # electrum seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool + ks2 = keystore.from_xpub('Zpub6y4oYeETXAbzLNg45wcFDGwEG3vpgsyMJybiAfi2pJtNF3i3fJVxK2BeZJaw7VeKZm192QHvXP3uHDNpNmNDbQft9FiMzkKUhNXQafUMYUY') + WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) + self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) + + w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') + self.assertEqual(w.txin_type, 'p2wsh') + + self.assertEqual(w.get_receiving_addresses()[0], 'bc1qvzezdcv6vs5h45ugkavp896e0nde5c5lg5h0fwe2xyfhnpkxq6gq7pnwlc') + self.assertEqual(w.get_change_addresses()[0], 'bc1qxqf840dqswcmu7a8v82fj6ej0msx08flvuy6kngr7axstjcaq6us9hrehd') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bip39_multisig_seed_bip45_standard(self, mock_write): + seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' + self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) + + ks1 = keystore.from_bip39_seed(seed_words, '', "m/45'/0") + self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) + self.assertEqual(ks1.xprv, 'xprv9vyEFyXf7pYVv4eDU3hhuCEAHPHNGuxX73nwtYdpbLcqwJCPwFKknAK8pHWuHHBirCzAPDZ7UJHrYdhLfn1NkGp9rk3rVz2aEqrT93qKRD9') + self.assertEqual(ks1.xpub, 'xpub69xafV4YxC6o8Yiga5EiGLAtqR7rgNgNUGiYgw3S9g9pp6XYUne1KxdcfYtxwmA3eBrzMFuYcNQKfqsXCygCo4GxQFHfywxpUbKNfYvGJka') + + # bip39 seed: tray machine cook badge night page project uncover ritual toward person enact + # der: m/45'/0 + ks2 = keystore.from_xpub('xpub6B26nSWddbWv7J3qQn9FbwPPQktSBdPQfLfHhRK4375QoZq8fvM8rQey1koGSTxC5xVoMzNMaBETMUmCqmXzjc8HyAbN7LqrvE4ovGRwNGg') + WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) + self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) + + w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') + self.assertEqual(w.txin_type, 'p2sh') + + self.assertEqual(w.get_receiving_addresses()[0], '3JPTQ2nitVxXBJ1yhMeDwH6q417UifE3bN') + self.assertEqual(w.get_change_addresses()[0], '3FGyDuxgUDn2pSZe5xAJH1yUwSdhzDMyEE') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bip39_multisig_seed_p2sh_segwit(self, mock_write): + # bip39 seed: pulse mixture jazz invite dune enrich minor weapon mosquito flight fly vapor + # der: m/49'/0'/0' + # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh + ks1 = keystore.from_xprv('YprvAUXFReVvDjrPerocC3FxVH748sJUTvYjkAhtKop5VnnzVzMEHr1CHrYQKZwfJn1As3X4LYMav6upxd5nDiLb6SCjRZrBH76EFvyQAG4cn79') + self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) + self.assertEqual(ks1.xpub, 'Ypub6hWbqA2p47QgsLt5J4nxrR3ngu8xsPGb7PdV8CDh48KyNngNqPKSqertAqYhQ4umELu1UsZUCYfj9XPA6AdSMZWDZQobwF7EJ8uNrECaZg1') + + # bip39 seed: slab mixture skin evoke harsh tattoo rare crew sphere extend balcony frost + # der: m/49'/0'/0' + ks2 = keystore.from_xpub('Ypub6iNDhL4WWq5kFZcdFqHHwX4YTH4rYGp8xbndpRrY7WNZFFRfogSrL7wRTajmVHgR46AT1cqUG1mrcRd7h1WXwBsgX2QvT3zFbBCDiSDLkau') + WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) + self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) + + w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') + self.assertEqual(w.txin_type, 'p2wsh-p2sh') + + self.assertEqual(w.get_receiving_addresses()[0], '35LeC45QgCVeRor1tJD6LiDgPbybBXisns') + self.assertEqual(w.get_change_addresses()[0], '39RhtDchc6igmx5tyoimhojFL1ZbQBrXa6') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bip32_extended_version_bytes(self, mock_write): + seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant' + self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) + bip32_seed = keystore.bip39_to_seed(seed_words, '') + self.assertEqual('0df68c16e522eea9c1d8e090cfb2139c3b3a2abed78cbcb3e20be2c29185d3b8df4e8ce4e52a1206a688aeb88bfee249585b41a7444673d1f16c0d45755fa8b9', + bh2u(bip32_seed)) + + def create_keystore_from_bip32seed(xtype): + ks = keystore.BIP32_KeyStore({}) + ks.add_xprv_from_seed(bip32_seed, xtype=xtype, derivation='m/') + return ks + + ks = create_keystore_from_bip32seed(xtype='standard') + self.assertEqual('033a05ec7ae9a9833b0696eb285a762f17379fa208b3dc28df1c501cf84fe415d0', ks.derive_pubkey(0, 0)) + self.assertEqual('02bf27f41683d84183e4e930e66d64fc8af5508b4b5bf3c473c505e4dbddaeed80', ks.derive_pubkey(1, 0)) + + ks = create_keystore_from_bip32seed(xtype='standard') # p2pkh + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K3nyWMZVjzGL4KKAE1zahmhTHuV5pdw4eK3o3igC5QywgQG7UTRe6TGBniPDpPFWzXMeMUFbBj8uYsfXGjyMmF54wdNt8QBm') + self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcGH3yTb2kMQGnsLziRTJZ8vNthsVSCGbdBr8CGDWKxnGAFYgyKTzBtwvPPmfVAWJuFmxRXjSbUTg87wDkWQ5GmzpfUcN9t8Z') + self.assertEqual(w.get_receiving_addresses()[0], '19fWEVaXqgJFFn7JYNr6ouxyjZy3uK7CdK') + self.assertEqual(w.get_change_addresses()[0], '1EEX7da31qndYyeKdbM665w1ze5gbkkAZZ') + + ks = create_keystore_from_bip32seed(xtype='p2wpkh-p2sh') + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(ks.xprv, 'yprvABrGsX5C9janu6AdBvHNCMRZVHJfxcaCgoyWgsyi1wSXN9cGyLMe33bpRU54TLJ1ruJbTrpNqusYQeFvBx1CXNb9k1DhKtBFWo8b1sLbXhN') + self.assertEqual(ks.xpub, 'ypub6QqdH2c5z7967aF6HwpNZVNJ3K9AN5J442u7VGPKaGyWEwwRWsftaqvJGkeZKNe7Jb3C9FG3dAfT94ZzFRrcGhMizGvB6Jtm3itJsEFhxMC') + self.assertEqual(w.get_receiving_addresses()[0], '34SAT5gGF5UaBhhSZ8qEuuxYvZ2cm7Zi23') + self.assertEqual(w.get_change_addresses()[0], '38unULZaetSGSKvDx7Krukh8zm8NQnxGiA') + + ks = create_keystore_from_bip32seed(xtype='p2wpkh') + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(ks.xprv, 'zprvAWgYBBk7JR8GkPMk2H4zQSX4fFT7uEZhbvVjUGsbPwpQRFRWDzXCf7FxSg2eTEwwGYRQDLQwJaE6HvsUueRDKcGkcLv7unzjnXCEQVWhrF9') + self.assertEqual(ks.xpub, 'zpub6jftahH18ngZxsSD8JbzmaToDHHcJhHYy9RLGfHCxHMPJ3kemXqTCuaSHxc9KHJ2iE9ztirc5q212MBYy8Gd4w3KrccbgDiFKSwxFpYKEH6') + self.assertEqual(w.get_receiving_addresses()[0], 'bc1qtuynwzd0d6wptvyqmc6ehkm70zcamxpshyzu5e') + self.assertEqual(w.get_change_addresses()[0], 'bc1qjy5zunxh6hjysele86qqywfa437z4xwmleq8wk') + + ks = create_keystore_from_bip32seed(xtype='standard') # p2sh + w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') + self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K3nyWMZVjzGL4KKAE1zahmhTHuV5pdw4eK3o3igC5QywgQG7UTRe6TGBniPDpPFWzXMeMUFbBj8uYsfXGjyMmF54wdNt8QBm') + self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcGH3yTb2kMQGnsLziRTJZ8vNthsVSCGbdBr8CGDWKxnGAFYgyKTzBtwvPPmfVAWJuFmxRXjSbUTg87wDkWQ5GmzpfUcN9t8Z') + self.assertEqual(w.get_receiving_addresses()[0], '3F4nm8Vunb7mxVvqhUP238PYge2hpU5qYv') + self.assertEqual(w.get_change_addresses()[0], '3N8jvKGmxzVHENn6B4zTdZt3N9bmRKjj96') + + ks = create_keystore_from_bip32seed(xtype='p2wsh-p2sh') + w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') + self.assertEqual(ks.xprv, 'YprvANkMzkodih9AKfL18akM2RmND5LwAyFo15dBc9FFPiGvzLBBjjjv8ATkEB2Y1mWv6NNaLSpVj8G3XosgVBA9frhpaUL6jHeFQXQTbqVPcv2') + self.assertEqual(ks.xpub, 'Ypub6bjiQGLXZ4hTY9QUEcHMPZi6m7BRaRyeNJYnQXerx3ous8WLHH4AfxnE5Tc2sos1Y47B1qGAWP3xGEBkYf1ZRBUPpk2aViMkwTABT6qoiBb') + self.assertEqual(w.get_receiving_addresses()[0], '3L1BxLLASGKE3DR1ruraWm3hZshGCKqcJx') + self.assertEqual(w.get_change_addresses()[0], '3NDGcbZVXTpaQWRhiuVPpXsNt4g2JiCX4E') + + ks = create_keystore_from_bip32seed(xtype='p2wsh') + w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') + self.assertEqual(ks.xprv, 'ZprvAhadJRUYsNgeAxX7xwXyEWrsP3VP7bFHvC9QPY98miep3RzQzPuUkE7tFNz81gAqW1VP5vR4BncbR6VFCsaAU6PRSp2XKCTjgFU6zRpk6Xp') + self.assertEqual(ks.xpub, 'Zpub6vZyhw1ShkEwPSbb4y4ybeobw5KsX3y9HR51BvYkL4BnvEKZXwDjJ2SN6fZcsiWvwhDymJriy3QW9WoKGMRaDR9zh5j15dBFDBDpqjK1ekQ') + self.assertEqual(w.get_receiving_addresses()[0], 'bc1q84x0yrztvcjg88qef4d6978zccxulcmc9y88xcg4ghjdau999x7q7zv2qe') + self.assertEqual(w.get_change_addresses()[0], 'bc1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3tsrkg3gs') + + +class TestWalletKeystoreAddressIntegrityForTestnet(TestCaseForTestnet): + + @mock.patch.object(storage.WalletStorage, '_write') + def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_write): + # bip39 seed: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose + # der: m/49'/1'/0' + # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh + ks1 = keystore.from_xprv('Uprv9BEixD3As2LK5h6G2SNT3cTqbZpsWYPceKTSuVAm1yuSybxSvQz2MV1o8cHTtctQmj4HAenb3eh5YJv4YRZjv35i8fofVnNbs4Dd2B4i5je') + self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) + self.assertEqual(ks1.xpub, 'Upub5QE5Mia4hPtcJBAj8TuTQkQa9bfMv17U1YP3hsaNaKSRrQHbTxJGuHLGyv3MbKZixuPyjfXGUdbTjE4KwyFcX8YD7PX5ybTDbP11UT8UpZR') + + # bip39 seed: square page wood spy oil story rebel give milk screen slide shuffle + # der: m/49'/1'/0' + ks2 = keystore.from_xpub('Upub5QRzUGRJuWJe5MxGzwgQAeyJjzcdGTXkkq77w6EfBkCyf5iWppSaZ4caY2MgWcU9LP4a4uE5apUFN4wLoENoe9tpu26mrUxeGsH84dN3JFh') + WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) + self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) + + w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') + self.assertEqual(w.txin_type, 'p2wsh-p2sh') + + self.assertEqual(w.get_receiving_addresses()[0], '2MzsfTfTGomPRne6TkctMmoDj6LwmVkDrMt') + self.assertEqual(w.get_change_addresses()[0], '2NFp9w8tbYYP9Ze2xQpeYBJQjx3gbXymHX7') + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bip32_extended_version_bytes(self, mock_write): + seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant' + self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) + bip32_seed = keystore.bip39_to_seed(seed_words, '') + self.assertEqual('0df68c16e522eea9c1d8e090cfb2139c3b3a2abed78cbcb3e20be2c29185d3b8df4e8ce4e52a1206a688aeb88bfee249585b41a7444673d1f16c0d45755fa8b9', + bh2u(bip32_seed)) + + def create_keystore_from_bip32seed(xtype): + ks = keystore.BIP32_KeyStore({}) + ks.add_xprv_from_seed(bip32_seed, xtype=xtype, derivation='m/') + return ks + + ks = create_keystore_from_bip32seed(xtype='standard') + self.assertEqual('033a05ec7ae9a9833b0696eb285a762f17379fa208b3dc28df1c501cf84fe415d0', ks.derive_pubkey(0, 0)) + self.assertEqual('02bf27f41683d84183e4e930e66d64fc8af5508b4b5bf3c473c505e4dbddaeed80', ks.derive_pubkey(1, 0)) + + ks = create_keystore_from_bip32seed(xtype='standard') # p2pkh + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(ks.xprv, 'tprv8ZgxMBicQKsPecD328MF9ux3dSaSFWci7FNQmuWH7uZ86eY8i3XpvjK8KSH8To2QphiZiUqaYc6nzDC6bTw8YCB9QJjaQL5pAApN4z7vh2B') + self.assertEqual(ks.xpub, 'tpubD6NzVbkrYhZ4Y5Epun1qZKcACU6NQqocgYyC4RYaYBMWw8nuLSMR7DvzVamkqxwRgrTJ1MBMhc8wwxT2vbHqMu8RBXy4BvjWMxR5EdZroxE') + self.assertEqual(w.get_receiving_addresses()[0], 'mpBTXYfWehjW2tavFwpUdqBJbZZkup13k2') + self.assertEqual(w.get_change_addresses()[0], 'mtkUQgf1psDtL67wMAKTv19LrdgPWy6GDQ') + + ks = create_keystore_from_bip32seed(xtype='p2wpkh-p2sh') + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(ks.xprv, 'uprv8tXDerPXZ1QsVuQ9rV8sN13YoQitC8cD2MtdZJQAVuw19kMMxhhPYnyGLeEiThgLELqNTxS91GTLsVofKAM9LRrkGeRzzEuJRtt1Tcostr7') + self.assertEqual(ks.xpub, 'upub57Wa4MvRPNyAiPUcxWfsj8zHMSZNbbL4PapEMgon4FTz2YgWWF1e6bHkBvpDKk2Rg2Zy9LsonXFFbv7jNeCZ5kdKWv8UkfcoxpdjJrZuBX6') + self.assertEqual(w.get_receiving_addresses()[0], '2MuzNWpcHrXyvPVKzEGT7Xrwp8uEnXXjWnK') + self.assertEqual(w.get_change_addresses()[0], '2MzTzY5VcGLwce7YmdEwjXhgQD7LYEKLJTm') + + ks = create_keystore_from_bip32seed(xtype='p2wpkh') + w = WalletIntegrityHelper.create_standard_wallet(ks) + self.assertEqual(ks.xprv, 'vprv9DMUxX4ShgxMMCbGgqvVa693yNsL8kbhwUQrLhJ3svJtCrAbDMrxArdQMrCJTcLFdyxBDS2hTvotknRE2rmA8fYM8z8Ra9inhcwerEsG6Ev') + self.assertEqual(ks.xpub, 'vpub5SLqN2bLY4WeZgfjnsTVwE5nXQhpYDKZJhLT95hfSFqs5eVjkuBCiewtD8moKegM5fgmtpUNFBboVCjJ6LcZszJvPFpuLaSJEYhNhUAnrCS') + self.assertEqual(w.get_receiving_addresses()[0], 'tb1qtuynwzd0d6wptvyqmc6ehkm70zcamxpsaze002') + self.assertEqual(w.get_change_addresses()[0], 'tb1qjy5zunxh6hjysele86qqywfa437z4xwm4lm549') + + ks = create_keystore_from_bip32seed(xtype='standard') # p2sh + w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') + self.assertEqual(ks.xprv, 'tprv8ZgxMBicQKsPecD328MF9ux3dSaSFWci7FNQmuWH7uZ86eY8i3XpvjK8KSH8To2QphiZiUqaYc6nzDC6bTw8YCB9QJjaQL5pAApN4z7vh2B') + self.assertEqual(ks.xpub, 'tpubD6NzVbkrYhZ4Y5Epun1qZKcACU6NQqocgYyC4RYaYBMWw8nuLSMR7DvzVamkqxwRgrTJ1MBMhc8wwxT2vbHqMu8RBXy4BvjWMxR5EdZroxE') + self.assertEqual(w.get_receiving_addresses()[0], '2N6czpsRwQ3d8AHZPNbztf5NotzEsaZmVQ8') + self.assertEqual(w.get_change_addresses()[0], '2NDgwz4CoaSzdSAQdrCcLFWsJaVowCNgiPA') + + ks = create_keystore_from_bip32seed(xtype='p2wsh-p2sh') + w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') + self.assertEqual(ks.xprv, 'Uprv95RJn67y7xyEvUZXo9brC5PMXCm9QVHoLdYJUZfhsgmQmvvGj75fduqC9MCC28uETouMLYSFtUqqzfRRcPW6UuyR77YQPeNJKd9t3XutF8b') + self.assertEqual(ks.xpub, 'Upub5JQfBberxLXY8xdzuB8rZDL65Ebdox1ehrTuGx5KS2JPejFRGePvBi9fzdmgtBFKuVdx1vsvfjdkj5jVfsMWEEjzMPEtA55orYubtrCZmRr') + self.assertEqual(w.get_receiving_addresses()[0], '2NBZQ25GC3ipaF13ZY3UT8i2xnDuS17pJqx') + self.assertEqual(w.get_change_addresses()[0], '2NDmUgLVX8vKvcJ4FQ37GSUre6QtBzKkb6k') + + ks = create_keystore_from_bip32seed(xtype='p2wsh') + w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') + self.assertEqual(ks.xprv, 'Vprv16YtLrHXxePM6noKqtFtMtmUgBE9bEpF3fPLmpvuPksssLostujtdHBwqhEeVuzESz22UY8hyPx9ed684SQpCmUKSVhpxPFbvVNY7qnviNR') + self.assertEqual(ks.xpub, 'Vpub5dEvVGKn7251zFq7jXvUmJRbFCk5ka19cxz84LyCp2gGhq4eXJZUomop1qjGt5uFK8kkmQUV8PzJcNM4PZmX2URbDiwJjyuJ8GyFHRrEmmG') + self.assertEqual(w.get_receiving_addresses()[0], 'tb1q84x0yrztvcjg88qef4d6978zccxulcmc9y88xcg4ghjdau999x7qf2696k') + self.assertEqual(w.get_change_addresses()[0], 'tb1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3ts5777jl') + + +class TestWalletSending(TestCaseForTestnet): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.electrum_path = tempfile.mkdtemp() + cls.config = SimpleConfig({'electrum_path': cls.electrum_path}) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + shutil.rmtree(cls.electrum_path) + + def create_standard_wallet_from_seed(self, seed_words): + ks = keystore.from_seed(seed_words, '', False) + return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=2) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_write): + wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver') + wallet2 = self.create_standard_wallet_from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song') + + # bootstrap wallet1 + funding_tx = Transaction('01000000014576dacce264c24d81887642b726f5d64aa7825b21b350c7b75a57f337da6845010000006b483045022100a3f8b6155c71a98ad9986edd6161b20d24fad99b6463c23b463856c0ee54826d02200f606017fd987696ebbe5200daedde922eee264325a184d5bbda965ba5160821012102e5c473c051dae31043c335266d0ef89c1daab2f34d885cc7706b267f3269c609ffffffff0240420f00000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c42a2ddb90e000000001976a914c384950342cb6f8df55175b48586838b03130fad88ac00000000') + funding_txid = funding_tx.txid() + funding_output_value = 1000000 + self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_txid) + wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # wallet1 -> wallet2 + outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 250000)] + tx = wallet1.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(wallet1.txin_type, tx.inputs()[0]['type']) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet1.is_mine(wallet1.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('010000000001010392c1940e2ec9f2372919ca3887327fe5b98b866022cc79bab5cbed5a53d2ad0000000000feffffff0290d00300000000001976a914ea7804a2c266063572cc009a63dc25dcc0e9d9b588ac285e0b0000000000160014690b59a8140602fb23cc2904ece9cc4daf361052024730440220608a5339ca894592da82119e1e4a1d09335d70a552c683687223b8ed724465e902201b3f0feccf391b1b6257e4b18970ae57d7ca060af2dae519b3690baad2b2a34e0121030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c00000000', + str(tx_copy)) + self.assertEqual('3c06ae4d9be8226a472b3e7f7c127c7e3016f525d658d26106b80b4c7e3228e2', tx_copy.txid()) + self.assertEqual('d8d930ae91dce73118c3fffabbdfcfb87f5d91673fb4c7dfd0fbe7cf03bf426b', tx_copy.wtxid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + + wallet1.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # TX_HEIGHT_UNCONF_PARENT but nvm + wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet2 -> wallet1 + outputs = [(bitcoin.TYPE_ADDRESS, wallet1.get_receiving_address(), 100000)] + tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('0100000001e228327e4c0bb80661d258d625f516307e7c127c7f3e2b476a22e89b4dae063c000000006b483045022100d3895b31e7c9766987c6f53794c7394f534f4acecefda5479d963236f9703d0b022026dd4e40700ceb788f136faf54bf85b966648dc7c2a608d8110604f2d22d59070121030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cffeffffff02a0860100000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c4268360200000000001976a914ca4c60999c46c2108326590b125aefd476dcb11888ac00000000', + str(tx_copy)) + self.assertEqual('5f25707571eb776bdf14142f9966bf2a681906e0a79501edbb99a972c2ceb972', tx_copy.txid()) + self.assertEqual('5f25707571eb776bdf14142f9966bf2a681906e0a79501edbb99a972c2ceb972', tx_copy.wtxid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + + wallet1.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet level checks + self.assertEqual((0, funding_output_value - 250000 - 5000 + 100000, 0), wallet1.get_balance()) + self.assertEqual((0, 250000 - 5000 - 100000, 0), wallet2.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_write): + wallet1a = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', '', True), + keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'), + keystore.from_xpub('tpubD6NzVbkrYhZ4XJzYkhsCbDCcZRmDAKSD7bXi9mdCni7acVt45fxbTVZyU6jRGh29ULKTjoapkfFsSJvQHitcVKbQgzgkkYsAmaovcro7Mhf') + ], + '2of3', gap_limit=2 + ) + wallet1b = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song', '', True), + keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'), + keystore.from_xpub('tpubD6NzVbkrYhZ4YARFMEZPckrqJkw59GZD1PXtQnw14ukvWDofR7Z1HMeSCxfYEZVvg4VdZ8zGok5VxHwdrLqew5cMdQntWc5mT7mh1CSgrnX') + ], + '2of3', gap_limit=2 + ) + # ^ third seed: ghost into match ivory badge robot record tackle radar elbow traffic loud + wallet2 = self.create_standard_wallet_from_seed('powerful random nobody notice nothing important anyway look away hidden message over') + + # bootstrap wallet1 + funding_tx = Transaction('010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300') + funding_txid = funding_tx.txid() + funding_output_value = 12000000 + self.assertEqual('b25cd55687c9e528c2cfd546054f35fb6741f7cf32d600f07dfecdf2e1d42071', funding_txid) + wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # wallet1 -> wallet2 + outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 370000)] + tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners + self.assertFalse(tx.is_complete()) + wallet1b.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('01000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb201000000fdfe0000483045022100f9ce5616683e613ae14b98d56436454b003348a8172e2ed598018e3d206e57d7022030c65c6551e839f9e9409812be624dbb4e36bd4152c9ed9b0988c10fd8201d1401483045022100d5cb94d4d1dcf01bb9e9280e8178a7e9ada3ad14378ca543afcc9f5667b27cb2022018e76b74800a21934e73b226b34cbbe45c877fba64693da8a20d3cb330f2eafd014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefeffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000', + str(tx_copy)) + self.assertEqual('26f3bdd0402e1cff19126244ebe3d32722cef0db507c7229ca8754f5e06ef25d', tx_copy.txid()) + self.assertEqual('26f3bdd0402e1cff19126244ebe3d32722cef0db507c7229ca8754f5e06ef25d', tx_copy.wtxid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + + wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet2 -> wallet1 + outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] + tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('01000000015df26ee0f55487ca29727c50dbf0ce2227d3e3eb44621219ff1c2e40d0bdf326000000008b483045022100bd9f61ba82507d3a28922fb8be129e14699dfa54ddd03cc9494f696d38ac4121022071afca6fad5bc5c09b0a675e6444be3e97dbbdbc283764ee5f4e27a032d933d80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff02a08601000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887280b0400000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac00000000', + str(tx_copy)) + self.assertEqual('c573b3f8464a4ed40dfc79d0889a780f44e917beef7a75883b2427c2987f3e95', tx_copy.txid()) + self.assertEqual('c573b3f8464a4ed40dfc79d0889a780f44e917beef7a75883b2427c2987f3e95', tx_copy.wtxid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + + wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet level checks + self.assertEqual((0, funding_output_value - 370000 - 5000 + 100000, 0), wallet1a.get_balance()) + self.assertEqual((0, 370000 - 5000 - 100000, 0), wallet2.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_write): + wallet1a = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', '', True), + keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'), + keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra') + ], + '2of3', gap_limit=2 + ) + wallet1b = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('snow nest raise royal more walk demise rotate smooth spirit canyon gun', '', True), + keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra'), + keystore.from_xpub('Vpub5gSKXzxK7FeKQedu2q1z9oJWxqvX72AArW3HSWpEhc8othDH8xMDu28gr7gf17sp492BuJod8Tn7anjvJrKpETwqnQqX7CS8fcYyUtedEMk') + ], + '2of3', gap_limit=2 + ) + # ^ third seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool + wallet2a = WalletIntegrityHelper.create_multisig_wallet( + [ + # bip39: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose, der: m/1234'/1'/0', p2wsh-p2sh multisig + keystore.from_xprv('Uprv9CvELvByqm8k2dpecJVjgLMX1z5DufEjY4fBC5YvdGF5WjGCa7GVJJ2fYni1tyuF7Hw83E6W2ZBjAhaFLZv2ri3rEsubkCd5avg4EHKoDBN'), + keystore.from_xpub('Upub5Qb8ik4Cnu8g97KLXKgVXHqY6tH8emQvqtBncjSKsyfTZuorPtTZgX7ovKKZHuuVGBVd1MTTBkWez1XXt2weN1sWBz6SfgRPQYEkNgz81QF') + ], + '2of2', gap_limit=2 + ) + wallet2b = WalletIntegrityHelper.create_multisig_wallet( + [ + # bip39: square page wood spy oil story rebel give milk screen slide shuffle, der: m/1234'/1'/0', p2wsh-p2sh multisig + keystore.from_xprv('Uprv9BbnKEXJxXaNvdEsRJ9VA9toYrSeFJh5UfGBpM2iKe8Uh7UhrM9K8ioL53s8gvCoGfirHHaqpABDAE7VUNw8LNU1DMJKVoWyeNKu9XcDC19'), + keystore.from_xpub('Upub5RuakRisg8h3F7u7iL2k3UJFa1uiK7xauHamzTxYBbn4PXbM7eajr6M9Q2VCr6cVGhfhqWQqxnABvtSATuVM1xzxk4nA189jJwzaMn1QX7V') + ], + '2of2', gap_limit=2 + ) + + # bootstrap wallet1 + funding_tx = Transaction('01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c11300') + funding_txid = funding_tx.txid() + funding_output_value = 200000 + self.assertEqual('d2bd6c9d332db8e2c50aa521cd50f963fba214645aab2f7556e061a412103e21', funding_txid) + wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # wallet1 -> wallet2 + outputs = [(bitcoin.TYPE_ADDRESS, wallet2a.get_receiving_address(), 165000)] + tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + txid = tx.txid() + tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners + self.assertEqual(txid, tx.txid()) + self.assertFalse(tx.is_complete()) + wallet1b.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400483045022100ea2fbd3d8681cfafdcae1bdaaa64f92fb9872fb8f6bf03a2b7effcf7390b66c8022021a79eff7975479934f958f3766d6ac61d708c79b785e398b3bcd84b1039e9b501483045022100dbc4f1ec18f0e0deb4ff88d7d5b3d3b7b500a80d0c0f33efbd3262f0c8689095022074fd226c0b52e3716ad907d14cba9c79aca482a8f4a51662ca83a5b9db49e15b016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000', + str(tx_copy)) + self.assertEqual('6e9c3cd8788bdb970a124ea06136d52bc01cec4f9b1e217627d5e90ebe77d049', tx_copy.txid()) + self.assertEqual('c58650fb77d04577fccb3e201deecbf691ab52ffb61cd2e57996c4d51f7e980b', tx_copy.wtxid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual(txid, tx_copy.txid()) + + wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet2 -> wallet1 + outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] + tx = wallet2a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + txid = tx.txid() + tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners + self.assertEqual(txid, tx.txid()) + self.assertFalse(tx.is_complete()) + wallet2b.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(wallet2a.txin_type, tx.inputs()[0]['type']) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet2a.is_mine(wallet2a.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('0100000000010149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e01000000232200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163cfeffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a0860100000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0400483045022100c254468bbe6b8bd1c8c01b6a223e46cc5c6b56fbba87d59575385ad249133b0e02207139688f8d6ae8076c92a266d98454d25c040d04c8e513a37bf7c32dad3e48210147304402204af5edbab2d674f6a9edef8c97b2f7fdf8ababedc7b287710cc7a64d4699358b022064e2d07f4bb32373be31b2003dc56b7b831a7c01419326efb3011c64b898b3f00147522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae00000000', + str(tx_copy)) + self.assertEqual('84b0dcb43022385f7a10e2710e5625a2be3cd6e390387b6100b55500d5eea8f6', tx_copy.txid()) + self.assertEqual('7e561e25da843326e61fd20a40b72fcaeb8690176fc7c3fcbadb3a0146c8396c', tx_copy.wtxid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual(txid, tx_copy.txid()) + + wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet level checks + self.assertEqual((0, funding_output_value - 165000 - 5000 + 100000, 0), wallet1a.get_balance()) + self.assertEqual((0, 165000 - 5000 - 100000, 0), wallet2a.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_write): + wallet1a = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('phone guilt ancient scan defy gasp off rotate approve ill word exchange', '', True), + keystore.from_xpub('tpubD6NzVbkrYhZ4YPZ3ntVjqSCxiUUv2jikrUBU73Q3iJ7Y8iR41oYf991L5fanv7ciHjbjokdK2bjYqg1BzEUDxucU9qM5WRdBiY738wmgLP4') + ], + '1of2', gap_limit=2 + ) + # ^ second seed: kingdom now gift initial age right velvet exotic harbor enforce kingdom kick + wallet2 = WalletIntegrityHelper.create_standard_wallet( + # bip39: uniform tank success logic lesson awesome stove elegant regular desert drip device, der: m/49'/1'/0' + keystore.from_xprv('uprv91HGbrNZTK4x8u22nbdYGzEuWPxjaHMREUi7CNhY64KsG5ZGnVM99uCa16EMSfrnaPTFxjbRdBZ2WiBkokoM8anzAy3Vpc52o88WPkitnxi'), + gap_limit=2 + ) + + # bootstrap wallet1 + funding_tx = Transaction('010000000001027e20990282eb29588375ad04936e1e991af3bc5b9c6f1ab62eca8c25becaef6a01000000171600140e6a17fadc8bafba830f3467a889f6b211d69a00fdffffff51847fd6bcbdfd1d1ea2c2d95c2d8de1e34c5f2bd9493e88a96a4e229f564e800100000017160014ecdf9fa06856f9643b1a73144bc76c24c67774a6fdffffff021e8501000000000017a91451991bfa68fbcb1e28aa0b1e060b7d24003352e38700093d000000000017a914b0b9f31bace76cdfae2c14abc03e223403d7dc4b870247304402205e19721b92c6afd70cd932acb50815a36ee32ab46a934147d62f02c13aeacf4702207289c4a4131ef86e27058ff70b6cb6bf0e8e81c6cbab6dddd7b0a9bc732960e4012103fe504411c21f7663caa0bbf28931f03fae7e0def7bc54851e0194dfb1e2c85ef02483045022100e969b65096fba4f8b24eb5bc622d2282076241621f3efe922cc2067f7a8a6be702203ec4047dd2a71b9c83eb6a0875a6d66b4d65864637576c06ed029d3d1a8654b0012102bbc8100dca67ba0297aba51296a4184d714204a5fc2eda34708360f37019a3dccfcc1300') + funding_txid = funding_tx.txid() + funding_output_value = 4000000 + self.assertEqual('1137c12de4ce0f5b08de8846ba14c0814351a7f0f31457c8ea51a5d4b3c891a3', funding_txid) + wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # wallet1 -> wallet2 + outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 1000000)] + tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('0100000001a391c8b3d4a551eac85714f3f0a7514381c014ba4688de085b0fcee42dc13711010000009200483045022100fcf03aeb97b66791372c18aa0dd651817cf458d941dd628c966f0305a023360f022016c534530e267b6a52f90e62aa9fb50ace609ffb21e472d3ba7b29db9b30050e014751210245c90e040d4f9d1fc136b3d4d6b7535bbb5df2bd27666c21977042cc1e05b5b02103c9a6bebfce6294488315e58137a279b2efe09f1f528ecf93b40675ded3cf0e5f52aefeffffff0240420f000000000017a9149573eb50f3136dff141ac304190f41c8becc92ce8738b32d000000000017a914b815d1b430ae9b632e3834ed537f7956325ee2a98700000000', + str(tx_copy)) + self.assertEqual('1b7e94860b9681d4e371928d40fdbd4641e991aa74f1a211f239c887047e4a2a', tx_copy.txid()) + self.assertEqual('1b7e94860b9681d4e371928d40fdbd4641e991aa74f1a211f239c887047e4a2a', tx_copy.wtxid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + + wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet2 -> wallet1 + outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 300000)] + tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('010000000001012a4a7e0487c839f211a2f174aa91e94146bdfd408d9271e3d481960b86947e1b00000000171600149fad840ed174584ee054bd26f3e411817338c5edfeffffff02e09304000000000017a914b0b9f31bace76cdfae2c14abc03e223403d7dc4b87d89a0a000000000017a9148ccd0efb2be5b412c4033715f560ed8f446c8ceb87024830450221009c816c3e0c40b37085244f0976f65635b8d711952bad9843c5f51e386fd37cc402202c34a4a7227182742d9f93e9f28c4bd30ded6514550f39614cb5ad00e46690070121038362bbf0b4918b37e9d7c75930ed3a78e3d445724cb5c37ade4a59b6e411fe4e00000000', + str(tx_copy)) + self.assertEqual('f65edb0843ff44436dc5964fb6b298e157502b9b4a83dac6b82dd2d2a3247d0a', tx_copy.txid()) + self.assertEqual('63efc09db4c7445eaaca9a5e7732202f42aec81a53b05d819f1918ce0cf3b84d', tx_copy.wtxid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + + wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet level checks + self.assertEqual((0, funding_output_value - 1000000 - 5000 + 300000, 0), wallet1a.get_balance()) + self.assertEqual((0, 1000000 - 5000 - 300000, 0), wallet2.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bump_fee_p2pkh(self, mock_write): + wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') + + # bootstrap wallet + funding_tx = Transaction('010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400') + funding_txid = funding_tx.txid() + funding_output_value = 10000000 + self.assertEqual('03052739fcfa2ead5f8e57e26021b0c2c546bcd3d74c6e708d5046dc58d90762', funding_txid) + wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create tx + outputs = [(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] + coins = wallet.get_spendable_coins(domain=None, config=self.config) + tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) + tx.set_rbf(True) + tx.locktime = 1325501 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006b483045022100df74e6a88085be1ff3a3fd96cf2ef03b5e33fa06788f56aa71649f0177d1bfc402206e36a7e6124863ac746d5288d6d47c1d1eac5d4ac3818e561a7a0f2c0a269429012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d7200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400', + str(tx_copy)) + self.assertEqual('44e6dd9529a253181112fc40cadd8ebb4c4359aacb91aa24c45556a1d00839b0', tx_copy.txid()) + self.assertEqual('44e6dd9529a253181112fc40cadd8ebb4c4359aacb91aa24c45556a1d00839b0', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) + + # bump tx + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) + tx.locktime = 1325501 + self.assertFalse(tx.is_complete()) + + wallet.sign_transaction(tx, password=None) + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + tx_copy = Transaction(tx.serialize()) + self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006a473044022055b7e6b7e89a55740f7aa2ad1ffcd4b5c913f0de63cf512438921534bc9c3a8d022043b3b27bdc2da4cc6265e4cc9673a3780ccd5cd6f0ee2eaedb51720c15b7a00a012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987d0497200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400', + str(tx_copy)) + self.assertEqual('f26edcf20991dccedf16058adbee923db7057c9b102db660156b8142b6a59bc7', tx_copy.txid()) + self.assertEqual('f26edcf20991dccedf16058adbee923db7057c9b102db660156b8142b6a59bc7', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, funding_output_value - 2500000 - 10000, 0), wallet.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_cpfp_p2pkh(self, mock_write): + wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') + + # bootstrap wallet + funding_tx = Transaction('010000000001010f40064d66d766144e17bb3276d96042fd5aee2196bcce7e415f839e55a83de800000000171600147b6d7c7763b9185b95f367cf28e4dc6d09441e73fdffffff02404b4c00000000001976a9141df43441a3a3ee563e560d3ddc7e07cc9f9c3cdb88ac009871000000000017a9143873281796131b1996d2f94ab265327ee5e9d6e28702473044022029c124e5a1e2c6fa12e45ccdbdddb45fec53f33b982389455b110fdb3fe4173102203b3b7656bca07e4eae3554900aa66200f46fec0af10e83daaa51d9e4e62a26f4012103c8f0460c245c954ef563df3b1743ea23b965f98b120497ac53bd6b8e8e9e0f9bbe391400') + funding_txid = funding_tx.txid() + funding_output_value = 5000000 + self.assertEqual('9973bf8918afa349b63934432386f585613b51034db6c8628b61ba2feb8a3668', funding_txid) + wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # cpfp tx + tx = wallet.cpfp(funding_tx, fee=50000) + tx.set_rbf(True) + tx.locktime = 1325502 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('010000000168368aeb2fba618b62c8b64d03513b6185f58623433439b649a3af1889bf7399000000006a47304402203a0b369e46c5fbacb83044b7ab9d69ff7998774041d6870993504915bc495d210220272833b870d8abca516adb7dc4cb27892b1b6e4b52fbfeb592a72c3e795eb213012102a7536f0bfbc60c5a8e86e2b9df26431fc062f9f454016dbc26f2467e0bc98b3ffdffffff01f0874b00000000001976a9141df43441a3a3ee563e560d3ddc7e07cc9f9c3cdb88acbe391400', + str(tx_copy)) + self.assertEqual('47500a425518b5542d94db1157f473b8cf322d31ea97a1a642fec19386cdb761', tx_copy.txid()) + self.assertEqual('47500a425518b5542d94db1157f473b8cf322d31ea97a1a642fec19386cdb761', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bump_fee_p2wpkh(self, mock_write): + wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') + + # bootstrap wallet + funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') + funding_txid = funding_tx.txid() + funding_output_value = 10000000 + self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid) + wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create tx + outputs = [(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] + coins = wallet.get_spendable_coins(domain=None, config=self.config) + tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) + tx.set_rbf(True) + tx.locktime = 1325499 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402205442705e988abe74bf391b293bb1b886674284a92ed0788c33024f9336d60aef022013a93049d3bed693254cd31a704d70bb988a36750f0b74d0a5b4d9e29c54ca9d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400', + str(tx_copy)) + self.assertEqual('b019bbad45a46ed25365e46e4cae6428fb12ae425977eb93011ffb294cb4977e', tx_copy.txid()) + self.assertEqual('ba87313e2b3b42f1cc478843d4d53c72d6e06f6c66ac8cfbe2a59cdac2fd532d', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) + + # bump tx + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) + tx.locktime = 1325500 + self.assertFalse(tx.is_complete()) + + wallet.sign_transaction(tx, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + tx_copy = Transaction(tx.serialize()) + self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987d049720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5024730440220517fed3a902b5b41fa718ffd5f229b835b8ed26f23433c4ea437d24eff66d15b0220526854a6ebcd351ab2373d0e7c4e20f17c420520b5d570c2df7ca1d773d6a55d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400', + str(tx_copy)) + self.assertEqual('9a1c0ef7e871798b86074c7f8dd1e81b6d9a758ff07e0059eee31dc6fbf4f438', tx_copy.txid()) + self.assertEqual('59144d30c911ac33359b0a32d5a3fdd2ca806982c85838e193eb95f5d315e813', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, funding_output_value - 2500000 - 10000, 0), wallet.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_cpfp_p2wpkh(self, mock_write): + wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') + + # bootstrap wallet + funding_tx = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400') + funding_txid = funding_tx.txid() + funding_output_value = 5000000 + self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid) + wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # cpfp tx + tx = wallet.cpfp(funding_tx, fee=50000) + tx.set_rbf(True) + tx.locktime = 1325501 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('010000000001014a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff01f0874b000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900248304502210098fbe458a9f1c595d6bf63962fad00300a7b60c6dd8b2e7625f3804a3bf1086602204bc8a46fb162be8f85a23644eccf9f4223fa092f5c861144676a34dc83a7c39d012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbd391400', + str(tx_copy)) + self.assertEqual('38a21c67336232c88ae15311f329197c69ee70e872f8acb5bc9c2b6417c35ad8', tx_copy.txid()) + self.assertEqual('b5b8264ed5f3e03d48ef82fa2a25278cd9c0563fa78e557f370b7e0558293172', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) + + @needs_test_with_all_ecc_implementations + def test_sweep_p2pk(self): + + class NetworkMock: + relay_fee = 1000 + def get_local_height(self): return 1325785 + def listunspent_for_scripthash(self, scripthash): + if scripthash == '460e4fb540b657d775d84ff4955c9b13bd954c2adc26a6b998331343f85b6a45': + return [{'tx_hash': 'ac24de8b58e826f60bd7b9ba31670bdfc3e8aedb2f28d0e91599d741569e3429', 'tx_pos': 1, 'height': 1325785, 'value': 1000000}] + else: + return [] + + privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu', ] + network = NetworkMock() + dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2' + tx = sweep(privkeys, network, config=None, recipient=dest_addr, fee=5000) + + tx_copy = Transaction(tx.serialize()) + self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400', + str(tx_copy)) + self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.txid()) + self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid()) + + +class TestWalletOfflineSigning(TestCaseForTestnet): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.electrum_path = tempfile.mkdtemp() + cls.config = SimpleConfig({'electrum_path': cls.electrum_path}) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + shutil.rmtree(cls.electrum_path) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + # bip39: "qwe", der: m/44'/1'/0' + keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), + gap_limit=4 + ) + wallet_online = WalletIntegrityHelper.create_standard_wallet( + keystore.from_xpub('tpubDDMN69wQjDZxaJz9afZQGa48hZS7X5oSegF2hg67yddNvqfpuTN9DqvDEp7YyVf7AzXnqBqHdLhzTAStHvsoMDDb8WoJQzNrcHgDJHVYgQF'), + gap_limit=4 + ) + + # bootstrap wallet_online + funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400') + funding_txid = funding_tx.txid() + self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325340 + + self.assertFalse(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.txid()) + self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + # bip39: "qwe", der: m/49'/1'/0' + keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), + gap_limit=4 + ) + wallet_online = WalletIntegrityHelper.create_standard_wallet( + keystore.from_xpub('upub5DGeFrwFEPfD711qQ6tKPaUYjBY6BRqfxcWPT77hiHz7VMo7oNGeom5EdXoKXEazePyoN3ueJMqHBfp3MwmsaD8k9dFHoa8KGeVXev7Pbg2'), + gap_limit=4 + ) + + # bootstrap wallet_online + funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400') + funding_txid = funding_tx.txid() + self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325341 + + self.assertFalse(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('3f0d188519237478258ad2bf881643618635d11c2bb95512e830fcf2eda3c522', tx_copy.txid()) + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual('3f0d188519237478258ad2bf881643618635d11c2bb95512e830fcf2eda3c522', tx.txid()) + self.assertEqual('27b78ec072a403b0545258e7a1a8d494e4b6fd48bf77f4251a12160c92207cbc', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + # bip39: "qwe", der: m/84'/1'/0' + keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), + gap_limit=4 + ) + wallet_online = WalletIntegrityHelper.create_standard_wallet( + keystore.from_xpub('vpub5Y941QgusZGvuD5nXTpUvVWohm8q41uftcRNronjRWs9jB2iVr4BbxqbRfAoQjWHgJtDCQEXChgfsPbEuBnidtkFztZSD3zDKTrtwXa2LCa'), + gap_limit=4 + ) + + # bootstrap wallet_online + funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400') + funding_txid = funding_tx.txid() + self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325341 + + self.assertFalse(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx_copy.txid()) + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid()) + self.assertEqual('729c2e40a2fccd6b731407c01ed304119c1ac329bdf9baae5b642d916c5f3272', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_wif_online_addr_p2pkh(self, mock_write): # compressed pubkey + wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) + wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', pw=None) + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG') + + # bootstrap wallet_online + funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') + funding_txid = funding_tx.txid() + self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325340 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid()) + self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) + wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', pw=None) + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8') + + # bootstrap wallet_online + funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') + funding_txid = funding_tx.txid() + self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325340 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid()) + self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) + wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', pw=None) + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529') + + # bootstrap wallet_online + funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') + funding_txid = funding_tx.txid() + self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325340 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx.txid()) + self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_xprv_online_addr_p2pkh(self, mock_write): # compressed pubkey + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + # bip39: "qwe", der: m/44'/1'/0' + keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), + gap_limit=4 + ) + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG') + + # bootstrap wallet_online + funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') + funding_txid = funding_tx.txid() + self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325340 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid()) + self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + # bip39: "qwe", der: m/49'/1'/0' + keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), + gap_limit=4 + ) + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8') + + # bootstrap wallet_online + funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') + funding_txid = funding_tx.txid() + self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325340 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid()) + self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + # bip39: "qwe", der: m/84'/1'/0' + keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), + gap_limit=4 + ) + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529') + + # bootstrap wallet_online + funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') + funding_txid = funding_tx.txid() + self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325340 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx.txid()) + self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_write): + # 2-of-3 legacy p2sh multisig + wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', '', True), + keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'), + keystore.from_xpub('tpubD6NzVbkrYhZ4XJzYkhsCbDCcZRmDAKSD7bXi9mdCni7acVt45fxbTVZyU6jRGh29ULKTjoapkfFsSJvQHitcVKbQgzgkkYsAmaovcro7Mhf') + ], + '2of3', gap_limit=2 + ) + wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song', '', True), + keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'), + keystore.from_xpub('tpubD6NzVbkrYhZ4YARFMEZPckrqJkw59GZD1PXtQnw14ukvWDofR7Z1HMeSCxfYEZVvg4VdZ8zGok5VxHwdrLqew5cMdQntWc5mT7mh1CSgrnX') + ], + '2of3', gap_limit=2 + ) + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('2N4z38eTKcWTZnfugCCfRyXtXWMLnn8HDfw') + + # bootstrap wallet_online + funding_tx = Transaction('010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400') + funding_txid = funding_tx.txid() + self.assertEqual('c59913a1fa9b1ef1f6928f0db490be67eeb9d7cb05aa565ee647e859642f3532', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, '2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325503 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx - first + tx = wallet_offline1.sign_transaction(tx_copy, password=None) + self.assertFalse(tx.is_complete()) + tx = Transaction(tx.serialize()) + + # sign tx - second + tx = wallet_offline2.sign_transaction(tx, password=None) + self.assertTrue(tx.is_complete()) + tx = Transaction(tx.serialize()) + + self.assertEqual('010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c500000000fdfe0000483045022100cfe41e783629a2ad0b1f17cd2dbd69db05763fa7a22691131fa321ba3140d7cb02203fbda2ccc6212315464cd814d4e909b4f80a2361e3af0f9deda06478f91a0f3901483045022100b84fd63e957f2409558f63962fc91ba58334efde8b88ff53ca71da3d0fe7219702206001c6caeb30e18a7525fc72de0003e12646bf815b12fb132c1aadd6ffa1989c014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400', + str(tx)) + self.assertEqual('bb4c28af28b970522c56ff0482cd98c2b78a90bec578bcede8a9e5cbec6ef5e7', tx.txid()) + self.assertEqual('bb4c28af28b970522c56ff0482cd98c2b78a90bec578bcede8a9e5cbec6ef5e7', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_write): + # 2-of-2 p2sh-embedded segwit multisig + wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( + [ + # bip39: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose, der: m/1234'/1'/0', p2wsh-p2sh multisig + keystore.from_xprv('Uprv9CvELvByqm8k2dpecJVjgLMX1z5DufEjY4fBC5YvdGF5WjGCa7GVJJ2fYni1tyuF7Hw83E6W2ZBjAhaFLZv2ri3rEsubkCd5avg4EHKoDBN'), + keystore.from_xpub('Upub5Qb8ik4Cnu8g97KLXKgVXHqY6tH8emQvqtBncjSKsyfTZuorPtTZgX7ovKKZHuuVGBVd1MTTBkWez1XXt2weN1sWBz6SfgRPQYEkNgz81QF') + ], + '2of2', gap_limit=2 + ) + wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet( + [ + # bip39: square page wood spy oil story rebel give milk screen slide shuffle, der: m/1234'/1'/0', p2wsh-p2sh multisig + keystore.from_xprv('Uprv9BbnKEXJxXaNvdEsRJ9VA9toYrSeFJh5UfGBpM2iKe8Uh7UhrM9K8ioL53s8gvCoGfirHHaqpABDAE7VUNw8LNU1DMJKVoWyeNKu9XcDC19'), + keystore.from_xpub('Upub5RuakRisg8h3F7u7iL2k3UJFa1uiK7xauHamzTxYBbn4PXbM7eajr6M9Q2VCr6cVGhfhqWQqxnABvtSATuVM1xzxk4nA189jJwzaMn1QX7V') + ], + '2of2', gap_limit=2 + ) + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('2MsHQRm1pNi6VsmXYRxYMcCTdPu7Xa1RyFe') + + # bootstrap wallet_online + funding_tx = Transaction('0100000000010118d494d28e5c3bf61566ca0313e22c3b561b888a317d689cc8b47b947adebd440000000017160014aec84704ea8508ddb94a3c6e53f0992d33a2a529fdffffff020f0925000000000017a91409f7aae0265787a02de22839d41e9c927768230287809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf989870247304402206b906369f4075ebcfc149f7429dcfc34e11e1b7bbfc85d1185d5e9c324be0d3702203ce7fc12fd3131920fbcbb733250f05dbf7d03e18a4656232ee69d5c54dd46bd0121028a4b697a37f3f57f6e53f90db077fa9696095b277454fda839c211d640d48649c0391400') + funding_txid = funding_tx.txid() + self.assertEqual('54356de9e156b85c8516fd4d51bdb68b5513f58b4a6147483978ae254627ee3e', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, '2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325504 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx - first + tx = wallet_offline1.sign_transaction(tx_copy, password=None) + self.assertFalse(tx.is_complete()) + self.assertEqual('6a58a51591142429203b62b6ddf6b799a6926882efac229998c51bee6c3573eb', tx.txid()) + tx = Transaction(tx.serialize()) + + # sign tx - second + tx = wallet_offline2.sign_transaction(tx, password=None) + self.assertTrue(tx.is_complete()) + tx = Transaction(tx.serialize()) + + self.assertEqual('010000000001013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d355401000000232200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987040047304402205a9dd9eb5676196893fb08f60079a2e9f567ee39614075d8c5d9fab0f11cbbc7022039640855188ebb7bccd9e3f00b397a888766d42d00d006f1ca7457c15449285f014730440220234f6648c5741eb195f0f4cd645298a10ce02f6ef557d05df93331e21c4f58cb022058ce2af0de1c238c4a8dd3b3c7a9a0da6e381ddad7593cddfc0480f9fe5baadf0147522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52aec0391400', + str(tx)) + self.assertEqual('6a58a51591142429203b62b6ddf6b799a6926882efac229998c51bee6c3573eb', tx.txid()) + self.assertEqual('96d0bca1001778c54e4c3a07929fab5562c5b5a23fd1ca3aa3870cc5df2bf97d', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_write): + # 2-of-3 p2wsh multisig + wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', '', True), + keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'), + keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra') + ], + '2of3', gap_limit=2 + ) + wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_seed('snow nest raise royal more walk demise rotate smooth spirit canyon gun', '', True), + keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra'), + keystore.from_xpub('Vpub5gSKXzxK7FeKQedu2q1z9oJWxqvX72AArW3HSWpEhc8othDH8xMDu28gr7gf17sp492BuJod8Tn7anjvJrKpETwqnQqX7CS8fcYyUtedEMk') + ], + '2of3', gap_limit=2 + ) + # ^ third seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool + wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) + wallet_online.import_address('tb1q83p6eqxkuvq4eumcha46crpzg4nj84s9p0hnynkxg8nhvfzqcc7q4erju6') + + # bootstrap wallet_online + funding_tx = Transaction('0100000000010132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c501000000171600142e5d579693b2a7679622935df94d9f3c84909b24fdffffff0280969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c83717d010000000017a91441b772909ad301b41b76f4a3c5058888a7fe6f9a8702483045022100de54689f74b8efcce7fdc91e40761084686003bcd56c886ee97e75a7e803526102204dea51ae5e7d01bd56a8c336c64841f7fe02a8b101fa892e13f2d079bb14e6bf012102024e2f73d632c49f4b821ccd3b6da66b155427b1e5b1c4688cefd5a4b4bfa404c1391400') + funding_txid = funding_tx.txid() + self.assertEqual('643a7ab9083d0227dd9df314ce56b18d279e6018ff975079dfaab82cd7a66fa3', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [(bitcoin.TYPE_ADDRESS, '2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325505 + + self.assertFalse(tx.is_complete()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx - first + tx = wallet_offline1.sign_transaction(tx_copy, password=None) + self.assertFalse(tx.is_complete()) + self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx.txid()) + tx = Transaction(tx.serialize()) + + # sign tx - second + tx = wallet_offline2.sign_transaction(tx, password=None) + self.assertTrue(tx.is_complete()) + tx = Transaction(tx.serialize()) + + self.assertEqual('01000000000101a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c04004730440220629d89626585f563202e6b38ceddc26ccd00737e0b7ee4239b9266ef9174ea2f02200b74828399a2e35ed46c9b484af4817438d5fea890606ebb201b821944db1fdc0147304402205d1a59c84c419992069e9764a7992abca6a812cc5dfd4f0d6515d4283e660ce802202597a38899f31545aaf305629bd488f36bf54e4a05fe983932cafbb3906efb8f016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153aec1391400', + str(tx)) + self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx.txid()) + self.assertEqual('4376fa5f1f6cb37b1f3956175d3bd4ef6882169294802b250a3c672f3ff431c1', tx.wtxid()) + + +class TestWalletHistory_SimpleRandomOrder(TestCaseForTestnet): + transactions = { + "0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67": "01000000029d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2000000008b483045022100a146a2078a318c1266e42265a369a8eef8993750cb3faa8dd80754d8d541d5d202207a6ab8864986919fd1a7fd5854f1e18a8a0431df924d7a878ec3dc283e3d75340141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff9d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2010000008a47304402201c7fa37b74a915668b0244c01f14a9756bbbec1031fb69390bcba236148ab37e02206151581f9aa0e6758b503064c1e661a726d75c6be3364a5a121a8c12cf618f64014104dc28da82e141416aaf771eb78128d00a55fdcbd13622afcbb7a3b911e58baa6a99841bfb7b99bcb7e1d47904fda5d13fdf9675cdbbe73e44efcc08165f49bac6feffffff02b0183101000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac005a6202000000001976a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac54c51200", + "2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d": "010000000132201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a050000006a47304402201d20bb5629a35b84ff9dd54788b98e265623022894f12152ac0e6158042550fe02204e98969e1f7043261912dd0660d3da64e15acf5435577fc02a00eccfe76b323f012103a336ad86546ab66b6184238fe63bb2955314be118b32fa45dd6bd9c4c5875167fdffffff0254959800000000001976a9148d2db0eb25b691829a47503006370070bc67400588ac80969800000000001976a914f96669095e6df76cfdf5c7e49a1909f002e123d088ace8ca1200", + "2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd": "010000000001036cdf8d2226c57d7cc8485636d8e823c14790d5f24e6cf38ba9323babc7f6db2901000000171600143fc0dbdc2f939c322aed5a9c3544468ec17f5c3efdffffff507dce91b2a8731636e058ccf252f02b5599489b624e003435a29b9862ccc38c0200000017160014c50ff91aa2a790b99aa98af039ae1b156e053375fdffffff6254162cf8ace3ddfb3ec242b8eade155fa91412c5bde7f55decfac5793743c1010000008b483045022100de9599dcd7764ca8d4fcbe39230602e130db296c310d4abb7f7ae4d139c4d46402200fbfd8e6dc94d90afa05b0c0eab3b84feb465754db3f984fbf059447282771c30141045eecefd39fabba7b0098c3d9e85794e652bdbf094f3f85a3de97a249b98b9948857ea1e8209ee4f196a6bbcfbad103a38698ee58766321ba1cdee0cbfb60e7b2fdffffff01e85af70100000000160014e8d29f07cd5f813317bec4defbef337942d85d74024730440220218049aee7bbd34a7fa17f972a8d24a0469b0131d943ef3e30860401eaa2247402203495973f006e6ee6ae74a83228623029f238f37390ee4b587d95cdb1d1aaee9901210392ba263f3a2b260826943ff0df25e9ca4ef603b98b0a916242c947ae0626575f02473044022002603e5ceabb4406d11aedc0cccbf654dd391ce68b6b2228a40e51cf8129310d0220533743120d93be8b6c1453973935b911b0a2322e74708d23e8b5f90e74b0f192012103221b4ee0f508ba595fc1b9c2252ed9d03e99c73b97344dae93263c68834f034800ed161300", + "31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f": "0100000000010454022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a000000008b483045022100ea8fe74db2aba23ad36ac66aaa481bad2b4d1b3c331869c1d60a28ce8cfad43c02206fa817281b33fbf74a6dd7352bdc5aa1d6d7966118a4ad5b7e153f37205f1ae80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a01000000171600146dfe07e12af3db7c715bf1c455f8517e19c361e7fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a020000006a47304402200b1fb89e9a772a8519294acd61a53a29473ce76077165447f49a686f1718db5902207466e2e8290f84114dc9d6c56419cb79a138f03d7af8756de02c810f19e4e03301210222bfebe09c2638cfa5aa8223fb422fe636ba9675c5e2f53c27a5d10514f49051fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a0300000000fdffffff018793140d000000001600144b3e27ddf4fc5f367421ee193da5332ef351b700000247304402207ba52959938a3853bcfd942d8a7e6a181349069cde3ea73dbde43fa9669b8d5302207a686b92073863203305cb5d5550d88bdab0d21b9e9761ba4a106ea3970e08d901210265c1e014112ed19c9f754143fb6a2ff89f8630d62b33eb5ae708c9ea576e61b50002473044022029e868a905aa3ecae6eafcbd5959aefff0e5f39c1fc7a131a174828806e74e5202202f0aaa7c3cb3d9a9d526e5428ce37c0f0af0d774aa30b09ded8bc2230e7ffaf2012102fe0104455dc52b1689bba130664e452642180eb865217acfc6997260b7d946ae22c71200", + "336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa": "0100000000010232201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a020000006a4730440220198c0ba2b2aefa78d8cca01401d408ecdebea5ac05affce36f079f6e5c8405ca02200eabb1b9a01ff62180cf061dfacedba6b2e07355841b9308de2d37d83489c7b80121031c663e5534fe2a6de816aded6bb9afca09b9e540695c23301f772acb29c64a05fdfffffffb28ff16811d3027a2405be68154be8fdaff77284dbce7a2314c4107c2c941600000000000fdffffff015e104f01000000001976a9146dfd56a0b5d0c9450d590ad21598ecfeaa438bd788ac000247304402207d6dc521e3a4577685535f098e5bac4601aa03658b924f30bf7afef1850e437e022045b76771d8b6ca1939352d6b759fca31029e5b2edffa44dc747fe49770e746cd012102c7f36d4ceed353b90594ebaf3907972b6d73289bdf4707e120de31ec4e1eb11679f31200", + "3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308": "010000000168091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0000000008a47304402202f683a63af571f405825066bd971945a35e7142a75c9a5255d364b25b7115d5602206c59a7214ae729a519757e45fdc87061d357813217848cf94df74125221267ac014104aecb9d427e10f0c370c32210fe75b6e72ccc4f415076cf1a6318fbed5537388862c914b29269751ab3a04962df06d96f5f4f54e393a0afcbfa44b590385ae61afdffffff0240420f00000000001976a9145f917fd451ca6448978ebb2734d2798274daf00b88aca8063d00000000001976a914e1232622a96a04f5e5a24ca0792bb9c28b089d6e88ace9ca1200", + "475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d": "01000000013a7e6f19a963adc7437d2f3eb0936f1fc9ef4ba7e083e19802eb1111525a59c2000000008b483045022100958d3931051306489d48fe69b32561e0a16e82a2447c07be9d1069317084b5e502202f70c2d9be8248276d334d07f08f934ffeea83977ad241f9c2de954a2d577f94014104d950039cec15ad10ad4fb658873bc746148bc861323959e0c84bf10f8633104aa90b64ce9f80916ab0a4238e025dcddf885b9a2dd6e901fe043a433731db8ab4fdffffff02a086010000000000160014bbfab2cc3267cea2df1b68c392cb3f0294978ca922940d00000000001976a914760f657c67273a06cad5b1d757a95f4ed79f5a4b88ac4c8d1300", + "56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216": "01000000000101614b142aeeb827d35d2b77a5b11f16655b6776110ddd9f34424ff49d85706cf90200000000fdffffff02784a4c00000000001600148464f47f35cbcda2e4e5968c5a3a862c43df65a1404b4c00000000001976a914c9efecf0ecba8b42dce0ae2b28e3ea0573d351c988ac0247304402207d8e559ed1f56cb2d02c4cb6c95b95c470f4b3cb3ce97696c3a58e39e55cd9b2022005c9c6f66a7154032a0bb2edc1af1f6c8f488bec52b6581a3a780312fb55681b0121024f83b87ac3440e9b30cec707b7e1461ecc411c2f45520b45a644655528b0a68ae9ca1200", + "6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254": "0100000000010496941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e000000001716001441aec99157d762708339d7faf7a63a8c479ed84cfdffffff96941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e0100000000fdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f000000006a4730440220652145460092ef42452437b942cb3f563bf15ad90d572d0b31d9f28449b7a8dd022052aae24f58b8f76bd2c9cf165cc98623f22870ccdbef1661b6dbe01c0ef9010f01210375b63dd8e93634bbf162d88b25d6110b5f5a9638f6fe080c85f8b21c2199a1fdfdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f010000008a47304402207517c52b241e6638a84b05385e0b3df806478c2e444f671ca34921f6232ee2e70220624af63d357b83e3abe7cdf03d680705df0049ec02f02918ee371170e3b4a73d014104de408e142c00615294813233cdfe9e7774615ae25d18ba4a1e3b70420bb6666d711464518457f8b947034076038c6f0cfc8940d85d3de0386e0ad88614885c7cfdffffff0480969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac809698000000000017a914f2a76207d7b54bd34282281205923841341d9e1f87002d3101000000001976a914b8d4651937cd7db5bcf5fc98e6d2d8cfa131e85088ac743db20a00000000160014c7d0df09e03173170aed0247243874c6872748ed02483045022100b932cda0aeb029922e126568a48c05d79317747dcd77e61dce44e190e140822002202d13f84338bb272c531c4086277ac11e166c59612f4aefa6e20f78455bdc09970121028e6808a8ac1e9ede621aaabfcad6f86662dbe0ace0236f078eb23c24bc88bd5e02483045022100d74a253262e3898626c12361ba9bb5866f9303b42eec0a55ced0578829e2e61e022059c08e61d90cd63c84de61c796c9d1bc1e2f8217892a7c07b383af357ddd7a730121028641e89822127336fc12ff99b1089eb1a124847639a0e98d17ff03a135ad578b000020c71200", + "72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112": "0100000002677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f000000008b4830450221009c50c0f56f34781dfa7b3d540ac724436c67ffdc2e5b2d5a395c9ebf72116ef802205a94a490ea14e4824f36f1658a384aeaecadd54839600141eb20375a49d476d1014104c291245c2ee3babb2a35c39389df56540867f93794215f743b9aa97f5ba114c4cdee8d49d877966728b76bc649bb349efd73adef1d77452a9aac26f8c51ae1ddfdffffff677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f010000008b483045022100ae0b286493491732e7d3f91ab4ac4cebf8fe8a3397e979cb689e62d350fdcf2802206cf7adf8b29159dd797905351da23a5f6dab9b9dbf5028611e86ccef9ff9012e014104c62c4c4201d5c6597e5999f297427139003fdb82e97c2112e84452d1cfdef31f92dd95e00e4d31a6f5f9af0dadede7f6f4284b84144e912ff15531f36358bda7fdffffff019f7093030000000022002027ce908c4ee5f5b76b4722775f23e20c5474f459619b94040258290395b88afb6ec51200", + "76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4": "0100000001f4ba9948cdc4face8315c7f0819c76643e813093ffe9fbcf83d798523c7965db000000006a473044022061df431a168483d144d4cffe1c5e860c0a431c19fc56f313a899feb5296a677c02200208474cc1d11ad89b9bebec5ec00b1e0af0adaba0e8b7f28eed4aaf8d409afb0121039742bf6ab70f12f6353e9455da6ed88f028257950450139209b6030e89927997fdffffff01d4f84b00000000001976a9140b93db89b6bf67b5c2db3370b73d806f458b3d0488ac0a171300", + "7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a": "01000000000102681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a00000000232200209adfa712053a06cc944237148bcefbc48b16eb1dbdc43d1377809bcef1bea9affdffffff681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a0100000023220020f40ed2e3fbffd150e5b74f162c3ce5dae0dfeba008a7f0f8271cf1cf58bfb442fdffffff02801d2c04000000001976a9140cc01e19090785d629cdcc98316f328df554de4f88ac6d455d05000000001976a914b9e828990a8731af4527bcb6d0cddf8d5ffe90ce88ac040047304402206eb65bd302eefae24eea05781e8317503e68584067d35af028a377f0751bb55b0220226453d00db341a4373f1bcac2391f886d3a6e4c30dd15133d1438018d2aad24014730440220343e578591fab0236d28fb361582002180d82cb1ba79eec9139a7a9519fca4260220723784bd708b4a8ed17bb4b83a5fd2e667895078e80eec55119015beb3592fd2016952210222eca5665ed166d090a5241d9a1eb27a92f85f125aaf8df510b2b5f701f3f534210227bca514c22353a7ae15c61506522872afecf10df75e599aabe4d562d0834fce2103601d7d49bada5a57a4832eafe4d1f1096d7b0b051de4a29cd5fc8ad62865e0a553ae0400483045022100b15ea9daacd809eb4d783a1449b7eb33e2965d4229e1a698db10869299dddc670220128871ffd27037a3e9dac6748ce30c14b145dd7f9d56cc9dcde482461fb6882601483045022100cb659e1de65f8b87f64d1b9e62929a5d565bbd13f73a1e6e9dd5f4efa024b6560220667b13ce2e1a3af2afdcedbe83e2120a6e8341198a79efb855b8bc5f93b4729f0169522102d038600af253cf5019f9d5637ca86763eca6827ed7b2b7f8cc6326dffab5eb68210315cdb32b7267e9b366fb93efe29d29705da3db966e8c8feae0c8eb51a7cf48e82103f0335f730b9414acddad5b3ee405da53961796efd8c003e76e5cd306fcc8600c53ae1fc71200", + "9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb": "010000000001013409c10fd732d9e4b3a9a1c4beb511fa5eb32bc51fd169102a21aa8519618f800000000000fdffffff0640420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac80841e00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac64064a000000000016001469825d422ca80f2a5438add92d741c7df45211f280969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac02483045022100b4369b18bccb74d72b6a38bd6db59122a9e8af3356890a5ecd84bdb8c7ffe317022076a5aa2b817be7b3637d179106fccebb91acbc34011343c8e8177acc2da4882e0121033c8112bbf60855f4c3ae489954500c4b8f3408665d8e1f63cf3216a76125c69865281300", + "a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d": "010000000400899af3606e93106a5d0f470e4e2e480dfc2fd56a7257a1f0f4d16fd5961a0f000000006a47304402205b32a834956da303f6d124e1626c7c48a30b8624e33f87a2ae04503c87946691022068aa7f936591fb4b3272046634cf526e4f8a018771c38aff2432a021eea243b70121034bb61618c932b948b9593d1b506092286d9eb70ea7814becef06c3dfcc277d67fdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753000000006b483045022100de775a580c6cb47061d5a00c6739033f468420c5719f9851f32c6992610abd3902204e6b296e812bb84a60c18c966f6166718922780e6344f243917d7840398eb3db0121025d7317c6910ad2ad3d29a748c7796ddf01e4a8bc5e3bf2a98032f0a20223e4aafdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753010000006a4730440220615a26f38bf6eb7043794c08fb81f273896b25783346332bec4de8dfaf7ed4d202201c2bc4515fc9b07ded5479d5be452c61ce785099f5e33715e9abd4dbec410e11012103caa46fcb1a6f2505bf66c17901320cc2378057c99e35f0630c41693e97ebb7cffdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753030000006b483045022100c8fba762dc50041ee3d5c7259c01763ed913063019eefec66678fb8603624faa02200727783ccbdbda8537a6201c63e30c0b2eb9afd0e26cb568d885e6151ef2a8540121027254a862a288cfd98853161f575c49ec0b38f79c3ef0bf1fb89986a3c36a8906fdffffff0240787d01000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac3bfc1502000000001976a914c30f2af6a79296b6531bf34dba14c8419be8fb7d88ac52c51200", + "c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462": "0100000003aabec9cb99096073ae47cfb84bfd5b0063ae7f157956fd37c5d1a79d74ee6e33000000008b4830450221008136fc880d5e24fdd9d2a43f5085f374fef013b814f625d44a8075104981d92a0220744526ec8fc7887c586968f22403f0180d54c9b7ff8db9b553a3c4497982e8250141047b8b4c91c5a93a1f2f171c619ca41770427aa07d6de5130c3ba23204b05510b3bd58b7a1b35b9c4409104cfe05e1677fc8b51c03eac98b206e5d6851b31d2368fdffffff16d23bdc750c7023c085a6fc76e3e468944919783535ea2c13826f181058a656010000008a47304402204148410f2d796b1bb976b83904167d28b65dcd7c21b3876022b4fa70abc86280022039ea474245c3dc8cd7e5a572a155df7a6a54496e50c73d9fed28e76a1cf998c00141044702781daed201e35aa07e74d7bda7069e487757a71e3334dc238144ad78819de4120d262e8488068e16c13eea6092e3ab2f729c13ef9a8c42136d6365820f7dfdffffff68091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0010000008b4830450221008228af51b61a4ee09f58b4a97f204a639c9c9d9787f79b2fc64ea54402c8547902201ed81fca828391d83df5fbd01a3fa5dd87168c455ed7451ba8ccb5bf06942c3b0141046fcdfab26ac08c827e68328dbbf417bbe7577a2baaa5acc29d3e33b3cc0c6366df34455a9f1754cb0952c48461f71ca296b379a574e33bcdbb5ed26bad31220bfdffffff0210791c00000000001976a914a4b991e7c72996c424fe0215f70be6aa7fcae22c88ac80c3c901000000001976a914b0f6e64ea993466f84050becc101062bb502b4e488ac7af31200", + "c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a": "01000000018557003cb450f53922f63740f0f77db892ef27e15b2614b56309bfcee96a0ad3010000006a473044022041923c905ae4b5ed9a21aa94c60b7dbcb8176d58d1eb1506d9fb1e293b65ce01022015d6e9d2e696925c6ad46ce97cc23dec455defa6309b839abf979effc83b8b160121029332bf6bed07dcca4be8a5a9d60648526e205d60c75a21291bffcdefccafdac3fdffffff01c01c0f00000000001976a914a2185918aa1006f96ed47897b8fb620f28a1b09988ac01171300", + "e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968": "01000000016d445091b7b4fa19cbbee30141071b2202d0c27d195b9d6d2bcc7085c9cd9127010000008b483045022100daf671b52393af79487667eddc92ebcc657e8ae743c387b25d1c1a2e19c7a4e7022015ef2a52ea7e94695de8898821f9da539815775516f18329896e5fc52a3563b30141041704a3daafaace77c8e6e54cf35ed27d0bf9bb8bcd54d1b955735ff63ec54fe82a80862d455c12e739108b345d585014bf6aa0cbd403817c89efa18b3c06d6b5fdffffff02144a4c00000000001976a9148942ac692ace81019176c4fb0ac408b18b49237f88ac404b4c00000000001976a914dd36d773acb68ac1041bc31b8a40ee504b164b2e88ace9ca1200", + "e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306": "010000000125af87b0c2ebb9539d644e97e6159ccb8e1aa80fe986d01f60d2f3f37f207ae8010000008b483045022100baed0747099f7b28a5624005d50adf1069120356ac68c471a56c511a5bf6972b022046fbf8ec6950a307c3c18ca32ad2955c559b0d9bbd9ec25b64f4806f78cadf770141041ea9afa5231dc4d65a2667789ebf6806829b6cf88bfe443228f95263730b7b70fb8b00b2b33777e168bcc7ad8e0afa5c7828842794ce3814c901e24193700f6cfdffffff02a0860100000000001976a914ade907333744c953140355ff60d341cedf7609fd88ac68830a00000000001976a9145d48feae4c97677e4ca7dcd73b0d9fd1399c962b88acc9cc1300", + "e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25": "01000000010db780fff7dfcef6dba9268ecf4f6df45a1a86b86cad6f59738a0ce29b145c47010000008a47304402202887ec6ec200e4e2b4178112633011cbdbc999e66d398b1ff3998e23f7c5541802204964bd07c0f18c48b7b9c00fbe34c7bc035efc479e21a4fa196027743f06095f0141044f1714ed25332bb2f74be169784577d0838aa66f2374f5d8cbbf216063626822d536411d13cbfcef1ff3cc1d58499578bc4a3c4a0be2e5184b2dd7963ef67713fdffffff02a0860100000000001600145bbdf3ba178f517d4812d286a40c436a9088076e6a0b0c00000000001976a9143fc16bef782f6856ff6638b1b99e4d3f863581d388acfbcb1300" + } + txid_list = sorted(list(transactions)) + + @classmethod + def create_old_wallet(cls): + ks = keystore.from_old_mpk('e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3') + # seed words: powerful random nobody notice nothing important anyway look away hidden message over + w = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=20) + # some txns are beyond gap limit: + w.create_new_address(for_change=True) + return w + + @mock.patch.object(storage.WalletStorage, '_write') + def test_restoring_old_wallet_txorder1(self, mock_write): + w = self.create_old_wallet() + for i in [2, 12, 7, 9, 11, 10, 16, 6, 17, 1, 13, 15, 5, 8, 4, 0, 14, 18, 3]: + tx = Transaction(self.transactions[self.txid_list[i]]) + w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual(27633300, sum(w.get_balance())) + + @mock.patch.object(storage.WalletStorage, '_write') + def test_restoring_old_wallet_txorder2(self, mock_write): + w = self.create_old_wallet() + for i in [9, 18, 2, 0, 13, 3, 1, 11, 4, 17, 7, 14, 12, 15, 10, 8, 5, 6, 16]: + tx = Transaction(self.transactions[self.txid_list[i]]) + w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual(27633300, sum(w.get_balance())) + + @mock.patch.object(storage.WalletStorage, '_write') + def test_restoring_old_wallet_txorder3(self, mock_write): + w = self.create_old_wallet() + for i in [5, 8, 17, 0, 9, 10, 12, 3, 15, 18, 2, 11, 14, 7, 16, 1, 4, 6, 13]: + tx = Transaction(self.transactions[self.txid_list[i]]) + w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual(27633300, sum(w.get_balance())) + + +class TestWalletHistory_EvilGapLimit(TestCaseForTestnet): + transactions = { + # txn A: + "511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4": "010000000001018aacc3c8f98964232ebb74e379d8ff4e800991eecfcf64bd1793954f5e50a8790100000000fdffffff0340420f0000000000160014dbf321e905d544b54b86a2f3ed95b0ac66a3ddb0ff0514000000000016001474f1c130d3db22894efb3b7612b2c924628d0d7e80841e000000000016001488492707677190c073b6555fb08d37e91bbb75d802483045022100cf2904e09ea9d2670367eccc184d92fcb8a9b9c79a12e4efe81df161077945db02203530276a3401d944cf7a292e0660f36ee1df4a1c92c131d2c0d31d267d52524901210215f523a412a5262612e1a5ef9842dc864b0d73dc61fb4c6bfd480a867bebb1632e181400", + # txn B: + "fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f": "01000000000101a317998ac6cc717de17213804e1459900fe257b9f4a3b9b9edd29806728277530100000000fdffffff03c0c62d00000000001600149543301687b1ca2c67718d55fbe10413c73ddec200093d00000000001600141bc12094a4475dcfbf24f9920dafddf9104ca95b3e4a4c0000000000160014b226a59f2609aa7da4026fe2c231b5ae7be12ac302483045022100f1082386d2ce81612a3957e2801803938f6c0066d76cfbd853918d4119f396df022077d05a2b482b89707a8a600013cb08448cf211218a462f2a23c2c0d80a8a0ca7012103f4aac7e189de53d95e0cb2e45d3c0b2be18e93420734934c61a6a5ad88dd541033181400", + # txn C: + "268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656": "010000000001014f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0100000000fdffffff0260e316000000000016001445e9879cf7cd5b4a15df7ddcaf5c6dca0e1508bacc242600000000001600141bc12094a4475dcfbf24f9920dafddf9104ca95b02483045022100ae3618912f341fefee11b67e0047c47c88c4fa031561c3fafe993259dd14d846022056fa0a5b5d8a65942fa68bcc2f848fd71fa455ba42bc2d421b67eb49ba62aa4e01210394d8f4f06c2ea9c569eb050c897737a7315e7f2104d9b536b49968cc89a1f11033181400", + } + + @classmethod + def create_wallet(cls): + ks = keystore.from_xpub('vpub5Vhmk4dEJKanDTTw6immKXa3thw45u3gbd1rPYjREB6viP13sVTWcH6kvbR2YeLtGjradr6SFLVt9PxWDBSrvw1Dc1nmd3oko3m24CQbfaJ') + # seed words: nephew work weather maze pyramid employ check permit garment scene kiwi smooth + w = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=20) + return w + + @mock.patch.object(storage.WalletStorage, '_write') + def test_restoring_wallet_txorder1(self, mock_write): + w = self.create_wallet() + w.storage.put('stored_height', 1316917 + 100) + for txid in self.transactions: + tx = Transaction(self.transactions[txid]) + w.transactions[tx.txid()] = tx + # txn A is an external incoming txn paying to addr (3) and (15) + # txn B is an external incoming txn paying to addr (4) and (25) + # txn C is an internal transfer txn from addr (25) -- to -- (1) and (25) + w.receive_history_callback('tb1qgh5c088he4d559wl0hw27hrdeg8p2z96pefn4q', # HD index 1 + [('268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656', 1316917)], + {}) + w.synchronize() + w.receive_history_callback('tb1qm0ejr6g964zt2jux5te7m9ds43n28hdsdz9ull', # HD index 3 + [('511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4', 1316912)], + {}) + w.synchronize() + w.receive_history_callback('tb1qj4pnq958k89zcem3342lhcgyz0rnmhkzl6x0cl', # HD index 4 + [('fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f', 1316917)], + {}) + w.synchronize() + w.receive_history_callback('tb1q3pyjwpm8wxgvquak240mprfhaydmkawcsl25je', # HD index 15 + [('511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4', 1316912)], + {}) + w.synchronize() + w.receive_history_callback('tb1qr0qjp99ygawul0eylxfqmt7alygye22mj33vej', # HD index 25 + [('fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f', 1316917), + ('268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656', 1316917)], + {}) + w.synchronize() + self.assertEqual(9999788, sum(w.get_balance())) diff --git a/electrum/transaction.py b/electrum/transaction.py @@ -0,0 +1,1229 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2011 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. + + + +# Note: The deserialization code originally comes from ABE. + +from typing import Sequence, Union + +from .util import print_error, profiler + +from . import ecc +from . import bitcoin +from .bitcoin import * +import struct +import traceback +import sys + +# +# Workalike python implementation of Bitcoin's CDataStream class. +# +from .keystore import xpubkey_to_address, xpubkey_to_pubkey + +NO_SIGNATURE = 'ff' +PARTIAL_TXN_HEADER_MAGIC = b'EPTF\xff' + + +class SerializationError(Exception): + """ Thrown when there's a problem deserializing or serializing """ + + +class UnknownTxinType(Exception): + pass + + +class NotRecognizedRedeemScript(Exception): + pass + + +class BCDataStream(object): + def __init__(self): + self.input = None + self.read_cursor = 0 + + def clear(self): + self.input = None + self.read_cursor = 0 + + def write(self, _bytes): # Initialize with string of _bytes + if self.input is None: + self.input = bytearray(_bytes) + else: + self.input += bytearray(_bytes) + + def read_string(self, encoding='ascii'): + # Strings are encoded depending on length: + # 0 to 252 : 1-byte-length followed by bytes (if any) + # 253 to 65,535 : byte'253' 2-byte-length followed by bytes + # 65,536 to 4,294,967,295 : byte '254' 4-byte-length followed by bytes + # ... and the Bitcoin client is coded to understand: + # greater than 4,294,967,295 : byte '255' 8-byte-length followed by bytes of string + # ... but I don't think it actually handles any strings that big. + if self.input is None: + raise SerializationError("call write(bytes) before trying to deserialize") + + length = self.read_compact_size() + + return self.read_bytes(length).decode(encoding) + + def write_string(self, string, encoding='ascii'): + string = to_bytes(string, encoding) + # Length-encoded as with read-string + self.write_compact_size(len(string)) + self.write(string) + + def read_bytes(self, length): + try: + result = self.input[self.read_cursor:self.read_cursor+length] + self.read_cursor += length + return result + except IndexError: + raise SerializationError("attempt to read past end of buffer") + + def can_read_more(self) -> bool: + if not self.input: + return False + return self.read_cursor < len(self.input) + + def read_boolean(self): return self.read_bytes(1)[0] != chr(0) + def read_int16(self): return self._read_num('<h') + def read_uint16(self): return self._read_num('<H') + def read_int32(self): return self._read_num('<i') + def read_uint32(self): return self._read_num('<I') + def read_int64(self): return self._read_num('<q') + def read_uint64(self): return self._read_num('<Q') + + def write_boolean(self, val): return self.write(chr(1) if val else chr(0)) + def write_int16(self, val): return self._write_num('<h', val) + def write_uint16(self, val): return self._write_num('<H', val) + def write_int32(self, val): return self._write_num('<i', val) + def write_uint32(self, val): return self._write_num('<I', val) + def write_int64(self, val): return self._write_num('<q', val) + def write_uint64(self, val): return self._write_num('<Q', val) + + def read_compact_size(self): + try: + size = self.input[self.read_cursor] + self.read_cursor += 1 + if size == 253: + size = self._read_num('<H') + elif size == 254: + size = self._read_num('<I') + elif size == 255: + size = self._read_num('<Q') + return size + except IndexError: + raise SerializationError("attempt to read past end of buffer") + + def write_compact_size(self, size): + if size < 0: + raise SerializationError("attempt to write size < 0") + elif size < 253: + self.write(bytes([size])) + elif size < 2**16: + self.write(b'\xfd') + self._write_num('<H', size) + elif size < 2**32: + self.write(b'\xfe') + self._write_num('<I', size) + elif size < 2**64: + self.write(b'\xff') + self._write_num('<Q', size) + + def _read_num(self, format): + try: + (i,) = struct.unpack_from(format, self.input, self.read_cursor) + self.read_cursor += struct.calcsize(format) + except Exception as e: + raise SerializationError(e) + return i + + def _write_num(self, format, num): + s = struct.pack(format, num) + self.write(s) + + +# enum-like type +# From the Python Cookbook, downloaded from http://code.activestate.com/recipes/67107/ +class EnumException(Exception): + pass + + +class Enumeration: + def __init__(self, name, enumList): + self.__doc__ = name + lookup = { } + reverseLookup = { } + i = 0 + uniqueNames = [ ] + uniqueValues = [ ] + for x in enumList: + if isinstance(x, tuple): + x, i = x + if not isinstance(x, str): + raise EnumException("enum name is not a string: " + x) + if not isinstance(i, int): + raise EnumException("enum value is not an integer: " + i) + if x in uniqueNames: + raise EnumException("enum name is not unique: " + x) + if i in uniqueValues: + raise EnumException("enum value is not unique for " + x) + uniqueNames.append(x) + uniqueValues.append(i) + lookup[x] = i + reverseLookup[i] = x + i = i + 1 + self.lookup = lookup + self.reverseLookup = reverseLookup + + def __getattr__(self, attr): + if attr not in self.lookup: + raise AttributeError + return self.lookup[attr] + def whatis(self, value): + return self.reverseLookup[value] + + +# This function comes from bitcointools, bct-LICENSE.txt. +def long_hex(bytes): + return bytes.encode('hex_codec') + +# This function comes from bitcointools, bct-LICENSE.txt. +def short_hex(bytes): + t = bytes.encode('hex_codec') + if len(t) < 11: + return t + return t[0:4]+"..."+t[-4:] + + + +opcodes = Enumeration("Opcodes", [ + ("OP_0", 0), ("OP_PUSHDATA1",76), "OP_PUSHDATA2", "OP_PUSHDATA4", "OP_1NEGATE", "OP_RESERVED", + "OP_1", "OP_2", "OP_3", "OP_4", "OP_5", "OP_6", "OP_7", + "OP_8", "OP_9", "OP_10", "OP_11", "OP_12", "OP_13", "OP_14", "OP_15", "OP_16", + "OP_NOP", "OP_VER", "OP_IF", "OP_NOTIF", "OP_VERIF", "OP_VERNOTIF", "OP_ELSE", "OP_ENDIF", "OP_VERIFY", + "OP_RETURN", "OP_TOALTSTACK", "OP_FROMALTSTACK", "OP_2DROP", "OP_2DUP", "OP_3DUP", "OP_2OVER", "OP_2ROT", "OP_2SWAP", + "OP_IFDUP", "OP_DEPTH", "OP_DROP", "OP_DUP", "OP_NIP", "OP_OVER", "OP_PICK", "OP_ROLL", "OP_ROT", + "OP_SWAP", "OP_TUCK", "OP_CAT", "OP_SUBSTR", "OP_LEFT", "OP_RIGHT", "OP_SIZE", "OP_INVERT", "OP_AND", + "OP_OR", "OP_XOR", "OP_EQUAL", "OP_EQUALVERIFY", "OP_RESERVED1", "OP_RESERVED2", "OP_1ADD", "OP_1SUB", "OP_2MUL", + "OP_2DIV", "OP_NEGATE", "OP_ABS", "OP_NOT", "OP_0NOTEQUAL", "OP_ADD", "OP_SUB", "OP_MUL", "OP_DIV", + "OP_MOD", "OP_LSHIFT", "OP_RSHIFT", "OP_BOOLAND", "OP_BOOLOR", + "OP_NUMEQUAL", "OP_NUMEQUALVERIFY", "OP_NUMNOTEQUAL", "OP_LESSTHAN", + "OP_GREATERTHAN", "OP_LESSTHANOREQUAL", "OP_GREATERTHANOREQUAL", "OP_MIN", "OP_MAX", + "OP_WITHIN", "OP_RIPEMD160", "OP_SHA1", "OP_SHA256", "OP_HASH160", + "OP_HASH256", "OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG", + "OP_CHECKMULTISIGVERIFY", + ("OP_NOP1", 0xB0), + ("OP_CHECKLOCKTIMEVERIFY", 0xB1), ("OP_CHECKSEQUENCEVERIFY", 0xB2), + "OP_NOP4", "OP_NOP5", "OP_NOP6", "OP_NOP7", "OP_NOP8", "OP_NOP9", "OP_NOP10", + ("OP_INVALIDOPCODE", 0xFF), +]) + + +def script_GetOp(_bytes : bytes): + i = 0 + while i < len(_bytes): + vch = None + opcode = _bytes[i] + i += 1 + + if opcode <= opcodes.OP_PUSHDATA4: + nSize = opcode + if opcode == opcodes.OP_PUSHDATA1: + nSize = _bytes[i] + i += 1 + elif opcode == opcodes.OP_PUSHDATA2: + (nSize,) = struct.unpack_from('<H', _bytes, i) + i += 2 + elif opcode == opcodes.OP_PUSHDATA4: + (nSize,) = struct.unpack_from('<I', _bytes, i) + i += 4 + vch = _bytes[i:i + nSize] + i += nSize + + yield opcode, vch, i + + +def script_GetOpName(opcode): + return (opcodes.whatis(opcode)).replace("OP_", "") + + +def decode_script(bytes): + result = '' + for (opcode, vch, i) in script_GetOp(bytes): + if len(result) > 0: result += " " + if opcode <= opcodes.OP_PUSHDATA4: + result += "%d:"%(opcode,) + result += short_hex(vch) + else: + result += script_GetOpName(opcode) + return result + + +def match_decoded(decoded, to_match): + if len(decoded) != len(to_match): + return False; + for i in range(len(decoded)): + if to_match[i] == opcodes.OP_PUSHDATA4 and decoded[i][0] <= opcodes.OP_PUSHDATA4 and decoded[i][0]>0: + continue # Opcodes below OP_PUSHDATA4 all just push data onto stack, and are equivalent. + if to_match[i] != decoded[i][0]: + return False + return True + + +def parse_sig(x_sig): + return [None if x == NO_SIGNATURE else x for x in x_sig] + +def safe_parse_pubkey(x): + try: + return xpubkey_to_pubkey(x) + except: + return x + +def parse_scriptSig(d, _bytes): + try: + decoded = [ x for x in script_GetOp(_bytes) ] + except Exception as e: + # coinbase transactions raise an exception + print_error("parse_scriptSig: cannot find address in input script (coinbase?)", + bh2u(_bytes)) + return + + match = [ opcodes.OP_PUSHDATA4 ] + if match_decoded(decoded, match): + item = decoded[0][1] + if item[0] == 0: + # segwit embedded into p2sh + # witness version 0 + d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) + if len(item) == 22: + d['type'] = 'p2wpkh-p2sh' + elif len(item) == 34: + d['type'] = 'p2wsh-p2sh' + else: + print_error("unrecognized txin type", bh2u(item)) + elif opcodes.OP_1 <= item[0] <= opcodes.OP_16: + # segwit embedded into p2sh + # witness version 1-16 + pass + else: + # assert item[0] == 0x30 + # pay-to-pubkey + d['type'] = 'p2pk' + d['address'] = "(pubkey)" + d['signatures'] = [bh2u(item)] + d['num_sig'] = 1 + d['x_pubkeys'] = ["(pubkey)"] + d['pubkeys'] = ["(pubkey)"] + return + + # p2pkh TxIn transactions push a signature + # (71-73 bytes) and then their public key + # (33 or 65 bytes) onto the stack: + match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ] + if match_decoded(decoded, match): + sig = bh2u(decoded[0][1]) + x_pubkey = bh2u(decoded[1][1]) + try: + signatures = parse_sig([sig]) + pubkey, address = xpubkey_to_address(x_pubkey) + except: + print_error("parse_scriptSig: cannot find address in input script (p2pkh?)", + bh2u(_bytes)) + return + d['type'] = 'p2pkh' + d['signatures'] = signatures + d['x_pubkeys'] = [x_pubkey] + d['num_sig'] = 1 + d['pubkeys'] = [pubkey] + d['address'] = address + return + + # p2sh transaction, m of n + match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1) + if match_decoded(decoded, match): + x_sig = [bh2u(x[1]) for x in decoded[1:-1]] + redeem_script_unsanitized = decoded[-1][1] # for partial multisig txn, this has x_pubkeys + try: + m, n, x_pubkeys, pubkeys, redeem_script = parse_redeemScript_multisig(redeem_script_unsanitized) + except NotRecognizedRedeemScript: + print_error("parse_scriptSig: cannot find address in input script (p2sh?)", + bh2u(_bytes)) + # we could still guess: + # d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1])) + return + # write result in d + d['type'] = 'p2sh' + d['num_sig'] = m + d['signatures'] = parse_sig(x_sig) + d['x_pubkeys'] = x_pubkeys + d['pubkeys'] = pubkeys + d['redeem_script'] = redeem_script + d['address'] = hash160_to_p2sh(hash_160(bfh(redeem_script))) + return + + # custom partial format for imported addresses + match = [ opcodes.OP_INVALIDOPCODE, opcodes.OP_0, opcodes.OP_PUSHDATA4 ] + if match_decoded(decoded, match): + x_pubkey = bh2u(decoded[2][1]) + pubkey, address = xpubkey_to_address(x_pubkey) + d['type'] = 'address' + d['address'] = address + d['num_sig'] = 1 + d['x_pubkeys'] = [x_pubkey] + d['pubkeys'] = None # get_sorted_pubkeys will populate this + d['signatures'] = [None] + return + + print_error("parse_scriptSig: cannot find address in input script (unknown)", + bh2u(_bytes)) + + +def parse_redeemScript_multisig(redeem_script: bytes): + dec2 = [ x for x in script_GetOp(redeem_script) ] + try: + m = dec2[0][0] - opcodes.OP_1 + 1 + n = dec2[-2][0] - opcodes.OP_1 + 1 + except IndexError: + raise NotRecognizedRedeemScript() + op_m = opcodes.OP_1 + m - 1 + op_n = opcodes.OP_1 + n - 1 + match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] + if not match_decoded(dec2, match_multisig): + raise NotRecognizedRedeemScript() + x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] + pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] + redeem_script2 = bfh(multisig_script(x_pubkeys, m)) + if redeem_script2 != redeem_script: + raise NotRecognizedRedeemScript() + redeem_script_sanitized = multisig_script(pubkeys, m) + return m, n, x_pubkeys, pubkeys, redeem_script_sanitized + + +def get_address_from_output_script(_bytes, *, net=None): + decoded = [x for x in script_GetOp(_bytes)] + + # The Genesis Block, self-payments, and pay-by-IP-address payments look like: + # 65 BYTES:... CHECKSIG + match = [ opcodes.OP_PUSHDATA4, opcodes.OP_CHECKSIG ] + if match_decoded(decoded, match): + return TYPE_PUBKEY, bh2u(decoded[0][1]) + + # Pay-by-Bitcoin-address TxOuts look like: + # DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG + match = [ opcodes.OP_DUP, opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG ] + if match_decoded(decoded, match): + return TYPE_ADDRESS, hash160_to_p2pkh(decoded[2][1], net=net) + + # p2sh + match = [ opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUAL ] + if match_decoded(decoded, match): + return TYPE_ADDRESS, hash160_to_p2sh(decoded[1][1], net=net) + + # segwit address + possible_witness_versions = [opcodes.OP_0] + list(range(opcodes.OP_1, opcodes.OP_16 + 1)) + for witver, opcode in enumerate(possible_witness_versions): + match = [ opcode, opcodes.OP_PUSHDATA4 ] + if match_decoded(decoded, match): + return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=witver, net=net) + + return TYPE_SCRIPT, bh2u(_bytes) + + +def parse_input(vds, full_parse: bool): + d = {} + prevout_hash = hash_encode(vds.read_bytes(32)) + prevout_n = vds.read_uint32() + scriptSig = vds.read_bytes(vds.read_compact_size()) + sequence = vds.read_uint32() + d['prevout_hash'] = prevout_hash + d['prevout_n'] = prevout_n + d['scriptSig'] = bh2u(scriptSig) + d['sequence'] = sequence + d['type'] = 'unknown' if prevout_hash != '00'*32 else 'coinbase' + d['address'] = None + d['num_sig'] = 0 + if not full_parse: + return d + d['x_pubkeys'] = [] + d['pubkeys'] = [] + d['signatures'] = {} + if d['type'] != 'coinbase' and scriptSig: + try: + parse_scriptSig(d, scriptSig) + except BaseException: + traceback.print_exc(file=sys.stderr) + print_error('failed to parse scriptSig', bh2u(scriptSig)) + return d + + +def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: + """Constructs a witness from the given stack items.""" + witness = var_int(len(items)) + for item in items: + if type(item) is int: + item = bitcoin.script_num_to_hex(item) + elif type(item) is bytes: + item = bh2u(item) + witness += bitcoin.witness_push(item) + return witness + + +def parse_witness(vds, txin, full_parse: bool): + n = vds.read_compact_size() + if n == 0: + txin['witness'] = '00' + return + if n == 0xffffffff: + txin['value'] = vds.read_uint64() + txin['witness_version'] = vds.read_uint16() + n = vds.read_compact_size() + # now 'n' is the number of items in the witness + w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n)) + txin['witness'] = construct_witness(w) + if not full_parse: + return + + try: + if txin.get('witness_version', 0) != 0: + raise UnknownTxinType() + if txin['type'] == 'coinbase': + pass + elif txin['type'] == 'address': + pass + elif txin['type'] == 'p2wsh-p2sh' or n > 2: + witness_script_unsanitized = w[-1] # for partial multisig txn, this has x_pubkeys + try: + m, n, x_pubkeys, pubkeys, witness_script = parse_redeemScript_multisig(bfh(witness_script_unsanitized)) + except NotRecognizedRedeemScript: + raise UnknownTxinType() + txin['signatures'] = parse_sig(w[1:-1]) + txin['num_sig'] = m + txin['x_pubkeys'] = x_pubkeys + txin['pubkeys'] = pubkeys + txin['witness_script'] = witness_script + if not txin.get('scriptSig'): # native segwit script + txin['type'] = 'p2wsh' + txin['address'] = bitcoin.script_to_p2wsh(witness_script) + elif txin['type'] == 'p2wpkh-p2sh' or n == 2: + txin['num_sig'] = 1 + txin['x_pubkeys'] = [w[1]] + txin['pubkeys'] = [safe_parse_pubkey(w[1])] + txin['signatures'] = parse_sig([w[0]]) + if not txin.get('scriptSig'): # native segwit script + txin['type'] = 'p2wpkh' + txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0])) + else: + raise UnknownTxinType() + except UnknownTxinType: + txin['type'] = 'unknown' + except BaseException: + txin['type'] = 'unknown' + traceback.print_exc(file=sys.stderr) + print_error('failed to parse witness', txin.get('witness')) + + +def parse_output(vds, i): + d = {} + d['value'] = vds.read_int64() + if d['value'] > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: + raise SerializationError('invalid output amount (too large)') + if d['value'] < 0: + raise SerializationError('invalid output amount (negative)') + scriptPubKey = vds.read_bytes(vds.read_compact_size()) + d['type'], d['address'] = get_address_from_output_script(scriptPubKey) + d['scriptPubKey'] = bh2u(scriptPubKey) + d['prevout_n'] = i + return d + + +def deserialize(raw: str, force_full_parse=False) -> dict: + raw_bytes = bfh(raw) + d = {} + if raw_bytes[:5] == PARTIAL_TXN_HEADER_MAGIC: + d['partial'] = is_partial = True + partial_format_version = raw_bytes[5] + if partial_format_version != 0: + raise SerializationError('unknown tx partial serialization format version: {}' + .format(partial_format_version)) + raw_bytes = raw_bytes[6:] + else: + d['partial'] = is_partial = False + full_parse = force_full_parse or is_partial + vds = BCDataStream() + vds.write(raw_bytes) + d['version'] = vds.read_int32() + n_vin = vds.read_compact_size() + is_segwit = (n_vin == 0) + if is_segwit: + marker = vds.read_bytes(1) + if marker != b'\x01': + raise ValueError('invalid txn marker byte: {}'.format(marker)) + n_vin = vds.read_compact_size() + d['segwit_ser'] = is_segwit + d['inputs'] = [parse_input(vds, full_parse=full_parse) for i in range(n_vin)] + n_vout = vds.read_compact_size() + d['outputs'] = [parse_output(vds, i) for i in range(n_vout)] + if is_segwit: + for i in range(n_vin): + txin = d['inputs'][i] + parse_witness(vds, txin, full_parse=full_parse) + d['lockTime'] = vds.read_uint32() + if vds.can_read_more(): + raise SerializationError('extra junk at the end') + return d + + +# pay & redeem scripts + + + +def multisig_script(public_keys: Sequence[str], m: int) -> str: + n = len(public_keys) + assert n <= 15 + assert m <= n + op_m = format(opcodes.OP_1 + m - 1, 'x') + op_n = format(opcodes.OP_1 + n - 1, 'x') + keylist = [op_push(len(k)//2) + k for k in public_keys] + return op_m + ''.join(keylist) + op_n + 'ae' + + + + +class Transaction: + + def __str__(self): + if self.raw is None: + self.raw = self.serialize() + return self.raw + + def __init__(self, raw): + if raw is None: + self.raw = None + elif isinstance(raw, str): + self.raw = raw.strip() if raw else None + elif isinstance(raw, dict): + self.raw = raw['hex'] + else: + raise Exception("cannot initialize transaction", raw) + self._inputs = None + self._outputs = None + self.locktime = 0 + self.version = 1 + # by default we assume this is a partial txn; + # this value will get properly set when deserializing + self.is_partial_originally = True + self._segwit_ser = None # None means "don't know" + + def update(self, raw): + self.raw = raw + self._inputs = None + self.deserialize() + + def inputs(self): + if self._inputs is None: + self.deserialize() + return self._inputs + + def outputs(self): + if self._outputs is None: + self.deserialize() + return self._outputs + + @classmethod + def get_sorted_pubkeys(self, txin): + # sort pubkeys and x_pubkeys, using the order of pubkeys + if txin['type'] == 'coinbase': + return [], [] + x_pubkeys = txin['x_pubkeys'] + pubkeys = txin.get('pubkeys') + if pubkeys is None: + pubkeys = [xpubkey_to_pubkey(x) for x in x_pubkeys] + pubkeys, x_pubkeys = zip(*sorted(zip(pubkeys, x_pubkeys))) + txin['pubkeys'] = pubkeys = list(pubkeys) + txin['x_pubkeys'] = x_pubkeys = list(x_pubkeys) + return pubkeys, x_pubkeys + + def update_signatures(self, signatures: Sequence[str]): + """Add new signatures to a transaction + + `signatures` is expected to be a list of sigs with signatures[i] + intended for self._inputs[i]. + This is used by the Trezor and KeepKey plugins. + """ + if self.is_complete(): + return + if len(self.inputs()) != len(signatures): + raise Exception('expected {} signatures; got {}'.format(len(self.inputs()), len(signatures))) + for i, txin in enumerate(self.inputs()): + pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) + sig = signatures[i] + if sig in txin.get('signatures'): + continue + pre_hash = Hash(bfh(self.serialize_preimage(i))) + sig_string = ecc.sig_string_from_der_sig(bfh(sig[:-2])) + for recid in range(4): + try: + public_key = ecc.ECPubkey.from_sig_string(sig_string, recid, pre_hash) + except ecc.InvalidECPointException: + # the point might not be on the curve for some recid values + continue + pubkey_hex = public_key.get_public_key_hex(compressed=True) + if pubkey_hex in pubkeys: + try: + public_key.verify_message_hash(sig_string, pre_hash) + except Exception: + traceback.print_exc(file=sys.stderr) + continue + j = pubkeys.index(pubkey_hex) + print_error("adding sig", i, j, pubkey_hex, sig) + self.add_signature_to_txin(i, j, sig) + #self._inputs[i]['x_pubkeys'][j] = pubkey + break + # redo raw + self.raw = self.serialize() + + def add_signature_to_txin(self, i, signingPos, sig): + txin = self._inputs[i] + txin['signatures'][signingPos] = sig + txin['scriptSig'] = None # force re-serialization + txin['witness'] = None # force re-serialization + self.raw = None + + def deserialize(self, force_full_parse=False): + if self.raw is None: + return + #self.raw = self.serialize() + if self._inputs is not None: + return + d = deserialize(self.raw, force_full_parse) + self._inputs = d['inputs'] + self._outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']] + self.locktime = d['lockTime'] + self.version = d['version'] + self.is_partial_originally = d['partial'] + self._segwit_ser = d['segwit_ser'] + return d + + @classmethod + def from_io(klass, inputs, outputs, locktime=0): + self = klass(None) + self._inputs = inputs + self._outputs = outputs + self.locktime = locktime + return self + + @classmethod + def pay_script(self, output_type, addr): + if output_type == TYPE_SCRIPT: + return addr + elif output_type == TYPE_ADDRESS: + return bitcoin.address_to_script(addr) + elif output_type == TYPE_PUBKEY: + return bitcoin.public_key_to_p2pk_script(addr) + else: + raise TypeError('Unknown output type') + + @classmethod + def estimate_pubkey_size_from_x_pubkey(cls, x_pubkey): + try: + if x_pubkey[0:2] in ['02', '03']: # compressed pubkey + return 0x21 + elif x_pubkey[0:2] == '04': # uncompressed pubkey + return 0x41 + elif x_pubkey[0:2] == 'ff': # bip32 extended pubkey + return 0x21 + elif x_pubkey[0:2] == 'fe': # old electrum extended pubkey + return 0x41 + except Exception as e: + pass + return 0x21 # just guess it is compressed + + @classmethod + def estimate_pubkey_size_for_txin(cls, txin): + pubkeys = txin.get('pubkeys', []) + x_pubkeys = txin.get('x_pubkeys', []) + if pubkeys and len(pubkeys) > 0: + return cls.estimate_pubkey_size_from_x_pubkey(pubkeys[0]) + elif x_pubkeys and len(x_pubkeys) > 0: + return cls.estimate_pubkey_size_from_x_pubkey(x_pubkeys[0]) + else: + return 0x21 # just guess it is compressed + + @classmethod + def get_siglist(self, txin, estimate_size=False): + # if we have enough signatures, we use the actual pubkeys + # otherwise, use extended pubkeys (with bip32 derivation) + if txin['type'] == 'coinbase': + return [], [] + num_sig = txin.get('num_sig', 1) + if estimate_size: + pubkey_size = self.estimate_pubkey_size_for_txin(txin) + pk_list = ["00" * pubkey_size] * len(txin.get('x_pubkeys', [None])) + # we assume that signature will be 0x48 bytes long + sig_list = [ "00" * 0x48 ] * num_sig + else: + pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) + x_signatures = txin['signatures'] + signatures = list(filter(None, x_signatures)) + is_complete = len(signatures) == num_sig + if is_complete: + pk_list = pubkeys + sig_list = signatures + else: + pk_list = x_pubkeys + sig_list = [sig if sig else NO_SIGNATURE for sig in x_signatures] + return pk_list, sig_list + + @classmethod + def serialize_witness(self, txin, estimate_size=False): + _type = txin['type'] + if not self.is_segwit_input(txin) and not self.is_input_value_needed(txin): + return '00' + if _type == 'coinbase': + return txin['witness'] + + witness = txin.get('witness', None) + if witness is None or estimate_size: + if _type == 'address' and estimate_size: + _type = self.guess_txintype_from_address(txin['address']) + pubkeys, sig_list = self.get_siglist(txin, estimate_size) + if _type in ['p2wpkh', 'p2wpkh-p2sh']: + witness = construct_witness([sig_list[0], pubkeys[0]]) + elif _type in ['p2wsh', 'p2wsh-p2sh']: + witness_script = multisig_script(pubkeys, txin['num_sig']) + witness = construct_witness([0] + sig_list + [witness_script]) + else: + witness = txin.get('witness', '00') + + if self.is_txin_complete(txin) or estimate_size: + partial_format_witness_prefix = '' + else: + input_value = int_to_hex(txin['value'], 8) + witness_version = int_to_hex(txin.get('witness_version', 0), 2) + partial_format_witness_prefix = var_int(0xffffffff) + input_value + witness_version + return partial_format_witness_prefix + witness + + @classmethod + def is_segwit_input(cls, txin, guess_for_address=False): + _type = txin['type'] + if _type == 'address' and guess_for_address: + _type = cls.guess_txintype_from_address(txin['address']) + has_nonzero_witness = txin.get('witness', '00') not in ('00', None) + return cls.is_segwit_inputtype(_type) or has_nonzero_witness + + @classmethod + def is_segwit_inputtype(cls, txin_type): + return txin_type in ('p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh') + + @classmethod + def is_input_value_needed(cls, txin): + return cls.is_segwit_input(txin) or txin['type'] == 'address' + + @classmethod + def guess_txintype_from_address(cls, addr): + # It's not possible to tell the script type in general + # just from an address. + # - "1" addresses are of course p2pkh + # - "3" addresses are p2sh but we don't know the redeem script.. + # - "bc1" addresses (if they are 42-long) are p2wpkh + # - "bc1" addresses that are 62-long are p2wsh but we don't know the script.. + # If we don't know the script, we _guess_ it is pubkeyhash. + # As this method is used e.g. for tx size estimation, + # the estimation will not be precise. + witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr) + if witprog is not None: + return 'p2wpkh' + addrtype, hash_160 = b58_address_to_hash160(addr) + if addrtype == constants.net.ADDRTYPE_P2PKH: + return 'p2pkh' + elif addrtype == constants.net.ADDRTYPE_P2SH: + return 'p2wpkh-p2sh' + + @classmethod + def input_script(self, txin, estimate_size=False): + _type = txin['type'] + if _type == 'coinbase': + return txin['scriptSig'] + + # If there is already a saved scriptSig, just return that. + # This allows manual creation of txins of any custom type. + # However, if the txin is not complete, we might have some garbage + # saved from our partial txn ser format, so we re-serialize then. + script_sig = txin.get('scriptSig', None) + if script_sig is not None and self.is_txin_complete(txin): + return script_sig + + pubkeys, sig_list = self.get_siglist(txin, estimate_size) + script = ''.join(push_script(x) for x in sig_list) + if _type == 'address' and estimate_size: + _type = self.guess_txintype_from_address(txin['address']) + if _type == 'p2pk': + pass + elif _type == 'p2sh': + # put op_0 before script + script = '00' + script + redeem_script = multisig_script(pubkeys, txin['num_sig']) + script += push_script(redeem_script) + elif _type == 'p2pkh': + script += push_script(pubkeys[0]) + elif _type in ['p2wpkh', 'p2wsh']: + return '' + elif _type == 'p2wpkh-p2sh': + pubkey = safe_parse_pubkey(pubkeys[0]) + scriptSig = bitcoin.p2wpkh_nested_script(pubkey) + return push_script(scriptSig) + elif _type == 'p2wsh-p2sh': + if estimate_size: + witness_script = '' + else: + witness_script = self.get_preimage_script(txin) + scriptSig = bitcoin.p2wsh_nested_script(witness_script) + return push_script(scriptSig) + elif _type == 'address': + return 'ff00' + push_script(pubkeys[0]) # fd extended pubkey + elif _type == 'unknown': + return txin['scriptSig'] + return script + + @classmethod + def is_txin_complete(cls, txin): + if txin['type'] == 'coinbase': + return True + num_sig = txin.get('num_sig', 1) + if num_sig == 0: + return True + x_signatures = txin['signatures'] + signatures = list(filter(None, x_signatures)) + return len(signatures) == num_sig + + @classmethod + def get_preimage_script(self, txin): + preimage_script = txin.get('preimage_script', None) + if preimage_script is not None: + return preimage_script + + pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) + if txin['type'] == 'p2pkh': + return bitcoin.address_to_script(txin['address']) + elif txin['type'] in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: + return multisig_script(pubkeys, txin['num_sig']) + elif txin['type'] in ['p2wpkh', 'p2wpkh-p2sh']: + pubkey = pubkeys[0] + pkh = bh2u(bitcoin.hash_160(bfh(pubkey))) + return '76a9' + push_script(pkh) + '88ac' + elif txin['type'] == 'p2pk': + pubkey = pubkeys[0] + return bitcoin.public_key_to_p2pk_script(pubkey) + else: + raise TypeError('Unknown txin type', txin['type']) + + @classmethod + def serialize_outpoint(self, txin): + return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4) + + @classmethod + def get_outpoint_from_txin(cls, txin): + if txin['type'] == 'coinbase': + return None + prevout_hash = txin['prevout_hash'] + prevout_n = txin['prevout_n'] + return prevout_hash + ':%d' % prevout_n + + @classmethod + def serialize_input(self, txin, script): + # Prev hash and index + s = self.serialize_outpoint(txin) + # Script length, script, sequence + s += var_int(len(script)//2) + s += script + s += int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) + return s + + def set_rbf(self, rbf): + nSequence = 0xffffffff - (2 if rbf else 1) + for txin in self.inputs(): + txin['sequence'] = nSequence + + def BIP_LI01_sort(self): + # See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki + self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) + self._outputs.sort(key = lambda o: (o[2], self.pay_script(o[0], o[1]))) + + def serialize_output(self, output): + output_type, addr, amount = output + s = int_to_hex(amount, 8) + script = self.pay_script(output_type, addr) + s += var_int(len(script)//2) + s += script + return s + + def serialize_preimage(self, i): + nVersion = int_to_hex(self.version, 4) + nHashType = int_to_hex(1, 4) + nLocktime = int_to_hex(self.locktime, 4) + inputs = self.inputs() + outputs = self.outputs() + txin = inputs[i] + # TODO: py3 hex + if self.is_segwit_input(txin): + hashPrevouts = bh2u(Hash(bfh(''.join(self.serialize_outpoint(txin) for txin in inputs)))) + hashSequence = bh2u(Hash(bfh(''.join(int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) for txin in inputs)))) + hashOutputs = bh2u(Hash(bfh(''.join(self.serialize_output(o) for o in outputs)))) + outpoint = self.serialize_outpoint(txin) + preimage_script = self.get_preimage_script(txin) + scriptCode = var_int(len(preimage_script) // 2) + preimage_script + amount = int_to_hex(txin['value'], 8) + nSequence = int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) + preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType + else: + txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.get_preimage_script(txin) if i==k else '') for k, txin in enumerate(inputs)) + txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs) + preimage = nVersion + txins + txouts + nLocktime + nHashType + return preimage + + def is_segwit(self, guess_for_address=False): + if not self.is_partial_originally: + return self._segwit_ser + return any(self.is_segwit_input(x, guess_for_address=guess_for_address) for x in self.inputs()) + + def serialize(self, estimate_size=False, witness=True): + network_ser = self.serialize_to_network(estimate_size, witness) + if estimate_size: + return network_ser + if self.is_partial_originally and not self.is_complete(): + partial_format_version = '00' + return bh2u(PARTIAL_TXN_HEADER_MAGIC) + partial_format_version + network_ser + else: + return network_ser + + def serialize_to_network(self, estimate_size=False, witness=True): + nVersion = int_to_hex(self.version, 4) + nLocktime = int_to_hex(self.locktime, 4) + inputs = self.inputs() + outputs = self.outputs() + txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.input_script(txin, estimate_size)) for txin in inputs) + txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs) + use_segwit_ser_for_estimate_size = estimate_size and self.is_segwit(guess_for_address=True) + use_segwit_ser_for_actual_use = not estimate_size and \ + (self.is_segwit() or any(txin['type'] == 'address' for txin in inputs)) + use_segwit_ser = use_segwit_ser_for_estimate_size or use_segwit_ser_for_actual_use + if witness and use_segwit_ser: + marker = '00' + flag = '01' + witness = ''.join(self.serialize_witness(x, estimate_size) for x in inputs) + return nVersion + marker + flag + txins + txouts + witness + nLocktime + else: + return nVersion + txins + txouts + nLocktime + + def txid(self): + self.deserialize() + all_segwit = all(self.is_segwit_input(x) for x in self.inputs()) + if not all_segwit and not self.is_complete(): + return None + ser = self.serialize_to_network(witness=False) + return bh2u(Hash(bfh(ser))[::-1]) + + def wtxid(self): + self.deserialize() + if not self.is_complete(): + return None + ser = self.serialize_to_network(witness=True) + return bh2u(Hash(bfh(ser))[::-1]) + + def add_inputs(self, inputs): + self._inputs.extend(inputs) + self.raw = None + + def add_outputs(self, outputs): + self._outputs.extend(outputs) + self.raw = None + + def input_value(self): + return sum(x['value'] for x in self.inputs()) + + def output_value(self): + return sum(val for tp, addr, val in self.outputs()) + + def get_fee(self): + return self.input_value() - self.output_value() + + def is_final(self): + return not any([x.get('sequence', 0xffffffff - 1) < 0xffffffff - 1 for x in self.inputs()]) + + @profiler + def estimated_size(self): + """Return an estimated virtual tx size in vbytes. + BIP-0141 defines 'Virtual transaction size' to be weight/4 rounded up. + This definition is only for humans, and has little meaning otherwise. + If we wanted sub-byte precision, fee calculation should use transaction + weights, but for simplicity we approximate that with (virtual_size)x4 + """ + weight = self.estimated_weight() + return self.virtual_size_from_weight(weight) + + @classmethod + def estimated_input_weight(cls, txin, is_segwit_tx): + '''Return an estimate of serialized input weight in weight units.''' + script = cls.input_script(txin, True) + input_size = len(cls.serialize_input(txin, script)) // 2 + + if cls.is_segwit_input(txin, guess_for_address=True): + witness_size = len(cls.serialize_witness(txin, True)) // 2 + else: + witness_size = 1 if is_segwit_tx else 0 + + return 4 * input_size + witness_size + + @classmethod + def estimated_output_size(cls, address): + """Return an estimate of serialized output size in bytes.""" + script = bitcoin.address_to_script(address) + # 8 byte value + 1 byte script len + script + return 9 + len(script) // 2 + + @classmethod + def virtual_size_from_weight(cls, weight): + return weight // 4 + (weight % 4 > 0) + + def estimated_total_size(self): + """Return an estimated total transaction size in bytes.""" + return len(self.serialize(True)) // 2 if not self.is_complete() or self.raw is None else len(self.raw) // 2 # ASCII hex string + + def estimated_witness_size(self): + """Return an estimate of witness size in bytes.""" + estimate = not self.is_complete() + if not self.is_segwit(guess_for_address=estimate): + return 0 + inputs = self.inputs() + witness = ''.join(self.serialize_witness(x, estimate) for x in inputs) + witness_size = len(witness) // 2 + 2 # include marker and flag + return witness_size + + def estimated_base_size(self): + """Return an estimated base transaction size in bytes.""" + return self.estimated_total_size() - self.estimated_witness_size() + + def estimated_weight(self): + """Return an estimate of transaction weight.""" + total_tx_size = self.estimated_total_size() + base_tx_size = self.estimated_base_size() + return 3 * base_tx_size + total_tx_size + + def signature_count(self): + r = 0 + s = 0 + for txin in self.inputs(): + if txin['type'] == 'coinbase': + continue + signatures = list(filter(None, txin.get('signatures',[]))) + s += len(signatures) + r += txin.get('num_sig',-1) + return s, r + + def is_complete(self): + if not self.is_partial_originally: + return True + s, r = self.signature_count() + return r == s + + def sign(self, keypairs) -> None: + # keypairs: (x_)pubkey -> secret_bytes + for i, txin in enumerate(self.inputs()): + pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) + for j, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)): + if self.is_txin_complete(txin): + break + if pubkey in keypairs: + _pubkey = pubkey + elif x_pubkey in keypairs: + _pubkey = x_pubkey + else: + continue + print_error("adding signature for", _pubkey) + sec, compressed = keypairs.get(_pubkey) + sig = self.sign_txin(i, sec) + self.add_signature_to_txin(i, j, sig) + + print_error("is_complete", self.is_complete()) + self.raw = self.serialize() + + def sign_txin(self, txin_index, privkey_bytes) -> str: + pre_hash = Hash(bfh(self.serialize_preimage(txin_index))) + privkey = ecc.ECPrivkey(privkey_bytes) + sig = privkey.sign_transaction(pre_hash) + sig = bh2u(sig) + '01' + return sig + + def get_outputs(self): + """convert pubkeys to addresses""" + o = [] + for type, x, v in self.outputs(): + if type == TYPE_ADDRESS: + addr = x + elif type == TYPE_PUBKEY: + # TODO do we really want this conversion? it's not really that address after all + addr = bitcoin.public_key_to_p2pkh(bfh(x)) + else: + addr = 'SCRIPT ' + x + o.append((addr,v)) # consider using yield (addr, v) + return o + + def get_output_addresses(self): + return [addr for addr, val in self.get_outputs()] + + + def has_address(self, addr): + return (addr in self.get_output_addresses()) or (addr in (tx.get("address") for tx in self.inputs())) + + def as_dict(self): + if self.raw is None: + self.raw = self.serialize() + self.deserialize() + out = { + 'hex': self.raw, + 'complete': self.is_complete(), + 'final': self.is_final(), + } + return out + + +def tx_from_str(txt): + "json or raw hexadecimal" + import json + txt = txt.strip() + if not txt: + raise ValueError("empty string") + try: + bfh(txt) + is_hex = True + except: + is_hex = False + if is_hex: + return txt + tx_dict = json.loads(str(txt)) + assert "hex" in tx_dict.keys() + return tx_dict["hex"] diff --git a/electrum/util.py b/electrum/util.py @@ -0,0 +1,903 @@ +# Electrum - lightweight Bitcoin client +# Copyright (C) 2011 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 binascii +import os, sys, re, json +from collections import defaultdict +from datetime import datetime +import decimal +from decimal import Decimal +import traceback +import urllib +import threading +import hmac +import stat + +from .i18n import _ + + +import urllib.request, urllib.parse, urllib.error +import queue + +def inv_dict(d): + return {v: k for k, v in d.items()} + + +base_units = {'BTC':8, 'mBTC':5, 'bits':2, 'sat':0} +base_units_inverse = inv_dict(base_units) +base_units_list = ['BTC', 'mBTC', 'bits', 'sat'] # list(dict) does not guarantee order + + +def decimal_point_to_base_unit_name(dp: int) -> str: + # e.g. 8 -> "BTC" + try: + return base_units_inverse[dp] + except KeyError: + raise Exception('Unknown base unit') + + +def base_unit_name_to_decimal_point(unit_name: str) -> int: + # e.g. "BTC" -> 8 + try: + return base_units[unit_name] + except KeyError: + raise Exception('Unknown base unit') + + +def normalize_version(v): + return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] + +class NotEnoughFunds(Exception): pass + + +class NoDynamicFeeEstimates(Exception): + def __str__(self): + return _('Dynamic fee estimates not available') + + +class InvalidPassword(Exception): + def __str__(self): + return _("Incorrect password") + + +class FileImportFailed(Exception): + def __init__(self, message=''): + self.message = str(message) + + def __str__(self): + return _("Failed to import from file.") + "\n" + self.message + + +class FileExportFailed(Exception): + def __init__(self, message=''): + self.message = str(message) + + def __str__(self): + return _("Failed to export to file.") + "\n" + self.message + + +class TimeoutException(Exception): + def __init__(self, message=''): + self.message = str(message) + + def __str__(self): + if not self.message: + return _("Operation timed out.") + return self.message + + +class WalletFileException(Exception): pass + + +class BitcoinException(Exception): pass + + +# Throw this exception to unwind the stack like when an error occurs. +# However unlike other exceptions the user won't be informed. +class UserCancelled(Exception): + '''An exception that is suppressed from the user''' + pass + +class Satoshis(object): + def __new__(cls, value): + self = super(Satoshis, cls).__new__(cls) + self.value = value + return self + + def __repr__(self): + return 'Satoshis(%d)'%self.value + + def __str__(self): + return format_satoshis(self.value) + " BTC" + +class Fiat(object): + def __new__(cls, value, ccy): + self = super(Fiat, cls).__new__(cls) + self.ccy = ccy + self.value = value + return self + + def __repr__(self): + return 'Fiat(%s)'% self.__str__() + + def __str__(self): + if self.value.is_nan(): + return _('No Data') + else: + return "{:.2f}".format(self.value) + ' ' + self.ccy + +class MyEncoder(json.JSONEncoder): + def default(self, obj): + from .transaction import Transaction + if isinstance(obj, Transaction): + return obj.as_dict() + if isinstance(obj, Satoshis): + return str(obj) + if isinstance(obj, Fiat): + return str(obj) + if isinstance(obj, Decimal): + return str(obj) + if isinstance(obj, datetime): + return obj.isoformat(' ')[:-3] + if isinstance(obj, set): + return list(obj) + return super(MyEncoder, self).default(obj) + +class PrintError(object): + '''A handy base class''' + def diagnostic_name(self): + return self.__class__.__name__ + + def print_error(self, *msg): + # only prints with --verbose flag + print_error("[%s]" % self.diagnostic_name(), *msg) + + def print_stderr(self, *msg): + print_stderr("[%s]" % self.diagnostic_name(), *msg) + + def print_msg(self, *msg): + print_msg("[%s]" % self.diagnostic_name(), *msg) + +class ThreadJob(PrintError): + """A job that is run periodically from a thread's main loop. run() is + called from that thread's context. + """ + + def run(self): + """Called periodically from the thread""" + pass + +class DebugMem(ThreadJob): + '''A handy class for debugging GC memory leaks''' + def __init__(self, classes, interval=30): + self.next_time = 0 + self.classes = classes + self.interval = interval + + def mem_stats(self): + import gc + self.print_error("Start memscan") + gc.collect() + objmap = defaultdict(list) + for obj in gc.get_objects(): + for class_ in self.classes: + if isinstance(obj, class_): + objmap[class_].append(obj) + for class_, objs in objmap.items(): + self.print_error("%s: %d" % (class_.__name__, len(objs))) + self.print_error("Finish memscan") + + def run(self): + if time.time() > self.next_time: + self.mem_stats() + self.next_time = time.time() + self.interval + +class DaemonThread(threading.Thread, PrintError): + """ daemon thread that terminates cleanly """ + + def __init__(self): + threading.Thread.__init__(self) + self.parent_thread = threading.currentThread() + self.running = False + self.running_lock = threading.Lock() + self.job_lock = threading.Lock() + self.jobs = [] + + def add_jobs(self, jobs): + with self.job_lock: + self.jobs.extend(jobs) + + def run_jobs(self): + # Don't let a throwing job disrupt the thread, future runs of + # itself, or other jobs. This is useful protection against + # malformed or malicious server responses + with self.job_lock: + for job in self.jobs: + try: + job.run() + except Exception as e: + traceback.print_exc(file=sys.stderr) + + def remove_jobs(self, jobs): + with self.job_lock: + for job in jobs: + self.jobs.remove(job) + + def start(self): + with self.running_lock: + self.running = True + return threading.Thread.start(self) + + def is_running(self): + with self.running_lock: + return self.running and self.parent_thread.is_alive() + + def stop(self): + with self.running_lock: + self.running = False + + def on_stop(self): + if 'ANDROID_DATA' in os.environ: + import jnius + jnius.detach() + self.print_error("jnius detach") + self.print_error("stopped") + + +# TODO: disable +is_verbose = True +def set_verbosity(b): + global is_verbose + is_verbose = b + + +def print_error(*args): + if not is_verbose: return + print_stderr(*args) + +def print_stderr(*args): + args = [str(item) for item in args] + sys.stderr.write(" ".join(args) + "\n") + sys.stderr.flush() + +def print_msg(*args): + # Stringify args + args = [str(item) for item in args] + sys.stdout.write(" ".join(args) + "\n") + sys.stdout.flush() + +def json_encode(obj): + try: + s = json.dumps(obj, sort_keys = True, indent = 4, cls=MyEncoder) + except TypeError: + s = repr(obj) + return s + +def json_decode(x): + try: + return json.loads(x, parse_float=Decimal) + except: + return x + + +# taken from Django Source Code +def constant_time_compare(val1, val2): + """Return True if the two strings are equal, False otherwise.""" + return hmac.compare_digest(to_bytes(val1, 'utf8'), to_bytes(val2, 'utf8')) + + +# decorator that prints execution time +def profiler(func): + def do_profile(func, args, kw_args): + n = func.__name__ + t0 = time.time() + o = func(*args, **kw_args) + t = time.time() - t0 + print_error("[profiler]", n, "%.4f"%t) + return o + return lambda *args, **kw_args: do_profile(func, args, kw_args) + + +def android_ext_dir(): + import jnius + env = jnius.autoclass('android.os.Environment') + return env.getExternalStorageDirectory().getPath() + +def android_data_dir(): + import jnius + PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity') + return PythonActivity.mActivity.getFilesDir().getPath() + '/data' + +def android_headers_dir(): + d = android_ext_dir() + '/org.electrum.electrum' + if not os.path.exists(d): + try: + os.mkdir(d) + except FileExistsError: + pass # in case of race + return d + +def android_check_data_dir(): + """ if needed, move old directory to sandbox """ + ext_dir = android_ext_dir() + data_dir = android_data_dir() + old_electrum_dir = ext_dir + '/electrum' + if not os.path.exists(data_dir) and os.path.exists(old_electrum_dir): + import shutil + new_headers_path = android_headers_dir() + '/blockchain_headers' + old_headers_path = old_electrum_dir + '/blockchain_headers' + if not os.path.exists(new_headers_path) and os.path.exists(old_headers_path): + print_error("Moving headers file to", new_headers_path) + shutil.move(old_headers_path, new_headers_path) + print_error("Moving data to", data_dir) + shutil.move(old_electrum_dir, data_dir) + return data_dir + + +def get_headers_dir(config): + return android_headers_dir() if 'ANDROID_DATA' in os.environ else config.path + + +def assert_datadir_available(config_path): + path = config_path + if os.path.exists(path): + return + else: + raise FileNotFoundError( + 'Electrum datadir does not exist. Was it deleted while running?' + '\n' + + 'Should be at {}'.format(path)) + + +def assert_file_in_datadir_available(path, config_path): + if os.path.exists(path): + return + else: + assert_datadir_available(config_path) + raise FileNotFoundError( + 'Cannot find file but datadir is there.' + '\n' + + 'Should be at {}'.format(path)) + + +def assert_bytes(*args): + """ + porting helper, assert args type + """ + try: + for x in args: + assert isinstance(x, (bytes, bytearray)) + except: + print('assert bytes failed', list(map(type, args))) + raise + + +def assert_str(*args): + """ + porting helper, assert args type + """ + for x in args: + assert isinstance(x, str) + + + +def to_string(x, enc): + if isinstance(x, (bytes, bytearray)): + return x.decode(enc) + if isinstance(x, str): + return x + else: + raise TypeError("Not a string or bytes like object") + +def to_bytes(something, encoding='utf8'): + """ + cast string to bytes() like object, but for python2 support it's bytearray copy + """ + if isinstance(something, bytes): + return something + if isinstance(something, str): + return something.encode(encoding) + elif isinstance(something, bytearray): + return bytes(something) + else: + raise TypeError("Not a string or bytes like object") + + +bfh = bytes.fromhex +hfu = binascii.hexlify + + +def bh2u(x): + """ + str with hex representation of a bytes-like object + + >>> x = bytes((1, 2, 10)) + >>> bh2u(x) + '01020A' + + :param x: bytes + :rtype: str + """ + return hfu(x).decode('ascii') + + +def user_dir(): + if 'ANDROID_DATA' in os.environ: + return android_check_data_dir() + elif os.name == 'posix': + return os.path.join(os.environ["HOME"], ".electrum") + elif "APPDATA" in os.environ: + return os.path.join(os.environ["APPDATA"], "Electrum") + elif "LOCALAPPDATA" in os.environ: + return os.path.join(os.environ["LOCALAPPDATA"], "Electrum") + else: + #raise Exception("No home directory found in environment variables.") + return + +def is_valid_email(s): + regexp = r"[^@]+@[^@]+\.[^@]+" + return re.match(regexp, s) is not None + + +def format_satoshis_plain(x, decimal_point = 8): + """Display a satoshi amount scaled. Always uses a '.' as a decimal + point and has no thousands separator""" + scale_factor = pow(10, decimal_point) + return "{:.8f}".format(Decimal(x) / scale_factor).rstrip('0').rstrip('.') + + +def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=False, whitespaces=False): + from locale import localeconv + if x is None: + return 'unknown' + if precision is None: + precision = decimal_point + decimal_format = ".0" + str(precision) if precision > 0 else "" + if is_diff: + decimal_format = '+' + decimal_format + result = ("{:" + decimal_format + "f}").format(x / pow (10, decimal_point)).rstrip('0') + integer_part, fract_part = result.split(".") + dp = localeconv()['decimal_point'] + if len(fract_part) < num_zeros: + fract_part += "0" * (num_zeros - len(fract_part)) + result = integer_part + dp + fract_part + if whitespaces: + result += " " * (decimal_point - len(fract_part)) + result = " " * (15 - len(result)) + result + return result + + +FEERATE_PRECISION = 1 # num fractional decimal places for sat/byte fee rates +_feerate_quanta = Decimal(10) ** (-FEERATE_PRECISION) + + +def format_fee_satoshis(fee, num_zeros=0): + return format_satoshis(fee, num_zeros, 0, precision=FEERATE_PRECISION) + + +def quantize_feerate(fee): + """Strip sat/byte fee rate of excess precision.""" + if fee is None: + return None + return Decimal(fee).quantize(_feerate_quanta, rounding=decimal.ROUND_HALF_DOWN) + + +def timestamp_to_datetime(timestamp): + if timestamp is None: + return None + return datetime.fromtimestamp(timestamp) + +def format_time(timestamp): + date = timestamp_to_datetime(timestamp) + return date.isoformat(' ')[:-3] if date else _("Unknown") + + +# Takes a timestamp and returns a string with the approximation of the age +def age(from_date, since_date = None, target_tz=None, include_seconds=False): + if from_date is None: + return "Unknown" + + from_date = datetime.fromtimestamp(from_date) + if since_date is None: + since_date = datetime.now(target_tz) + + td = time_difference(from_date - since_date, include_seconds) + return td + " ago" if from_date < since_date else "in " + td + + +def time_difference(distance_in_time, include_seconds): + #distance_in_time = since_date - from_date + distance_in_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds))) + distance_in_minutes = int(round(distance_in_seconds/60)) + + if distance_in_minutes <= 1: + if include_seconds: + for remainder in [5, 10, 20]: + if distance_in_seconds < remainder: + return "less than %s seconds" % remainder + if distance_in_seconds < 40: + return "half a minute" + elif distance_in_seconds < 60: + return "less than a minute" + else: + return "1 minute" + else: + if distance_in_minutes == 0: + return "less than a minute" + else: + return "1 minute" + elif distance_in_minutes < 45: + return "%s minutes" % distance_in_minutes + elif distance_in_minutes < 90: + return "about 1 hour" + elif distance_in_minutes < 1440: + return "about %d hours" % (round(distance_in_minutes / 60.0)) + elif distance_in_minutes < 2880: + return "1 day" + elif distance_in_minutes < 43220: + return "%d days" % (round(distance_in_minutes / 1440)) + elif distance_in_minutes < 86400: + return "about 1 month" + elif distance_in_minutes < 525600: + return "%d months" % (round(distance_in_minutes / 43200)) + elif distance_in_minutes < 1051200: + return "about 1 year" + else: + return "over %d years" % (round(distance_in_minutes / 525600)) + +mainnet_block_explorers = { + 'Biteasy.com': ('https://www.biteasy.com/blockchain/', + {'tx': 'transactions/', 'addr': 'addresses/'}), + 'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/', + {'tx': 'Transaction/', 'addr': 'Address/'}), + 'Blockchain.info': ('https://blockchain.info/', + {'tx': 'tx/', 'addr': 'address/'}), + 'blockchainbdgpzk.onion': ('https://blockchainbdgpzk.onion/', + {'tx': 'tx/', 'addr': 'address/'}), + 'Blockr.io': ('https://btc.blockr.io/', + {'tx': 'tx/info/', 'addr': 'address/info/'}), + 'Blocktrail.com': ('https://www.blocktrail.com/BTC/', + {'tx': 'tx/', 'addr': 'address/'}), + 'BTC.com': ('https://chain.btc.com/', + {'tx': 'tx/', 'addr': 'address/'}), + 'Chain.so': ('https://www.chain.so/', + {'tx': 'tx/BTC/', 'addr': 'address/BTC/'}), + 'Insight.is': ('https://insight.bitpay.com/', + {'tx': 'tx/', 'addr': 'address/'}), + 'TradeBlock.com': ('https://tradeblock.com/blockchain/', + {'tx': 'tx/', 'addr': 'address/'}), + 'BlockCypher.com': ('https://live.blockcypher.com/btc/', + {'tx': 'tx/', 'addr': 'address/'}), + 'Blockchair.com': ('https://blockchair.com/bitcoin/', + {'tx': 'transaction/', 'addr': 'address/'}), + 'blockonomics.co': ('https://www.blockonomics.co/', + {'tx': 'api/tx?txid=', 'addr': '#/search?q='}), + 'OXT.me': ('https://oxt.me/', + {'tx': 'transaction/', 'addr': 'address/'}), + 'system default': ('blockchain:/', + {'tx': 'tx/', 'addr': 'address/'}), +} + +testnet_block_explorers = { + 'Blocktrail.com': ('https://www.blocktrail.com/tBTC/', + {'tx': 'tx/', 'addr': 'address/'}), + 'system default': ('blockchain://000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943/', + {'tx': 'tx/', 'addr': 'address/'}), +} + +def block_explorer_info(): + from . import constants + return testnet_block_explorers if constants.net.TESTNET else mainnet_block_explorers + +def block_explorer(config): + return config.get('block_explorer', 'Blocktrail.com') + +def block_explorer_tuple(config): + return block_explorer_info().get(block_explorer(config)) + +def block_explorer_URL(config, kind, item): + be_tuple = block_explorer_tuple(config) + if not be_tuple: + return + kind_str = be_tuple[1].get(kind) + if not kind_str: + return + url_parts = [be_tuple[0], kind_str, item] + return ''.join(url_parts) + +# URL decode +#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) +#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) + +def parse_URI(uri, on_pr=None): + from . import bitcoin + from .bitcoin import COIN + + if ':' not in uri: + if not bitcoin.is_address(uri): + raise Exception("Not a bitcoin address") + return {'address': uri} + + u = urllib.parse.urlparse(uri) + if u.scheme != 'bitcoin': + raise Exception("Not a bitcoin URI") + address = u.path + + # python for android fails to parse query + if address.find('?') > 0: + address, query = u.path.split('?') + pq = urllib.parse.parse_qs(query) + else: + pq = urllib.parse.parse_qs(u.query) + + for k, v in pq.items(): + if len(v)!=1: + raise Exception('Duplicate Key', k) + + out = {k: v[0] for k, v in pq.items()} + if address: + if not bitcoin.is_address(address): + raise Exception("Invalid bitcoin address:" + address) + out['address'] = address + if 'amount' in out: + am = out['amount'] + m = re.match('([0-9\.]+)X([0-9])', am) + if m: + k = int(m.group(2)) - 8 + amount = Decimal(m.group(1)) * pow( Decimal(10) , k) + else: + amount = Decimal(am) * COIN + out['amount'] = int(amount) + if 'message' in out: + out['message'] = out['message'] + out['memo'] = out['message'] + if 'time' in out: + out['time'] = int(out['time']) + if 'exp' in out: + out['exp'] = int(out['exp']) + if 'sig' in out: + out['sig'] = bh2u(bitcoin.base_decode(out['sig'], None, base=58)) + + r = out.get('r') + sig = out.get('sig') + name = out.get('name') + if on_pr and (r or (name and sig)): + def get_payment_request_thread(): + from . import paymentrequest as pr + if name and sig: + s = pr.serialize_request(out).SerializeToString() + request = pr.PaymentRequest(s) + else: + request = pr.get_payment_request(r) + if on_pr: + on_pr(request) + t = threading.Thread(target=get_payment_request_thread) + t.setDaemon(True) + t.start() + + return out + + +def create_URI(addr, amount, message): + from . import bitcoin + if not bitcoin.is_address(addr): + return "" + query = [] + if amount: + query.append('amount=%s'%format_satoshis_plain(amount)) + if message: + query.append('message=%s'%urllib.parse.quote(message)) + p = urllib.parse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='') + return urllib.parse.urlunparse(p) + + +# Python bug (http://bugs.python.org/issue1927) causes raw_input +# to be redirected improperly between stdin/stderr on Unix systems +#TODO: py3 +def raw_input(prompt=None): + if prompt: + sys.stdout.write(prompt) + return builtin_raw_input() + +import builtins +builtin_raw_input = builtins.input +builtins.input = raw_input + + +def parse_json(message): + # TODO: check \r\n pattern + n = message.find(b'\n') + if n==-1: + return None, message + try: + j = json.loads(message[0:n].decode('utf8')) + except: + j = None + return j, message[n+1:] + + +class timeout(Exception): + pass + +import socket +import json +import ssl +import time + + +class SocketPipe: + def __init__(self, socket): + self.socket = socket + self.message = b'' + self.set_timeout(0.1) + self.recv_time = time.time() + + def set_timeout(self, t): + self.socket.settimeout(t) + + def idle_time(self): + return time.time() - self.recv_time + + def get(self): + while True: + response, self.message = parse_json(self.message) + if response is not None: + return response + try: + data = self.socket.recv(1024) + except socket.timeout: + raise timeout + except ssl.SSLError: + raise timeout + except socket.error as err: + if err.errno == 60: + raise timeout + elif err.errno in [11, 35, 10035]: + print_error("socket errno %d (resource temporarily unavailable)"% err.errno) + time.sleep(0.2) + raise timeout + else: + print_error("pipe: socket error", err) + data = b'' + except: + traceback.print_exc(file=sys.stderr) + data = b'' + + if not data: # Connection closed remotely + return None + self.message += data + self.recv_time = time.time() + + def send(self, request): + out = json.dumps(request) + '\n' + out = out.encode('utf8') + self._send(out) + + def send_all(self, requests): + out = b''.join(map(lambda x: (json.dumps(x) + '\n').encode('utf8'), requests)) + self._send(out) + + def _send(self, out): + while out: + try: + sent = self.socket.send(out) + out = out[sent:] + except ssl.SSLError as e: + print_error("SSLError:", e) + time.sleep(0.1) + continue + + +class QueuePipe: + + def __init__(self, send_queue=None, get_queue=None): + self.send_queue = send_queue if send_queue else queue.Queue() + self.get_queue = get_queue if get_queue else queue.Queue() + self.set_timeout(0.1) + + def get(self): + try: + return self.get_queue.get(timeout=self.timeout) + except queue.Empty: + raise timeout + + def get_all(self): + responses = [] + while True: + try: + r = self.get_queue.get_nowait() + responses.append(r) + except queue.Empty: + break + return responses + + def set_timeout(self, t): + self.timeout = t + + def send(self, request): + self.send_queue.put(request) + + def send_all(self, requests): + for request in requests: + self.send(request) + + + + +def setup_thread_excepthook(): + """ + Workaround for `sys.excepthook` thread bug from: + http://bugs.python.org/issue1230540 + + Call once from the main thread before creating any threads. + """ + + init_original = threading.Thread.__init__ + + def init(self, *args, **kwargs): + + init_original(self, *args, **kwargs) + run_original = self.run + + def run_with_except_hook(*args2, **kwargs2): + try: + run_original(*args2, **kwargs2) + except Exception: + sys.excepthook(*sys.exc_info()) + + self.run = run_with_except_hook + + threading.Thread.__init__ = init + + +def versiontuple(v): + return tuple(map(int, (v.split(".")))) + + +def import_meta(path, validater, load_meta): + try: + with open(path, 'r', encoding='utf-8') as f: + d = validater(json.loads(f.read())) + load_meta(d) + #backwards compatibility for JSONDecodeError + except ValueError: + traceback.print_exc(file=sys.stderr) + raise FileImportFailed(_("Invalid JSON code.")) + except BaseException as e: + traceback.print_exc(file=sys.stdout) + raise FileImportFailed(e) + + +def export_meta(meta, fileName): + try: + with open(fileName, 'w+', encoding='utf-8') as f: + json.dump(meta, f, indent=4, sort_keys=True) + except (IOError, os.error) as e: + traceback.print_exc(file=sys.stderr) + raise FileExportFailed(e) + + +def make_dir(path, allow_symlink=True): + """Make directory if it does not yet exist.""" + if not os.path.exists(path): + if not allow_symlink and os.path.islink(path): + raise Exception('Dangling link: ' + path) + os.mkdir(path) + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) diff --git a/electrum/verifier.py b/electrum/verifier.py @@ -0,0 +1,158 @@ +# Electrum - Lightweight Bitcoin Client +# Copyright (c) 2012 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 .util import ThreadJob, bh2u +from .bitcoin import Hash, hash_decode, hash_encode +from .transaction import Transaction + + +class InnerNodeOfSpvProofIsValidTx(Exception): pass + + +class SPV(ThreadJob): + """ Simple Payment Verification """ + + def __init__(self, network, wallet): + self.wallet = wallet + self.network = network + self.blockchain = network.blockchain() + self.merkle_roots = {} # txid -> merkle root (once it has been verified) + self.requested_merkle = set() # txid set of pending requests + + def run(self): + interface = self.network.interface + if not interface: + return + + blockchain = interface.blockchain + if not blockchain: + return + + local_height = self.network.get_local_height() + unverified = self.wallet.get_unverified_txs() + for tx_hash, tx_height in unverified.items(): + # do not request merkle branch before headers are available + if tx_height <= 0 or tx_height > local_height: + continue + + header = blockchain.read_header(tx_height) + if header is None: + index = tx_height // 2016 + if index < len(blockchain.checkpoints): + self.network.request_chunk(interface, index) + elif (tx_hash not in self.requested_merkle + and tx_hash not in self.merkle_roots): + self.network.get_merkle_for_transaction( + tx_hash, + tx_height, + self.verify_merkle) + self.print_error('requested merkle', tx_hash) + self.requested_merkle.add(tx_hash) + + if self.network.blockchain() != self.blockchain: + self.blockchain = self.network.blockchain() + self.undo_verifications() + + def verify_merkle(self, response): + if self.wallet.verifier is None: + return # we have been killed, this was just an orphan callback + if response.get('error'): + self.print_error('received an error:', response) + return + params = response['params'] + merkle = response['result'] + # Verify the hash of the server-provided merkle branch to a + # transaction matches the merkle root of its block + tx_hash = params[0] + tx_height = merkle.get('block_height') + pos = merkle.get('pos') + try: + merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos) + except InnerNodeOfSpvProofIsValidTx: + self.print_error("merkle verification failed for {} (inner node looks like tx)" + .format(tx_hash)) + return + header = self.network.blockchain().read_header(tx_height) + # FIXME: if verification fails below, + # we should make a fresh connection to a server to + # recover from this, as this TX will now never verify + if not header: + self.print_error( + "merkle verification failed for {} (missing header {})" + .format(tx_hash, tx_height)) + return + if header.get('merkle_root') != merkle_root: + self.print_error( + "merkle verification failed for {} (merkle root mismatch {} != {})" + .format(tx_hash, header.get('merkle_root'), merkle_root)) + return + # we passed all the tests + self.merkle_roots[tx_hash] = merkle_root + try: + # note: we could pop in the beginning, but then we would request + # this proof again in case of verification failure from the same server + self.requested_merkle.remove(tx_hash) + except KeyError: pass + self.print_error("verified %s" % tx_hash) + self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos)) + if self.is_up_to_date() and self.wallet.is_up_to_date(): + self.wallet.save_verified_tx(write=True) + + @classmethod + def hash_merkle_root(cls, merkle_s, target_hash, pos): + h = hash_decode(target_hash) + for i in range(len(merkle_s)): + item = merkle_s[i] + h = Hash(hash_decode(item) + h) if ((pos >> i) & 1) else Hash(h + hash_decode(item)) + cls._raise_if_valid_tx(bh2u(h)) + return hash_encode(h) + + @classmethod + def _raise_if_valid_tx(cls, raw_tx: str): + # If an inner node of the merkle proof is also a valid tx, chances are, this is an attack. + # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-June/016105.html + # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/attachments/20180609/9f4f5b1f/attachment-0001.pdf + # https://bitcoin.stackexchange.com/questions/76121/how-is-the-leaf-node-weakness-in-merkle-trees-exploitable/76122#76122 + tx = Transaction(raw_tx) + try: + tx.deserialize() + except: + pass + else: + raise InnerNodeOfSpvProofIsValidTx() + + def undo_verifications(self): + height = self.blockchain.get_checkpoint() + tx_hashes = self.wallet.undo_verifications(self.blockchain, height) + for tx_hash in tx_hashes: + self.print_error("redoing", tx_hash) + self.remove_spv_proof_for_tx(tx_hash) + + def remove_spv_proof_for_tx(self, tx_hash): + self.merkle_roots.pop(tx_hash, None) + try: + self.requested_merkle.remove(tx_hash) + except KeyError: + pass + + def is_up_to_date(self): + return not self.requested_merkle diff --git a/electrum/version.py b/electrum/version.py @@ -0,0 +1,18 @@ +ELECTRUM_VERSION = '3.2.2' # version of the client package +APK_VERSION = '3.2.2.0' # read by buildozer.spec + +PROTOCOL_VERSION = '1.2' # protocol version requested + +# The hash of the mnemonic seed must begin with this +SEED_PREFIX = '01' # Standard wallet +SEED_PREFIX_2FA = '101' # Two-factor authentication +SEED_PREFIX_SW = '100' # Segwit wallet + + +def seed_prefix(seed_type): + if seed_type == 'standard': + return SEED_PREFIX + elif seed_type == 'segwit': + return SEED_PREFIX_SW + elif seed_type == '2fa': + return SEED_PREFIX_2FA diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -0,0 +1,2374 @@ +# 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. + +# Wallet classes: +# - Imported_Wallet: imported address, no keystore +# - Standard_Wallet: one keystore, P2PKH +# - Multisig_Wallet: several keystores, P2SH + + +import os +import threading +import random +import time +import json +import copy +import errno +import traceback +from functools import partial +from collections import defaultdict +from numbers import Number +from decimal import Decimal +import itertools + +import sys + +from .i18n import _ +from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, + format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, + TimeoutException, WalletFileException, BitcoinException, + InvalidPassword) + +from .bitcoin import * +from .version import * +from .keystore import load_keystore, Hardware_KeyStore +from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW + +from . import transaction, bitcoin, coinchooser, paymentrequest, contacts +from .transaction import Transaction +from .plugin import run_hook +from .synchronizer import Synchronizer +from .verifier import SPV + +from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED +from .paymentrequest import InvoiceStore +from .contacts import Contacts + +TX_STATUS = [ + _('Unconfirmed'), + _('Unconfirmed parent'), + _('Not Verified'), + _('Local'), +] + +TX_HEIGHT_LOCAL = -2 +TX_HEIGHT_UNCONF_PARENT = -1 +TX_HEIGHT_UNCONFIRMED = 0 + + +def relayfee(network): + from .simple_config import FEERATE_DEFAULT_RELAY + MAX_RELAY_FEE = 50000 + f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY + return min(f, MAX_RELAY_FEE) + +def dust_threshold(network): + # Change <= dust threshold is added to the tx fee + return 182 * 3 * relayfee(network) / 1000 + + +def append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax): + if txin_type != 'p2pk': + address = bitcoin.pubkey_to_address(txin_type, pubkey) + scripthash = bitcoin.address_to_scripthash(address) + else: + script = bitcoin.public_key_to_p2pk_script(pubkey) + scripthash = bitcoin.script_to_scripthash(script) + address = '(pubkey)' + + u = network.listunspent_for_scripthash(scripthash) + for item in u: + if len(inputs) >= imax: + break + item['address'] = address + item['type'] = txin_type + item['prevout_hash'] = item['tx_hash'] + item['prevout_n'] = int(item['tx_pos']) + item['pubkeys'] = [pubkey] + item['x_pubkeys'] = [pubkey] + item['signatures'] = [None] + item['num_sig'] = 1 + inputs.append(item) + +def sweep_preparations(privkeys, network, imax=100): + + def find_utxos_for_privkey(txin_type, privkey, compressed): + pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) + append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax) + keypairs[pubkey] = privkey, compressed + inputs = [] + keypairs = {} + for sec in privkeys: + txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) + find_utxos_for_privkey(txin_type, privkey, compressed) + # do other lookups to increase support coverage + if is_minikey(sec): + # minikeys don't have a compressed byte + # we lookup both compressed and uncompressed pubkeys + find_utxos_for_privkey(txin_type, privkey, not compressed) + elif txin_type == 'p2pkh': + # WIF serialization does not distinguish p2pkh and p2pk + # we also search for pay-to-pubkey outputs + find_utxos_for_privkey('p2pk', privkey, compressed) + if not inputs: + raise Exception(_('No inputs found. (Note that inputs need to be confirmed)')) + # FIXME actually inputs need not be confirmed now, see https://github.com/kyuupichan/electrumx/issues/365 + return inputs, keypairs + + +def sweep(privkeys, network, config, recipient, fee=None, imax=100): + inputs, keypairs = sweep_preparations(privkeys, network, imax) + total = sum(i.get('value') for i in inputs) + if fee is None: + outputs = [(TYPE_ADDRESS, recipient, total)] + tx = Transaction.from_io(inputs, outputs) + fee = config.estimate_fee(tx.estimated_size()) + if total - fee < 0: + raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee)) + if total - fee < dust_threshold(network): + raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network))) + + outputs = [(TYPE_ADDRESS, recipient, total - fee)] + locktime = network.get_local_height() + + tx = Transaction.from_io(inputs, outputs, locktime=locktime) + tx.BIP_LI01_sort() + tx.set_rbf(True) + tx.sign(keypairs) + return tx + + +class AddTransactionException(Exception): + pass + + +class UnrelatedTransactionException(AddTransactionException): + def __str__(self): + return _("Transaction is unrelated to this wallet.") + + +class CannotBumpFee(Exception): pass + + +class Abstract_Wallet(PrintError): + """ + Wallet classes are created to handle various address generation methods. + Completion states (watching-only, single account, no seed, etc) are handled inside classes. + """ + + max_change_outputs = 3 + + def __init__(self, storage): + self.electrum_version = ELECTRUM_VERSION + self.storage = storage + self.network = None + # verifier (SPV) and synchronizer are started in start_threads + self.synchronizer = None + self.verifier = None + + self.gap_limit_for_change = 6 # constant + + # locks: if you need to take multiple ones, acquire them in the order they are defined here! + self.lock = threading.RLock() + self.transaction_lock = threading.RLock() + + # saved fields + self.use_change = storage.get('use_change', True) + self.multiple_change = storage.get('multiple_change', False) + self.labels = storage.get('labels', {}) + self.frozen_addresses = set(storage.get('frozen_addresses',[])) + self.history = storage.get('addr_history',{}) # address -> list(txid, height) + self.fiat_value = storage.get('fiat_value', {}) + self.receive_requests = storage.get('payment_requests', {}) + + # Verified transactions. txid -> (height, timestamp, block_pos). Access with self.lock. + self.verified_tx = storage.get('verified_tx3', {}) + # Transactions pending verification. txid -> tx_height. Access with self.lock. + self.unverified_tx = defaultdict(int) + + self.load_keystore() + self.load_addresses() + self.test_addresses_sanity() + self.load_transactions() + self.load_local_history() + self.check_history() + self.load_unverified_transactions() + self.remove_local_transactions_we_dont_have() + + # wallet.up_to_date is true when the wallet is synchronized + self.up_to_date = False + + # save wallet type the first time + if self.storage.get('wallet_type') is None: + self.storage.put('wallet_type', self.wallet_type) + + # invoices and contacts + self.invoices = InvoiceStore(self.storage) + self.contacts = Contacts(self.storage) + + self.coin_price_cache = {} + + + def diagnostic_name(self): + return self.basename() + + def __str__(self): + return self.basename() + + def get_master_public_key(self): + return None + + @profiler + def load_transactions(self): + # load txi, txo, tx_fees + self.txi = self.storage.get('txi', {}) + for txid, d in list(self.txi.items()): + for addr, lst in d.items(): + self.txi[txid][addr] = set([tuple(x) for x in lst]) + self.txo = self.storage.get('txo', {}) + self.tx_fees = self.storage.get('tx_fees', {}) + tx_list = self.storage.get('transactions', {}) + # load transactions + self.transactions = {} + for tx_hash, raw in tx_list.items(): + tx = Transaction(raw) + self.transactions[tx_hash] = tx + if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None: + self.print_error("removing unreferenced tx", tx_hash) + self.transactions.pop(tx_hash) + # load spent_outpoints + _spent_outpoints = self.storage.get('spent_outpoints', {}) + self.spent_outpoints = defaultdict(dict) + for prevout_hash, d in _spent_outpoints.items(): + for prevout_n_str, spending_txid in d.items(): + prevout_n = int(prevout_n_str) + self.spent_outpoints[prevout_hash][prevout_n] = spending_txid + + @profiler + def load_local_history(self): + self._history_local = {} # address -> set(txid) + for txid in itertools.chain(self.txi, self.txo): + self._add_tx_to_local_history(txid) + + def remove_local_transactions_we_dont_have(self): + txid_set = set(self.txi) | set(self.txo) + for txid in txid_set: + tx_height = self.get_tx_height(txid)[0] + if tx_height == TX_HEIGHT_LOCAL and txid not in self.transactions: + self.remove_transaction(txid) + + @profiler + def save_transactions(self, write=False): + with self.transaction_lock: + tx = {} + for k,v in self.transactions.items(): + tx[k] = str(v) + self.storage.put('transactions', tx) + self.storage.put('txi', self.txi) + self.storage.put('txo', self.txo) + self.storage.put('tx_fees', self.tx_fees) + self.storage.put('addr_history', self.history) + self.storage.put('spent_outpoints', self.spent_outpoints) + if write: + self.storage.write() + + def save_verified_tx(self, write=False): + with self.lock: + self.storage.put('verified_tx3', self.verified_tx) + if write: + self.storage.write() + + def clear_history(self): + with self.lock: + with self.transaction_lock: + self.txi = {} + self.txo = {} + self.tx_fees = {} + self.spent_outpoints = defaultdict(dict) + self.history = {} + self.verified_tx = {} + self.transactions = {} + self.save_transactions() + + @profiler + def check_history(self): + save = False + + hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.history.keys())) + hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.history.keys())) + + for addr in hist_addrs_not_mine: + self.history.pop(addr) + save = True + + for addr in hist_addrs_mine: + hist = self.history[addr] + + for tx_hash, tx_height in hist: + if self.txi.get(tx_hash) or self.txo.get(tx_hash): + continue + tx = self.transactions.get(tx_hash) + if tx is not None: + self.add_transaction(tx_hash, tx, allow_unrelated=True) + save = True + if save: + self.save_transactions() + + def basename(self): + return os.path.basename(self.storage.path) + + def save_addresses(self): + self.storage.put('addresses', {'receiving':self.receiving_addresses, 'change':self.change_addresses}) + + def load_addresses(self): + d = self.storage.get('addresses', {}) + if type(d) != dict: d={} + self.receiving_addresses = d.get('receiving', []) + self.change_addresses = d.get('change', []) + + def test_addresses_sanity(self): + addrs = self.get_receiving_addresses() + if len(addrs) > 0: + if not bitcoin.is_address(addrs[0]): + raise WalletFileException('The addresses in this wallet are not bitcoin addresses.') + + def synchronize(self): + pass + + def is_deterministic(self): + return self.keystore.is_deterministic() + + def set_up_to_date(self, up_to_date): + with self.lock: + self.up_to_date = up_to_date + if up_to_date: + self.save_transactions(write=True) + # if the verifier is also up to date, persist that too; + # otherwise it will persist its results when it finishes + if self.verifier and self.verifier.is_up_to_date(): + self.save_verified_tx(write=True) + + def is_up_to_date(self): + with self.lock: return self.up_to_date + + def set_label(self, name, text = None): + changed = False + old_text = self.labels.get(name) + if text: + text = text.replace("\n", " ") + if old_text != text: + self.labels[name] = text + changed = True + else: + if old_text: + self.labels.pop(name) + changed = True + if changed: + run_hook('set_label', self, name, text) + self.storage.put('labels', self.labels) + return changed + + def set_fiat_value(self, txid, ccy, text): + if txid not in self.transactions: + return + if not text: + d = self.fiat_value.get(ccy, {}) + if d and txid in d: + d.pop(txid) + else: + return + else: + try: + Decimal(text) + except: + return + if ccy not in self.fiat_value: + self.fiat_value[ccy] = {} + self.fiat_value[ccy][txid] = text + self.storage.put('fiat_value', self.fiat_value) + + def get_fiat_value(self, txid, ccy): + fiat_value = self.fiat_value.get(ccy, {}).get(txid) + try: + return Decimal(fiat_value) + except: + return + + def is_mine(self, address): + return address in self.get_addresses() + + def is_change(self, address): + if not self.is_mine(address): + return False + return self.get_address_index(address)[0] + + def get_address_index(self, address): + raise NotImplementedError() + + def get_redeem_script(self, address): + return None + + def export_private_key(self, address, password): + if self.is_watching_only(): + return [] + index = self.get_address_index(address) + pk, compressed = self.keystore.get_private_key(index, password) + txin_type = self.get_txin_type(address) + redeem_script = self.get_redeem_script(address) + serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type) + return serialized_privkey, redeem_script + + def get_public_keys(self, address): + return [self.get_public_key(address)] + + def add_unverified_tx(self, tx_hash, tx_height): + if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \ + and tx_hash in self.verified_tx: + with self.lock: + self.verified_tx.pop(tx_hash) + if self.verifier: + self.verifier.remove_spv_proof_for_tx(tx_hash) + + # tx will be verified only if height > 0 + if tx_hash not in self.verified_tx: + with self.lock: + self.unverified_tx[tx_hash] = tx_height + + def add_verified_tx(self, tx_hash, info): + # Remove from the unverified map and add to the verified map + with self.lock: + self.unverified_tx.pop(tx_hash, None) + self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos) + height, conf, timestamp = self.get_tx_height(tx_hash) + self.network.trigger_callback('verified', tx_hash, height, conf, timestamp) + + def get_unverified_txs(self): + '''Returns a map from tx hash to transaction height''' + with self.lock: + return dict(self.unverified_tx) # copy + + def undo_verifications(self, blockchain, height): + '''Used by the verifier when a reorg has happened''' + txs = set() + with self.lock: + for tx_hash, item in list(self.verified_tx.items()): + tx_height, timestamp, pos = item + if tx_height >= height: + header = blockchain.read_header(tx_height) + # fixme: use block hash, not timestamp + if not header or header.get('timestamp') != timestamp: + self.verified_tx.pop(tx_hash, None) + txs.add(tx_hash) + return txs + + def get_local_height(self): + """ return last known height if we are offline """ + return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) + + def get_tx_height(self, tx_hash): + """ Given a transaction, returns (height, conf, timestamp) """ + with self.lock: + if tx_hash in self.verified_tx: + height, timestamp, pos = self.verified_tx[tx_hash] + conf = max(self.get_local_height() - height + 1, 0) + return height, conf, timestamp + elif tx_hash in self.unverified_tx: + height = self.unverified_tx[tx_hash] + return height, 0, None + else: + # local transaction + return TX_HEIGHT_LOCAL, 0, None + + def get_txpos(self, tx_hash): + "return position, even if the tx is unverified" + with self.lock: + if tx_hash in self.verified_tx: + height, timestamp, pos = self.verified_tx[tx_hash] + return height, pos + elif tx_hash in self.unverified_tx: + height = self.unverified_tx[tx_hash] + return (height, 0) if height > 0 else ((1e9 - height), 0) + else: + return (1e9+1, 0) + + def is_found(self): + return self.history.values() != [[]] * len(self.history) + + def get_num_tx(self, address): + """ return number of transactions where address is involved """ + return len(self.history.get(address, [])) + + def get_tx_delta(self, tx_hash, address): + "effect of tx on address" + delta = 0 + # substract the value of coins sent from address + d = self.txi.get(tx_hash, {}).get(address, []) + for n, v in d: + delta -= v + # add the value of the coins received at address + d = self.txo.get(tx_hash, {}).get(address, []) + for n, v, cb in d: + delta += v + return delta + + def get_tx_value(self, txid): + " effect of tx on the entire domain" + delta = 0 + for addr, d in self.txi.get(txid, {}).items(): + for n, v in d: + delta -= v + for addr, d in self.txo.get(txid, {}).items(): + for n, v, cb in d: + delta += v + return delta + + def get_wallet_delta(self, tx): + """ effect of tx on wallet """ + is_relevant = False # "related to wallet?" + is_mine = False + is_pruned = False + is_partial = False + v_in = v_out = v_out_mine = 0 + for txin in tx.inputs(): + addr = self.get_txin_address(txin) + if self.is_mine(addr): + is_mine = True + is_relevant = True + d = self.txo.get(txin['prevout_hash'], {}).get(addr, []) + for n, v, cb in d: + if n == txin['prevout_n']: + value = v + break + else: + value = None + if value is None: + is_pruned = True + else: + v_in += value + else: + is_partial = True + if not is_mine: + is_partial = False + for addr, value in tx.get_outputs(): + v_out += value + if self.is_mine(addr): + v_out_mine += value + is_relevant = True + if is_pruned: + # some inputs are mine: + fee = None + if is_mine: + v = v_out_mine - v_out + else: + # no input is mine + v = v_out_mine + else: + v = v_out_mine - v_in + if is_partial: + # some inputs are mine, but not all + fee = None + else: + # all inputs are mine + fee = v_in - v_out + if not is_mine: + fee = None + return is_relevant, is_mine, v, fee + + def get_tx_info(self, tx): + is_relevant, is_mine, v, fee = self.get_wallet_delta(tx) + exp_n = None + can_broadcast = False + can_bump = False + label = '' + height = conf = timestamp = None + tx_hash = tx.txid() + if tx.is_complete(): + if tx_hash in self.transactions.keys(): + label = self.get_label(tx_hash) + height, conf, timestamp = self.get_tx_height(tx_hash) + if height > 0: + if conf: + status = _("{} confirmations").format(conf) + else: + status = _('Not verified') + elif height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED): + status = _('Unconfirmed') + if fee is None: + fee = self.tx_fees.get(tx_hash) + if fee and self.network and self.network.config.has_fee_mempool(): + size = tx.estimated_size() + fee_per_byte = fee / size + exp_n = self.network.config.fee_to_depth(fee_per_byte) + can_bump = is_mine and not tx.is_final() + else: + status = _('Local') + can_broadcast = self.network is not None + else: + status = _("Signed") + can_broadcast = self.network is not None + else: + s, r = tx.signature_count() + status = _("Unsigned") if s == 0 else _('Partially signed') + ' (%d/%d)'%(s,r) + + if is_relevant: + if is_mine: + if fee is not None: + amount = v + fee + else: + amount = v + else: + amount = v + else: + amount = None + + return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n + + def get_addr_io(self, address): + h = self.get_address_history(address) + received = {} + sent = {} + for tx_hash, height in h: + l = self.txo.get(tx_hash, {}).get(address, []) + for n, v, is_cb in l: + received[tx_hash + ':%d'%n] = (height, v, is_cb) + for tx_hash, height in h: + l = self.txi.get(tx_hash, {}).get(address, []) + for txi, v in l: + sent[txi] = height + return received, sent + + def get_addr_utxo(self, address): + coins, spent = self.get_addr_io(address) + for txi in spent: + coins.pop(txi) + out = {} + for txo, v in coins.items(): + tx_height, value, is_cb = v + prevout_hash, prevout_n = txo.split(':') + x = { + 'address':address, + 'value':value, + 'prevout_n':int(prevout_n), + 'prevout_hash':prevout_hash, + 'height':tx_height, + 'coinbase':is_cb + } + out[txo] = x + return out + + # return the total amount ever received by an address + def get_addr_received(self, address): + received, sent = self.get_addr_io(address) + return sum([v for height, v, is_cb in received.values()]) + + # return the balance of a bitcoin address: confirmed and matured, unconfirmed, unmatured + def get_addr_balance(self, address): + received, sent = self.get_addr_io(address) + c = u = x = 0 + local_height = self.get_local_height() + for txo, (tx_height, v, is_cb) in received.items(): + if is_cb and tx_height + COINBASE_MATURITY > local_height: + x += v + elif tx_height > 0: + c += v + else: + u += v + if txo in sent: + if sent[txo] > 0: + c -= v + else: + u -= v + return c, u, x + + def get_spendable_coins(self, domain, config): + confirmed_only = config.get('confirmed_only', False) + return self.get_utxos(domain, exclude_frozen=True, mature=True, confirmed_only=confirmed_only) + + def get_utxos(self, domain = None, exclude_frozen = False, mature = False, confirmed_only = False): + coins = [] + if domain is None: + domain = self.get_addresses() + domain = set(domain) + if exclude_frozen: + domain = set(domain) - self.frozen_addresses + for addr in domain: + utxos = self.get_addr_utxo(addr) + for x in utxos.values(): + if confirmed_only and x['height'] <= 0: + continue + if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height(): + continue + coins.append(x) + continue + return coins + + def dummy_address(self): + return self.get_receiving_addresses()[0] + + def get_addresses(self): + out = [] + out += self.get_receiving_addresses() + out += self.get_change_addresses() + return out + + def get_frozen_balance(self): + return self.get_balance(self.frozen_addresses) + + def get_balance(self, domain=None): + if domain is None: + domain = self.get_addresses() + domain = set(domain) + cc = uu = xx = 0 + for addr in domain: + c, u, x = self.get_addr_balance(addr) + cc += c + uu += u + xx += x + return cc, uu, xx + + def get_address_history(self, addr): + h = [] + # we need self.transaction_lock but get_tx_height will take self.lock + # so we need to take that too here, to enforce order of locks + with self.lock, self.transaction_lock: + related_txns = self._history_local.get(addr, set()) + for tx_hash in related_txns: + tx_height = self.get_tx_height(tx_hash)[0] + h.append((tx_hash, tx_height)) + return h + + def _add_tx_to_local_history(self, txid): + with self.transaction_lock: + for addr in itertools.chain(self.txi.get(txid, []), self.txo.get(txid, [])): + cur_hist = self._history_local.get(addr, set()) + cur_hist.add(txid) + self._history_local[addr] = cur_hist + + def _remove_tx_from_local_history(self, txid): + with self.transaction_lock: + for addr in itertools.chain(self.txi.get(txid, []), self.txo.get(txid, [])): + cur_hist = self._history_local.get(addr, set()) + try: + cur_hist.remove(txid) + except KeyError: + pass + else: + self._history_local[addr] = cur_hist + + def get_txin_address(self, txi): + addr = txi.get('address') + if addr and addr != "(pubkey)": + return addr + prevout_hash = txi.get('prevout_hash') + prevout_n = txi.get('prevout_n') + dd = self.txo.get(prevout_hash, {}) + for addr, l in dd.items(): + for n, v, is_cb in l: + if n == prevout_n: + return addr + return None + + def get_txout_address(self, txo): + _type, x, v = txo + if _type == TYPE_ADDRESS: + addr = x + elif _type == TYPE_PUBKEY: + addr = bitcoin.public_key_to_p2pkh(bfh(x)) + else: + addr = None + return addr + + def get_conflicting_transactions(self, tx): + """Returns a set of transaction hashes from the wallet history that are + directly conflicting with tx, i.e. they have common outpoints being + spent with tx. If the tx is already in wallet history, that will not be + reported as a conflict. + """ + conflicting_txns = set() + with self.transaction_lock: + for txin in tx.inputs(): + if txin['type'] == 'coinbase': + continue + prevout_hash = txin['prevout_hash'] + prevout_n = txin['prevout_n'] + spending_tx_hash = self.spent_outpoints[prevout_hash].get(prevout_n) + if spending_tx_hash is None: + continue + # this outpoint has already been spent, by spending_tx + assert spending_tx_hash in self.transactions + conflicting_txns |= {spending_tx_hash} + txid = tx.txid() + if txid in conflicting_txns: + # this tx is already in history, so it conflicts with itself + if len(conflicting_txns) > 1: + raise Exception('Found conflicting transactions already in wallet history.') + conflicting_txns -= {txid} + return conflicting_txns + + def add_transaction(self, tx_hash, tx, allow_unrelated=False): + assert tx_hash, tx_hash + assert tx, tx + assert tx.is_complete() + # we need self.transaction_lock but get_tx_height will take self.lock + # so we need to take that too here, to enforce order of locks + with self.lock, self.transaction_lock: + # NOTE: returning if tx in self.transactions might seem like a good idea + # BUT we track is_mine inputs in a txn, and during subsequent calls + # of add_transaction tx, we might learn of more-and-more inputs of + # being is_mine, as we roll the gap_limit forward + is_coinbase = tx.inputs()[0]['type'] == 'coinbase' + tx_height = self.get_tx_height(tx_hash)[0] + if not allow_unrelated: + # note that during sync, if the transactions are not properly sorted, + # it could happen that we think tx is unrelated but actually one of the inputs is is_mine. + # this is the main motivation for allow_unrelated + is_mine = any([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()]) + is_for_me = any([self.is_mine(self.get_txout_address(txo)) for txo in tx.outputs()]) + if not is_mine and not is_for_me: + raise UnrelatedTransactionException() + # Find all conflicting transactions. + # In case of a conflict, + # 1. confirmed > mempool > local + # 2. this new txn has priority over existing ones + # When this method exits, there must NOT be any conflict, so + # either keep this txn and remove all conflicting (along with dependencies) + # or drop this txn + conflicting_txns = self.get_conflicting_transactions(tx) + if conflicting_txns: + existing_mempool_txn = any( + self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) + for tx_hash2 in conflicting_txns) + existing_confirmed_txn = any( + self.get_tx_height(tx_hash2)[0] > 0 + for tx_hash2 in conflicting_txns) + if existing_confirmed_txn and tx_height <= 0: + # this is a non-confirmed tx that conflicts with confirmed txns; drop. + return False + if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL: + # this is a local tx that conflicts with non-local txns; drop. + return False + # keep this txn and remove all conflicting + to_remove = set() + to_remove |= conflicting_txns + for conflicting_tx_hash in conflicting_txns: + to_remove |= self.get_depending_transactions(conflicting_tx_hash) + for tx_hash2 in to_remove: + self.remove_transaction(tx_hash2) + # add inputs + def add_value_from_prev_output(): + dd = self.txo.get(prevout_hash, {}) + # note: this nested loop takes linear time in num is_mine outputs of prev_tx + for addr, outputs in dd.items(): + # note: instead of [(n, v, is_cb), ...]; we could store: {n -> (v, is_cb)} + for n, v, is_cb in outputs: + if n == prevout_n: + if addr and self.is_mine(addr): + if d.get(addr) is None: + d[addr] = set() + d[addr].add((ser, v)) + return + self.txi[tx_hash] = d = {} + for txi in tx.inputs(): + if txi['type'] == 'coinbase': + continue + prevout_hash = txi['prevout_hash'] + prevout_n = txi['prevout_n'] + ser = prevout_hash + ':%d' % prevout_n + self.spent_outpoints[prevout_hash][prevout_n] = tx_hash + add_value_from_prev_output() + # add outputs + self.txo[tx_hash] = d = {} + for n, txo in enumerate(tx.outputs()): + v = txo[2] + ser = tx_hash + ':%d'%n + addr = self.get_txout_address(txo) + if addr and self.is_mine(addr): + if d.get(addr) is None: + d[addr] = [] + d[addr].append((n, v, is_coinbase)) + # give v to txi that spends me + next_tx = self.spent_outpoints[tx_hash].get(n) + if next_tx is not None: + dd = self.txi.get(next_tx, {}) + if dd.get(addr) is None: + dd[addr] = set() + if (ser, v) not in dd[addr]: + dd[addr].add((ser, v)) + self._add_tx_to_local_history(next_tx) + # add to local history + self._add_tx_to_local_history(tx_hash) + # save + self.transactions[tx_hash] = tx + return True + + def remove_transaction(self, tx_hash): + def remove_from_spent_outpoints(): + # undo spends in spent_outpoints + if tx is not None: # if we have the tx, this branch is faster + for txin in tx.inputs(): + if txin['type'] == 'coinbase': + continue + prevout_hash = txin['prevout_hash'] + prevout_n = txin['prevout_n'] + self.spent_outpoints[prevout_hash].pop(prevout_n, None) + if not self.spent_outpoints[prevout_hash]: + self.spent_outpoints.pop(prevout_hash) + else: # expensive but always works + for prevout_hash, d in list(self.spent_outpoints.items()): + for prevout_n, spending_txid in d.items(): + if spending_txid == tx_hash: + self.spent_outpoints[prevout_hash].pop(prevout_n, None) + if not self.spent_outpoints[prevout_hash]: + self.spent_outpoints.pop(prevout_hash) + # Remove this tx itself; if nothing spends from it. + # It is not so clear what to do if other txns spend from it, but it will be + # removed when those other txns are removed. + if not self.spent_outpoints[tx_hash]: + self.spent_outpoints.pop(tx_hash) + + with self.transaction_lock: + self.print_error("removing tx from history", tx_hash) + tx = self.transactions.pop(tx_hash, None) + remove_from_spent_outpoints() + self._remove_tx_from_local_history(tx_hash) + self.txi.pop(tx_hash, None) + self.txo.pop(tx_hash, None) + + def receive_tx_callback(self, tx_hash, tx, tx_height): + self.add_unverified_tx(tx_hash, tx_height) + self.add_transaction(tx_hash, tx, allow_unrelated=True) + + def receive_history_callback(self, addr, hist, tx_fees): + with self.lock: + old_hist = self.get_address_history(addr) + for tx_hash, height in old_hist: + if (tx_hash, height) not in hist: + # make tx local + self.unverified_tx.pop(tx_hash, None) + self.verified_tx.pop(tx_hash, None) + if self.verifier: + self.verifier.remove_spv_proof_for_tx(tx_hash) + self.history[addr] = hist + + for tx_hash, tx_height in hist: + # add it in case it was previously unconfirmed + self.add_unverified_tx(tx_hash, tx_height) + # if addr is new, we have to recompute txi and txo + tx = self.transactions.get(tx_hash) + if tx is None: + continue + self.add_transaction(tx_hash, tx, allow_unrelated=True) + + # Store fees + self.tx_fees.update(tx_fees) + + def get_history(self, domain=None): + # get domain + if domain is None: + domain = self.get_addresses() + domain = set(domain) + # 1. Get the history of each address in the domain, maintain the + # delta of a tx as the sum of its deltas on domain addresses + tx_deltas = defaultdict(int) + for addr in domain: + h = self.get_address_history(addr) + for tx_hash, height in h: + delta = self.get_tx_delta(tx_hash, addr) + if delta is None or tx_deltas[tx_hash] is None: + tx_deltas[tx_hash] = None + else: + tx_deltas[tx_hash] += delta + + # 2. create sorted history + history = [] + for tx_hash in tx_deltas: + delta = tx_deltas[tx_hash] + height, conf, timestamp = self.get_tx_height(tx_hash) + history.append((tx_hash, height, conf, timestamp, delta)) + history.sort(key = lambda x: self.get_txpos(x[0])) + history.reverse() + + # 3. add balance + c, u, x = self.get_balance(domain) + balance = c + u + x + h2 = [] + for tx_hash, height, conf, timestamp, delta in history: + h2.append((tx_hash, height, conf, timestamp, delta, balance)) + if balance is None or delta is None: + balance = None + else: + balance -= delta + h2.reverse() + + # fixme: this may happen if history is incomplete + if balance not in [None, 0]: + self.print_error("Error: history not synchronized") + return [] + + return h2 + + def balance_at_timestamp(self, domain, target_timestamp): + h = self.get_history(domain) + for tx_hash, height, conf, timestamp, value, balance in h: + if timestamp > target_timestamp: + return balance - value + # return last balance + return balance + + @profiler + def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False): + from .util import timestamp_to_datetime, Satoshis, Fiat + out = [] + income = 0 + expenditures = 0 + capital_gains = Decimal(0) + fiat_income = Decimal(0) + fiat_expenditures = Decimal(0) + h = self.get_history(domain) + for tx_hash, height, conf, timestamp, value, balance in h: + if from_timestamp and (timestamp or time.time()) < from_timestamp: + continue + if to_timestamp and (timestamp or time.time()) >= to_timestamp: + continue + item = { + 'txid':tx_hash, + 'height':height, + 'confirmations':conf, + 'timestamp':timestamp, + 'value': Satoshis(value), + 'balance': Satoshis(balance) + } + item['date'] = timestamp_to_datetime(timestamp) + item['label'] = self.get_label(tx_hash) + if show_addresses: + tx = self.transactions.get(tx_hash) + item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs())) + item['outputs'] = list(map(lambda x:{'address':x[0], 'value':Satoshis(x[1])}, tx.get_outputs())) + # value may be None if wallet is not fully synchronized + if value is None: + continue + # fixme: use in and out values + if value < 0: + expenditures += -value + else: + income += value + # fiat computations + if fx and fx.is_enabled(): + date = timestamp_to_datetime(timestamp) + fiat_value = self.get_fiat_value(tx_hash, fx.ccy) + fiat_default = fiat_value is None + fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) + item['fiat_value'] = Fiat(fiat_value, fx.ccy) + item['fiat_default'] = fiat_default + if value < 0: + acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy) + liquidation_price = - fiat_value + item['acquisition_price'] = Fiat(acquisition_price, fx.ccy) + cg = liquidation_price - acquisition_price + item['capital_gain'] = Fiat(cg, fx.ccy) + capital_gains += cg + fiat_expenditures += -fiat_value + else: + fiat_income += fiat_value + out.append(item) + # add summary + if out: + b, v = out[0]['balance'].value, out[0]['value'].value + start_balance = None if b is None or v is None else b - v + end_balance = out[-1]['balance'].value + if from_timestamp is not None and to_timestamp is not None: + start_date = timestamp_to_datetime(from_timestamp) + end_date = timestamp_to_datetime(to_timestamp) + else: + start_date = None + end_date = None + summary = { + 'start_date': start_date, + 'end_date': end_date, + 'start_balance': Satoshis(start_balance), + 'end_balance': Satoshis(end_balance), + 'income': Satoshis(income), + 'expenditures': Satoshis(expenditures) + } + if fx and fx.is_enabled(): + unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy) + summary['capital_gains'] = Fiat(capital_gains, fx.ccy) + summary['fiat_income'] = Fiat(fiat_income, fx.ccy) + summary['fiat_expenditures'] = Fiat(fiat_expenditures, fx.ccy) + summary['unrealized_gains'] = Fiat(unrealized, fx.ccy) + summary['start_fiat_balance'] = Fiat(fx.historical_value(start_balance, start_date), fx.ccy) + summary['end_fiat_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy) + summary['start_fiat_value'] = Fiat(fx.historical_value(COIN, start_date), fx.ccy) + summary['end_fiat_value'] = Fiat(fx.historical_value(COIN, end_date), fx.ccy) + else: + summary = {} + return { + 'transactions': out, + 'summary': summary + } + + def get_label(self, tx_hash): + label = self.labels.get(tx_hash, '') + if label is '': + label = self.get_default_label(tx_hash) + return label + + def get_default_label(self, tx_hash): + if self.txi.get(tx_hash) == {}: + d = self.txo.get(tx_hash, {}) + labels = [] + for addr in d.keys(): + label = self.labels.get(addr) + if label: + labels.append(label) + return ', '.join(labels) + return '' + + def get_tx_status(self, tx_hash, height, conf, timestamp): + from .util import format_time + extra = [] + if conf == 0: + tx = self.transactions.get(tx_hash) + if not tx: + return 2, 'unknown' + is_final = tx and tx.is_final() + if not is_final: + extra.append('rbf') + fee = self.get_wallet_delta(tx)[3] + if fee is None: + fee = self.tx_fees.get(tx_hash) + if fee is not None: + size = tx.estimated_size() + fee_per_byte = fee / size + extra.append(format_fee_satoshis(fee_per_byte) + ' sat/b') + if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \ + and self.network and self.network.config.has_fee_mempool(): + exp_n = self.network.config.fee_to_depth(fee_per_byte) + if exp_n: + extra.append('%.2f MB'%(exp_n/1000000)) + if height == TX_HEIGHT_LOCAL: + status = 3 + elif height == TX_HEIGHT_UNCONF_PARENT: + status = 1 + elif height == TX_HEIGHT_UNCONFIRMED: + status = 0 + else: + status = 2 + else: + status = 3 + min(conf, 6) + time_str = format_time(timestamp) if timestamp else _("unknown") + status_str = TX_STATUS[status] if status < 4 else time_str + if extra: + status_str += ' [%s]'%(', '.join(extra)) + return status, status_str + + def relayfee(self): + return relayfee(self.network) + + def dust_threshold(self): + return dust_threshold(self.network) + + def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, + change_addr=None, is_sweep=False): + # check outputs + i_max = None + for i, o in enumerate(outputs): + _type, data, value = o + if _type == TYPE_ADDRESS: + if not is_address(data): + raise Exception("Invalid bitcoin address: {}".format(data)) + if value == '!': + if i_max is not None: + raise Exception("More than one output set to spend max") + i_max = i + + # Avoid index-out-of-range with inputs[0] below + if not inputs: + raise NotEnoughFunds() + + if fixed_fee is None and config.fee_per_kb() is None: + raise NoDynamicFeeEstimates() + + for item in inputs: + self.add_input_info(item) + + # change address + if change_addr: + change_addrs = [change_addr] + else: + addrs = self.get_change_addresses()[-self.gap_limit_for_change:] + if self.use_change and addrs: + # New change addresses are created only after a few + # confirmations. Select the unused addresses within the + # gap limit; if none take one at random + change_addrs = [addr for addr in addrs if + self.get_num_tx(addr) == 0] + if not change_addrs: + change_addrs = [random.choice(addrs)] + else: + # coin_chooser will set change address + change_addrs = [] + + # Fee estimator + if fixed_fee is None: + fee_estimator = config.estimate_fee + elif isinstance(fixed_fee, Number): + fee_estimator = lambda size: fixed_fee + elif callable(fixed_fee): + fee_estimator = fixed_fee + else: + raise Exception('Invalid argument fixed_fee: %s' % fixed_fee) + + if i_max is None: + # Let the coin chooser select the coins to spend + max_change = self.max_change_outputs if self.multiple_change else 1 + coin_chooser = coinchooser.get_coin_chooser(config) + tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change], + fee_estimator, self.dust_threshold()) + else: + # FIXME?? this might spend inputs with negative effective value... + sendable = sum(map(lambda x:x['value'], inputs)) + _type, data, value = outputs[i_max] + outputs[i_max] = (_type, data, 0) + tx = Transaction.from_io(inputs, outputs[:]) + fee = fee_estimator(tx.estimated_size()) + amount = sendable - tx.output_value() - fee + if amount < 0: + raise NotEnoughFunds() + outputs[i_max] = (_type, data, amount) + tx = Transaction.from_io(inputs, outputs[:]) + + # Sort the inputs and outputs deterministically + tx.BIP_LI01_sort() + # Timelock tx to current height. + tx.locktime = self.get_local_height() + run_hook('make_unsigned_transaction', self, tx) + return tx + + def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None): + coins = self.get_spendable_coins(domain, config) + tx = self.make_unsigned_transaction(coins, outputs, config, fee, change_addr) + self.sign_transaction(tx, password) + return tx + + def is_frozen(self, addr): + return addr in self.frozen_addresses + + def set_frozen_state(self, addrs, freeze): + '''Set frozen state of the addresses to FREEZE, True or False''' + if all(self.is_mine(addr) for addr in addrs): + if freeze: + self.frozen_addresses |= set(addrs) + else: + self.frozen_addresses -= set(addrs) + self.storage.put('frozen_addresses', list(self.frozen_addresses)) + return True + return False + + def load_unverified_transactions(self): + # review transactions that are in the history + for addr, hist in self.history.items(): + for tx_hash, tx_height in hist: + # add it in case it was previously unconfirmed + self.add_unverified_tx(tx_hash, tx_height) + + def start_threads(self, network): + self.network = network + if self.network is not None: + self.verifier = SPV(self.network, self) + self.synchronizer = Synchronizer(self, network) + network.add_jobs([self.verifier, self.synchronizer]) + else: + self.verifier = None + self.synchronizer = None + + def stop_threads(self): + if self.network: + self.network.remove_jobs([self.synchronizer, self.verifier]) + self.synchronizer.release() + self.synchronizer = None + self.verifier = None + # Now no references to the synchronizer or verifier + # remain so they will be GC-ed + self.storage.put('stored_height', self.get_local_height()) + self.save_transactions() + self.save_verified_tx() + self.storage.write() + + def wait_until_synchronized(self, callback=None): + def wait_for_wallet(): + self.set_up_to_date(False) + while not self.is_up_to_date(): + if callback: + msg = "%s\n%s %d"%( + _("Please wait..."), + _("Addresses generated:"), + len(self.addresses(True))) + callback(msg) + time.sleep(0.1) + def wait_for_network(): + while not self.network.is_connected(): + if callback: + msg = "%s \n" % (_("Connecting...")) + callback(msg) + time.sleep(0.1) + # wait until we are connected, because the user + # might have selected another server + if self.network: + wait_for_network() + wait_for_wallet() + else: + self.synchronize() + + def can_export(self): + return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key') + + def is_used(self, address): + h = self.history.get(address,[]) + if len(h) == 0: + return False + c, u, x = self.get_addr_balance(address) + return c + u + x == 0 + + def is_empty(self, address): + c, u, x = self.get_addr_balance(address) + return c+u+x == 0 + + def address_is_old(self, address, age_limit=2): + age = -1 + h = self.history.get(address, []) + for tx_hash, tx_height in h: + if tx_height <= 0: + tx_age = 0 + else: + tx_age = self.get_local_height() - tx_height + 1 + if tx_age > age: + age = tx_age + return age > age_limit + + def bump_fee(self, tx, delta): + if tx.is_final(): + raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final')) + tx = Transaction(tx.serialize()) + tx.deserialize(force_full_parse=True) # need to parse inputs + inputs = copy.deepcopy(tx.inputs()) + outputs = copy.deepcopy(tx.outputs()) + for txin in inputs: + txin['signatures'] = [None] * len(txin['signatures']) + self.add_input_info(txin) + # use own outputs + s = list(filter(lambda x: self.is_mine(x[1]), outputs)) + # ... unless there is none + if not s: + s = outputs + x_fee = run_hook('get_tx_extra_fee', self, tx) + if x_fee: + x_fee_address, x_fee_amount = x_fee + s = filter(lambda x: x[1]!=x_fee_address, s) + + # prioritize low value outputs, to get rid of dust + s = sorted(s, key=lambda x: x[2]) + for o in s: + i = outputs.index(o) + otype, address, value = o + if value - delta >= self.dust_threshold(): + outputs[i] = otype, address, value - delta + delta = 0 + break + else: + del outputs[i] + delta -= value + if delta > 0: + continue + if delta > 0: + raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) + locktime = self.get_local_height() + tx_new = Transaction.from_io(inputs, outputs, locktime=locktime) + tx_new.BIP_LI01_sort() + return tx_new + + def cpfp(self, tx, fee): + txid = tx.txid() + for i, o in enumerate(tx.outputs()): + otype, address, value = o + if otype == TYPE_ADDRESS and self.is_mine(address): + break + else: + return + coins = self.get_addr_utxo(address) + item = coins.get(txid+':%d'%i) + if not item: + return + self.add_input_info(item) + inputs = [item] + outputs = [(TYPE_ADDRESS, address, value - fee)] + locktime = self.get_local_height() + # note: no need to call tx.BIP_LI01_sort() here - single input/output + return Transaction.from_io(inputs, outputs, locktime=locktime) + + def add_input_sig_info(self, txin, address): + raise NotImplementedError() # implemented by subclasses + + def add_input_info(self, txin): + address = txin['address'] + if self.is_mine(address): + txin['type'] = self.get_txin_type(address) + # segwit needs value to sign + if txin.get('value') is None and Transaction.is_input_value_needed(txin): + received, spent = self.get_addr_io(address) + item = received.get(txin['prevout_hash']+':%d'%txin['prevout_n']) + tx_height, value, is_cb = item + txin['value'] = value + self.add_input_sig_info(txin, address) + + def add_input_info_to_all_inputs(self, tx): + if tx.is_complete(): + return + for txin in tx.inputs(): + self.add_input_info(txin) + + def can_sign(self, tx): + if tx.is_complete(): + return False + # add info to inputs if we can; otherwise we might return a false negative: + self.add_input_info_to_all_inputs(tx) # though note that this is a side-effect + for k in self.get_keystores(): + if k.can_sign(tx): + return True + return False + + def get_input_tx(self, tx_hash, ignore_timeout=False): + # First look up an input transaction in the wallet where it + # will likely be. If co-signing a transaction it may not have + # all the input txs, in which case we ask the network. + tx = self.transactions.get(tx_hash, None) + if not tx and self.network: + try: + tx = Transaction(self.network.get_transaction(tx_hash)) + except TimeoutException as e: + self.print_error('getting input txn from network timed out for {}'.format(tx_hash)) + if not ignore_timeout: + raise e + return tx + + def add_hw_info(self, tx): + # add previous tx for hw wallets + for txin in tx.inputs(): + tx_hash = txin['prevout_hash'] + # segwit inputs might not be needed for some hw wallets + ignore_timeout = Transaction.is_segwit_input(txin) + txin['prev_tx'] = self.get_input_tx(tx_hash, ignore_timeout) + # add output info for hw wallets + info = {} + xpubs = self.get_master_public_keys() + for txout in tx.outputs(): + _type, addr, amount = txout + if self.is_mine(addr): + index = self.get_address_index(addr) + pubkeys = self.get_public_keys(addr) + # sort xpubs using the order of pubkeys + sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) + info[addr] = index, sorted_xpubs, self.m if isinstance(self, Multisig_Wallet) else None + tx.output_info = info + + def sign_transaction(self, tx, password): + if self.is_watching_only(): + return + self.add_input_info_to_all_inputs(tx) + # hardware wallets require extra info + if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]): + self.add_hw_info(tx) + # sign. start with ready keystores. + for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True): + try: + if k.can_sign(tx): + k.sign_transaction(tx, password) + except UserCancelled: + continue + return tx + + def get_unused_addresses(self): + # fixme: use slots from expired requests + domain = self.get_receiving_addresses() + return [addr for addr in domain if not self.history.get(addr) + and addr not in self.receive_requests.keys()] + + def get_unused_address(self): + addrs = self.get_unused_addresses() + if addrs: + return addrs[0] + + def get_receiving_address(self): + # always return an address + domain = self.get_receiving_addresses() + if not domain: + return + choice = domain[0] + for addr in domain: + if not self.history.get(addr): + if addr not in self.receive_requests.keys(): + return addr + else: + choice = addr + return choice + + def get_payment_status(self, address, amount): + local_height = self.get_local_height() + received, sent = self.get_addr_io(address) + l = [] + for txo, x in received.items(): + h, v, is_cb = x + txid, n = txo.split(':') + info = self.verified_tx.get(txid) + if info: + tx_height, timestamp, pos = info + conf = local_height - tx_height + else: + conf = 0 + l.append((conf, v)) + vsum = 0 + for conf, v in reversed(sorted(l)): + vsum += v + if vsum >= amount: + return True, conf + return False, None + + def get_payment_request(self, addr, config): + r = self.receive_requests.get(addr) + if not r: + return + out = copy.copy(r) + out['URI'] = 'bitcoin:' + addr + '?amount=' + format_satoshis(out.get('amount')) + status, conf = self.get_request_status(addr) + out['status'] = status + if conf is not None: + out['confirmations'] = conf + # check if bip70 file exists + rdir = config.get('requests_dir') + if rdir: + key = out.get('id', addr) + path = os.path.join(rdir, 'req', key[0], key[1], key) + if os.path.exists(path): + baseurl = 'file://' + rdir + rewrite = config.get('url_rewrite') + if rewrite: + try: + baseurl = baseurl.replace(*rewrite) + except BaseException as e: + self.print_stderr('Invalid config setting for "url_rewrite". err:', e) + out['request_url'] = os.path.join(baseurl, 'req', key[0], key[1], key, key) + out['URI'] += '&r=' + out['request_url'] + out['index_url'] = os.path.join(baseurl, 'index.html') + '?id=' + key + websocket_server_announce = config.get('websocket_server_announce') + if websocket_server_announce: + out['websocket_server'] = websocket_server_announce + else: + out['websocket_server'] = config.get('websocket_server', 'localhost') + websocket_port_announce = config.get('websocket_port_announce') + if websocket_port_announce: + out['websocket_port'] = websocket_port_announce + else: + out['websocket_port'] = config.get('websocket_port', 9999) + return out + + def get_request_status(self, key): + r = self.receive_requests.get(key) + if r is None: + return PR_UNKNOWN + address = r['address'] + amount = r.get('amount') + timestamp = r.get('time', 0) + if timestamp and type(timestamp) != int: + timestamp = 0 + expiration = r.get('exp') + if expiration and type(expiration) != int: + expiration = 0 + conf = None + if amount: + if self.up_to_date: + paid, conf = self.get_payment_status(address, amount) + status = PR_PAID if paid else PR_UNPAID + if status == PR_UNPAID and expiration is not None and time.time() > timestamp + expiration: + status = PR_EXPIRED + else: + status = PR_UNKNOWN + else: + status = PR_UNKNOWN + return status, conf + + def make_payment_request(self, addr, amount, message, expiration): + timestamp = int(time.time()) + _id = bh2u(Hash(addr + "%d"%timestamp))[0:10] + r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id} + return r + + def sign_payment_request(self, key, alias, alias_addr, password): + req = self.receive_requests.get(key) + alias_privkey = self.export_private_key(alias_addr, password)[0] + pr = paymentrequest.make_unsigned_request(req) + paymentrequest.sign_request_with_alias(pr, alias, alias_privkey) + req['name'] = pr.pki_data + req['sig'] = bh2u(pr.signature) + self.receive_requests[key] = req + self.storage.put('payment_requests', self.receive_requests) + + def add_payment_request(self, req, config): + addr = req['address'] + if not bitcoin.is_address(addr): + raise Exception(_('Invalid Bitcoin address.')) + if not self.is_mine(addr): + raise Exception(_('Address not in wallet.')) + + amount = req.get('amount') + message = req.get('memo') + self.receive_requests[addr] = req + self.storage.put('payment_requests', self.receive_requests) + self.set_label(addr, message) # should be a default label + + rdir = config.get('requests_dir') + if rdir and amount is not None: + key = req.get('id', addr) + pr = paymentrequest.make_request(config, req) + path = os.path.join(rdir, 'req', key[0], key[1], key) + if not os.path.exists(path): + try: + os.makedirs(path) + except OSError as exc: + if exc.errno != errno.EEXIST: + raise + with open(os.path.join(path, key), 'wb') as f: + f.write(pr.SerializeToString()) + # reload + req = self.get_payment_request(addr, config) + with open(os.path.join(path, key + '.json'), 'w', encoding='utf-8') as f: + f.write(json.dumps(req)) + return req + + def remove_payment_request(self, addr, config): + if addr not in self.receive_requests: + return False + r = self.receive_requests.pop(addr) + rdir = config.get('requests_dir') + if rdir: + key = r.get('id', addr) + for s in ['.json', '']: + n = os.path.join(rdir, 'req', key[0], key[1], key, key + s) + if os.path.exists(n): + os.unlink(n) + self.storage.put('payment_requests', self.receive_requests) + return True + + def get_sorted_requests(self, config): + def f(addr): + try: + return self.get_address_index(addr) + except: + return + keys = map(lambda x: (f(x), x), self.receive_requests.keys()) + sorted_keys = sorted(filter(lambda x: x[0] is not None, keys)) + return [self.get_payment_request(x[1], config) for x in sorted_keys] + + def get_fingerprint(self): + raise NotImplementedError() + + def can_import_privkey(self): + return False + + def can_import_address(self): + return False + + def can_delete_address(self): + return False + + def add_address(self, address): + if address not in self.history: + self.history[address] = [] + if self.synchronizer: + self.synchronizer.add(address) + + def has_password(self): + return self.has_keystore_encryption() or self.has_storage_encryption() + + def can_have_keystore_encryption(self): + return self.keystore and self.keystore.may_have_password() + + def get_available_storage_encryption_version(self): + """Returns the type of storage encryption offered to the user. + + A wallet file (storage) is either encrypted with this version + or is stored in plaintext. + """ + if isinstance(self.keystore, Hardware_KeyStore): + return STO_EV_XPUB_PW + else: + return STO_EV_USER_PW + + def has_keystore_encryption(self): + """Returns whether encryption is enabled for the keystore. + + If True, e.g. signing a transaction will require a password. + """ + if self.can_have_keystore_encryption(): + return self.storage.get('use_encryption', False) + return False + + def has_storage_encryption(self): + """Returns whether encryption is enabled for the wallet file on disk.""" + return self.storage.is_encrypted() + + @classmethod + def may_have_password(cls): + return True + + def check_password(self, password): + if self.has_keystore_encryption(): + self.keystore.check_password(password) + self.storage.check_password(password) + + def update_password(self, old_pw, new_pw, encrypt_storage=False): + if old_pw is None and self.has_password(): + raise InvalidPassword() + self.check_password(old_pw) + + if encrypt_storage: + enc_version = self.get_available_storage_encryption_version() + else: + enc_version = STO_EV_PLAINTEXT + self.storage.set_password(new_pw, enc_version) + + # note: Encrypting storage with a hw device is currently only + # allowed for non-multisig wallets. Further, + # Hardware_KeyStore.may_have_password() == False. + # If these were not the case, + # extra care would need to be taken when encrypting keystores. + self._update_password_for_keystore(old_pw, new_pw) + encrypt_keystore = self.can_have_keystore_encryption() + self.storage.set_keystore_encryption(bool(new_pw) and encrypt_keystore) + + self.storage.write() + + def sign_message(self, address, message, password): + index = self.get_address_index(address) + return self.keystore.sign_message(index, message, password) + + def decrypt_message(self, pubkey, message, password): + addr = self.pubkeys_to_address(pubkey) + index = self.get_address_index(addr) + return self.keystore.decrypt_message(index, message, password) + + def get_depending_transactions(self, tx_hash): + """Returns all (grand-)children of tx_hash in this wallet.""" + children = set() + # TODO rewrite this to use self.spent_outpoints + for other_hash, tx in self.transactions.items(): + for input in (tx.inputs()): + if input["prevout_hash"] == tx_hash: + children.add(other_hash) + children |= self.get_depending_transactions(other_hash) + return children + + def txin_value(self, txin): + txid = txin['prevout_hash'] + prev_n = txin['prevout_n'] + for address, d in self.txo.get(txid, {}).items(): + for n, v, cb in d: + if n == prev_n: + return v + # may occur if wallet is not synchronized + return None + + def price_at_timestamp(self, txid, price_func): + """Returns fiat price of bitcoin at the time tx got confirmed.""" + height, conf, timestamp = self.get_tx_height(txid) + return price_func(timestamp if timestamp else time.time()) + + def unrealized_gains(self, domain, price_func, ccy): + coins = self.get_utxos(domain) + now = time.time() + p = price_func(now) + ap = sum(self.coin_price(coin['prevout_hash'], price_func, ccy, self.txin_value(coin)) for coin in coins) + lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN) + return lp - ap + + def average_price(self, txid, price_func, ccy): + """ Average acquisition price of the inputs of a transaction """ + input_value = 0 + total_price = 0 + for addr, d in self.txi.get(txid, {}).items(): + for ser, v in d: + input_value += v + total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v) + return total_price / (input_value/Decimal(COIN)) + + def coin_price(self, txid, price_func, ccy, txin_value): + """ + Acquisition price of a coin. + This assumes that either all inputs are mine, or no input is mine. + """ + if txin_value is None: + return Decimal('NaN') + cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value)) + result = self.coin_price_cache.get(cache_key, None) + if result is not None: + return result + if self.txi.get(txid, {}) != {}: + result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN) + self.coin_price_cache[cache_key] = result + return result + else: + fiat_value = self.get_fiat_value(txid, ccy) + if fiat_value is not None: + return fiat_value + else: + p = self.price_at_timestamp(txid, price_func) + return p * txin_value/Decimal(COIN) + + def is_billing_address(self, addr): + # overloaded for TrustedCoin wallets + return False + + +class Simple_Wallet(Abstract_Wallet): + # wallet with a single keystore + + def get_keystore(self): + return self.keystore + + def get_keystores(self): + return [self.keystore] + + def is_watching_only(self): + return self.keystore.is_watching_only() + + def _update_password_for_keystore(self, old_pw, new_pw): + if self.keystore and self.keystore.may_have_password(): + self.keystore.update_password(old_pw, new_pw) + self.save_keystore() + + def save_keystore(self): + self.storage.put('keystore', self.keystore.dump()) + + +class Imported_Wallet(Simple_Wallet): + # wallet made of imported addresses + + wallet_type = 'imported' + txin_type = 'address' + + def __init__(self, storage): + Abstract_Wallet.__init__(self, storage) + + def is_watching_only(self): + return self.keystore is None + + def get_keystores(self): + return [self.keystore] if self.keystore else [] + + def can_import_privkey(self): + return bool(self.keystore) + + def load_keystore(self): + self.keystore = load_keystore(self.storage, 'keystore') if self.storage.get('keystore') else None + + def save_keystore(self): + self.storage.put('keystore', self.keystore.dump()) + + def load_addresses(self): + self.addresses = self.storage.get('addresses', {}) + # fixme: a reference to addresses is needed + if self.keystore: + self.keystore.addresses = self.addresses + + def save_addresses(self): + self.storage.put('addresses', self.addresses) + + def can_import_address(self): + return self.is_watching_only() + + def can_delete_address(self): + return True + + def has_seed(self): + return False + + def is_deterministic(self): + return False + + def is_change(self, address): + return False + + def get_master_public_keys(self): + return [] + + def is_beyond_limit(self, address): + return False + + def is_mine(self, address): + return address in self.addresses + + def get_fingerprint(self): + return '' + + def get_addresses(self, include_change=False): + return sorted(self.addresses.keys()) + + def get_receiving_addresses(self): + return self.get_addresses() + + def get_change_addresses(self): + return [] + + def import_address(self, address): + if not bitcoin.is_address(address): + return '' + if address in self.addresses: + return '' + self.addresses[address] = {} + self.storage.put('addresses', self.addresses) + self.storage.write() + self.add_address(address) + return address + + def delete_address(self, address): + if address not in self.addresses: + return + + transactions_to_remove = set() # only referred to by this address + transactions_new = set() # txs that are not only referred to by address + with self.lock: + for addr, details in self.history.items(): + if addr == address: + for tx_hash, height in details: + transactions_to_remove.add(tx_hash) + else: + for tx_hash, height in details: + transactions_new.add(tx_hash) + transactions_to_remove -= transactions_new + self.history.pop(address, None) + + for tx_hash in transactions_to_remove: + self.remove_transaction(tx_hash) + self.tx_fees.pop(tx_hash, None) + self.verified_tx.pop(tx_hash, None) + self.unverified_tx.pop(tx_hash, None) + self.transactions.pop(tx_hash, None) + self.storage.put('verified_tx3', self.verified_tx) + self.save_transactions() + + self.set_label(address, None) + self.remove_payment_request(address, {}) + self.set_frozen_state([address], False) + + pubkey = self.get_public_key(address) + self.addresses.pop(address) + if pubkey: + # delete key iff no other address uses it (e.g. p2pkh and p2wpkh for same key) + for txin_type in bitcoin.WIF_SCRIPT_TYPES.keys(): + try: + addr2 = bitcoin.pubkey_to_address(txin_type, pubkey) + except NotImplementedError: + pass + else: + if addr2 in self.addresses: + break + else: + self.keystore.delete_imported_key(pubkey) + self.save_keystore() + self.storage.put('addresses', self.addresses) + + self.storage.write() + + def get_address_index(self, address): + return self.get_public_key(address) + + def get_public_key(self, address): + return self.addresses[address].get('pubkey') + + def import_private_key(self, sec, pw, redeem_script=None): + try: + txin_type, pubkey = self.keystore.import_privkey(sec, pw) + except Exception: + neutered_privkey = str(sec)[:3] + '..' + str(sec)[-2:] + raise BitcoinException('Invalid private key: {}'.format(neutered_privkey)) + if txin_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: + if redeem_script is not None: + raise BitcoinException('Cannot use redeem script with script type {}'.format(txin_type)) + addr = bitcoin.pubkey_to_address(txin_type, pubkey) + elif txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: + if redeem_script is None: + raise BitcoinException('Redeem script required for script type {}'.format(txin_type)) + addr = bitcoin.redeem_script_to_address(txin_type, redeem_script) + else: + raise NotImplementedError(txin_type) + self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':redeem_script} + self.save_keystore() + self.save_addresses() + self.storage.write() + self.add_address(addr) + return addr + + def get_redeem_script(self, address): + d = self.addresses[address] + redeem_script = d['redeem_script'] + return redeem_script + + def get_txin_type(self, address): + return self.addresses[address].get('type', 'address') + + def add_input_sig_info(self, txin, address): + if self.is_watching_only(): + x_pubkey = 'fd' + address_to_script(address) + txin['x_pubkeys'] = [x_pubkey] + txin['signatures'] = [None] + return + if txin['type'] in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: + pubkey = self.addresses[address]['pubkey'] + txin['num_sig'] = 1 + txin['x_pubkeys'] = [pubkey] + txin['signatures'] = [None] + else: + raise NotImplementedError('imported wallets for p2sh are not implemented') + + def pubkeys_to_address(self, pubkey): + for addr, v in self.addresses.items(): + if v.get('pubkey') == pubkey: + return addr + +class Deterministic_Wallet(Abstract_Wallet): + + def __init__(self, storage): + Abstract_Wallet.__init__(self, storage) + self.gap_limit = storage.get('gap_limit', 20) + + def has_seed(self): + return self.keystore.has_seed() + + def get_receiving_addresses(self): + return self.receiving_addresses + + def get_change_addresses(self): + return self.change_addresses + + def get_seed(self, password): + return self.keystore.get_seed(password) + + def add_seed(self, seed, pw): + self.keystore.add_seed(seed, pw) + + def change_gap_limit(self, value): + '''This method is not called in the code, it is kept for console use''' + if value >= self.gap_limit: + self.gap_limit = value + self.storage.put('gap_limit', self.gap_limit) + return True + elif value >= self.min_acceptable_gap(): + addresses = self.get_receiving_addresses() + k = self.num_unused_trailing_addresses(addresses) + n = len(addresses) - k + value + self.receiving_addresses = self.receiving_addresses[0:n] + self.gap_limit = value + self.storage.put('gap_limit', self.gap_limit) + self.save_addresses() + return True + else: + return False + + def num_unused_trailing_addresses(self, addresses): + k = 0 + for a in addresses[::-1]: + if self.history.get(a):break + k = k + 1 + return k + + def min_acceptable_gap(self): + # fixme: this assumes wallet is synchronized + n = 0 + nmax = 0 + addresses = self.get_receiving_addresses() + k = self.num_unused_trailing_addresses(addresses) + for a in addresses[0:-k]: + if self.history.get(a): + n = 0 + else: + n += 1 + if n > nmax: nmax = n + return nmax + 1 + + def load_addresses(self): + super().load_addresses() + self._addr_to_addr_index = {} # key: address, value: (is_change, index) + for i, addr in enumerate(self.receiving_addresses): + self._addr_to_addr_index[addr] = (False, i) + for i, addr in enumerate(self.change_addresses): + self._addr_to_addr_index[addr] = (True, i) + + def create_new_address(self, for_change=False): + assert type(for_change) is bool + with self.lock: + addr_list = self.change_addresses if for_change else self.receiving_addresses + n = len(addr_list) + x = self.derive_pubkeys(for_change, n) + address = self.pubkeys_to_address(x) + addr_list.append(address) + self._addr_to_addr_index[address] = (for_change, n) + self.save_addresses() + self.add_address(address) + return address + + def synchronize_sequence(self, for_change): + limit = self.gap_limit_for_change if for_change else self.gap_limit + while True: + addresses = self.get_change_addresses() if for_change else self.get_receiving_addresses() + if len(addresses) < limit: + self.create_new_address(for_change) + continue + if list(map(lambda a: self.address_is_old(a), addresses[-limit:] )) == limit*[False]: + break + else: + self.create_new_address(for_change) + + def synchronize(self): + with self.lock: + self.synchronize_sequence(False) + self.synchronize_sequence(True) + + def is_beyond_limit(self, address): + is_change, i = self.get_address_index(address) + addr_list = self.get_change_addresses() if is_change else self.get_receiving_addresses() + limit = self.gap_limit_for_change if is_change else self.gap_limit + if i < limit: + return False + prev_addresses = addr_list[max(0, i - limit):max(0, i)] + for addr in prev_addresses: + if self.history.get(addr): + return False + return True + + def is_mine(self, address): + return address in self._addr_to_addr_index + + def get_address_index(self, address): + return self._addr_to_addr_index[address] + + def get_master_public_keys(self): + return [self.get_master_public_key()] + + def get_fingerprint(self): + return self.get_master_public_key() + + def get_txin_type(self, address): + return self.txin_type + + +class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet): + + """ Deterministic Wallet with a single pubkey per address """ + + def __init__(self, storage): + Deterministic_Wallet.__init__(self, storage) + + def get_public_key(self, address): + sequence = self.get_address_index(address) + pubkey = self.get_pubkey(*sequence) + return pubkey + + def load_keystore(self): + self.keystore = load_keystore(self.storage, 'keystore') + try: + xtype = bitcoin.xpub_type(self.keystore.xpub) + except: + xtype = 'standard' + self.txin_type = 'p2pkh' if xtype == 'standard' else xtype + + def get_pubkey(self, c, i): + return self.derive_pubkeys(c, i) + + def add_input_sig_info(self, txin, address): + derivation = self.get_address_index(address) + x_pubkey = self.keystore.get_xpubkey(*derivation) + txin['x_pubkeys'] = [x_pubkey] + txin['signatures'] = [None] + txin['num_sig'] = 1 + + def get_master_public_key(self): + return self.keystore.get_master_public_key() + + def derive_pubkeys(self, c, i): + return self.keystore.derive_pubkey(c, i) + + + + + + +class Standard_Wallet(Simple_Deterministic_Wallet): + wallet_type = 'standard' + + def pubkeys_to_address(self, pubkey): + return bitcoin.pubkey_to_address(self.txin_type, pubkey) + + +class Multisig_Wallet(Deterministic_Wallet): + # generic m of n + gap_limit = 20 + + def __init__(self, storage): + self.wallet_type = storage.get('wallet_type') + self.m, self.n = multisig_type(self.wallet_type) + Deterministic_Wallet.__init__(self, storage) + + def get_pubkeys(self, c, i): + return self.derive_pubkeys(c, i) + + def get_public_keys(self, address): + sequence = self.get_address_index(address) + return self.get_pubkeys(*sequence) + + def pubkeys_to_address(self, pubkeys): + redeem_script = self.pubkeys_to_redeem_script(pubkeys) + return bitcoin.redeem_script_to_address(self.txin_type, redeem_script) + + def pubkeys_to_redeem_script(self, pubkeys): + return transaction.multisig_script(sorted(pubkeys), self.m) + + def get_redeem_script(self, address): + pubkeys = self.get_public_keys(address) + redeem_script = self.pubkeys_to_redeem_script(pubkeys) + return redeem_script + + def derive_pubkeys(self, c, i): + return [k.derive_pubkey(c, i) for k in self.get_keystores()] + + def load_keystore(self): + self.keystores = {} + for i in range(self.n): + name = 'x%d/'%(i+1) + self.keystores[name] = load_keystore(self.storage, name) + self.keystore = self.keystores['x1/'] + xtype = bitcoin.xpub_type(self.keystore.xpub) + self.txin_type = 'p2sh' if xtype == 'standard' else xtype + + def save_keystore(self): + for name, k in self.keystores.items(): + self.storage.put(name, k.dump()) + + def get_keystore(self): + return self.keystores.get('x1/') + + def get_keystores(self): + return [self.keystores[i] for i in sorted(self.keystores.keys())] + + def can_have_keystore_encryption(self): + return any([k.may_have_password() for k in self.get_keystores()]) + + def _update_password_for_keystore(self, old_pw, new_pw): + for name, keystore in self.keystores.items(): + if keystore.may_have_password(): + keystore.update_password(old_pw, new_pw) + self.storage.put(name, keystore.dump()) + + def check_password(self, password): + for name, keystore in self.keystores.items(): + if keystore.may_have_password(): + keystore.check_password(password) + self.storage.check_password(password) + + def get_available_storage_encryption_version(self): + # multisig wallets are not offered hw device encryption + return STO_EV_USER_PW + + def has_seed(self): + return self.keystore.has_seed() + + def is_watching_only(self): + return not any([not k.is_watching_only() for k in self.get_keystores()]) + + def get_master_public_key(self): + return self.keystore.get_master_public_key() + + def get_master_public_keys(self): + return [k.get_master_public_key() for k in self.get_keystores()] + + def get_fingerprint(self): + return ''.join(sorted(self.get_master_public_keys())) + + def add_input_sig_info(self, txin, address): + # x_pubkeys are not sorted here because it would be too slow + # they are sorted in transaction.get_sorted_pubkeys + # pubkeys is set to None to signal that x_pubkeys are unsorted + derivation = self.get_address_index(address) + x_pubkeys_expected = [k.get_xpubkey(*derivation) for k in self.get_keystores()] + x_pubkeys_actual = txin.get('x_pubkeys') + # if 'x_pubkeys' is already set correctly (ignoring order, as above), leave it. + # otherwise we might delete signatures + if x_pubkeys_actual and set(x_pubkeys_actual) == set(x_pubkeys_expected): + return + txin['x_pubkeys'] = x_pubkeys_expected + txin['pubkeys'] = None + # we need n place holders + txin['signatures'] = [None] * self.n + txin['num_sig'] = self.m + + +wallet_types = ['standard', 'multisig', 'imported'] + +def register_wallet_type(category): + wallet_types.append(category) + +wallet_constructors = { + 'standard': Standard_Wallet, + 'old': Standard_Wallet, + 'xpub': Standard_Wallet, + 'imported': Imported_Wallet +} + +def register_constructor(wallet_type, constructor): + wallet_constructors[wallet_type] = constructor + +# former WalletFactory +class Wallet(object): + """The main wallet "entry point". + This class is actually a factory that will return a wallet of the correct + type when passed a WalletStorage instance.""" + + def __new__(self, storage): + wallet_type = storage.get('wallet_type') + WalletClass = Wallet.wallet_class(wallet_type) + wallet = WalletClass(storage) + # Convert hardware wallets restored with older versions of + # Electrum to BIP44 wallets. A hardware wallet does not have + # a seed and plugins do not need to handle having one. + rwc = getattr(wallet, 'restore_wallet_class', None) + if rwc and storage.get('seed', ''): + storage.print_error("converting wallet type to " + rwc.wallet_type) + storage.put('wallet_type', rwc.wallet_type) + wallet = rwc(storage) + return wallet + + @staticmethod + def wallet_class(wallet_type): + if multisig_type(wallet_type): + return Multisig_Wallet + if wallet_type in wallet_constructors: + return wallet_constructors[wallet_type] + raise RuntimeError("Unknown wallet type: " + str(wallet_type)) diff --git a/electrum/websockets.py b/electrum/websockets.py @@ -0,0 +1,140 @@ +#!/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 queue +import threading, os, json +from collections import defaultdict +try: + from SimpleWebSocketServer import WebSocket, SimpleSSLWebSocketServer +except ImportError: + import sys + sys.exit("install SimpleWebSocketServer") + +from . import util +from . import bitcoin + +request_queue = queue.Queue() + +class ElectrumWebSocket(WebSocket): + + def handleMessage(self): + assert self.data[0:3] == 'id:' + util.print_error("message received", self.data) + request_id = self.data[3:] + request_queue.put((self, request_id)) + + def handleConnected(self): + util.print_error("connected", self.address) + + def handleClose(self): + util.print_error("closed", self.address) + + + +class WsClientThread(util.DaemonThread): + + def __init__(self, config, network): + util.DaemonThread.__init__(self) + self.network = network + self.config = config + self.response_queue = queue.Queue() + self.subscriptions = defaultdict(list) + + def make_request(self, request_id): + # read json file + rdir = self.config.get('requests_dir') + n = os.path.join(rdir, 'req', request_id[0], request_id[1], request_id, request_id + '.json') + with open(n, encoding='utf-8') as f: + s = f.read() + d = json.loads(s) + addr = d.get('address') + amount = d.get('amount') + return addr, amount + + def reading_thread(self): + while self.is_running(): + try: + ws, request_id = request_queue.get() + except queue.Empty: + continue + try: + addr, amount = self.make_request(request_id) + except: + continue + l = self.subscriptions.get(addr, []) + l.append((ws, amount)) + self.subscriptions[addr] = l + self.network.subscribe_to_addresses([addr], self.response_queue.put) + + def run(self): + threading.Thread(target=self.reading_thread).start() + while self.is_running(): + try: + r = self.response_queue.get(timeout=0.1) + except queue.Empty: + continue + util.print_error('response', r) + method = r.get('method') + result = r.get('result') + if result is None: + continue + if method == 'blockchain.scripthash.subscribe': + addr = r.get('params')[0] + scripthash = bitcoin.address_to_scripthash(addr) + self.network.get_balance_for_scripthash( + scripthash, self.response_queue.put) + elif method == 'blockchain.scripthash.get_balance': + scripthash = r.get('params')[0] + addr = self.network.h2addr.get(scripthash, None) + if addr is None: + util.print_error( + "can't find address for scripthash: %s" % scripthash) + l = self.subscriptions.get(addr, []) + for ws, amount in l: + if not ws.closed: + if sum(result.values()) >=amount: + ws.sendMessage('paid') + + + +class WebSocketServer(threading.Thread): + + def __init__(self, config, ns): + threading.Thread.__init__(self) + self.config = config + self.net_server = ns + self.daemon = True + + def run(self): + t = WsClientThread(self.config, self.net_server) + t.start() + + host = self.config.get('websocket_server') + port = self.config.get('websocket_port', 9999) + certfile = self.config.get('ssl_chain') + keyfile = self.config.get('ssl_privkey') + self.server = SimpleSSLWebSocketServer(host, port, ElectrumWebSocket, certfile, keyfile) + self.server.serveforever() + + diff --git a/electrum/wordlist/chinese_simplified.txt b/electrum/wordlist/chinese_simplified.txt @@ -0,0 +1,2048 @@ +的 +一 +是 +在 +不 +了 +有 +和 +人 +这 +中 +大 +为 +上 +个 +国 +我 +以 +要 +他 +时 +来 +用 +们 +生 +到 +作 +地 +于 +出 +就 +分 +对 +成 +会 +可 +主 +发 +年 +动 +同 +工 +也 +能 +下 +过 +子 +说 +产 +种 +面 +而 +方 +后 +多 +定 +行 +学 +法 +所 +民 +得 +经 +十 +三 +之 +进 +着 +等 +部 +度 +家 +电 +力 +里 +如 +水 +化 +高 +自 +二 +理 +起 +小 +物 +现 +实 +加 +量 +都 +两 +体 +制 +机 +当 +使 +点 +从 +业 +本 +去 +把 +性 +好 +应 +开 +它 +合 +还 +因 +由 +其 +些 +然 +前 +外 +天 +政 +四 +日 +那 +社 +义 +事 +平 +形 +相 +全 +表 +间 +样 +与 +关 +各 +重 +新 +线 +内 +数 +正 +心 +反 +你 +明 +看 +原 +又 +么 +利 +比 +或 +但 +质 +气 +第 +向 +道 +命 +此 +变 +条 +只 +没 +结 +解 +问 +意 +建 +月 +公 +无 +系 +军 +很 +情 +者 +最 +立 +代 +想 +已 +通 +并 +提 +直 +题 +党 +程 +展 +五 +果 +料 +象 +员 +革 +位 +入 +常 +文 +总 +次 +品 +式 +活 +设 +及 +管 +特 +件 +长 +求 +老 +头 +基 +资 +边 +流 +路 +级 +少 +图 +山 +统 +接 +知 +较 +将 +组 +见 +计 +别 +她 +手 +角 +期 +根 +论 +运 +农 +指 +几 +九 +区 +强 +放 +决 +西 +被 +干 +做 +必 +战 +先 +回 +则 +任 +取 +据 +处 +队 +南 +给 +色 +光 +门 +即 +保 +治 +北 +造 +百 +规 +热 +领 +七 +海 +口 +东 +导 +器 +压 +志 +世 +金 +增 +争 +济 +阶 +油 +思 +术 +极 +交 +受 +联 +什 +认 +六 +共 +权 +收 +证 +改 +清 +美 +再 +采 +转 +更 +单 +风 +切 +打 +白 +教 +速 +花 +带 +安 +场 +身 +车 +例 +真 +务 +具 +万 +每 +目 +至 +达 +走 +积 +示 +议 +声 +报 +斗 +完 +类 +八 +离 +华 +名 +确 +才 +科 +张 +信 +马 +节 +话 +米 +整 +空 +元 +况 +今 +集 +温 +传 +土 +许 +步 +群 +广 +石 +记 +需 +段 +研 +界 +拉 +林 +律 +叫 +且 +究 +观 +越 +织 +装 +影 +算 +低 +持 +音 +众 +书 +布 +复 +容 +儿 +须 +际 +商 +非 +验 +连 +断 +深 +难 +近 +矿 +千 +周 +委 +素 +技 +备 +半 +办 +青 +省 +列 +习 +响 +约 +支 +般 +史 +感 +劳 +便 +团 +往 +酸 +历 +市 +克 +何 +除 +消 +构 +府 +称 +太 +准 +精 +值 +号 +率 +族 +维 +划 +选 +标 +写 +存 +候 +毛 +亲 +快 +效 +斯 +院 +查 +江 +型 +眼 +王 +按 +格 +养 +易 +置 +派 +层 +片 +始 +却 +专 +状 +育 +厂 +京 +识 +适 +属 +圆 +包 +火 +住 +调 +满 +县 +局 +照 +参 +红 +细 +引 +听 +该 +铁 +价 +严 +首 +底 +液 +官 +德 +随 +病 +苏 +失 +尔 +死 +讲 +配 +女 +黄 +推 +显 +谈 +罪 +神 +艺 +呢 +席 +含 +企 +望 +密 +批 +营 +项 +防 +举 +球 +英 +氧 +势 +告 +李 +台 +落 +木 +帮 +轮 +破 +亚 +师 +围 +注 +远 +字 +材 +排 +供 +河 +态 +封 +另 +施 +减 +树 +溶 +怎 +止 +案 +言 +士 +均 +武 +固 +叶 +鱼 +波 +视 +仅 +费 +紧 +爱 +左 +章 +早 +朝 +害 +续 +轻 +服 +试 +食 +充 +兵 +源 +判 +护 +司 +足 +某 +练 +差 +致 +板 +田 +降 +黑 +犯 +负 +击 +范 +继 +兴 +似 +余 +坚 +曲 +输 +修 +故 +城 +夫 +够 +送 +笔 +船 +占 +右 +财 +吃 +富 +春 +职 +觉 +汉 +画 +功 +巴 +跟 +虽 +杂 +飞 +检 +吸 +助 +升 +阳 +互 +初 +创 +抗 +考 +投 +坏 +策 +古 +径 +换 +未 +跑 +留 +钢 +曾 +端 +责 +站 +简 +述 +钱 +副 +尽 +帝 +射 +草 +冲 +承 +独 +令 +限 +阿 +宣 +环 +双 +请 +超 +微 +让 +控 +州 +良 +轴 +找 +否 +纪 +益 +依 +优 +顶 +础 +载 +倒 +房 +突 +坐 +粉 +敌 +略 +客 +袁 +冷 +胜 +绝 +析 +块 +剂 +测 +丝 +协 +诉 +念 +陈 +仍 +罗 +盐 +友 +洋 +错 +苦 +夜 +刑 +移 +频 +逐 +靠 +混 +母 +短 +皮 +终 +聚 +汽 +村 +云 +哪 +既 +距 +卫 +停 +烈 +央 +察 +烧 +迅 +境 +若 +印 +洲 +刻 +括 +激 +孔 +搞 +甚 +室 +待 +核 +校 +散 +侵 +吧 +甲 +游 +久 +菜 +味 +旧 +模 +湖 +货 +损 +预 +阻 +毫 +普 +稳 +乙 +妈 +植 +息 +扩 +银 +语 +挥 +酒 +守 +拿 +序 +纸 +医 +缺 +雨 +吗 +针 +刘 +啊 +急 +唱 +误 +训 +愿 +审 +附 +获 +茶 +鲜 +粮 +斤 +孩 +脱 +硫 +肥 +善 +龙 +演 +父 +渐 +血 +欢 +械 +掌 +歌 +沙 +刚 +攻 +谓 +盾 +讨 +晚 +粒 +乱 +燃 +矛 +乎 +杀 +药 +宁 +鲁 +贵 +钟 +煤 +读 +班 +伯 +香 +介 +迫 +句 +丰 +培 +握 +兰 +担 +弦 +蛋 +沉 +假 +穿 +执 +答 +乐 +谁 +顺 +烟 +缩 +征 +脸 +喜 +松 +脚 +困 +异 +免 +背 +星 +福 +买 +染 +井 +概 +慢 +怕 +磁 +倍 +祖 +皇 +促 +静 +补 +评 +翻 +肉 +践 +尼 +衣 +宽 +扬 +棉 +希 +伤 +操 +垂 +秋 +宜 +氢 +套 +督 +振 +架 +亮 +末 +宪 +庆 +编 +牛 +触 +映 +雷 +销 +诗 +座 +居 +抓 +裂 +胞 +呼 +娘 +景 +威 +绿 +晶 +厚 +盟 +衡 +鸡 +孙 +延 +危 +胶 +屋 +乡 +临 +陆 +顾 +掉 +呀 +灯 +岁 +措 +束 +耐 +剧 +玉 +赵 +跳 +哥 +季 +课 +凯 +胡 +额 +款 +绍 +卷 +齐 +伟 +蒸 +殖 +永 +宗 +苗 +川 +炉 +岩 +弱 +零 +杨 +奏 +沿 +露 +杆 +探 +滑 +镇 +饭 +浓 +航 +怀 +赶 +库 +夺 +伊 +灵 +税 +途 +灭 +赛 +归 +召 +鼓 +播 +盘 +裁 +险 +康 +唯 +录 +菌 +纯 +借 +糖 +盖 +横 +符 +私 +努 +堂 +域 +枪 +润 +幅 +哈 +竟 +熟 +虫 +泽 +脑 +壤 +碳 +欧 +遍 +侧 +寨 +敢 +彻 +虑 +斜 +薄 +庭 +纳 +弹 +饲 +伸 +折 +麦 +湿 +暗 +荷 +瓦 +塞 +床 +筑 +恶 +户 +访 +塔 +奇 +透 +梁 +刀 +旋 +迹 +卡 +氯 +遇 +份 +毒 +泥 +退 +洗 +摆 +灰 +彩 +卖 +耗 +夏 +择 +忙 +铜 +献 +硬 +予 +繁 +圈 +雪 +函 +亦 +抽 +篇 +阵 +阴 +丁 +尺 +追 +堆 +雄 +迎 +泛 +爸 +楼 +避 +谋 +吨 +野 +猪 +旗 +累 +偏 +典 +馆 +索 +秦 +脂 +潮 +爷 +豆 +忽 +托 +惊 +塑 +遗 +愈 +朱 +替 +纤 +粗 +倾 +尚 +痛 +楚 +谢 +奋 +购 +磨 +君 +池 +旁 +碎 +骨 +监 +捕 +弟 +暴 +割 +贯 +殊 +释 +词 +亡 +壁 +顿 +宝 +午 +尘 +闻 +揭 +炮 +残 +冬 +桥 +妇 +警 +综 +招 +吴 +付 +浮 +遭 +徐 +您 +摇 +谷 +赞 +箱 +隔 +订 +男 +吹 +园 +纷 +唐 +败 +宋 +玻 +巨 +耕 +坦 +荣 +闭 +湾 +键 +凡 +驻 +锅 +救 +恩 +剥 +凝 +碱 +齿 +截 +炼 +麻 +纺 +禁 +废 +盛 +版 +缓 +净 +睛 +昌 +婚 +涉 +筒 +嘴 +插 +岸 +朗 +庄 +街 +藏 +姑 +贸 +腐 +奴 +啦 +惯 +乘 +伙 +恢 +匀 +纱 +扎 +辩 +耳 +彪 +臣 +亿 +璃 +抵 +脉 +秀 +萨 +俄 +网 +舞 +店 +喷 +纵 +寸 +汗 +挂 +洪 +贺 +闪 +柬 +爆 +烯 +津 +稻 +墙 +软 +勇 +像 +滚 +厘 +蒙 +芳 +肯 +坡 +柱 +荡 +腿 +仪 +旅 +尾 +轧 +冰 +贡 +登 +黎 +削 +钻 +勒 +逃 +障 +氨 +郭 +峰 +币 +港 +伏 +轨 +亩 +毕 +擦 +莫 +刺 +浪 +秘 +援 +株 +健 +售 +股 +岛 +甘 +泡 +睡 +童 +铸 +汤 +阀 +休 +汇 +舍 +牧 +绕 +炸 +哲 +磷 +绩 +朋 +淡 +尖 +启 +陷 +柴 +呈 +徒 +颜 +泪 +稍 +忘 +泵 +蓝 +拖 +洞 +授 +镜 +辛 +壮 +锋 +贫 +虚 +弯 +摩 +泰 +幼 +廷 +尊 +窗 +纲 +弄 +隶 +疑 +氏 +宫 +姐 +震 +瑞 +怪 +尤 +琴 +循 +描 +膜 +违 +夹 +腰 +缘 +珠 +穷 +森 +枝 +竹 +沟 +催 +绳 +忆 +邦 +剩 +幸 +浆 +栏 +拥 +牙 +贮 +礼 +滤 +钠 +纹 +罢 +拍 +咱 +喊 +袖 +埃 +勤 +罚 +焦 +潜 +伍 +墨 +欲 +缝 +姓 +刊 +饱 +仿 +奖 +铝 +鬼 +丽 +跨 +默 +挖 +链 +扫 +喝 +袋 +炭 +污 +幕 +诸 +弧 +励 +梅 +奶 +洁 +灾 +舟 +鉴 +苯 +讼 +抱 +毁 +懂 +寒 +智 +埔 +寄 +届 +跃 +渡 +挑 +丹 +艰 +贝 +碰 +拔 +爹 +戴 +码 +梦 +芽 +熔 +赤 +渔 +哭 +敬 +颗 +奔 +铅 +仲 +虎 +稀 +妹 +乏 +珍 +申 +桌 +遵 +允 +隆 +螺 +仓 +魏 +锐 +晓 +氮 +兼 +隐 +碍 +赫 +拨 +忠 +肃 +缸 +牵 +抢 +博 +巧 +壳 +兄 +杜 +讯 +诚 +碧 +祥 +柯 +页 +巡 +矩 +悲 +灌 +龄 +伦 +票 +寻 +桂 +铺 +圣 +恐 +恰 +郑 +趣 +抬 +荒 +腾 +贴 +柔 +滴 +猛 +阔 +辆 +妻 +填 +撤 +储 +签 +闹 +扰 +紫 +砂 +递 +戏 +吊 +陶 +伐 +喂 +疗 +瓶 +婆 +抚 +臂 +摸 +忍 +虾 +蜡 +邻 +胸 +巩 +挤 +偶 +弃 +槽 +劲 +乳 +邓 +吉 +仁 +烂 +砖 +租 +乌 +舰 +伴 +瓜 +浅 +丙 +暂 +燥 +橡 +柳 +迷 +暖 +牌 +秧 +胆 +详 +簧 +踏 +瓷 +谱 +呆 +宾 +糊 +洛 +辉 +愤 +竞 +隙 +怒 +粘 +乃 +绪 +肩 +籍 +敏 +涂 +熙 +皆 +侦 +悬 +掘 +享 +纠 +醒 +狂 +锁 +淀 +恨 +牲 +霸 +爬 +赏 +逆 +玩 +陵 +祝 +秒 +浙 +貌 +役 +彼 +悉 +鸭 +趋 +凤 +晨 +畜 +辈 +秩 +卵 +署 +梯 +炎 +滩 +棋 +驱 +筛 +峡 +冒 +啥 +寿 +译 +浸 +泉 +帽 +迟 +硅 +疆 +贷 +漏 +稿 +冠 +嫩 +胁 +芯 +牢 +叛 +蚀 +奥 +鸣 +岭 +羊 +凭 +串 +塘 +绘 +酵 +融 +盆 +锡 +庙 +筹 +冻 +辅 +摄 +袭 +筋 +拒 +僚 +旱 +钾 +鸟 +漆 +沈 +眉 +疏 +添 +棒 +穗 +硝 +韩 +逼 +扭 +侨 +凉 +挺 +碗 +栽 +炒 +杯 +患 +馏 +劝 +豪 +辽 +勃 +鸿 +旦 +吏 +拜 +狗 +埋 +辊 +掩 +饮 +搬 +骂 +辞 +勾 +扣 +估 +蒋 +绒 +雾 +丈 +朵 +姆 +拟 +宇 +辑 +陕 +雕 +偿 +蓄 +崇 +剪 +倡 +厅 +咬 +驶 +薯 +刷 +斥 +番 +赋 +奉 +佛 +浇 +漫 +曼 +扇 +钙 +桃 +扶 +仔 +返 +俗 +亏 +腔 +鞋 +棱 +覆 +框 +悄 +叔 +撞 +骗 +勘 +旺 +沸 +孤 +吐 +孟 +渠 +屈 +疾 +妙 +惜 +仰 +狠 +胀 +谐 +抛 +霉 +桑 +岗 +嘛 +衰 +盗 +渗 +脏 +赖 +涌 +甜 +曹 +阅 +肌 +哩 +厉 +烃 +纬 +毅 +昨 +伪 +症 +煮 +叹 +钉 +搭 +茎 +笼 +酷 +偷 +弓 +锥 +恒 +杰 +坑 +鼻 +翼 +纶 +叙 +狱 +逮 +罐 +络 +棚 +抑 +膨 +蔬 +寺 +骤 +穆 +冶 +枯 +册 +尸 +凸 +绅 +坯 +牺 +焰 +轰 +欣 +晋 +瘦 +御 +锭 +锦 +丧 +旬 +锻 +垄 +搜 +扑 +邀 +亭 +酯 +迈 +舒 +脆 +酶 +闲 +忧 +酚 +顽 +羽 +涨 +卸 +仗 +陪 +辟 +惩 +杭 +姚 +肚 +捉 +飘 +漂 +昆 +欺 +吾 +郎 +烷 +汁 +呵 +饰 +萧 +雅 +邮 +迁 +燕 +撒 +姻 +赴 +宴 +烦 +债 +帐 +斑 +铃 +旨 +醇 +董 +饼 +雏 +姿 +拌 +傅 +腹 +妥 +揉 +贤 +拆 +歪 +葡 +胺 +丢 +浩 +徽 +昂 +垫 +挡 +览 +贪 +慰 +缴 +汪 +慌 +冯 +诺 +姜 +谊 +凶 +劣 +诬 +耀 +昏 +躺 +盈 +骑 +乔 +溪 +丛 +卢 +抹 +闷 +咨 +刮 +驾 +缆 +悟 +摘 +铒 +掷 +颇 +幻 +柄 +惠 +惨 +佳 +仇 +腊 +窝 +涤 +剑 +瞧 +堡 +泼 +葱 +罩 +霍 +捞 +胎 +苍 +滨 +俩 +捅 +湘 +砍 +霞 +邵 +萄 +疯 +淮 +遂 +熊 +粪 +烘 +宿 +档 +戈 +驳 +嫂 +裕 +徙 +箭 +捐 +肠 +撑 +晒 +辨 +殿 +莲 +摊 +搅 +酱 +屏 +疫 +哀 +蔡 +堵 +沫 +皱 +畅 +叠 +阁 +莱 +敲 +辖 +钩 +痕 +坝 +巷 +饿 +祸 +丘 +玄 +溜 +曰 +逻 +彭 +尝 +卿 +妨 +艇 +吞 +韦 +怨 +矮 +歇 diff --git a/electrum/wordlist/english.txt b/electrum/wordlist/english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo diff --git a/electrum/wordlist/japanese.txt b/electrum/wordlist/japanese.txt @@ -0,0 +1,2048 @@ +あいこくしん +あいさつ +あいだ +あおぞら +あかちゃん +あきる +あけがた +あける +あこがれる +あさい +あさひ +あしあと +あじわう +あずかる +あずき +あそぶ +あたえる +あたためる +あたりまえ +あたる +あつい +あつかう +あっしゅく +あつまり +あつめる +あてな +あてはまる +あひる +あぶら +あぶる +あふれる +あまい +あまど +あまやかす +あまり +あみもの +あめりか +あやまる +あゆむ +あらいぐま +あらし +あらすじ +あらためる +あらゆる +あらわす +ありがとう +あわせる +あわてる +あんい +あんがい +あんこ +あんぜん +あんてい +あんない +あんまり +いいだす +いおん +いがい +いがく +いきおい +いきなり +いきもの +いきる +いくじ +いくぶん +いけばな +いけん +いこう +いこく +いこつ +いさましい +いさん +いしき +いじゅう +いじょう +いじわる +いずみ +いずれ +いせい +いせえび +いせかい +いせき +いぜん +いそうろう +いそがしい +いだい +いだく +いたずら +いたみ +いたりあ +いちおう +いちじ +いちど +いちば +いちぶ +いちりゅう +いつか +いっしゅん +いっせい +いっそう +いったん +いっち +いってい +いっぽう +いてざ +いてん +いどう +いとこ +いない +いなか +いねむり +いのち +いのる +いはつ +いばる +いはん +いびき +いひん +いふく +いへん +いほう +いみん +いもうと +いもたれ +いもり +いやがる +いやす +いよかん +いよく +いらい +いらすと +いりぐち +いりょう +いれい +いれもの +いれる +いろえんぴつ +いわい +いわう +いわかん +いわば +いわゆる +いんげんまめ +いんさつ +いんしょう +いんよう +うえき +うえる +うおざ +うがい +うかぶ +うかべる +うきわ +うくらいな +うくれれ +うけたまわる +うけつけ +うけとる +うけもつ +うける +うごかす +うごく +うこん +うさぎ +うしなう +うしろがみ +うすい +うすぎ +うすぐらい +うすめる +うせつ +うちあわせ +うちがわ +うちき +うちゅう +うっかり +うつくしい +うったえる +うつる +うどん +うなぎ +うなじ +うなずく +うなる +うねる +うのう +うぶげ +うぶごえ +うまれる +うめる +うもう +うやまう +うよく +うらがえす +うらぐち +うらない +うりあげ +うりきれ +うるさい +うれしい +うれゆき +うれる +うろこ +うわき +うわさ +うんこう +うんちん +うんてん +うんどう +えいえん +えいが +えいきょう +えいご +えいせい +えいぶん +えいよう +えいわ +えおり +えがお +えがく +えきたい +えくせる +えしゃく +えすて +えつらん +えのぐ +えほうまき +えほん +えまき +えもじ +えもの +えらい +えらぶ +えりあ +えんえん +えんかい +えんぎ +えんげき +えんしゅう +えんぜつ +えんそく +えんちょう +えんとつ +おいかける +おいこす +おいしい +おいつく +おうえん +おうさま +おうじ +おうせつ +おうたい +おうふく +おうべい +おうよう +おえる +おおい +おおう +おおどおり +おおや +おおよそ +おかえり +おかず +おがむ +おかわり +おぎなう +おきる +おくさま +おくじょう +おくりがな +おくる +おくれる +おこす +おこなう +おこる +おさえる +おさない +おさめる +おしいれ +おしえる +おじぎ +おじさん +おしゃれ +おそらく +おそわる +おたがい +おたく +おだやか +おちつく +おっと +おつり +おでかけ +おとしもの +おとなしい +おどり +おどろかす +おばさん +おまいり +おめでとう +おもいで +おもう +おもたい +おもちゃ +おやつ +おやゆび +およぼす +おらんだ +おろす +おんがく +おんけい +おんしゃ +おんせん +おんだん +おんちゅう +おんどけい +かあつ +かいが +がいき +がいけん +がいこう +かいさつ +かいしゃ +かいすいよく +かいぜん +かいぞうど +かいつう +かいてん +かいとう +かいふく +がいへき +かいほう +かいよう +がいらい +かいわ +かえる +かおり +かかえる +かがく +かがし +かがみ +かくご +かくとく +かざる +がぞう +かたい +かたち +がちょう +がっきゅう +がっこう +がっさん +がっしょう +かなざわし +かのう +がはく +かぶか +かほう +かほご +かまう +かまぼこ +かめれおん +かゆい +かようび +からい +かるい +かろう +かわく +かわら +がんか +かんけい +かんこう +かんしゃ +かんそう +かんたん +かんち +がんばる +きあい +きあつ +きいろ +ぎいん +きうい +きうん +きえる +きおう +きおく +きおち +きおん +きかい +きかく +きかんしゃ +ききて +きくばり +きくらげ +きけんせい +きこう +きこえる +きこく +きさい +きさく +きさま +きさらぎ +ぎじかがく +ぎしき +ぎじたいけん +ぎじにってい +ぎじゅつしゃ +きすう +きせい +きせき +きせつ +きそう +きぞく +きぞん +きたえる +きちょう +きつえん +ぎっちり +きつつき +きつね +きてい +きどう +きどく +きない +きなが +きなこ +きぬごし +きねん +きのう +きのした +きはく +きびしい +きひん +きふく +きぶん +きぼう +きほん +きまる +きみつ +きむずかしい +きめる +きもだめし +きもち +きもの +きゃく +きやく +ぎゅうにく +きよう +きょうりゅう +きらい +きらく +きりん +きれい +きれつ +きろく +ぎろん +きわめる +ぎんいろ +きんかくじ +きんじょ +きんようび +ぐあい +くいず +くうかん +くうき +くうぐん +くうこう +ぐうせい +くうそう +ぐうたら +くうふく +くうぼ +くかん +くきょう +くげん +ぐこう +くさい +くさき +くさばな +くさる +くしゃみ +くしょう +くすのき +くすりゆび +くせげ +くせん +ぐたいてき +くださる +くたびれる +くちこみ +くちさき +くつした +ぐっすり +くつろぐ +くとうてん +くどく +くなん +くねくね +くのう +くふう +くみあわせ +くみたてる +くめる +くやくしょ +くらす +くらべる +くるま +くれる +くろう +くわしい +ぐんかん +ぐんしょく +ぐんたい +ぐんて +けあな +けいかく +けいけん +けいこ +けいさつ +げいじゅつ +けいたい +げいのうじん +けいれき +けいろ +けおとす +けおりもの +げきか +げきげん +げきだん +げきちん +げきとつ +げきは +げきやく +げこう +げこくじょう +げざい +けさき +げざん +けしき +けしごむ +けしょう +げすと +けたば +けちゃっぷ +けちらす +けつあつ +けつい +けつえき +けっこん +けつじょ +けっせき +けってい +けつまつ +げつようび +げつれい +けつろん +げどく +けとばす +けとる +けなげ +けなす +けなみ +けぬき +げねつ +けねん +けはい +げひん +けぶかい +げぼく +けまり +けみかる +けむし +けむり +けもの +けらい +けろけろ +けわしい +けんい +けんえつ +けんお +けんか +げんき +けんげん +けんこう +けんさく +けんしゅう +けんすう +げんそう +けんちく +けんてい +けんとう +けんない +けんにん +げんぶつ +けんま +けんみん +けんめい +けんらん +けんり +こあくま +こいぬ +こいびと +ごうい +こうえん +こうおん +こうかん +ごうきゅう +ごうけい +こうこう +こうさい +こうじ +こうすい +ごうせい +こうそく +こうたい +こうちゃ +こうつう +こうてい +こうどう +こうない +こうはい +ごうほう +ごうまん +こうもく +こうりつ +こえる +こおり +ごかい +ごがつ +ごかん +こくご +こくさい +こくとう +こくない +こくはく +こぐま +こけい +こける +ここのか +こころ +こさめ +こしつ +こすう +こせい +こせき +こぜん +こそだて +こたい +こたえる +こたつ +こちょう +こっか +こつこつ +こつばん +こつぶ +こてい +こてん +ことがら +ことし +ことば +ことり +こなごな +こねこね +このまま +このみ +このよ +ごはん +こひつじ +こふう +こふん +こぼれる +ごまあぶら +こまかい +ごますり +こまつな +こまる +こむぎこ +こもじ +こもち +こもの +こもん +こやく +こやま +こゆう +こゆび +こよい +こよう +こりる +これくしょん +ころっけ +こわもて +こわれる +こんいん +こんかい +こんき +こんしゅう +こんすい +こんだて +こんとん +こんなん +こんびに +こんぽん +こんまけ +こんや +こんれい +こんわく +ざいえき +さいかい +さいきん +ざいげん +ざいこ +さいしょ +さいせい +ざいたく +ざいちゅう +さいてき +ざいりょう +さうな +さかいし +さがす +さかな +さかみち +さがる +さぎょう +さくし +さくひん +さくら +さこく +さこつ +さずかる +ざせき +さたん +さつえい +ざつおん +ざっか +ざつがく +さっきょく +ざっし +さつじん +ざっそう +さつたば +さつまいも +さてい +さといも +さとう +さとおや +さとし +さとる +さのう +さばく +さびしい +さべつ +さほう +さほど +さます +さみしい +さみだれ +さむけ +さめる +さやえんどう +さゆう +さよう +さよく +さらだ +ざるそば +さわやか +さわる +さんいん +さんか +さんきゃく +さんこう +さんさい +ざんしょ +さんすう +さんせい +さんそ +さんち +さんま +さんみ +さんらん +しあい +しあげ +しあさって +しあわせ +しいく +しいん +しうち +しえい +しおけ +しかい +しかく +じかん +しごと +しすう +じだい +したうけ +したぎ +したて +したみ +しちょう +しちりん +しっかり +しつじ +しつもん +してい +してき +してつ +じてん +じどう +しなぎれ +しなもの +しなん +しねま +しねん +しのぐ +しのぶ +しはい +しばかり +しはつ +しはらい +しはん +しひょう +しふく +じぶん +しへい +しほう +しほん +しまう +しまる +しみん +しむける +じむしょ +しめい +しめる +しもん +しゃいん +しゃうん +しゃおん +じゃがいも +しやくしょ +しゃくほう +しゃけん +しゃこ +しゃざい +しゃしん +しゃせん +しゃそう +しゃたい +しゃちょう +しゃっきん +じゃま +しゃりん +しゃれい +じゆう +じゅうしょ +しゅくはく +じゅしん +しゅっせき +しゅみ +しゅらば +じゅんばん +しょうかい +しょくたく +しょっけん +しょどう +しょもつ +しらせる +しらべる +しんか +しんこう +じんじゃ +しんせいじ +しんちく +しんりん +すあげ +すあし +すあな +ずあん +すいえい +すいか +すいとう +ずいぶん +すいようび +すうがく +すうじつ +すうせん +すおどり +すきま +すくう +すくない +すける +すごい +すこし +ずさん +すずしい +すすむ +すすめる +すっかり +ずっしり +ずっと +すてき +すてる +すねる +すのこ +すはだ +すばらしい +ずひょう +ずぶぬれ +すぶり +すふれ +すべて +すべる +ずほう +すぼん +すまい +すめし +すもう +すやき +すらすら +するめ +すれちがう +すろっと +すわる +すんぜん +すんぽう +せあぶら +せいかつ +せいげん +せいじ +せいよう +せおう +せかいかん +せきにん +せきむ +せきゆ +せきらんうん +せけん +せこう +せすじ +せたい +せたけ +せっかく +せっきゃく +ぜっく +せっけん +せっこつ +せっさたくま +せつぞく +せつだん +せつでん +せっぱん +せつび +せつぶん +せつめい +せつりつ +せなか +せのび +せはば +せびろ +せぼね +せまい +せまる +せめる +せもたれ +せりふ +ぜんあく +せんい +せんえい +せんか +せんきょ +せんく +せんげん +ぜんご +せんさい +せんしゅ +せんすい +せんせい +せんぞ +せんたく +せんちょう +せんてい +せんとう +せんぬき +せんねん +せんぱい +ぜんぶ +ぜんぽう +せんむ +せんめんじょ +せんもん +せんやく +せんゆう +せんよう +ぜんら +ぜんりゃく +せんれい +せんろ +そあく +そいとげる +そいね +そうがんきょう +そうき +そうご +そうしん +そうだん +そうなん +そうび +そうめん +そうり +そえもの +そえん +そがい +そげき +そこう +そこそこ +そざい +そしな +そせい +そせん +そそぐ +そだてる +そつう +そつえん +そっかん +そつぎょう +そっけつ +そっこう +そっせん +そっと +そとがわ +そとづら +そなえる +そなた +そふぼ +そぼく +そぼろ +そまつ +そまる +そむく +そむりえ +そめる +そもそも +そよかぜ +そらまめ +そろう +そんかい +そんけい +そんざい +そんしつ +そんぞく +そんちょう +ぞんび +ぞんぶん +そんみん +たあい +たいいん +たいうん +たいえき +たいおう +だいがく +たいき +たいぐう +たいけん +たいこ +たいざい +だいじょうぶ +だいすき +たいせつ +たいそう +だいたい +たいちょう +たいてい +だいどころ +たいない +たいねつ +たいのう +たいはん +だいひょう +たいふう +たいへん +たいほ +たいまつばな +たいみんぐ +たいむ +たいめん +たいやき +たいよう +たいら +たいりょく +たいる +たいわん +たうえ +たえる +たおす +たおる +たおれる +たかい +たかね +たきび +たくさん +たこく +たこやき +たさい +たしざん +だじゃれ +たすける +たずさわる +たそがれ +たたかう +たたく +ただしい +たたみ +たちばな +だっかい +だっきゃく +だっこ +だっしゅつ +だったい +たてる +たとえる +たなばた +たにん +たぬき +たのしみ +たはつ +たぶん +たべる +たぼう +たまご +たまる +だむる +ためいき +ためす +ためる +たもつ +たやすい +たよる +たらす +たりきほんがん +たりょう +たりる +たると +たれる +たれんと +たろっと +たわむれる +だんあつ +たんい +たんおん +たんか +たんき +たんけん +たんご +たんさん +たんじょうび +だんせい +たんそく +たんたい +だんち +たんてい +たんとう +だんな +たんにん +だんねつ +たんのう +たんぴん +だんぼう +たんまつ +たんめい +だんれつ +だんろ +だんわ +ちあい +ちあん +ちいき +ちいさい +ちえん +ちかい +ちから +ちきゅう +ちきん +ちけいず +ちけん +ちこく +ちさい +ちしき +ちしりょう +ちせい +ちそう +ちたい +ちたん +ちちおや +ちつじょ +ちてき +ちてん +ちぬき +ちぬり +ちのう +ちひょう +ちへいせん +ちほう +ちまた +ちみつ +ちみどろ +ちめいど +ちゃんこなべ +ちゅうい +ちゆりょく +ちょうし +ちょさくけん +ちらし +ちらみ +ちりがみ +ちりょう +ちるど +ちわわ +ちんたい +ちんもく +ついか +ついたち +つうか +つうじょう +つうはん +つうわ +つかう +つかれる +つくね +つくる +つけね +つける +つごう +つたえる +つづく +つつじ +つつむ +つとめる +つながる +つなみ +つねづね +つのる +つぶす +つまらない +つまる +つみき +つめたい +つもり +つもる +つよい +つるぼ +つるみく +つわもの +つわり +てあし +てあて +てあみ +ていおん +ていか +ていき +ていけい +ていこく +ていさつ +ていし +ていせい +ていたい +ていど +ていねい +ていひょう +ていへん +ていぼう +てうち +ておくれ +てきとう +てくび +でこぼこ +てさぎょう +てさげ +てすり +てそう +てちがい +てちょう +てつがく +てつづき +でっぱ +てつぼう +てつや +でぬかえ +てぬき +てぬぐい +てのひら +てはい +てぶくろ +てふだ +てほどき +てほん +てまえ +てまきずし +てみじか +てみやげ +てらす +てれび +てわけ +てわたし +でんあつ +てんいん +てんかい +てんき +てんぐ +てんけん +てんごく +てんさい +てんし +てんすう +でんち +てんてき +てんとう +てんない +てんぷら +てんぼうだい +てんめつ +てんらんかい +でんりょく +でんわ +どあい +といれ +どうかん +とうきゅう +どうぐ +とうし +とうむぎ +とおい +とおか +とおく +とおす +とおる +とかい +とかす +ときおり +ときどき +とくい +とくしゅう +とくてん +とくに +とくべつ +とけい +とける +とこや +とさか +としょかん +とそう +とたん +とちゅう +とっきゅう +とっくん +とつぜん +とつにゅう +とどける +ととのえる +とない +となえる +となり +とのさま +とばす +どぶがわ +とほう +とまる +とめる +ともだち +ともる +どようび +とらえる +とんかつ +どんぶり +ないかく +ないこう +ないしょ +ないす +ないせん +ないそう +なおす +ながい +なくす +なげる +なこうど +なさけ +なたでここ +なっとう +なつやすみ +ななおし +なにごと +なにもの +なにわ +なのか +なふだ +なまいき +なまえ +なまみ +なみだ +なめらか +なめる +なやむ +ならう +ならび +ならぶ +なれる +なわとび +なわばり +にあう +にいがた +にうけ +におい +にかい +にがて +にきび +にくしみ +にくまん +にげる +にさんかたんそ +にしき +にせもの +にちじょう +にちようび +にっか +にっき +にっけい +にっこう +にっさん +にっしょく +にっすう +にっせき +にってい +になう +にほん +にまめ +にもつ +にやり +にゅういん +にりんしゃ +にわとり +にんい +にんか +にんき +にんげん +にんしき +にんずう +にんそう +にんたい +にんち +にんてい +にんにく +にんぷ +にんまり +にんむ +にんめい +にんよう +ぬいくぎ +ぬかす +ぬぐいとる +ぬぐう +ぬくもり +ぬすむ +ぬまえび +ぬめり +ぬらす +ぬんちゃく +ねあげ +ねいき +ねいる +ねいろ +ねぐせ +ねくたい +ねくら +ねこぜ +ねこむ +ねさげ +ねすごす +ねそべる +ねだん +ねつい +ねっしん +ねつぞう +ねったいぎょ +ねぶそく +ねふだ +ねぼう +ねほりはほり +ねまき +ねまわし +ねみみ +ねむい +ねむたい +ねもと +ねらう +ねわざ +ねんいり +ねんおし +ねんかん +ねんきん +ねんぐ +ねんざ +ねんし +ねんちゃく +ねんど +ねんぴ +ねんぶつ +ねんまつ +ねんりょう +ねんれい +のいず +のおづま +のがす +のきなみ +のこぎり +のこす +のこる +のせる +のぞく +のぞむ +のたまう +のちほど +のっく +のばす +のはら +のべる +のぼる +のみもの +のやま +のらいぬ +のらねこ +のりもの +のりゆき +のれん +のんき +ばあい +はあく +ばあさん +ばいか +ばいく +はいけん +はいご +はいしん +はいすい +はいせん +はいそう +はいち +ばいばい +はいれつ +はえる +はおる +はかい +ばかり +はかる +はくしゅ +はけん +はこぶ +はさみ +はさん +はしご +ばしょ +はしる +はせる +ぱそこん +はそん +はたん +はちみつ +はつおん +はっかく +はづき +はっきり +はっくつ +はっけん +はっこう +はっさん +はっしん +はったつ +はっちゅう +はってん +はっぴょう +はっぽう +はなす +はなび +はにかむ +はぶらし +はみがき +はむかう +はめつ +はやい +はやし +はらう +はろうぃん +はわい +はんい +はんえい +はんおん +はんかく +はんきょう +ばんぐみ +はんこ +はんしゃ +はんすう +はんだん +ぱんち +ぱんつ +はんてい +はんとし +はんのう +はんぱ +はんぶん +はんぺん +はんぼうき +はんめい +はんらん +はんろん +ひいき +ひうん +ひえる +ひかく +ひかり +ひかる +ひかん +ひくい +ひけつ +ひこうき +ひこく +ひさい +ひさしぶり +ひさん +びじゅつかん +ひしょ +ひそか +ひそむ +ひたむき +ひだり +ひたる +ひつぎ +ひっこし +ひっし +ひつじゅひん +ひっす +ひつぜん +ぴったり +ぴっちり +ひつよう +ひてい +ひとごみ +ひなまつり +ひなん +ひねる +ひはん +ひびく +ひひょう +ひほう +ひまわり +ひまん +ひみつ +ひめい +ひめじし +ひやけ +ひやす +ひよう +びょうき +ひらがな +ひらく +ひりつ +ひりょう +ひるま +ひるやすみ +ひれい +ひろい +ひろう +ひろき +ひろゆき +ひんかく +ひんけつ +ひんこん +ひんしゅ +ひんそう +ぴんち +ひんぱん +びんぼう +ふあん +ふいうち +ふうけい +ふうせん +ぷうたろう +ふうとう +ふうふ +ふえる +ふおん +ふかい +ふきん +ふくざつ +ふくぶくろ +ふこう +ふさい +ふしぎ +ふじみ +ふすま +ふせい +ふせぐ +ふそく +ぶたにく +ふたん +ふちょう +ふつう +ふつか +ふっかつ +ふっき +ふっこく +ぶどう +ふとる +ふとん +ふのう +ふはい +ふひょう +ふへん +ふまん +ふみん +ふめつ +ふめん +ふよう +ふりこ +ふりる +ふるい +ふんいき +ぶんがく +ぶんぐ +ふんしつ +ぶんせき +ふんそう +ぶんぽう +へいあん +へいおん +へいがい +へいき +へいげん +へいこう +へいさ +へいしゃ +へいせつ +へいそ +へいたく +へいてん +へいねつ +へいわ +へきが +へこむ +べにいろ +べにしょうが +へらす +へんかん +べんきょう +べんごし +へんさい +へんたい +べんり +ほあん +ほいく +ぼうぎょ +ほうこく +ほうそう +ほうほう +ほうもん +ほうりつ +ほえる +ほおん +ほかん +ほきょう +ぼきん +ほくろ +ほけつ +ほけん +ほこう +ほこる +ほしい +ほしつ +ほしゅ +ほしょう +ほせい +ほそい +ほそく +ほたて +ほたる +ぽちぶくろ +ほっきょく +ほっさ +ほったん +ほとんど +ほめる +ほんい +ほんき +ほんけ +ほんしつ +ほんやく +まいにち +まかい +まかせる +まがる +まける +まこと +まさつ +まじめ +ますく +まぜる +まつり +まとめ +まなぶ +まぬけ +まねく +まほう +まもる +まゆげ +まよう +まろやか +まわす +まわり +まわる +まんが +まんきつ +まんぞく +まんなか +みいら +みうち +みえる +みがく +みかた +みかん +みけん +みこん +みじかい +みすい +みすえる +みせる +みっか +みつかる +みつける +みてい +みとめる +みなと +みなみかさい +みねらる +みのう +みのがす +みほん +みもと +みやげ +みらい +みりょく +みわく +みんか +みんぞく +むいか +むえき +むえん +むかい +むかう +むかえ +むかし +むぎちゃ +むける +むげん +むさぼる +むしあつい +むしば +むじゅん +むしろ +むすう +むすこ +むすぶ +むすめ +むせる +むせん +むちゅう +むなしい +むのう +むやみ +むよう +むらさき +むりょう +むろん +めいあん +めいうん +めいえん +めいかく +めいきょく +めいさい +めいし +めいそう +めいぶつ +めいれい +めいわく +めぐまれる +めざす +めした +めずらしい +めだつ +めまい +めやす +めんきょ +めんせき +めんどう +もうしあげる +もうどうけん +もえる +もくし +もくてき +もくようび +もちろん +もどる +もらう +もんく +もんだい +やおや +やける +やさい +やさしい +やすい +やすたろう +やすみ +やせる +やそう +やたい +やちん +やっと +やっぱり +やぶる +やめる +ややこしい +やよい +やわらかい +ゆうき +ゆうびんきょく +ゆうべ +ゆうめい +ゆけつ +ゆしゅつ +ゆせん +ゆそう +ゆたか +ゆちゃく +ゆでる +ゆにゅう +ゆびわ +ゆらい +ゆれる +ようい +ようか +ようきゅう +ようじ +ようす +ようちえん +よかぜ +よかん +よきん +よくせい +よくぼう +よけい +よごれる +よさん +よしゅう +よそう +よそく +よっか +よてい +よどがわく +よねつ +よやく +よゆう +よろこぶ +よろしい +らいう +らくがき +らくご +らくさつ +らくだ +らしんばん +らせん +らぞく +らたい +らっか +られつ +りえき +りかい +りきさく +りきせつ +りくぐん +りくつ +りけん +りこう +りせい +りそう +りそく +りてん +りねん +りゆう +りゅうがく +りよう +りょうり +りょかん +りょくちゃ +りょこう +りりく +りれき +りろん +りんご +るいけい +るいさい +るいじ +るいせき +るすばん +るりがわら +れいかん +れいぎ +れいせい +れいぞうこ +れいとう +れいぼう +れきし +れきだい +れんあい +れんけい +れんこん +れんさい +れんしゅう +れんぞく +れんらく +ろうか +ろうご +ろうじん +ろうそく +ろくが +ろこつ +ろじうら +ろしゅつ +ろせん +ろてん +ろめん +ろれつ +ろんぎ +ろんぱ +ろんぶん +ろんり +わかす +わかめ +わかやま +わかれる +わしつ +わじまし +わすれもの +わらう +われる diff --git a/electrum/wordlist/portuguese.txt b/electrum/wordlist/portuguese.txt @@ -0,0 +1,1654 @@ +# Copyright (c) 2014, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +abaular +abdominal +abeto +abissinio +abjeto +ablucao +abnegar +abotoar +abrutalhar +absurdo +abutre +acautelar +accessorios +acetona +achocolatado +acirrar +acne +acovardar +acrostico +actinomicete +acustico +adaptavel +adeus +adivinho +adjunto +admoestar +adnominal +adotivo +adquirir +adriatico +adsorcao +adutora +advogar +aerossol +afazeres +afetuoso +afixo +afluir +afortunar +afrouxar +aftosa +afunilar +agentes +agito +aglutinar +aiatola +aimore +aino +aipo +airoso +ajeitar +ajoelhar +ajudante +ajuste +alazao +albumina +alcunha +alegria +alexandre +alforriar +alguns +alhures +alivio +almoxarife +alotropico +alpiste +alquimista +alsaciano +altura +aluviao +alvura +amazonico +ambulatorio +ametodico +amizades +amniotico +amovivel +amurada +anatomico +ancorar +anexo +anfora +aniversario +anjo +anotar +ansioso +anturio +anuviar +anverso +anzol +aonde +apaziguar +apito +aplicavel +apoteotico +aprimorar +aprumo +apto +apuros +aquoso +arauto +arbusto +arduo +aresta +arfar +arguto +aritmetico +arlequim +armisticio +aromatizar +arpoar +arquivo +arrumar +arsenio +arturiano +aruaque +arvores +asbesto +ascorbico +aspirina +asqueroso +assustar +astuto +atazanar +ativo +atletismo +atmosferico +atormentar +atroz +aturdir +audivel +auferir +augusto +aula +aumento +aurora +autuar +avatar +avexar +avizinhar +avolumar +avulso +axiomatico +azerbaijano +azimute +azoto +azulejo +bacteriologista +badulaque +baforada +baixote +bajular +balzaquiana +bambuzal +banzo +baoba +baqueta +barulho +bastonete +batuta +bauxita +bavaro +bazuca +bcrepuscular +beato +beduino +begonia +behaviorista +beisebol +belzebu +bemol +benzido +beocio +bequer +berro +besuntar +betume +bexiga +bezerro +biatlon +biboca +bicuspide +bidirecional +bienio +bifurcar +bigorna +bijuteria +bimotor +binormal +bioxido +bipolarizacao +biquini +birutice +bisturi +bituca +biunivoco +bivalve +bizarro +blasfemo +blenorreia +blindar +bloqueio +blusao +boazuda +bofete +bojudo +bolso +bombordo +bonzo +botina +boquiaberto +bostoniano +botulismo +bourbon +bovino +boximane +bravura +brevidade +britar +broxar +bruno +bruxuleio +bubonico +bucolico +buda +budista +bueiro +buffer +bugre +bujao +bumerangue +burundines +busto +butique +buzios +caatinga +cabuqui +cacunda +cafuzo +cajueiro +camurca +canudo +caquizeiro +carvoeiro +casulo +catuaba +cauterizar +cebolinha +cedula +ceifeiro +celulose +cerzir +cesto +cetro +ceus +cevar +chavena +cheroqui +chita +chovido +chuvoso +ciatico +cibernetico +cicuta +cidreira +cientistas +cifrar +cigarro +cilio +cimo +cinzento +cioso +cipriota +cirurgico +cisto +citrico +ciumento +civismo +clavicula +clero +clitoris +cluster +coaxial +cobrir +cocota +codorniz +coexistir +cogumelo +coito +colusao +compaixao +comutativo +contentamento +convulsivo +coordenativa +coquetel +correto +corvo +costureiro +cotovia +covil +cozinheiro +cretino +cristo +crivo +crotalo +cruzes +cubo +cucuia +cueiro +cuidar +cujo +cultural +cunilingua +cupula +curvo +custoso +cutucar +czarismo +dablio +dacota +dados +daguerreotipo +daiquiri +daltonismo +damista +dantesco +daquilo +darwinista +dasein +dativo +deao +debutantes +decurso +deduzir +defunto +degustar +dejeto +deltoide +demover +denunciar +deputado +deque +dervixe +desvirtuar +deturpar +deuteronomio +devoto +dextrose +dezoito +diatribe +dicotomico +didatico +dietista +difuso +digressao +diluvio +diminuto +dinheiro +dinossauro +dioxido +diplomatico +dique +dirimivel +disturbio +diurno +divulgar +dizivel +doar +dobro +docura +dodoi +doer +dogue +doloso +domo +donzela +doping +dorsal +dossie +dote +doutro +doze +dravidico +dreno +driver +dropes +druso +dubnio +ducto +dueto +dulija +dundum +duodeno +duquesa +durou +duvidoso +duzia +ebano +ebrio +eburneo +echarpe +eclusa +ecossistema +ectoplasma +ecumenismo +eczema +eden +editorial +edredom +edulcorar +efetuar +efigie +efluvio +egiptologo +egresso +egua +einsteiniano +eira +eivar +eixos +ejetar +elastomero +eldorado +elixir +elmo +eloquente +elucidativo +emaranhar +embutir +emerito +emfa +emitir +emotivo +empuxo +emulsao +enamorar +encurvar +enduro +enevoar +enfurnar +enguico +enho +enigmista +enlutar +enormidade +enpreendimento +enquanto +enriquecer +enrugar +entusiastico +enunciar +envolvimento +enxuto +enzimatico +eolico +epiteto +epoxi +epura +equivoco +erario +erbio +ereto +erguido +erisipela +ermo +erotizar +erros +erupcao +ervilha +esburacar +escutar +esfuziante +esguio +esloveno +esmurrar +esoterismo +esperanca +espirito +espurio +essencialmente +esturricar +esvoacar +etario +eterno +etiquetar +etnologo +etos +etrusco +euclidiano +euforico +eugenico +eunuco +europio +eustaquio +eutanasia +evasivo +eventualidade +evitavel +evoluir +exaustor +excursionista +exercito +exfoliado +exito +exotico +expurgo +exsudar +extrusora +exumar +fabuloso +facultativo +fado +fagulha +faixas +fajuto +faltoso +famoso +fanzine +fapesp +faquir +fartura +fastio +faturista +fausto +favorito +faxineira +fazer +fealdade +febril +fecundo +fedorento +feerico +feixe +felicidade +felipe +feltro +femur +fenotipo +fervura +festivo +feto +feudo +fevereiro +fezinha +fiasco +fibra +ficticio +fiduciario +fiesp +fifa +figurino +fijiano +filtro +finura +fiorde +fiquei +firula +fissurar +fitoteca +fivela +fixo +flavio +flexor +flibusteiro +flotilha +fluxograma +fobos +foco +fofura +foguista +foie +foliculo +fominha +fonte +forum +fosso +fotossintese +foxtrote +fraudulento +frevo +frivolo +frouxo +frutose +fuba +fucsia +fugitivo +fuinha +fujao +fulustreco +fumo +funileiro +furunculo +fustigar +futurologo +fuxico +fuzue +gabriel +gado +gaelico +gafieira +gaguejo +gaivota +gajo +galvanoplastico +gamo +ganso +garrucha +gastronomo +gatuno +gaussiano +gaviao +gaxeta +gazeteiro +gear +geiser +geminiano +generoso +genuino +geossinclinal +gerundio +gestual +getulista +gibi +gigolo +gilete +ginseng +giroscopio +glaucio +glacial +gleba +glifo +glote +glutonia +gnostico +goela +gogo +goitaca +golpista +gomo +gonzo +gorro +gostou +goticula +gourmet +governo +gozo +graxo +grevista +grito +grotesco +gruta +guaxinim +gude +gueto +guizo +guloso +gume +guru +gustativo +gustavo +gutural +habitue +haitiano +halterofilista +hamburguer +hanseniase +happening +harpista +hastear +haveres +hebreu +hectometro +hedonista +hegira +helena +helminto +hemorroidas +henrique +heptassilabo +hertziano +hesitar +heterossexual +heuristico +hexagono +hiato +hibrido +hidrostatico +hieroglifo +hifenizar +higienizar +hilario +himen +hino +hippie +hirsuto +historiografia +hitlerista +hodometro +hoje +holograma +homus +honroso +hoquei +horto +hostilizar +hotentote +huguenote +humilde +huno +hurra +hutu +iaia +ialorixa +iambico +iansa +iaque +iara +iatista +iberico +ibis +icar +iceberg +icosagono +idade +ideologo +idiotice +idoso +iemenita +iene +igarape +iglu +ignorar +igreja +iguaria +iidiche +ilativo +iletrado +ilharga +ilimitado +ilogismo +ilustrissimo +imaturo +imbuzeiro +imerso +imitavel +imovel +imputar +imutavel +inaveriguavel +incutir +induzir +inextricavel +infusao +ingua +inhame +iniquo +injusto +inning +inoxidavel +inquisitorial +insustentavel +intumescimento +inutilizavel +invulneravel +inzoneiro +iodo +iogurte +ioio +ionosfera +ioruba +iota +ipsilon +irascivel +iris +irlandes +irmaos +iroques +irrupcao +isca +isento +islandes +isotopo +isqueiro +israelita +isso +isto +iterbio +itinerario +itrio +iuane +iugoslavo +jabuticabeira +jacutinga +jade +jagunco +jainista +jaleco +jambo +jantarada +japones +jaqueta +jarro +jasmim +jato +jaula +javel +jazz +jegue +jeitoso +jejum +jenipapo +jeova +jequitiba +jersei +jesus +jetom +jiboia +jihad +jilo +jingle +jipe +jocoso +joelho +joguete +joio +jojoba +jorro +jota +joule +joviano +jubiloso +judoca +jugular +juizo +jujuba +juliano +jumento +junto +jururu +justo +juta +juventude +labutar +laguna +laico +lajota +lanterninha +lapso +laquear +lastro +lauto +lavrar +laxativo +lazer +leasing +lebre +lecionar +ledo +leguminoso +leitura +lele +lemure +lento +leonardo +leopardo +lepton +leque +leste +letreiro +leucocito +levitico +lexicologo +lhama +lhufas +liame +licoroso +lidocaina +liliputiano +limusine +linotipo +lipoproteina +liquidos +lirismo +lisura +liturgico +livros +lixo +lobulo +locutor +lodo +logro +lojista +lombriga +lontra +loop +loquaz +lorota +losango +lotus +louvor +luar +lubrificavel +lucros +lugubre +luis +luminoso +luneta +lustroso +luto +luvas +luxuriante +luzeiro +maduro +maestro +mafioso +magro +maiuscula +majoritario +malvisto +mamute +manutencao +mapoteca +maquinista +marzipa +masturbar +matuto +mausoleu +mavioso +maxixe +mazurca +meandro +mecha +medusa +mefistofelico +megera +meirinho +melro +memorizar +menu +mequetrefe +mertiolate +mestria +metroviario +mexilhao +mezanino +miau +microssegundo +midia +migratorio +mimosa +minuto +miosotis +mirtilo +misturar +mitzvah +miudos +mixuruca +mnemonico +moagem +mobilizar +modulo +moer +mofo +mogno +moita +molusco +monumento +moqueca +morubixaba +mostruario +motriz +mouse +movivel +mozarela +muarra +muculmano +mudo +mugir +muitos +mumunha +munir +muon +muquira +murros +musselina +nacoes +nado +naftalina +nago +naipe +naja +nalgum +namoro +nanquim +napolitano +naquilo +nascimento +nautilo +navios +nazista +nebuloso +nectarina +nefrologo +negus +nelore +nenufar +nepotismo +nervura +neste +netuno +neutron +nevoeiro +newtoniano +nexo +nhenhenhem +nhoque +nigeriano +niilista +ninho +niobio +niponico +niquelar +nirvana +nisto +nitroglicerina +nivoso +nobreza +nocivo +noel +nogueira +noivo +nojo +nominativo +nonuplo +noruegues +nostalgico +noturno +nouveau +nuanca +nublar +nucleotideo +nudista +nulo +numismatico +nunquinha +nupcias +nutritivo +nuvens +oasis +obcecar +obeso +obituario +objetos +oblongo +obnoxio +obrigatorio +obstruir +obtuso +obus +obvio +ocaso +occipital +oceanografo +ocioso +oclusivo +ocorrer +ocre +octogono +odalisca +odisseia +odorifico +oersted +oeste +ofertar +ofidio +oftalmologo +ogiva +ogum +oigale +oitavo +oitocentos +ojeriza +olaria +oleoso +olfato +olhos +oliveira +olmo +olor +olvidavel +ombudsman +omeleteira +omitir +omoplata +onanismo +ondular +oneroso +onomatopeico +ontologico +onus +onze +opalescente +opcional +operistico +opio +oposto +oprobrio +optometrista +opusculo +oratorio +orbital +orcar +orfao +orixa +orla +ornitologo +orquidea +ortorrombico +orvalho +osculo +osmotico +ossudo +ostrogodo +otario +otite +ouro +ousar +outubro +ouvir +ovario +overnight +oviparo +ovni +ovoviviparo +ovulo +oxala +oxente +oxiuro +oxossi +ozonizar +paciente +pactuar +padronizar +paete +pagodeiro +paixao +pajem +paludismo +pampas +panturrilha +papudo +paquistanes +pastoso +patua +paulo +pauzinhos +pavoroso +paxa +pazes +peao +pecuniario +pedunculo +pegaso +peixinho +pejorativo +pelvis +penuria +pequno +petunia +pezada +piauiense +pictorico +pierro +pigmeu +pijama +pilulas +pimpolho +pintura +piorar +pipocar +piqueteiro +pirulito +pistoleiro +pituitaria +pivotar +pixote +pizzaria +plistoceno +plotar +pluviometrico +pneumonico +poco +podridao +poetisa +pogrom +pois +polvorosa +pomposo +ponderado +pontudo +populoso +poquer +porvir +posudo +potro +pouso +povoar +prazo +prezar +privilegios +proximo +prussiano +pseudopode +psoriase +pterossauros +ptialina +ptolemaico +pudor +pueril +pufe +pugilista +puir +pujante +pulverizar +pumba +punk +purulento +pustula +putsch +puxe +quatrocentos +quetzal +quixotesco +quotizavel +rabujice +racista +radonio +rafia +ragu +rajado +ralo +rampeiro +ranzinza +raptor +raquitismo +raro +rasurar +ratoeira +ravioli +razoavel +reavivar +rebuscar +recusavel +reduzivel +reexposicao +refutavel +regurgitar +reivindicavel +rejuvenescimento +relva +remuneravel +renunciar +reorientar +repuxo +requisito +resumo +returno +reutilizar +revolvido +rezonear +riacho +ribossomo +ricota +ridiculo +rifle +rigoroso +rijo +rimel +rins +rios +riqueza +riquixa +rissole +ritualistico +rivalizar +rixa +robusto +rococo +rodoviario +roer +rogo +rojao +rolo +rompimento +ronronar +roqueiro +rorqual +rosto +rotundo +rouxinol +roxo +royal +ruas +rucula +rudimentos +ruela +rufo +rugoso +ruivo +rule +rumoroso +runico +ruptura +rural +rustico +rutilar +saariano +sabujo +sacudir +sadomasoquista +safra +sagui +sais +samurai +santuario +sapo +saquear +sartriano +saturno +saude +sauva +saveiro +saxofonista +sazonal +scherzo +script +seara +seborreia +secura +seduzir +sefardim +seguro +seja +selvas +sempre +senzala +sepultura +sequoia +sestercio +setuplo +seus +seviciar +sezonismo +shalom +siames +sibilante +sicrano +sidra +sifilitico +signos +silvo +simultaneo +sinusite +sionista +sirio +sisudo +situar +sivan +slide +slogan +soar +sobrio +socratico +sodomizar +soerguer +software +sogro +soja +solver +somente +sonso +sopro +soquete +sorveteiro +sossego +soturno +sousafone +sovinice +sozinho +suavizar +subverter +sucursal +sudoriparo +sufragio +sugestoes +suite +sujo +sultao +sumula +suntuoso +suor +supurar +suruba +susto +suturar +suvenir +tabuleta +taco +tadjique +tafeta +tagarelice +taitiano +talvez +tampouco +tanzaniano +taoista +tapume +taquion +tarugo +tascar +tatuar +tautologico +tavola +taxionomista +tchecoslovaco +teatrologo +tectonismo +tedioso +teflon +tegumento +teixo +telurio +temporas +tenue +teosofico +tepido +tequila +terrorista +testosterona +tetrico +teutonico +teve +texugo +tiara +tibia +tiete +tifoide +tigresa +tijolo +tilintar +timpano +tintureiro +tiquete +tiroteio +tisico +titulos +tive +toar +toboga +tofu +togoles +toicinho +tolueno +tomografo +tontura +toponimo +toquio +torvelinho +tostar +toto +touro +toxina +trazer +trezentos +trivialidade +trovoar +truta +tuaregue +tubular +tucano +tudo +tufo +tuiste +tulipa +tumultuoso +tunisino +tupiniquim +turvo +tutu +ucraniano +udenista +ufanista +ufologo +ugaritico +uiste +uivo +ulceroso +ulema +ultravioleta +umbilical +umero +umido +umlaut +unanimidade +unesco +ungulado +unheiro +univoco +untuoso +urano +urbano +urdir +uretra +urgente +urinol +urna +urologo +urro +ursulina +urtiga +urupe +usavel +usbeque +usei +usineiro +usurpar +utero +utilizar +utopico +uvular +uxoricidio +vacuo +vadio +vaguear +vaivem +valvula +vampiro +vantajoso +vaporoso +vaquinha +varziano +vasto +vaticinio +vaudeville +vazio +veado +vedico +veemente +vegetativo +veio +veja +veludo +venusiano +verdade +verve +vestuario +vetusto +vexatorio +vezes +viavel +vibratorio +victor +vicunha +vidros +vietnamita +vigoroso +vilipendiar +vime +vintem +violoncelo +viquingue +virus +visualizar +vituperio +viuvo +vivo +vizir +voar +vociferar +vodu +vogar +voile +volver +vomito +vontade +vortice +vosso +voto +vovozinha +voyeuse +vozes +vulva +vupt +western +xadrez +xale +xampu +xango +xarope +xaual +xavante +xaxim +xenonio +xepa +xerox +xicara +xifopago +xiita +xilogravura +xinxim +xistoso +xixi +xodo +xogum +xucro +zabumba +zagueiro +zambiano +zanzar +zarpar +zebu +zefiro +zeloso +zenite +zumbi diff --git a/electrum/wordlist/spanish.txt b/electrum/wordlist/spanish.txt @@ -0,0 +1,2048 @@ +ábaco +abdomen +abeja +abierto +abogado +abono +aborto +abrazo +abrir +abuelo +abuso +acabar +academia +acceso +acción +aceite +acelga +acento +aceptar +ácido +aclarar +acné +acoger +acoso +activo +acto +actriz +actuar +acudir +acuerdo +acusar +adicto +admitir +adoptar +adorno +aduana +adulto +aéreo +afectar +afición +afinar +afirmar +ágil +agitar +agonía +agosto +agotar +agregar +agrio +agua +agudo +águila +aguja +ahogo +ahorro +aire +aislar +ajedrez +ajeno +ajuste +alacrán +alambre +alarma +alba +álbum +alcalde +aldea +alegre +alejar +alerta +aleta +alfiler +alga +algodón +aliado +aliento +alivio +alma +almeja +almíbar +altar +alteza +altivo +alto +altura +alumno +alzar +amable +amante +amapola +amargo +amasar +ámbar +ámbito +ameno +amigo +amistad +amor +amparo +amplio +ancho +anciano +ancla +andar +andén +anemia +ángulo +anillo +ánimo +anís +anotar +antena +antiguo +antojo +anual +anular +anuncio +añadir +añejo +año +apagar +aparato +apetito +apio +aplicar +apodo +aporte +apoyo +aprender +aprobar +apuesta +apuro +arado +araña +arar +árbitro +árbol +arbusto +archivo +arco +arder +ardilla +arduo +área +árido +aries +armonía +arnés +aroma +arpa +arpón +arreglo +arroz +arruga +arte +artista +asa +asado +asalto +ascenso +asegurar +aseo +asesor +asiento +asilo +asistir +asno +asombro +áspero +astilla +astro +astuto +asumir +asunto +atajo +ataque +atar +atento +ateo +ático +atleta +átomo +atraer +atroz +atún +audaz +audio +auge +aula +aumento +ausente +autor +aval +avance +avaro +ave +avellana +avena +avestruz +avión +aviso +ayer +ayuda +ayuno +azafrán +azar +azote +azúcar +azufre +azul +baba +babor +bache +bahía +baile +bajar +balanza +balcón +balde +bambú +banco +banda +baño +barba +barco +barniz +barro +báscula +bastón +basura +batalla +batería +batir +batuta +baúl +bazar +bebé +bebida +bello +besar +beso +bestia +bicho +bien +bingo +blanco +bloque +blusa +boa +bobina +bobo +boca +bocina +boda +bodega +boina +bola +bolero +bolsa +bomba +bondad +bonito +bono +bonsái +borde +borrar +bosque +bote +botín +bóveda +bozal +bravo +brazo +brecha +breve +brillo +brinco +brisa +broca +broma +bronce +brote +bruja +brusco +bruto +buceo +bucle +bueno +buey +bufanda +bufón +búho +buitre +bulto +burbuja +burla +burro +buscar +butaca +buzón +caballo +cabeza +cabina +cabra +cacao +cadáver +cadena +caer +café +caída +caimán +caja +cajón +cal +calamar +calcio +caldo +calidad +calle +calma +calor +calvo +cama +cambio +camello +camino +campo +cáncer +candil +canela +canguro +canica +canto +caña +cañón +caoba +caos +capaz +capitán +capote +captar +capucha +cara +carbón +cárcel +careta +carga +cariño +carne +carpeta +carro +carta +casa +casco +casero +caspa +castor +catorce +catre +caudal +causa +cazo +cebolla +ceder +cedro +celda +célebre +celoso +célula +cemento +ceniza +centro +cerca +cerdo +cereza +cero +cerrar +certeza +césped +cetro +chacal +chaleco +champú +chancla +chapa +charla +chico +chiste +chivo +choque +choza +chuleta +chupar +ciclón +ciego +cielo +cien +cierto +cifra +cigarro +cima +cinco +cine +cinta +ciprés +circo +ciruela +cisne +cita +ciudad +clamor +clan +claro +clase +clave +cliente +clima +clínica +cobre +cocción +cochino +cocina +coco +código +codo +cofre +coger +cohete +cojín +cojo +cola +colcha +colegio +colgar +colina +collar +colmo +columna +combate +comer +comida +cómodo +compra +conde +conejo +conga +conocer +consejo +contar +copa +copia +corazón +corbata +corcho +cordón +corona +correr +coser +cosmos +costa +cráneo +cráter +crear +crecer +creído +crema +cría +crimen +cripta +crisis +cromo +crónica +croqueta +crudo +cruz +cuadro +cuarto +cuatro +cubo +cubrir +cuchara +cuello +cuento +cuerda +cuesta +cueva +cuidar +culebra +culpa +culto +cumbre +cumplir +cuna +cuneta +cuota +cupón +cúpula +curar +curioso +curso +curva +cutis +dama +danza +dar +dardo +dátil +deber +débil +década +decir +dedo +defensa +definir +dejar +delfín +delgado +delito +demora +denso +dental +deporte +derecho +derrota +desayuno +deseo +desfile +desnudo +destino +desvío +detalle +detener +deuda +día +diablo +diadema +diamante +diana +diario +dibujo +dictar +diente +dieta +diez +difícil +digno +dilema +diluir +dinero +directo +dirigir +disco +diseño +disfraz +diva +divino +doble +doce +dolor +domingo +don +donar +dorado +dormir +dorso +dos +dosis +dragón +droga +ducha +duda +duelo +dueño +dulce +dúo +duque +durar +dureza +duro +ébano +ebrio +echar +eco +ecuador +edad +edición +edificio +editor +educar +efecto +eficaz +eje +ejemplo +elefante +elegir +elemento +elevar +elipse +élite +elixir +elogio +eludir +embudo +emitir +emoción +empate +empeño +empleo +empresa +enano +encargo +enchufe +encía +enemigo +enero +enfado +enfermo +engaño +enigma +enlace +enorme +enredo +ensayo +enseñar +entero +entrar +envase +envío +época +equipo +erizo +escala +escena +escolar +escribir +escudo +esencia +esfera +esfuerzo +espada +espejo +espía +esposa +espuma +esquí +estar +este +estilo +estufa +etapa +eterno +ética +etnia +evadir +evaluar +evento +evitar +exacto +examen +exceso +excusa +exento +exigir +exilio +existir +éxito +experto +explicar +exponer +extremo +fábrica +fábula +fachada +fácil +factor +faena +faja +falda +fallo +falso +faltar +fama +familia +famoso +faraón +farmacia +farol +farsa +fase +fatiga +fauna +favor +fax +febrero +fecha +feliz +feo +feria +feroz +fértil +fervor +festín +fiable +fianza +fiar +fibra +ficción +ficha +fideo +fiebre +fiel +fiera +fiesta +figura +fijar +fijo +fila +filete +filial +filtro +fin +finca +fingir +finito +firma +flaco +flauta +flecha +flor +flota +fluir +flujo +flúor +fobia +foca +fogata +fogón +folio +folleto +fondo +forma +forro +fortuna +forzar +fosa +foto +fracaso +frágil +franja +frase +fraude +freír +freno +fresa +frío +frito +fruta +fuego +fuente +fuerza +fuga +fumar +función +funda +furgón +furia +fusil +fútbol +futuro +gacela +gafas +gaita +gajo +gala +galería +gallo +gamba +ganar +gancho +ganga +ganso +garaje +garza +gasolina +gastar +gato +gavilán +gemelo +gemir +gen +género +genio +gente +geranio +gerente +germen +gesto +gigante +gimnasio +girar +giro +glaciar +globo +gloria +gol +golfo +goloso +golpe +goma +gordo +gorila +gorra +gota +goteo +gozar +grada +gráfico +grano +grasa +gratis +grave +grieta +grillo +gripe +gris +grito +grosor +grúa +grueso +grumo +grupo +guante +guapo +guardia +guerra +guía +guiño +guion +guiso +guitarra +gusano +gustar +haber +hábil +hablar +hacer +hacha +hada +hallar +hamaca +harina +haz +hazaña +hebilla +hebra +hecho +helado +helio +hembra +herir +hermano +héroe +hervir +hielo +hierro +hígado +higiene +hijo +himno +historia +hocico +hogar +hoguera +hoja +hombre +hongo +honor +honra +hora +hormiga +horno +hostil +hoyo +hueco +huelga +huerta +hueso +huevo +huida +huir +humano +húmedo +humilde +humo +hundir +huracán +hurto +icono +ideal +idioma +ídolo +iglesia +iglú +igual +ilegal +ilusión +imagen +imán +imitar +impar +imperio +imponer +impulso +incapaz +índice +inerte +infiel +informe +ingenio +inicio +inmenso +inmune +innato +insecto +instante +interés +íntimo +intuir +inútil +invierno +ira +iris +ironía +isla +islote +jabalí +jabón +jamón +jarabe +jardín +jarra +jaula +jazmín +jefe +jeringa +jinete +jornada +joroba +joven +joya +juerga +jueves +juez +jugador +jugo +juguete +juicio +junco +jungla +junio +juntar +júpiter +jurar +justo +juvenil +juzgar +kilo +koala +labio +lacio +lacra +lado +ladrón +lagarto +lágrima +laguna +laico +lamer +lámina +lámpara +lana +lancha +langosta +lanza +lápiz +largo +larva +lástima +lata +látex +latir +laurel +lavar +lazo +leal +lección +leche +lector +leer +legión +legumbre +lejano +lengua +lento +leña +león +leopardo +lesión +letal +letra +leve +leyenda +libertad +libro +licor +líder +lidiar +lienzo +liga +ligero +lima +límite +limón +limpio +lince +lindo +línea +lingote +lino +linterna +líquido +liso +lista +litera +litio +litro +llaga +llama +llanto +llave +llegar +llenar +llevar +llorar +llover +lluvia +lobo +loción +loco +locura +lógica +logro +lombriz +lomo +lonja +lote +lucha +lucir +lugar +lujo +luna +lunes +lupa +lustro +luto +luz +maceta +macho +madera +madre +maduro +maestro +mafia +magia +mago +maíz +maldad +maleta +malla +malo +mamá +mambo +mamut +manco +mando +manejar +manga +maniquí +manjar +mano +manso +manta +mañana +mapa +máquina +mar +marco +marea +marfil +margen +marido +mármol +marrón +martes +marzo +masa +máscara +masivo +matar +materia +matiz +matriz +máximo +mayor +mazorca +mecha +medalla +medio +médula +mejilla +mejor +melena +melón +memoria +menor +mensaje +mente +menú +mercado +merengue +mérito +mes +mesón +meta +meter +método +metro +mezcla +miedo +miel +miembro +miga +mil +milagro +militar +millón +mimo +mina +minero +mínimo +minuto +miope +mirar +misa +miseria +misil +mismo +mitad +mito +mochila +moción +moda +modelo +moho +mojar +molde +moler +molino +momento +momia +monarca +moneda +monja +monto +moño +morada +morder +moreno +morir +morro +morsa +mortal +mosca +mostrar +motivo +mover +móvil +mozo +mucho +mudar +mueble +muela +muerte +muestra +mugre +mujer +mula +muleta +multa +mundo +muñeca +mural +muro +músculo +museo +musgo +música +muslo +nácar +nación +nadar +naipe +naranja +nariz +narrar +nasal +natal +nativo +natural +náusea +naval +nave +navidad +necio +néctar +negar +negocio +negro +neón +nervio +neto +neutro +nevar +nevera +nicho +nido +niebla +nieto +niñez +niño +nítido +nivel +nobleza +noche +nómina +noria +norma +norte +nota +noticia +novato +novela +novio +nube +nuca +núcleo +nudillo +nudo +nuera +nueve +nuez +nulo +número +nutria +oasis +obeso +obispo +objeto +obra +obrero +observar +obtener +obvio +oca +ocaso +océano +ochenta +ocho +ocio +ocre +octavo +octubre +oculto +ocupar +ocurrir +odiar +odio +odisea +oeste +ofensa +oferta +oficio +ofrecer +ogro +oído +oír +ojo +ola +oleada +olfato +olivo +olla +olmo +olor +olvido +ombligo +onda +onza +opaco +opción +ópera +opinar +oponer +optar +óptica +opuesto +oración +orador +oral +órbita +orca +orden +oreja +órgano +orgía +orgullo +oriente +origen +orilla +oro +orquesta +oruga +osadía +oscuro +osezno +oso +ostra +otoño +otro +oveja +óvulo +óxido +oxígeno +oyente +ozono +pacto +padre +paella +página +pago +país +pájaro +palabra +palco +paleta +pálido +palma +paloma +palpar +pan +panal +pánico +pantera +pañuelo +papá +papel +papilla +paquete +parar +parcela +pared +parir +paro +párpado +parque +párrafo +parte +pasar +paseo +pasión +paso +pasta +pata +patio +patria +pausa +pauta +pavo +payaso +peatón +pecado +pecera +pecho +pedal +pedir +pegar +peine +pelar +peldaño +pelea +peligro +pellejo +pelo +peluca +pena +pensar +peñón +peón +peor +pepino +pequeño +pera +percha +perder +pereza +perfil +perico +perla +permiso +perro +persona +pesa +pesca +pésimo +pestaña +pétalo +petróleo +pez +pezuña +picar +pichón +pie +piedra +pierna +pieza +pijama +pilar +piloto +pimienta +pino +pintor +pinza +piña +piojo +pipa +pirata +pisar +piscina +piso +pista +pitón +pizca +placa +plan +plata +playa +plaza +pleito +pleno +plomo +pluma +plural +pobre +poco +poder +podio +poema +poesía +poeta +polen +policía +pollo +polvo +pomada +pomelo +pomo +pompa +poner +porción +portal +posada +poseer +posible +poste +potencia +potro +pozo +prado +precoz +pregunta +premio +prensa +preso +previo +primo +príncipe +prisión +privar +proa +probar +proceso +producto +proeza +profesor +programa +prole +promesa +pronto +propio +próximo +prueba +público +puchero +pudor +pueblo +puerta +puesto +pulga +pulir +pulmón +pulpo +pulso +puma +punto +puñal +puño +pupa +pupila +puré +quedar +queja +quemar +querer +queso +quieto +química +quince +quitar +rábano +rabia +rabo +ración +radical +raíz +rama +rampa +rancho +rango +rapaz +rápido +rapto +rasgo +raspa +rato +rayo +raza +razón +reacción +realidad +rebaño +rebote +recaer +receta +rechazo +recoger +recreo +recto +recurso +red +redondo +reducir +reflejo +reforma +refrán +refugio +regalo +regir +regla +regreso +rehén +reino +reír +reja +relato +relevo +relieve +relleno +reloj +remar +remedio +remo +rencor +rendir +renta +reparto +repetir +reposo +reptil +res +rescate +resina +respeto +resto +resumen +retiro +retorno +retrato +reunir +revés +revista +rey +rezar +rico +riego +rienda +riesgo +rifa +rígido +rigor +rincón +riñón +río +riqueza +risa +ritmo +rito +rizo +roble +roce +rociar +rodar +rodeo +rodilla +roer +rojizo +rojo +romero +romper +ron +ronco +ronda +ropa +ropero +rosa +rosca +rostro +rotar +rubí +rubor +rudo +rueda +rugir +ruido +ruina +ruleta +rulo +rumbo +rumor +ruptura +ruta +rutina +sábado +saber +sabio +sable +sacar +sagaz +sagrado +sala +saldo +salero +salir +salmón +salón +salsa +salto +salud +salvar +samba +sanción +sandía +sanear +sangre +sanidad +sano +santo +sapo +saque +sardina +sartén +sastre +satán +sauna +saxofón +sección +seco +secreto +secta +sed +seguir +seis +sello +selva +semana +semilla +senda +sensor +señal +señor +separar +sepia +sequía +ser +serie +sermón +servir +sesenta +sesión +seta +setenta +severo +sexo +sexto +sidra +siesta +siete +siglo +signo +sílaba +silbar +silencio +silla +símbolo +simio +sirena +sistema +sitio +situar +sobre +socio +sodio +sol +solapa +soldado +soledad +sólido +soltar +solución +sombra +sondeo +sonido +sonoro +sonrisa +sopa +soplar +soporte +sordo +sorpresa +sorteo +sostén +sótano +suave +subir +suceso +sudor +suegra +suelo +sueño +suerte +sufrir +sujeto +sultán +sumar +superar +suplir +suponer +supremo +sur +surco +sureño +surgir +susto +sutil +tabaco +tabique +tabla +tabú +taco +tacto +tajo +talar +talco +talento +talla +talón +tamaño +tambor +tango +tanque +tapa +tapete +tapia +tapón +taquilla +tarde +tarea +tarifa +tarjeta +tarot +tarro +tarta +tatuaje +tauro +taza +tazón +teatro +techo +tecla +técnica +tejado +tejer +tejido +tela +teléfono +tema +temor +templo +tenaz +tender +tener +tenis +tenso +teoría +terapia +terco +término +ternura +terror +tesis +tesoro +testigo +tetera +texto +tez +tibio +tiburón +tiempo +tienda +tierra +tieso +tigre +tijera +tilde +timbre +tímido +timo +tinta +tío +típico +tipo +tira +tirón +titán +títere +título +tiza +toalla +tobillo +tocar +tocino +todo +toga +toldo +tomar +tono +tonto +topar +tope +toque +tórax +torero +tormenta +torneo +toro +torpedo +torre +torso +tortuga +tos +tosco +toser +tóxico +trabajo +tractor +traer +tráfico +trago +traje +tramo +trance +trato +trauma +trazar +trébol +tregua +treinta +tren +trepar +tres +tribu +trigo +tripa +triste +triunfo +trofeo +trompa +tronco +tropa +trote +trozo +truco +trueno +trufa +tubería +tubo +tuerto +tumba +tumor +túnel +túnica +turbina +turismo +turno +tutor +ubicar +úlcera +umbral +unidad +unir +universo +uno +untar +uña +urbano +urbe +urgente +urna +usar +usuario +útil +utopía +uva +vaca +vacío +vacuna +vagar +vago +vaina +vajilla +vale +válido +valle +valor +válvula +vampiro +vara +variar +varón +vaso +vecino +vector +vehículo +veinte +vejez +vela +velero +veloz +vena +vencer +venda +veneno +vengar +venir +venta +venus +ver +verano +verbo +verde +vereda +verja +verso +verter +vía +viaje +vibrar +vicio +víctima +vida +vídeo +vidrio +viejo +viernes +vigor +vil +villa +vinagre +vino +viñedo +violín +viral +virgo +virtud +visor +víspera +vista +vitamina +viudo +vivaz +vivero +vivir +vivo +volcán +volumen +volver +voraz +votar +voto +voz +vuelo +vulgar +yacer +yate +yegua +yema +yerno +yeso +yodo +yoga +yogur +zafiro +zanja +zapato +zarza +zona +zorro +zumo +zurdo diff --git a/electrum/x509.py b/electrum/x509.py @@ -0,0 +1,341 @@ +#!/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. +from . import util +from .util import profiler, bh2u +import ecdsa +import hashlib + +# algo OIDs +ALGO_RSA_SHA1 = '1.2.840.113549.1.1.5' +ALGO_RSA_SHA256 = '1.2.840.113549.1.1.11' +ALGO_RSA_SHA384 = '1.2.840.113549.1.1.12' +ALGO_RSA_SHA512 = '1.2.840.113549.1.1.13' +ALGO_ECDSA_SHA256 = '1.2.840.10045.4.3.2' + +# prefixes, see http://stackoverflow.com/questions/3713774/c-sharp-how-to-calculate-asn-1-der-encoding-of-a-particular-hash-algorithm +PREFIX_RSA_SHA256 = bytearray( + [0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20]) +PREFIX_RSA_SHA384 = bytearray( + [0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30]) +PREFIX_RSA_SHA512 = bytearray( + [0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40]) + +# types used in ASN1 structured data +ASN1_TYPES = { + 'BOOLEAN' : 0x01, + 'INTEGER' : 0x02, + 'BIT STRING' : 0x03, + 'OCTET STRING' : 0x04, + 'NULL' : 0x05, + 'OBJECT IDENTIFIER': 0x06, + 'SEQUENCE' : 0x70, + 'SET' : 0x71, + 'PrintableString' : 0x13, + 'IA5String' : 0x16, + 'UTCTime' : 0x17, + 'GeneralizedTime' : 0x18, + 'ENUMERATED' : 0x0A, + 'UTF8String' : 0x0C, +} + + +class CertificateError(Exception): + pass + + +# helper functions +def bitstr_to_bytestr(s): + if s[0] != 0x00: + raise TypeError('no padding') + return s[1:] + + +def bytestr_to_int(s): + i = 0 + for char in s: + i <<= 8 + i |= char + return i + + +def decode_OID(s): + r = [] + r.append(s[0] // 40) + r.append(s[0] % 40) + k = 0 + for i in s[1:]: + if i < 128: + r.append(i + 128 * k) + k = 0 + else: + k = (i - 128) + 128 * k + return '.'.join(map(str, r)) + + +def encode_OID(oid): + x = [int(i) for i in oid.split('.')] + s = chr(x[0] * 40 + x[1]) + for i in x[2:]: + ss = chr(i % 128) + while i > 128: + i //= 128 + ss = chr(128 + i % 128) + ss + s += ss + return s + + +class ASN1_Node(bytes): + def get_node(self, ix): + # return index of first byte, first content byte and last byte. + first = self[ix + 1] + if (first & 0x80) == 0: + length = first + ixf = ix + 2 + ixl = ixf + length - 1 + else: + lengthbytes = first & 0x7F + length = bytestr_to_int(self[ix + 2:ix + 2 + lengthbytes]) + ixf = ix + 2 + lengthbytes + ixl = ixf + length - 1 + return ix, ixf, ixl + + def root(self): + return self.get_node(0) + + def next_node(self, node): + ixs, ixf, ixl = node + return self.get_node(ixl + 1) + + def first_child(self, node): + ixs, ixf, ixl = node + if self[ixs] & 0x20 != 0x20: + raise TypeError('Can only open constructed types.', hex(self[ixs])) + return self.get_node(ixf) + + def is_child_of(node1, node2): + ixs, ixf, ixl = node1 + jxs, jxf, jxl = node2 + return ((ixf <= jxs) and (jxl <= ixl)) or ((jxf <= ixs) and (ixl <= jxl)) + + def get_all(self, node): + # return type + length + value + ixs, ixf, ixl = node + return self[ixs:ixl + 1] + + def get_value_of_type(self, node, asn1_type): + # verify type byte and return content + ixs, ixf, ixl = node + if ASN1_TYPES[asn1_type] != self[ixs]: + raise TypeError('Wrong type:', hex(self[ixs]), hex(ASN1_TYPES[asn1_type])) + return self[ixf:ixl + 1] + + def get_value(self, node): + ixs, ixf, ixl = node + return self[ixf:ixl + 1] + + def get_children(self, node): + nodes = [] + ii = self.first_child(node) + nodes.append(ii) + while ii[2] < node[2]: + ii = self.next_node(ii) + nodes.append(ii) + return nodes + + def get_sequence(self): + return list(map(lambda j: self.get_value(j), self.get_children(self.root()))) + + def get_dict(self, node): + p = {} + for ii in self.get_children(node): + for iii in self.get_children(ii): + iiii = self.first_child(iii) + oid = decode_OID(self.get_value_of_type(iiii, 'OBJECT IDENTIFIER')) + iiii = self.next_node(iiii) + value = self.get_value(iiii) + p[oid] = value + return p + + +class X509(object): + def __init__(self, b): + + self.bytes = bytearray(b) + + der = ASN1_Node(b) + root = der.root() + cert = der.first_child(root) + # data for signature + self.data = der.get_all(cert) + + # optional version field + if der.get_value(cert)[0] == 0xa0: + version = der.first_child(cert) + serial_number = der.next_node(version) + else: + serial_number = der.first_child(cert) + self.serial_number = bytestr_to_int(der.get_value_of_type(serial_number, 'INTEGER')) + + # signature algorithm + sig_algo = der.next_node(serial_number) + ii = der.first_child(sig_algo) + self.sig_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER')) + + # issuer + issuer = der.next_node(sig_algo) + self.issuer = der.get_dict(issuer) + + # validity + validity = der.next_node(issuer) + ii = der.first_child(validity) + try: + self.notBefore = der.get_value_of_type(ii, 'UTCTime') + except TypeError: + self.notBefore = der.get_value_of_type(ii, 'GeneralizedTime')[2:] # strip year + ii = der.next_node(ii) + try: + self.notAfter = der.get_value_of_type(ii, 'UTCTime') + except TypeError: + self.notAfter = der.get_value_of_type(ii, 'GeneralizedTime')[2:] # strip year + + # subject + subject = der.next_node(validity) + self.subject = der.get_dict(subject) + subject_pki = der.next_node(subject) + public_key_algo = der.first_child(subject_pki) + ii = der.first_child(public_key_algo) + self.public_key_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER')) + + if self.public_key_algo != '1.2.840.10045.2.1': # for non EC public key + # pubkey modulus and exponent + subject_public_key = der.next_node(public_key_algo) + spk = der.get_value_of_type(subject_public_key, 'BIT STRING') + spk = ASN1_Node(bitstr_to_bytestr(spk)) + r = spk.root() + modulus = spk.first_child(r) + exponent = spk.next_node(modulus) + rsa_n = spk.get_value_of_type(modulus, 'INTEGER') + rsa_e = spk.get_value_of_type(exponent, 'INTEGER') + self.modulus = ecdsa.util.string_to_number(rsa_n) + self.exponent = ecdsa.util.string_to_number(rsa_e) + else: + subject_public_key = der.next_node(public_key_algo) + spk = der.get_value_of_type(subject_public_key, 'BIT STRING') + self.ec_public_key = spk + + # extensions + self.CA = False + self.AKI = None + self.SKI = None + i = subject_pki + while i[2] < cert[2]: + i = der.next_node(i) + d = der.get_dict(i) + for oid, value in d.items(): + value = ASN1_Node(value) + if oid == '2.5.29.19': + # Basic Constraints + self.CA = bool(value) + elif oid == '2.5.29.14': + # Subject Key Identifier + r = value.root() + value = value.get_value_of_type(r, 'OCTET STRING') + self.SKI = bh2u(value) + elif oid == '2.5.29.35': + # Authority Key Identifier + self.AKI = bh2u(value.get_sequence()[0]) + else: + pass + + # cert signature + cert_sig_algo = der.next_node(cert) + ii = der.first_child(cert_sig_algo) + self.cert_sig_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER')) + cert_sig = der.next_node(cert_sig_algo) + self.signature = der.get_value(cert_sig)[1:] + + def get_keyID(self): + # http://security.stackexchange.com/questions/72077/validating-an-ssl-certificate-chain-according-to-rfc-5280-am-i-understanding-th + return self.SKI if self.SKI else repr(self.subject) + + def get_issuer_keyID(self): + return self.AKI if self.AKI else repr(self.issuer) + + def get_common_name(self): + return self.subject.get('2.5.4.3', b'unknown').decode() + + def get_signature(self): + return self.cert_sig_algo, self.signature, self.data + + def check_ca(self): + return self.CA + + def check_date(self): + import time + now = time.time() + TIMESTAMP_FMT = '%y%m%d%H%M%SZ' + not_before = time.mktime(time.strptime(self.notBefore.decode('ascii'), TIMESTAMP_FMT)) + not_after = time.mktime(time.strptime(self.notAfter.decode('ascii'), TIMESTAMP_FMT)) + if not_before > now: + raise CertificateError('Certificate has not entered its valid date range. (%s)' % self.get_common_name()) + if not_after <= now: + raise CertificateError('Certificate has expired. (%s)' % self.get_common_name()) + + def getFingerprint(self): + return hashlib.sha1(self.bytes).digest() + + +@profiler +def load_certificates(ca_path): + from . import pem + ca_list = {} + ca_keyID = {} + # ca_path = '/tmp/tmp.txt' + with open(ca_path, 'r', encoding='utf-8') as f: + s = f.read() + bList = pem.dePemList(s, "CERTIFICATE") + for b in bList: + try: + x = X509(b) + x.check_date() + except BaseException as e: + # with open('/tmp/tmp.txt', 'w') as f: + # f.write(pem.pem(b, 'CERTIFICATE').decode('ascii')) + util.print_error("cert error:", e) + continue + + fp = x.getFingerprint() + ca_list[fp] = x + ca_keyID[x.get_keyID()] = fp + + return ca_list, ca_keyID + + +if __name__ == "__main__": + import requests + + util.set_verbosity(True) + ca_path = requests.certs.where() + ca_list, ca_keyID = load_certificates(ca_path) diff --git a/gui/kivy/Makefile b/gui/kivy/Makefile @@ -1,32 +0,0 @@ -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 ../../electrum ../../main.py - @-if [ ! -d "../../.buildozer" ];then \ - cd ../..; buildozer android debug;\ - cp -f 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 diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv @@ -1,464 +0,0 @@ -#: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://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://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://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://gui/kivy/theming/light/tab_btn' - background_down: 'atlas://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: 'gui/kivy/data/background.png' - - ActionBar: - - ActionView: - id: av - ActionPrevious: - app_icon: 'atlas://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 diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py @@ -1,1028 +0,0 @@ -import re -import os -import sys -import time -import datetime -import traceback -from decimal import Decimal -import threading - -import electrum -from electrum.bitcoin import TYPE_ADDRESS -from electrum import WalletStorage, Wallet -from electrum_gui.kivy.i18n import _ -from electrum.paymentrequest import InvoiceStore -from electrum.util import profiler, InvalidPassword -from electrum.plugins 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', - 'gui/kivy/data/fonts/Roboto.ttf', - 'gui/kivy/data/fonts/Roboto.ttf', - 'gui/kivy/data/fonts/Roboto-Bold.ttf', - '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('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('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('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('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('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://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://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://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)) diff --git a/gui/kivy/nfc_scanner/__init__.py b/gui/kivy/nfc_scanner/__init__.py @@ -1,44 +0,0 @@ -__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') diff --git a/gui/kivy/nfc_scanner/scanner_android.py b/gui/kivy/nfc_scanner/scanner_android.py @@ -1,242 +0,0 @@ -'''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 electrum_gui.kivy.nfc_scanner 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() diff --git a/gui/kivy/nfc_scanner/scanner_dummy.py b/gui/kivy/nfc_scanner/scanner_dummy.py @@ -1,52 +0,0 @@ -''' Dummy NFC Provider to be used on desktops in case no other provider is found -''' -from electrum_gui.kivy.nfc_scanner 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') diff --git a/gui/kivy/uix/context_menu.py b/gui/kivy/uix/context_menu.py @@ -1,56 +0,0 @@ -#!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() diff --git a/gui/kivy/uix/dialogs/__init__.py b/gui/kivy/uix/dialogs/__init__.py @@ -1,220 +0,0 @@ -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 diff --git a/gui/kivy/uix/dialogs/addresses.py b/gui/kivy/uix/dialogs/addresses.py @@ -1,180 +0,0 @@ -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 diff --git a/gui/kivy/uix/dialogs/bump_fee_dialog.py b/gui/kivy/uix/dialogs/bump_fee_dialog.py @@ -1,118 +0,0 @@ -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() diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py @@ -1,131 +0,0 @@ -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() diff --git a/gui/kivy/uix/dialogs/fx_dialog.py b/gui/kivy/uix/dialogs/fx_dialog.py @@ -1,111 +0,0 @@ -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()) diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py @@ -1,1038 +0,0 @@ - -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://gui/kivy/theming/light/create_act_text_active' - background_normal: 'atlas://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: '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://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://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://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() diff --git a/gui/kivy/uix/dialogs/invoices.py b/gui/kivy/uix/dialogs/invoices.py @@ -1,169 +0,0 @@ -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://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://gui/kivy/theming/light/important', - PR_UNKNOWN: 'atlas://gui/kivy/theming/light/important', - PR_PAID: 'atlas://gui/kivy/theming/light/confirmed', - PR_EXPIRED: 'atlas://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 diff --git a/gui/kivy/uix/dialogs/label_dialog.py b/gui/kivy/uix/dialogs/label_dialog.py @@ -1,55 +0,0 @@ -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://gui/kivy/theming/light/tab_btn' - background_active: 'atlas://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 diff --git a/gui/kivy/uix/dialogs/nfc_transaction.py b/gui/kivy/uix/dialogs/nfc_transaction.py @@ -1,32 +0,0 @@ -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 diff --git a/gui/kivy/uix/dialogs/password_dialog.py b/gui/kivy/uix/dialogs/password_dialog.py @@ -1,142 +0,0 @@ -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 = '' diff --git a/gui/kivy/uix/dialogs/qr_scanner.py b/gui/kivy/uix/dialogs/qr_scanner.py @@ -1,44 +0,0 @@ -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://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) -''') diff --git a/gui/kivy/uix/dialogs/question.py b/gui/kivy/uix/dialogs/question.py @@ -1,53 +0,0 @@ -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 diff --git a/gui/kivy/uix/dialogs/requests.py b/gui/kivy/uix/dialogs/requests.py @@ -1,157 +0,0 @@ -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://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://gui/kivy/theming/light/important', - PR_UNKNOWN: 'atlas://gui/kivy/theming/light/important', - PR_PAID: 'atlas://gui/kivy/theming/light/confirmed', - PR_EXPIRED: 'atlas://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 diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py @@ -1,220 +0,0 @@ -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.plugins 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('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() diff --git a/gui/kivy/uix/dialogs/tx_dialog.py b/gui/kivy/uix/dialogs/tx_dialog.py @@ -1,184 +0,0 @@ -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://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) diff --git a/gui/kivy/uix/menus.py b/gui/kivy/uix/menus.py @@ -1,95 +0,0 @@ -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 electrum_gui.kivy.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) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py @@ -1,484 +0,0 @@ -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.plugins 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('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://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) diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv @@ -1,78 +0,0 @@ -#:import _ electrum_gui.kivy.i18n._ -#:import Factory kivy.factory.Factory -#:set font_light '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://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' diff --git a/gui/kivy/uix/ui_screens/receive.kv b/gui/kivy/uix/ui_screens/receive.kv @@ -1,142 +0,0 @@ -#:import _ electrum_gui.kivy.i18n._ -#:import Decimal decimal.Decimal -#:set btc_symbol chr(171) -#:set mbtc_symbol chr(187) -#:set font_light '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://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://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://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://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://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()) diff --git a/gui/kivy/uix/ui_screens/send.kv b/gui/kivy/uix/ui_screens/send.kv @@ -1,127 +0,0 @@ -#:import _ electrum_gui.kivy.i18n._ -#:import Decimal decimal.Decimal -#:set btc_symbol chr(171) -#:set mbtc_symbol chr(187) -#:set font_light '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://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://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://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://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://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://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 - - diff --git a/gui/qt/__init__.py b/gui/qt/__init__.py @@ -1,313 +0,0 @@ -#!/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.plugins import run_hook -from electrum 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 gui/qt/icons_rc.py', and reinstall Electrum") - 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() diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py @@ -1,195 +0,0 @@ -#!/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.plugins 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 diff --git a/gui/qt/completion_text_edit.py b/gui/qt/completion_text_edit.py @@ -1,120 +0,0 @@ -#!/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_()- \ No newline at end of file diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py @@ -1,98 +0,0 @@ -#!/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.plugins 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) diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py @@ -1,643 +0,0 @@ - -import os -import sys -import threading -import traceback - -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * - -from electrum import Wallet, 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) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -1,3221 +0,0 @@ -#!/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 -from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS -from electrum import constants -from electrum.plugins 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 import Transaction -from electrum import util, bitcoin, commands, coinchooser -from electrum import paymentrequest -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()): - name = descr['__name__'] - 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 diff --git a/gui/qt/password_dialog.py b/gui/qt/password_dialog.py @@ -1,305 +0,0 @@ -#!/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.plugins 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() diff --git a/gui/qt/qrtextedit.py b/gui/qt/qrtextedit.py @@ -1,76 +0,0 @@ - -from electrum.i18n import _ -from electrum.plugins 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()) diff --git a/gui/qt/qrwindow.py b/gui/qt/qrwindow.py @@ -1,89 +0,0 @@ -#!/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 electrum_gui.qt.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) diff --git a/gui/qt/request_list.py b/gui/qt/request_list.py @@ -1,129 +0,0 @@ -#!/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.plugins 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)) diff --git a/gui/qt/seed_dialog.py b/gui/qt/seed_dialog.py @@ -1,211 +0,0 @@ -#!/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.plugins 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))) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py @@ -1,328 +0,0 @@ -#!/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.plugins 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) diff --git a/gui/text.py b/gui/text.py @@ -1,503 +0,0 @@ -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 electrum 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 diff --git a/lib/__init__.py b/lib/__init__.py @@ -1,14 +0,0 @@ -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 .plugins import BasePlugin -from .commands import Commands, known_commands diff --git a/lib/base_crash_reporter.py b/lib/base_crash_reporter.py @@ -1,127 +0,0 @@ -# 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 electrum import ELECTRUM_VERSION, constants -from electrum.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 diff --git a/lib/commands.py b/lib/commands.py @@ -1,892 +0,0 @@ -#!/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 .plugins 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 diff --git a/lib/daemon.py b/lib/daemon.py @@ -1,316 +0,0 @@ -#!/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 .plugins 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_gui']) - 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 diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py @@ -1,573 +0,0 @@ -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) diff --git a/lib/i18n.py b/lib/i18n.py @@ -1,81 +0,0 @@ -#!/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') -} diff --git a/lib/interface.py b/lib/interface.py @@ -1,407 +0,0 @@ -#!/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() diff --git a/lib/jsonrpc.py b/lib/jsonrpc.py @@ -1,98 +0,0 @@ -#!/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() diff --git a/lib/keystore.py b/lib/keystore.py @@ -1,799 +0,0 @@ -#!/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 -from .bitcoin import * -from .ecc import string_to_number, number_to_string -from .crypto import pw_decode, pw_encode -from . import constants -from .util import (PrintError, InvalidPassword, hfu, WalletFileException, - BitcoinException) -from .mnemonic import Mnemonic, load_wordlist -from .plugins 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: {}'.format(hw_type)) - -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 diff --git a/lib/mnemonic.py b/lib/mnemonic.py @@ -1,183 +0,0 @@ -#!/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 diff --git a/lib/msqr.py b/lib/msqr.py @@ -1,94 +0,0 @@ -# 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 diff --git a/lib/network.py b/lib/network.py @@ -1,1297 +0,0 @@ -# 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) diff --git a/lib/old_mnemonic.py b/lib/old_mnemonic.py @@ -1,1697 +0,0 @@ -#!/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:])) diff --git a/lib/paymentrequest.proto b/lib/paymentrequest.proto @@ -1,47 +0,0 @@ -// -// 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 -} diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py @@ -1,528 +0,0 @@ -#!/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=lib/ --python_out=lib/ lib/paymentrequest.proto'") - -from . import bitcoin -from . import ecc -from . import util -from .util import print_error, bh2u, bfh -from .util import export_meta, import_meta -from . import transaction -from . import x509 -from . import rsakey - -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())] diff --git a/lib/paymentrequest_pb2.py b/lib/paymentrequest_pb2.py @@ -1,367 +0,0 @@ -# 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', - serialized_pb=_b('\n\x14paymentrequest.proto\x12\x08payments\"+\n\x06Output\x12\x11\n\x06\x61mount\x18\x01 \x01(\x04:\x01\x30\x12\x0e\n\x06script\x18\x02 \x02(\x0c\"\xa3\x01\n\x0ePaymentDetails\x12\x15\n\x07network\x18\x01 \x01(\t:\x04main\x12!\n\x07outputs\x18\x02 \x03(\x0b\x32\x10.payments.Output\x12\x0c\n\x04time\x18\x03 \x02(\x04\x12\x0f\n\x07\x65xpires\x18\x04 \x01(\x04\x12\x0c\n\x04memo\x18\x05 \x01(\t\x12\x13\n\x0bpayment_url\x18\x06 \x01(\t\x12\x15\n\rmerchant_data\x18\x07 \x01(\x0c\"\x95\x01\n\x0ePaymentRequest\x12\"\n\x17payment_details_version\x18\x01 \x01(\r:\x01\x31\x12\x16\n\x08pki_type\x18\x02 \x01(\t:\x04none\x12\x10\n\x08pki_data\x18\x03 \x01(\x0c\x12\"\n\x1aserialized_payment_details\x18\x04 \x02(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"\'\n\x10X509Certificates\x12\x13\n\x0b\x63\x65rtificate\x18\x01 \x03(\x0c\"i\n\x07Payment\x12\x15\n\rmerchant_data\x18\x01 \x01(\x0c\x12\x14\n\x0ctransactions\x18\x02 \x03(\x0c\x12#\n\trefund_to\x18\x03 \x03(\x0b\x32\x10.payments.Output\x12\x0c\n\x04memo\x18\x04 \x01(\t\">\n\nPaymentACK\x12\"\n\x07payment\x18\x01 \x02(\x0b\x32\x11.payments.Payment\x12\x0c\n\x04memo\x18\x02 \x01(\tB(\n\x1eorg.bitcoin.protocols.paymentsB\x06Protos') -) -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - - - - -_OUTPUT = _descriptor.Descriptor( - name='Output', - full_name='payments.Output', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='amount', full_name='payments.Output.amount', index=0, - number=1, type=4, cpp_type=4, label=1, - has_default_value=True, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='script', full_name='payments.Output.script', index=1, - number=2, type=12, cpp_type=9, label=2, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=34, - serialized_end=77, -) - - -_PAYMENTDETAILS = _descriptor.Descriptor( - name='PaymentDetails', - full_name='payments.PaymentDetails', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='network', full_name='payments.PaymentDetails.network', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=True, default_value=_b("main").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='outputs', full_name='payments.PaymentDetails.outputs', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='time', full_name='payments.PaymentDetails.time', index=2, - number=3, type=4, cpp_type=4, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='expires', full_name='payments.PaymentDetails.expires', index=3, - number=4, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='memo', full_name='payments.PaymentDetails.memo', index=4, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='payment_url', full_name='payments.PaymentDetails.payment_url', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='merchant_data', full_name='payments.PaymentDetails.merchant_data', index=6, - number=7, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=80, - serialized_end=243, -) - - -_PAYMENTREQUEST = _descriptor.Descriptor( - name='PaymentRequest', - full_name='payments.PaymentRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='payment_details_version', full_name='payments.PaymentRequest.payment_details_version', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=True, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='pki_type', full_name='payments.PaymentRequest.pki_type', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=True, default_value=_b("none").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='pki_data', full_name='payments.PaymentRequest.pki_data', index=2, - number=3, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='serialized_payment_details', full_name='payments.PaymentRequest.serialized_payment_details', index=3, - number=4, type=12, cpp_type=9, label=2, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='signature', full_name='payments.PaymentRequest.signature', index=4, - number=5, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=246, - serialized_end=395, -) - - -_X509CERTIFICATES = _descriptor.Descriptor( - name='X509Certificates', - full_name='payments.X509Certificates', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='certificate', full_name='payments.X509Certificates.certificate', index=0, - number=1, type=12, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=397, - serialized_end=436, -) - - -_PAYMENT = _descriptor.Descriptor( - name='Payment', - full_name='payments.Payment', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='merchant_data', full_name='payments.Payment.merchant_data', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='transactions', full_name='payments.Payment.transactions', index=1, - number=2, type=12, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='refund_to', full_name='payments.Payment.refund_to', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='memo', full_name='payments.Payment.memo', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=438, - serialized_end=543, -) - - -_PAYMENTACK = _descriptor.Descriptor( - name='PaymentACK', - full_name='payments.PaymentACK', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='payment', full_name='payments.PaymentACK.payment', index=0, - number=1, type=11, cpp_type=10, label=2, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='memo', full_name='payments.PaymentACK.memo', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - oneofs=[ - ], - serialized_start=545, - serialized_end=607, -) - -_PAYMENTDETAILS.fields_by_name['outputs'].message_type = _OUTPUT -_PAYMENT.fields_by_name['refund_to'].message_type = _OUTPUT -_PAYMENTACK.fields_by_name['payment'].message_type = _PAYMENT -DESCRIPTOR.message_types_by_name['Output'] = _OUTPUT -DESCRIPTOR.message_types_by_name['PaymentDetails'] = _PAYMENTDETAILS -DESCRIPTOR.message_types_by_name['PaymentRequest'] = _PAYMENTREQUEST -DESCRIPTOR.message_types_by_name['X509Certificates'] = _X509CERTIFICATES -DESCRIPTOR.message_types_by_name['Payment'] = _PAYMENT -DESCRIPTOR.message_types_by_name['PaymentACK'] = _PAYMENTACK - -Output = _reflection.GeneratedProtocolMessageType('Output', (_message.Message,), dict( - DESCRIPTOR = _OUTPUT, - __module__ = 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.Output) - )) -_sym_db.RegisterMessage(Output) - -PaymentDetails = _reflection.GeneratedProtocolMessageType('PaymentDetails', (_message.Message,), dict( - DESCRIPTOR = _PAYMENTDETAILS, - __module__ = 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.PaymentDetails) - )) -_sym_db.RegisterMessage(PaymentDetails) - -PaymentRequest = _reflection.GeneratedProtocolMessageType('PaymentRequest', (_message.Message,), dict( - DESCRIPTOR = _PAYMENTREQUEST, - __module__ = 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.PaymentRequest) - )) -_sym_db.RegisterMessage(PaymentRequest) - -X509Certificates = _reflection.GeneratedProtocolMessageType('X509Certificates', (_message.Message,), dict( - DESCRIPTOR = _X509CERTIFICATES, - __module__ = 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.X509Certificates) - )) -_sym_db.RegisterMessage(X509Certificates) - -Payment = _reflection.GeneratedProtocolMessageType('Payment', (_message.Message,), dict( - DESCRIPTOR = _PAYMENT, - __module__ = 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.Payment) - )) -_sym_db.RegisterMessage(Payment) - -PaymentACK = _reflection.GeneratedProtocolMessageType('PaymentACK', (_message.Message,), dict( - DESCRIPTOR = _PAYMENTACK, - __module__ = 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.PaymentACK) - )) -_sym_db.RegisterMessage(PaymentACK) - - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\036org.bitcoin.protocols.paymentsB\006Protos')) -# @@protoc_insertion_point(module_scope) diff --git a/lib/pem.py b/lib/pem.py @@ -1,191 +0,0 @@ -#!/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. - - -# This module uses code from TLSLlite -# TLSLite Author: Trevor Perrin) - - -import binascii - -from .x509 import ASN1_Node, bytestr_to_int, decode_OID - - -def a2b_base64(s): - try: - b = bytearray(binascii.a2b_base64(s)) - except Exception as e: - raise SyntaxError("base64 error: %s" % e) - return b - -def b2a_base64(b): - return binascii.b2a_base64(b) - - -def dePem(s, name): - """Decode a PEM string into a bytearray of its payload. - - The input must contain an appropriate PEM prefix and postfix - based on the input name string, e.g. for name="CERTIFICATE": - - -----BEGIN CERTIFICATE----- - MIIBXDCCAUSgAwIBAgIBADANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQDEwRUQUNL - ... - KoZIhvcNAQEFBQADAwA5kw== - -----END CERTIFICATE----- - - The first such PEM block in the input will be found, and its - payload will be base64 decoded and returned. - """ - prefix = "-----BEGIN %s-----" % name - postfix = "-----END %s-----" % name - start = s.find(prefix) - if start == -1: - raise SyntaxError("Missing PEM prefix") - end = s.find(postfix, start+len(prefix)) - if end == -1: - raise SyntaxError("Missing PEM postfix") - s = s[start+len("-----BEGIN %s-----" % name) : end] - retBytes = a2b_base64(s) # May raise SyntaxError - return retBytes - -def dePemList(s, name): - """Decode a sequence of PEM blocks into a list of bytearrays. - - The input must contain any number of PEM blocks, each with the appropriate - PEM prefix and postfix based on the input name string, e.g. for - name="TACK BREAK SIG". Arbitrary text can appear between and before and - after the PEM blocks. For example: - - " Created by TACK.py 0.9.3 Created at 2012-02-01T00:30:10Z -----BEGIN TACK - BREAK SIG----- - ATKhrz5C6JHJW8BF5fLVrnQss6JnWVyEaC0p89LNhKPswvcC9/s6+vWLd9snYTUv - YMEBdw69PUP8JB4AdqA3K6Ap0Fgd9SSTOECeAKOUAym8zcYaXUwpk0+WuPYa7Zmm - SkbOlK4ywqt+amhWbg9txSGUwFO5tWUHT3QrnRlE/e3PeNFXLx5Bckg= -----END TACK - BREAK SIG----- Created by TACK.py 0.9.3 Created at 2012-02-01T00:30:11Z - -----BEGIN TACK BREAK SIG----- - ATKhrz5C6JHJW8BF5fLVrnQss6JnWVyEaC0p89LNhKPswvcC9/s6+vWLd9snYTUv - YMEBdw69PUP8JB4AdqA3K6BVCWfcjN36lx6JwxmZQncS6sww7DecFO/qjSePCxwM - +kdDqX/9/183nmjx6bf0ewhPXkA0nVXsDYZaydN8rJU1GaMlnjcIYxY= -----END TACK - BREAK SIG----- " - - All such PEM blocks will be found, decoded, and return in an ordered list - of bytearrays, which may have zero elements if not PEM blocks are found. - """ - bList = [] - prefix = "-----BEGIN %s-----" % name - postfix = "-----END %s-----" % name - while 1: - start = s.find(prefix) - if start == -1: - return bList - end = s.find(postfix, start+len(prefix)) - if end == -1: - raise SyntaxError("Missing PEM postfix") - s2 = s[start+len(prefix) : end] - retBytes = a2b_base64(s2) # May raise SyntaxError - bList.append(retBytes) - s = s[end+len(postfix) : ] - -def pem(b, name): - """Encode a payload bytearray into a PEM string. - - The input will be base64 encoded, then wrapped in a PEM prefix/postfix - based on the name string, e.g. for name="CERTIFICATE": - - -----BEGIN CERTIFICATE----- - MIIBXDCCAUSgAwIBAgIBADANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQDEwRUQUNL - ... - KoZIhvcNAQEFBQADAwA5kw== - -----END CERTIFICATE----- - """ - s1 = b2a_base64(b)[:-1] # remove terminating \n - s2 = b"" - while s1: - s2 += s1[:64] + b"\n" - s1 = s1[64:] - s = ("-----BEGIN %s-----\n" % name).encode('ascii') + s2 + \ - ("-----END %s-----\n" % name).encode('ascii') - return s - -def pemSniff(inStr, name): - searchStr = "-----BEGIN %s-----" % name - return searchStr in inStr - - -def parse_private_key(s): - """Parse a string containing a PEM-encoded <privateKey>.""" - if pemSniff(s, "PRIVATE KEY"): - bytes = dePem(s, "PRIVATE KEY") - return _parsePKCS8(bytes) - elif pemSniff(s, "RSA PRIVATE KEY"): - bytes = dePem(s, "RSA PRIVATE KEY") - return _parseSSLeay(bytes) - else: - raise SyntaxError("Not a PEM private key file") - - -def _parsePKCS8(_bytes): - s = ASN1_Node(_bytes) - root = s.root() - version_node = s.first_child(root) - version = bytestr_to_int(s.get_value_of_type(version_node, 'INTEGER')) - if version != 0: - raise SyntaxError("Unrecognized PKCS8 version") - rsaOID_node = s.next_node(version_node) - ii = s.first_child(rsaOID_node) - rsaOID = decode_OID(s.get_value_of_type(ii, 'OBJECT IDENTIFIER')) - if rsaOID != '1.2.840.113549.1.1.1': - raise SyntaxError("Unrecognized AlgorithmIdentifier") - privkey_node = s.next_node(rsaOID_node) - value = s.get_value_of_type(privkey_node, 'OCTET STRING') - return _parseASN1PrivateKey(value) - - -def _parseSSLeay(bytes): - return _parseASN1PrivateKey(ASN1_Node(bytes)) - - -def bytesToNumber(s): - return int(binascii.hexlify(s), 16) - - -def _parseASN1PrivateKey(s): - s = ASN1_Node(s) - root = s.root() - version_node = s.first_child(root) - version = bytestr_to_int(s.get_value_of_type(version_node, 'INTEGER')) - if version != 0: - raise SyntaxError("Unrecognized RSAPrivateKey version") - n = s.next_node(version_node) - e = s.next_node(n) - d = s.next_node(e) - p = s.next_node(d) - q = s.next_node(p) - dP = s.next_node(q) - dQ = s.next_node(dP) - qInv = s.next_node(dQ) - return list(map(lambda x: bytesToNumber(s.get_value_of_type(x, 'INTEGER')), [n, e, d, p, q, dP, dQ, qInv])) - diff --git a/lib/plot.py b/lib/plot.py @@ -1,63 +0,0 @@ -import datetime -from collections import defaultdict - -import matplotlib -matplotlib.use('Qt5Agg') -import matplotlib.pyplot as plt -import matplotlib.dates as md - -from .i18n import _ -from .bitcoin import COIN - - -class NothingToPlotException(Exception): - def __str__(self): - return _("Nothing to plot.") - - -def plot_history(history): - if len(history) == 0: - raise NothingToPlotException() - hist_in = defaultdict(int) - hist_out = defaultdict(int) - for item in history: - if not item['confirmations']: - continue - if item['timestamp'] is None: - continue - value = item['value'].value/COIN - date = item['date'] - datenum = int(md.date2num(datetime.date(date.year, date.month, 1))) - if value > 0: - hist_in[datenum] += value - else: - hist_out[datenum] -= value - - f, axarr = plt.subplots(2, sharex=True) - plt.subplots_adjust(bottom=0.2) - plt.xticks( rotation=25 ) - ax = plt.gca() - plt.ylabel('BTC') - plt.xlabel('Month') - xfmt = md.DateFormatter('%Y-%m-%d') - ax.xaxis.set_major_formatter(xfmt) - axarr[0].set_title('Monthly Volume') - xfmt = md.DateFormatter('%Y-%m') - ax.xaxis.set_major_formatter(xfmt) - width = 20 - - r1 = None - r2 = None - dates_values = list(zip(*sorted(hist_in.items()))) - if dates_values and len(dates_values) == 2: - dates, values = dates_values - r1 = axarr[0].bar(dates, values, width, label='incoming') - axarr[0].legend(loc='upper left') - dates_values = list(zip(*sorted(hist_out.items()))) - if dates_values and len(dates_values) == 2: - dates, values = dates_values - r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing') - axarr[1].legend(loc='upper left') - if r1 is None and r2 is None: - raise NothingToPlotException() - return plt diff --git a/lib/plugins.py b/lib/plugins.py @@ -1,572 +0,0 @@ -#!/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 collections import namedtuple -import traceback -import sys -import os -import imp -import pkgutil -import time -import threading - -from .util import print_error -from .i18n import _ -from .util import profiler, PrintError, DaemonThread, UserCancelled, ThreadJob -from . import bitcoin - -plugin_loaders = {} -hook_names = set() -hooks = {} - - -class Plugins(DaemonThread): - - @profiler - def __init__(self, config, is_local, gui_name): - DaemonThread.__init__(self) - if is_local: - find = imp.find_module('plugins') - plugins = imp.load_module('electrum_plugins', *find) - else: - import electrum_plugins as plugins - self.pkgpath = os.path.dirname(plugins.__file__) - self.config = config - self.hw_wallets = {} - self.plugins = {} - self.gui_name = gui_name - self.descriptions = {} - self.device_manager = DeviceMgr(config) - self.load_plugins() - self.add_jobs(self.device_manager.thread_jobs()) - self.start() - - def load_plugins(self): - for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]): - # do not load deprecated plugins - if name in ['plot', 'exchange_rate']: - continue - m = loader.find_module(name).load_module(name) - d = m.__dict__ - gui_good = self.gui_name in d.get('available_for', []) - if not gui_good: - continue - details = d.get('registers_wallet_type') - if details: - self.register_wallet_type(name, gui_good, details) - details = d.get('registers_keystore') - if details: - self.register_keystore(name, gui_good, details) - self.descriptions[name] = d - if not d.get('requires_wallet_type') and self.config.get('use_' + name): - try: - self.load_plugin(name) - except BaseException as e: - traceback.print_exc(file=sys.stdout) - self.print_error("cannot initialize plugin %s:" % name, str(e)) - - def get(self, name): - return self.plugins.get(name) - - def count(self): - return len(self.plugins) - - def load_plugin(self, name): - if name in self.plugins: - return self.plugins[name] - full_name = 'electrum_plugins.' + name + '.' + self.gui_name - loader = pkgutil.find_loader(full_name) - if not loader: - raise RuntimeError("%s implementation for %s plugin not found" - % (self.gui_name, name)) - p = loader.load_module(full_name) - plugin = p.Plugin(self, self.config, name) - self.add_jobs(plugin.thread_jobs()) - self.plugins[name] = plugin - self.print_error("loaded", name) - return plugin - - def close_plugin(self, plugin): - self.remove_jobs(plugin.thread_jobs()) - - def enable(self, name): - self.config.set_key('use_' + name, True, True) - p = self.get(name) - if p: - return p - return self.load_plugin(name) - - def disable(self, name): - self.config.set_key('use_' + name, False, True) - p = self.get(name) - if not p: - return - self.plugins.pop(name) - p.close() - self.print_error("closed", name) - - def toggle(self, name): - p = self.get(name) - return self.disable(name) if p else self.enable(name) - - def is_available(self, name, w): - d = self.descriptions.get(name) - if not d: - return False - deps = d.get('requires', []) - for dep, s in deps: - try: - __import__(dep) - except ImportError: - return False - requires = d.get('requires_wallet_type', []) - return not requires or w.wallet_type in requires - - def get_hardware_support(self): - out = [] - for name, (gui_good, details) in self.hw_wallets.items(): - if gui_good: - try: - p = self.get_plugin(name) - if p.is_enabled(): - out.append([name, details[2], p]) - except: - traceback.print_exc() - self.print_error("cannot load plugin for:", name) - return out - - def register_wallet_type(self, name, gui_good, wallet_type): - from .wallet import register_wallet_type, register_constructor - self.print_error("registering wallet type", (wallet_type, name)) - def loader(): - plugin = self.get_plugin(name) - register_constructor(wallet_type, plugin.wallet_class) - register_wallet_type(wallet_type) - plugin_loaders[wallet_type] = loader - - def register_keystore(self, name, gui_good, details): - from .keystore import register_keystore - def dynamic_constructor(d): - return self.get_plugin(name).keystore_class(d) - if details[0] == 'hardware': - self.hw_wallets[name] = (gui_good, details) - self.print_error("registering hardware %s: %s" %(name, details)) - register_keystore(details[1], dynamic_constructor) - - def get_plugin(self, name): - if not name in self.plugins: - self.load_plugin(name) - return self.plugins[name] - - def run(self): - while self.is_running(): - time.sleep(0.1) - self.run_jobs() - self.on_stop() - - -def hook(func): - hook_names.add(func.__name__) - return func - -def run_hook(name, *args): - results = [] - f_list = hooks.get(name, []) - for p, f in f_list: - if p.is_enabled(): - try: - r = f(*args) - except Exception: - print_error("Plugin error") - traceback.print_exc(file=sys.stdout) - r = False - if r: - results.append(r) - - if results: - assert len(results) == 1, results - return results[0] - - -class BasePlugin(PrintError): - - def __init__(self, parent, config, name): - self.parent = parent # The plugins object - self.name = name - self.config = config - self.wallet = None - # add self to hooks - for k in dir(self): - if k in hook_names: - l = hooks.get(k, []) - l.append((self, getattr(self, k))) - hooks[k] = l - - def diagnostic_name(self): - return self.name - - def __str__(self): - return self.name - - def close(self): - # remove self from hooks - for k in dir(self): - if k in hook_names: - l = hooks.get(k, []) - l.remove((self, getattr(self, k))) - hooks[k] = l - self.parent.close_plugin(self) - self.on_close() - - def on_close(self): - pass - - def requires_settings(self): - return False - - def thread_jobs(self): - return [] - - def is_enabled(self): - return self.is_available() and self.config.get('use_'+self.name) is True - - def is_available(self): - return True - - def can_user_disable(self): - return True - - def settings_dialog(self): - pass - - -class DeviceNotFoundError(Exception): - pass - -class DeviceUnpairableError(Exception): - pass - -Device = namedtuple("Device", "path interface_number id_ product_key usage_page") -DeviceInfo = namedtuple("DeviceInfo", "device label initialized") - -class DeviceMgr(ThreadJob, PrintError): - '''Manages hardware clients. A client communicates over a hardware - channel with the device. - - In addition to tracking device HID IDs, the device manager tracks - hardware wallets and manages wallet pairing. A HID ID may be - paired with a wallet when it is confirmed that the hardware device - matches the wallet, i.e. they have the same master public key. A - HID ID can be unpaired if e.g. it is wiped. - - Because of hotplugging, a wallet must request its client - dynamically each time it is required, rather than caching it - itself. - - The device manager is shared across plugins, so just one place - does hardware scans when needed. By tracking HID IDs, if a device - is plugged into a different port the wallet is automatically - re-paired. - - Wallets are informed on connect / disconnect events. It must - implement connected(), disconnected() callbacks. Being connected - implies a pairing. Callbacks can happen in any thread context, - and we do them without holding the lock. - - Confusingly, the HID ID (serial number) reported by the HID system - doesn't match the device ID reported by the device itself. We use - the HID IDs. - - This plugin is thread-safe. Currently only devices supported by - hidapi are implemented.''' - - def __init__(self, config): - super(DeviceMgr, self).__init__() - # Keyed by xpub. The value is the device id - # has been paired, and None otherwise. - self.xpub_ids = {} - # A list of clients. The key is the client, the value is - # a (path, id_) pair. - self.clients = {} - # What we recognise. Each entry is a (vendor_id, product_id) - # pair. - self.recognised_hardware = set() - # Custom enumerate functions for devices we don't know about. - self.enumerate_func = set() - # For synchronization - self.lock = threading.RLock() - self.hid_lock = threading.RLock() - self.config = config - - def thread_jobs(self): - # Thread job to handle device timeouts - return [self] - - def run(self): - '''Handle device timeouts. Runs in the context of the Plugins - thread.''' - with self.lock: - clients = list(self.clients.keys()) - cutoff = time.time() - self.config.get_session_timeout() - for client in clients: - client.timeout(cutoff) - - def register_devices(self, device_pairs): - for pair in device_pairs: - self.recognised_hardware.add(pair) - - def register_enumerate_func(self, func): - self.enumerate_func.add(func) - - def create_client(self, device, handler, plugin): - # Get from cache first - client = self.client_lookup(device.id_) - if client: - return client - client = plugin.create_client(device, handler) - if client: - self.print_error("Registering", client) - with self.lock: - self.clients[client] = (device.path, device.id_) - return client - - def xpub_id(self, xpub): - with self.lock: - return self.xpub_ids.get(xpub) - - def xpub_by_id(self, id_): - with self.lock: - for xpub, xpub_id in self.xpub_ids.items(): - if xpub_id == id_: - return xpub - return None - - def unpair_xpub(self, xpub): - with self.lock: - if not xpub in self.xpub_ids: - return - _id = self.xpub_ids.pop(xpub) - self._close_client(_id) - - def unpair_id(self, id_): - xpub = self.xpub_by_id(id_) - if xpub: - self.unpair_xpub(xpub) - else: - self._close_client(id_) - - def _close_client(self, id_): - client = self.client_lookup(id_) - self.clients.pop(client, None) - if client: - client.close() - - def pair_xpub(self, xpub, id_): - with self.lock: - self.xpub_ids[xpub] = id_ - - def client_lookup(self, id_): - with self.lock: - for client, (path, client_id) in self.clients.items(): - if client_id == id_: - return client - return None - - def client_by_id(self, id_): - '''Returns a client for the device ID if one is registered. If - a device is wiped or in bootloader mode pairing is impossible; - in such cases we communicate by device ID and not wallet.''' - self.scan_devices() - return self.client_lookup(id_) - - def client_for_keystore(self, plugin, handler, keystore, force_pair): - self.print_error("getting client for keystore") - if handler is None: - raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing.")) - handler.update_status(False) - devices = self.scan_devices() - xpub = keystore.xpub - derivation = keystore.get_derivation() - client = self.client_by_xpub(plugin, xpub, handler, devices) - if client is None and force_pair: - info = self.select_device(plugin, handler, keystore, devices) - client = self.force_pair_xpub(plugin, handler, info, xpub, derivation, devices) - if client: - handler.update_status(True) - self.print_error("end client for keystore") - return client - - def client_by_xpub(self, plugin, xpub, handler, devices): - _id = self.xpub_id(xpub) - client = self.client_lookup(_id) - if client: - # An unpaired client might have another wallet's handler - # from a prior scan. Replace to fix dialog parenting. - client.handler = handler - return client - - for device in devices: - if device.id_ == _id: - return self.create_client(device, handler, plugin) - - - def force_pair_xpub(self, plugin, handler, info, xpub, derivation, devices): - # The wallet has not been previously paired, so let the user - # choose an unpaired device and compare its first address. - xtype = bitcoin.xpub_type(xpub) - client = self.client_lookup(info.device.id_) - if client and client.is_pairable(): - # See comment above for same code - client.handler = handler - # This will trigger a PIN/passphrase entry request - try: - client_xpub = client.get_xpub(derivation, xtype) - except (UserCancelled, RuntimeError): - # Bad / cancelled PIN / passphrase - client_xpub = None - if client_xpub == xpub: - self.pair_xpub(xpub, info.device.id_) - return client - - # The user input has wrong PIN or passphrase, or cancelled input, - # or it is not pairable - raise DeviceUnpairableError( - _('Electrum cannot pair with your {}.\n\n' - 'Before you request bitcoins to be sent to addresses in this ' - 'wallet, ensure you can pair with your device, or that you have ' - 'its seed (and passphrase, if any). Otherwise all bitcoins you ' - 'receive will be unspendable.').format(plugin.device)) - - def unpaired_device_infos(self, handler, plugin, devices=None): - '''Returns a list of DeviceInfo objects: one for each connected, - unpaired device accepted by the plugin.''' - if not plugin.libraries_available: - raise Exception('Missing libraries for {}'.format(plugin.name)) - if devices is None: - devices = self.scan_devices() - devices = [dev for dev in devices if not self.xpub_by_id(dev.id_)] - infos = [] - for device in devices: - if device.product_key not in plugin.DEVICE_IDS: - continue - client = self.create_client(device, handler, plugin) - if not client: - continue - infos.append(DeviceInfo(device, client.label(), client.is_initialized())) - - return infos - - def select_device(self, plugin, handler, keystore, devices=None): - '''Ask the user to select a device to use if there is more than one, - and return the DeviceInfo for the device.''' - while True: - infos = self.unpaired_device_infos(handler, plugin, devices) - if infos: - break - msg = _('Please insert your {}').format(plugin.device) - if keystore.label: - msg += ' ({})'.format(keystore.label) - msg += '. {}\n\n{}'.format( - _('Verify the cable is connected and that ' - 'no other application is using it.'), - _('Try to connect again?') - ) - if not handler.yes_no_question(msg): - raise UserCancelled() - devices = None - if len(infos) == 1: - return infos[0] - # select device by label - for info in infos: - if info.label == keystore.label: - return info - msg = _("Please select which {} device to use:").format(plugin.device) - descriptions = [str(info.label) + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos] - c = handler.query_choice(msg, descriptions) - if c is None: - raise UserCancelled() - info = infos[c] - # save new label - keystore.set_label(info.label) - if handler.win.wallet is not None: - handler.win.wallet.save_keystore() - return info - - def _scan_devices_with_hid(self): - try: - import hid - except ImportError: - return [] - - with self.hid_lock: - hid_list = hid.enumerate(0, 0) - - devices = [] - for d in hid_list: - product_key = (d['vendor_id'], d['product_id']) - if product_key in self.recognised_hardware: - # Older versions of hid don't provide interface_number - interface_number = d.get('interface_number', -1) - usage_page = d['usage_page'] - id_ = d['serial_number'] - if len(id_) == 0: - id_ = str(d['path']) - id_ += str(interface_number) + str(usage_page) - devices.append(Device(d['path'], interface_number, - id_, product_key, usage_page)) - return devices - - def scan_devices(self): - self.print_error("scanning devices...") - - # First see what's connected that we know about - devices = self._scan_devices_with_hid() - - # Let plugin handlers enumerate devices we don't know about - for f in self.enumerate_func: - try: - new_devices = f() - except BaseException as e: - self.print_error('custom device enum failed. func {}, error {}' - .format(str(f), str(e))) - else: - devices.extend(new_devices) - - # find out what was disconnected - pairs = [(dev.path, dev.id_) for dev in devices] - disconnected_ids = [] - with self.lock: - connected = {} - for client, pair in self.clients.items(): - if pair in pairs and client.has_usable_connection_with_device(): - connected[client] = pair - else: - disconnected_ids.append(pair[1]) - self.clients = connected - - # Unpair disconnected devices - for id_ in disconnected_ids: - self.unpair_id(id_) - - return devices diff --git a/lib/qrscanner.py b/lib/qrscanner.py @@ -1,88 +0,0 @@ -#!/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 os -import sys -import ctypes - -if sys.platform == 'darwin': - name = 'libzbar.dylib' -elif sys.platform in ('windows', 'win32'): - name = 'libzbar-0.dll' -else: - name = 'libzbar.so.0' - -try: - libzbar = ctypes.cdll.LoadLibrary(name) -except BaseException: - libzbar = None - - -def scan_barcode(device='', timeout=-1, display=True, threaded=False, try_again=True): - if libzbar is None: - raise RuntimeError("Cannot start QR scanner; zbar not available.") - libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p - libzbar.zbar_processor_create.restype = ctypes.POINTER(ctypes.c_int) - libzbar.zbar_processor_get_results.restype = ctypes.POINTER(ctypes.c_int) - libzbar.zbar_symbol_set_first_symbol.restype = ctypes.POINTER(ctypes.c_int) - proc = libzbar.zbar_processor_create(threaded) - libzbar.zbar_processor_request_size(proc, 640, 480) - if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0: - if try_again: - # workaround for a bug in "ZBar for Windows" - # libzbar.zbar_processor_init always seem to fail the first time around - return scan_barcode(device, timeout, display, threaded, try_again=False) - raise RuntimeError("Can not start QR scanner; initialization failed.") - libzbar.zbar_processor_set_visible(proc) - if libzbar.zbar_process_one(proc, timeout): - symbols = libzbar.zbar_processor_get_results(proc) - else: - symbols = None - libzbar.zbar_processor_destroy(proc) - if symbols is None: - return - if not libzbar.zbar_symbol_set_get_size(symbols): - return - symbol = libzbar.zbar_symbol_set_first_symbol(symbols) - data = libzbar.zbar_symbol_get_data(symbol) - return data.decode('utf8') - -def _find_system_cameras(): - device_root = "/sys/class/video4linux" - devices = {} # Name -> device - if os.path.exists(device_root): - for device in os.listdir(device_root): - try: - with open(os.path.join(device_root, device, 'name')) as f: - name = f.read() - except IOError: - continue - name = name.strip('\n') - devices[name] = os.path.join("/dev", device) - return devices - - -if __name__ == "__main__": - print(scan_barcode()) diff --git a/lib/ripemd.py b/lib/ripemd.py @@ -1,393 +0,0 @@ -## ripemd.py - pure Python implementation of the RIPEMD-160 algorithm. -## Bjorn Edstrom <be@bjrn.se> 16 december 2007. -## -## Copyrights -## ========== -## -## This code is a derived from an implementation by Markus Friedl which is -## subject to the following license. This Python implementation is not -## subject to any other license. -## -##/* -## * Copyright (c) 2001 Markus Friedl. All rights reserved. -## * -## * Redistribution and use in source and binary forms, with or without -## * modification, are permitted provided that the following conditions -## * are met: -## * 1. Redistributions of source code must retain the above copyright -## * notice, this list of conditions and the following disclaimer. -## * 2. Redistributions in binary form must reproduce the above copyright -## * notice, this list of conditions and the following disclaimer in the -## * documentation and/or other materials provided with the distribution. -## * -## * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -## * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -## * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -## * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -## * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -## * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -## * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -## * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -## * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -## * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -## */ -##/* -## * Preneel, Bosselaers, Dobbertin, "The Cryptographic Hash Function RIPEMD-160", -## * RSA Laboratories, CryptoBytes, Volume 3, Number 2, Autumn 1997, -## * ftp://ftp.rsasecurity.com/pub/cryptobytes/crypto3n2.pdf -## */ - -#block_size = 1 -digest_size = 20 -digestsize = 20 - -class RIPEMD160: - """Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed.""" - - def __init__(self, arg=None): - self.ctx = RMDContext() - if arg: - self.update(arg) - self.dig = None - - def update(self, arg): - """update(arg)""" - RMD160Update(self.ctx, arg, len(arg)) - self.dig = None - - def digest(self): - """digest()""" - if self.dig: - return self.dig - ctx = self.ctx.copy() - self.dig = RMD160Final(self.ctx) - self.ctx = ctx - return self.dig - - def hexdigest(self): - """hexdigest()""" - dig = self.digest() - hex_digest = '' - for d in dig: - hex_digest += '%02x' % d - return hex_digest - - def copy(self): - """copy()""" - import copy - return copy.deepcopy(self) - - - -def new(arg=None): - """Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed.""" - return RIPEMD160(arg) - - - -# -# Private. -# - -class RMDContext: - def __init__(self): - self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, - 0x10325476, 0xC3D2E1F0] # uint32 - self.count = 0 # uint64 - self.buffer = [0]*64 # uchar - def copy(self): - ctx = RMDContext() - ctx.state = self.state[:] - ctx.count = self.count - ctx.buffer = self.buffer[:] - return ctx - -K0 = 0x00000000 -K1 = 0x5A827999 -K2 = 0x6ED9EBA1 -K3 = 0x8F1BBCDC -K4 = 0xA953FD4E - -KK0 = 0x50A28BE6 -KK1 = 0x5C4DD124 -KK2 = 0x6D703EF3 -KK3 = 0x7A6D76E9 -KK4 = 0x00000000 - -def ROL(n, x): - return ((x << n) & 0xffffffff) | (x >> (32 - n)) - -def F0(x, y, z): - return x ^ y ^ z - -def F1(x, y, z): - return (x & y) | (((~x) % 0x100000000) & z) - -def F2(x, y, z): - return (x | ((~y) % 0x100000000)) ^ z - -def F3(x, y, z): - return (x & z) | (((~z) % 0x100000000) & y) - -def F4(x, y, z): - return x ^ (y | ((~z) % 0x100000000)) - -def R(a, b, c, d, e, Fj, Kj, sj, rj, X): - a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e - c = ROL(10, c) - return a % 0x100000000, c - -PADDING = [0x80] + [0]*63 - -import sys -import struct - -def RMD160Transform(state, block): #uint32 state[5], uchar block[64] - x = [0]*16 - if sys.byteorder == 'little': - x = struct.unpack('<16L', bytes([x for x in block[0:64]])) - else: - raise "Error!!" - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - #/* Round 1 */ - a, c = R(a, b, c, d, e, F0, K0, 11, 0, x); - e, b = R(e, a, b, c, d, F0, K0, 14, 1, x); - d, a = R(d, e, a, b, c, F0, K0, 15, 2, x); - c, e = R(c, d, e, a, b, F0, K0, 12, 3, x); - b, d = R(b, c, d, e, a, F0, K0, 5, 4, x); - a, c = R(a, b, c, d, e, F0, K0, 8, 5, x); - e, b = R(e, a, b, c, d, F0, K0, 7, 6, x); - d, a = R(d, e, a, b, c, F0, K0, 9, 7, x); - c, e = R(c, d, e, a, b, F0, K0, 11, 8, x); - b, d = R(b, c, d, e, a, F0, K0, 13, 9, x); - a, c = R(a, b, c, d, e, F0, K0, 14, 10, x); - e, b = R(e, a, b, c, d, F0, K0, 15, 11, x); - d, a = R(d, e, a, b, c, F0, K0, 6, 12, x); - c, e = R(c, d, e, a, b, F0, K0, 7, 13, x); - b, d = R(b, c, d, e, a, F0, K0, 9, 14, x); - a, c = R(a, b, c, d, e, F0, K0, 8, 15, x); #/* #15 */ - #/* Round 2 */ - e, b = R(e, a, b, c, d, F1, K1, 7, 7, x); - d, a = R(d, e, a, b, c, F1, K1, 6, 4, x); - c, e = R(c, d, e, a, b, F1, K1, 8, 13, x); - b, d = R(b, c, d, e, a, F1, K1, 13, 1, x); - a, c = R(a, b, c, d, e, F1, K1, 11, 10, x); - e, b = R(e, a, b, c, d, F1, K1, 9, 6, x); - d, a = R(d, e, a, b, c, F1, K1, 7, 15, x); - c, e = R(c, d, e, a, b, F1, K1, 15, 3, x); - b, d = R(b, c, d, e, a, F1, K1, 7, 12, x); - a, c = R(a, b, c, d, e, F1, K1, 12, 0, x); - e, b = R(e, a, b, c, d, F1, K1, 15, 9, x); - d, a = R(d, e, a, b, c, F1, K1, 9, 5, x); - c, e = R(c, d, e, a, b, F1, K1, 11, 2, x); - b, d = R(b, c, d, e, a, F1, K1, 7, 14, x); - a, c = R(a, b, c, d, e, F1, K1, 13, 11, x); - e, b = R(e, a, b, c, d, F1, K1, 12, 8, x); #/* #31 */ - #/* Round 3 */ - d, a = R(d, e, a, b, c, F2, K2, 11, 3, x); - c, e = R(c, d, e, a, b, F2, K2, 13, 10, x); - b, d = R(b, c, d, e, a, F2, K2, 6, 14, x); - a, c = R(a, b, c, d, e, F2, K2, 7, 4, x); - e, b = R(e, a, b, c, d, F2, K2, 14, 9, x); - d, a = R(d, e, a, b, c, F2, K2, 9, 15, x); - c, e = R(c, d, e, a, b, F2, K2, 13, 8, x); - b, d = R(b, c, d, e, a, F2, K2, 15, 1, x); - a, c = R(a, b, c, d, e, F2, K2, 14, 2, x); - e, b = R(e, a, b, c, d, F2, K2, 8, 7, x); - d, a = R(d, e, a, b, c, F2, K2, 13, 0, x); - c, e = R(c, d, e, a, b, F2, K2, 6, 6, x); - b, d = R(b, c, d, e, a, F2, K2, 5, 13, x); - a, c = R(a, b, c, d, e, F2, K2, 12, 11, x); - e, b = R(e, a, b, c, d, F2, K2, 7, 5, x); - d, a = R(d, e, a, b, c, F2, K2, 5, 12, x); #/* #47 */ - #/* Round 4 */ - c, e = R(c, d, e, a, b, F3, K3, 11, 1, x); - b, d = R(b, c, d, e, a, F3, K3, 12, 9, x); - a, c = R(a, b, c, d, e, F3, K3, 14, 11, x); - e, b = R(e, a, b, c, d, F3, K3, 15, 10, x); - d, a = R(d, e, a, b, c, F3, K3, 14, 0, x); - c, e = R(c, d, e, a, b, F3, K3, 15, 8, x); - b, d = R(b, c, d, e, a, F3, K3, 9, 12, x); - a, c = R(a, b, c, d, e, F3, K3, 8, 4, x); - e, b = R(e, a, b, c, d, F3, K3, 9, 13, x); - d, a = R(d, e, a, b, c, F3, K3, 14, 3, x); - c, e = R(c, d, e, a, b, F3, K3, 5, 7, x); - b, d = R(b, c, d, e, a, F3, K3, 6, 15, x); - a, c = R(a, b, c, d, e, F3, K3, 8, 14, x); - e, b = R(e, a, b, c, d, F3, K3, 6, 5, x); - d, a = R(d, e, a, b, c, F3, K3, 5, 6, x); - c, e = R(c, d, e, a, b, F3, K3, 12, 2, x); #/* #63 */ - #/* Round 5 */ - b, d = R(b, c, d, e, a, F4, K4, 9, 4, x); - a, c = R(a, b, c, d, e, F4, K4, 15, 0, x); - e, b = R(e, a, b, c, d, F4, K4, 5, 5, x); - d, a = R(d, e, a, b, c, F4, K4, 11, 9, x); - c, e = R(c, d, e, a, b, F4, K4, 6, 7, x); - b, d = R(b, c, d, e, a, F4, K4, 8, 12, x); - a, c = R(a, b, c, d, e, F4, K4, 13, 2, x); - e, b = R(e, a, b, c, d, F4, K4, 12, 10, x); - d, a = R(d, e, a, b, c, F4, K4, 5, 14, x); - c, e = R(c, d, e, a, b, F4, K4, 12, 1, x); - b, d = R(b, c, d, e, a, F4, K4, 13, 3, x); - a, c = R(a, b, c, d, e, F4, K4, 14, 8, x); - e, b = R(e, a, b, c, d, F4, K4, 11, 11, x); - d, a = R(d, e, a, b, c, F4, K4, 8, 6, x); - c, e = R(c, d, e, a, b, F4, K4, 5, 15, x); - b, d = R(b, c, d, e, a, F4, K4, 6, 13, x); #/* #79 */ - - aa = a; - bb = b; - cc = c; - dd = d; - ee = e; - - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - #/* Parallel round 1 */ - a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) - e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) - d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) - c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) - b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) - a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) - e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) - d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) - c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) - b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) - a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) - e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) - d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) - c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) - b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) - a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) #/* #15 */ - #/* Parallel round 2 */ - e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) - d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) - c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) - a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) - e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) - d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) - c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) - a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) - e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) - d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) - c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) - b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) - a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) - e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) #/* #31 */ - #/* Parallel round 3 */ - d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) - c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) - b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) - a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) - e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) - d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) - c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) - b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) - a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) - e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) - c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) - b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) - a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) - e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) #/* #47 */ - #/* Parallel round 4 */ - c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) - b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) - a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) - e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) - d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) - c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) - b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) - a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) - e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) - d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) - c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) - b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) - a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) - e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) - d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) - c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) #/* #63 */ - #/* Parallel round 5 */ - b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) - e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) - d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) - c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) - b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) - a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) - e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) - d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) - c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) - b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) - e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) - d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) - c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) - b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) #/* #79 */ - - t = (state[1] + cc + d) % 0x100000000; - state[1] = (state[2] + dd + e) % 0x100000000; - state[2] = (state[3] + ee + a) % 0x100000000; - state[3] = (state[4] + aa + b) % 0x100000000; - state[4] = (state[0] + bb + c) % 0x100000000; - state[0] = t % 0x100000000; - - pass - - -def RMD160Update(ctx, inp, inplen): - if type(inp) == str: - inp = [ord(i)&0xff for i in inp] - - have = (ctx.count // 8) % 64 - need = 64 - have - ctx.count += 8 * inplen - off = 0 - if inplen >= need: - if have: - for i in range(need): - ctx.buffer[have+i] = inp[i] - RMD160Transform(ctx.state, ctx.buffer) - off = need - have = 0 - while off + 64 <= inplen: - RMD160Transform(ctx.state, inp[off:]) #<--- - off += 64 - if off < inplen: - # memcpy(ctx->buffer + have, input+off, len-off); - for i in range(inplen - off): - ctx.buffer[have+i] = inp[off+i] - -def RMD160Final(ctx): - size = struct.pack("<Q", ctx.count) - padlen = 64 - ((ctx.count // 8) % 64) - if padlen < 1+8: - padlen += 64 - RMD160Update(ctx, PADDING, padlen-8) - RMD160Update(ctx, size, 8) - return struct.pack("<5L", *ctx.state) - - -assert '37f332f68db77bd9d7edd4969571ad671cf9dd3b' == \ - new(b'The quick brown fox jumps over the lazy dog').hexdigest() -assert '132072df690933835eb8b6ad0b77e7b6f14acad7' == \ - new(b'The quick brown fox jumps over the lazy cog').hexdigest() -assert '9c1185a5c5e9fc54612808977ee8f548b2258d31' == \ - new('').hexdigest() diff --git a/lib/rsakey.py b/lib/rsakey.py @@ -1,542 +0,0 @@ -#!/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. - -# This module uses functions from TLSLite (public domain) -# -# TLSLite Authors: -# Trevor Perrin -# Martin von Loewis - python 3 port -# Yngve Pettersen (ported by Paul Sokolovsky) - TLS 1.2 -# - -"""Pure-Python RSA implementation.""" - -import os -import math -import hashlib - -from .pem import * - - -def SHA1(x): - return hashlib.sha1(x).digest() - - -# ************************************************************************** -# PRNG Functions -# ************************************************************************** - -# Check that os.urandom works -import zlib -length = len(zlib.compress(os.urandom(1000))) -assert(length > 900) - -def getRandomBytes(howMany): - b = bytearray(os.urandom(howMany)) - assert(len(b) == howMany) - return b - -prngName = "os.urandom" - - -# ************************************************************************** -# Converter Functions -# ************************************************************************** - -def bytesToNumber(b): - total = 0 - multiplier = 1 - for count in range(len(b)-1, -1, -1): - byte = b[count] - total += multiplier * byte - multiplier *= 256 - return total - -def numberToByteArray(n, howManyBytes=None): - """Convert an integer into a bytearray, zero-pad to howManyBytes. - - The returned bytearray may be smaller than howManyBytes, but will - not be larger. The returned bytearray will contain a big-endian - encoding of the input integer (n). - """ - if howManyBytes == None: - howManyBytes = numBytes(n) - b = bytearray(howManyBytes) - for count in range(howManyBytes-1, -1, -1): - b[count] = int(n % 256) - n >>= 8 - return b - -def mpiToNumber(mpi): #mpi is an openssl-format bignum string - if (ord(mpi[4]) & 0x80) !=0: #Make sure this is a positive number - raise AssertionError() - b = bytearray(mpi[4:]) - return bytesToNumber(b) - -def numberToMPI(n): - b = numberToByteArray(n) - ext = 0 - #If the high-order bit is going to be set, - #add an extra byte of zeros - if (numBits(n) & 0x7)==0: - ext = 1 - length = numBytes(n) + ext - b = bytearray(4+ext) + b - b[0] = (length >> 24) & 0xFF - b[1] = (length >> 16) & 0xFF - b[2] = (length >> 8) & 0xFF - b[3] = length & 0xFF - return bytes(b) - - -# ************************************************************************** -# Misc. Utility Functions -# ************************************************************************** - -def numBits(n): - if n==0: - return 0 - s = "%x" % n - return ((len(s)-1)*4) + \ - {'0':0, '1':1, '2':2, '3':2, - '4':3, '5':3, '6':3, '7':3, - '8':4, '9':4, 'a':4, 'b':4, - 'c':4, 'd':4, 'e':4, 'f':4, - }[s[0]] - return int(math.floor(math.log(n, 2))+1) - -def numBytes(n): - if n==0: - return 0 - bits = numBits(n) - return int(math.ceil(bits / 8.0)) - -# ************************************************************************** -# Big Number Math -# ************************************************************************** - -def getRandomNumber(low, high): - if low >= high: - raise AssertionError() - howManyBits = numBits(high) - howManyBytes = numBytes(high) - lastBits = howManyBits % 8 - while 1: - bytes = getRandomBytes(howManyBytes) - if lastBits: - bytes[0] = bytes[0] % (1 << lastBits) - n = bytesToNumber(bytes) - if n >= low and n < high: - return n - -def gcd(a,b): - a, b = max(a,b), min(a,b) - while b: - a, b = b, a % b - return a - -def lcm(a, b): - return (a * b) // gcd(a, b) - -#Returns inverse of a mod b, zero if none -#Uses Extended Euclidean Algorithm -def invMod(a, b): - c, d = a, b - uc, ud = 1, 0 - while c != 0: - q = d // c - c, d = d-(q*c), c - uc, ud = ud - (q * uc), uc - if d == 1: - return ud % b - return 0 - - -def powMod(base, power, modulus): - if power < 0: - result = pow(base, power*-1, modulus) - result = invMod(result, modulus) - return result - else: - return pow(base, power, modulus) - -#Pre-calculate a sieve of the ~100 primes < 1000: -def makeSieve(n): - sieve = list(range(n)) - for count in range(2, int(math.sqrt(n))+1): - if sieve[count] == 0: - continue - x = sieve[count] * 2 - while x < len(sieve): - sieve[x] = 0 - x += sieve[count] - sieve = [x for x in sieve[2:] if x] - return sieve - -sieve = makeSieve(1000) - -def isPrime(n, iterations=5, display=False): - #Trial division with sieve - for x in sieve: - if x >= n: return True - if n % x == 0: return False - #Passed trial division, proceed to Rabin-Miller - #Rabin-Miller implemented per Ferguson & Schneier - #Compute s, t for Rabin-Miller - if display: print("*", end=' ') - s, t = n-1, 0 - while s % 2 == 0: - s, t = s//2, t+1 - #Repeat Rabin-Miller x times - a = 2 #Use 2 as a base for first iteration speedup, per HAC - for count in range(iterations): - v = powMod(a, s, n) - if v==1: - continue - i = 0 - while v != n-1: - if i == t-1: - return False - else: - v, i = powMod(v, 2, n), i+1 - a = getRandomNumber(2, n) - return True - -def getRandomPrime(bits, display=False): - if bits < 10: - raise AssertionError() - #The 1.5 ensures the 2 MSBs are set - #Thus, when used for p,q in RSA, n will have its MSB set - # - #Since 30 is lcm(2,3,5), we'll set our test numbers to - #29 % 30 and keep them there - low = ((2 ** (bits-1)) * 3) // 2 - high = 2 ** bits - 30 - p = getRandomNumber(low, high) - p += 29 - (p % 30) - while 1: - if display: print(".", end=' ') - p += 30 - if p >= high: - p = getRandomNumber(low, high) - p += 29 - (p % 30) - if isPrime(p, display=display): - return p - -#Unused at the moment... -def getRandomSafePrime(bits, display=False): - if bits < 10: - raise AssertionError() - #The 1.5 ensures the 2 MSBs are set - #Thus, when used for p,q in RSA, n will have its MSB set - # - #Since 30 is lcm(2,3,5), we'll set our test numbers to - #29 % 30 and keep them there - low = (2 ** (bits-2)) * 3//2 - high = (2 ** (bits-1)) - 30 - q = getRandomNumber(low, high) - q += 29 - (q % 30) - while 1: - if display: print(".", end=' ') - q += 30 - if (q >= high): - q = getRandomNumber(low, high) - q += 29 - (q % 30) - #Ideas from Tom Wu's SRP code - #Do trial division on p and q before Rabin-Miller - if isPrime(q, 0, display=display): - p = (2 * q) + 1 - if isPrime(p, display=display): - if isPrime(q, display=display): - return p - - -class RSAKey(object): - - def __init__(self, n=0, e=0, d=0, p=0, q=0, dP=0, dQ=0, qInv=0): - if (n and not e) or (e and not n): - raise AssertionError() - self.n = n - self.e = e - self.d = d - self.p = p - self.q = q - self.dP = dP - self.dQ = dQ - self.qInv = qInv - self.blinder = 0 - self.unblinder = 0 - - def __len__(self): - """Return the length of this key in bits. - - @rtype: int - """ - return numBits(self.n) - - def hasPrivateKey(self): - return self.d != 0 - - def hashAndSign(self, bytes): - """Hash and sign the passed-in bytes. - - This requires the key to have a private component. It performs - a PKCS1-SHA1 signature on the passed-in data. - - @type bytes: str or L{bytearray} of unsigned bytes - @param bytes: The value which will be hashed and signed. - - @rtype: L{bytearray} of unsigned bytes. - @return: A PKCS1-SHA1 signature on the passed-in data. - """ - hashBytes = SHA1(bytearray(bytes)) - prefixedHashBytes = self._addPKCS1SHA1Prefix(hashBytes) - sigBytes = self.sign(prefixedHashBytes) - return sigBytes - - def hashAndVerify(self, sigBytes, bytes): - """Hash and verify the passed-in bytes with the signature. - - This verifies a PKCS1-SHA1 signature on the passed-in data. - - @type sigBytes: L{bytearray} of unsigned bytes - @param sigBytes: A PKCS1-SHA1 signature. - - @type bytes: str or L{bytearray} of unsigned bytes - @param bytes: The value which will be hashed and verified. - - @rtype: bool - @return: Whether the signature matches the passed-in data. - """ - hashBytes = SHA1(bytearray(bytes)) - - # Try it with/without the embedded NULL - prefixedHashBytes1 = self._addPKCS1SHA1Prefix(hashBytes, False) - prefixedHashBytes2 = self._addPKCS1SHA1Prefix(hashBytes, True) - result1 = self.verify(sigBytes, prefixedHashBytes1) - result2 = self.verify(sigBytes, prefixedHashBytes2) - return (result1 or result2) - - def sign(self, bytes): - """Sign the passed-in bytes. - - This requires the key to have a private component. It performs - a PKCS1 signature on the passed-in data. - - @type bytes: L{bytearray} of unsigned bytes - @param bytes: The value which will be signed. - - @rtype: L{bytearray} of unsigned bytes. - @return: A PKCS1 signature on the passed-in data. - """ - if not self.hasPrivateKey(): - raise AssertionError() - paddedBytes = self._addPKCS1Padding(bytes, 1) - m = bytesToNumber(paddedBytes) - if m >= self.n: - raise ValueError() - c = self._rawPrivateKeyOp(m) - sigBytes = numberToByteArray(c, numBytes(self.n)) - return sigBytes - - def verify(self, sigBytes, bytes): - """Verify the passed-in bytes with the signature. - - This verifies a PKCS1 signature on the passed-in data. - - @type sigBytes: L{bytearray} of unsigned bytes - @param sigBytes: A PKCS1 signature. - - @type bytes: L{bytearray} of unsigned bytes - @param bytes: The value which will be verified. - - @rtype: bool - @return: Whether the signature matches the passed-in data. - """ - if len(sigBytes) != numBytes(self.n): - return False - paddedBytes = self._addPKCS1Padding(bytes, 1) - c = bytesToNumber(sigBytes) - if c >= self.n: - return False - m = self._rawPublicKeyOp(c) - checkBytes = numberToByteArray(m, numBytes(self.n)) - return checkBytes == paddedBytes - - def encrypt(self, bytes): - """Encrypt the passed-in bytes. - - This performs PKCS1 encryption of the passed-in data. - - @type bytes: L{bytearray} of unsigned bytes - @param bytes: The value which will be encrypted. - - @rtype: L{bytearray} of unsigned bytes. - @return: A PKCS1 encryption of the passed-in data. - """ - paddedBytes = self._addPKCS1Padding(bytes, 2) - m = bytesToNumber(paddedBytes) - if m >= self.n: - raise ValueError() - c = self._rawPublicKeyOp(m) - encBytes = numberToByteArray(c, numBytes(self.n)) - return encBytes - - def decrypt(self, encBytes): - """Decrypt the passed-in bytes. - - This requires the key to have a private component. It performs - PKCS1 decryption of the passed-in data. - - @type encBytes: L{bytearray} of unsigned bytes - @param encBytes: The value which will be decrypted. - - @rtype: L{bytearray} of unsigned bytes or None. - @return: A PKCS1 decryption of the passed-in data or None if - the data is not properly formatted. - """ - if not self.hasPrivateKey(): - raise AssertionError() - if len(encBytes) != numBytes(self.n): - return None - c = bytesToNumber(encBytes) - if c >= self.n: - return None - m = self._rawPrivateKeyOp(c) - decBytes = numberToByteArray(m, numBytes(self.n)) - #Check first two bytes - if decBytes[0] != 0 or decBytes[1] != 2: - return None - #Scan through for zero separator - for x in range(1, len(decBytes)-1): - if decBytes[x]== 0: - break - else: - return None - return decBytes[x+1:] #Return everything after the separator - - - - - # ************************************************************************** - # Helper Functions for RSA Keys - # ************************************************************************** - - def _addPKCS1SHA1Prefix(self, bytes, withNULL=True): - # There is a long history of confusion over whether the SHA1 - # algorithmIdentifier should be encoded with a NULL parameter or - # with the parameter omitted. While the original intention was - # apparently to omit it, many toolkits went the other way. TLS 1.2 - # specifies the NULL should be included, and this behavior is also - # mandated in recent versions of PKCS #1, and is what tlslite has - # always implemented. Anyways, verification code should probably - # accept both. However, nothing uses this code yet, so this is - # all fairly moot. - if not withNULL: - prefixBytes = bytearray(\ - [0x30,0x1f,0x30,0x07,0x06,0x05,0x2b,0x0e,0x03,0x02,0x1a,0x04,0x14]) - else: - prefixBytes = bytearray(\ - [0x30,0x21,0x30,0x09,0x06,0x05,0x2b,0x0e,0x03,0x02,0x1a,0x05,0x00,0x04,0x14]) - prefixedBytes = prefixBytes + bytes - return prefixedBytes - - def _addPKCS1Padding(self, bytes, blockType): - padLength = (numBytes(self.n) - (len(bytes)+3)) - if blockType == 1: #Signature padding - pad = [0xFF] * padLength - elif blockType == 2: #Encryption padding - pad = bytearray(0) - while len(pad) < padLength: - padBytes = getRandomBytes(padLength * 2) - pad = [b for b in padBytes if b != 0] - pad = pad[:padLength] - else: - raise AssertionError() - - padding = bytearray([0,blockType] + pad + [0]) - paddedBytes = padding + bytes - return paddedBytes - - - - - def _rawPrivateKeyOp(self, m): - #Create blinding values, on the first pass: - if not self.blinder: - self.unblinder = getRandomNumber(2, self.n) - self.blinder = powMod(invMod(self.unblinder, self.n), self.e, - self.n) - - #Blind the input - m = (m * self.blinder) % self.n - - #Perform the RSA operation - c = self._rawPrivateKeyOpHelper(m) - - #Unblind the output - c = (c * self.unblinder) % self.n - - #Update blinding values - self.blinder = (self.blinder * self.blinder) % self.n - self.unblinder = (self.unblinder * self.unblinder) % self.n - - #Return the output - return c - - - def _rawPrivateKeyOpHelper(self, m): - #Non-CRT version - #c = powMod(m, self.d, self.n) - - #CRT version (~3x faster) - s1 = powMod(m, self.dP, self.p) - s2 = powMod(m, self.dQ, self.q) - h = ((s1 - s2) * self.qInv) % self.p - c = s2 + self.q * h - return c - - def _rawPublicKeyOp(self, c): - m = powMod(c, self.e, self.n) - return m - - def acceptsPassword(self): - return False - - def generate(bits): - key = RSAKey() - p = getRandomPrime(bits//2, False) - q = getRandomPrime(bits//2, False) - t = lcm(p-1, q-1) - key.n = p * q - key.e = 65537 - key.d = invMod(key.e, t) - key.p = p - key.q = q - key.dP = key.d % (p-1) - key.dQ = key.d % (q-1) - key.qInv = invMod(q, p) - return key - generate = staticmethod(generate) diff --git a/lib/segwit_addr.py b/lib/segwit_addr.py @@ -1,122 +0,0 @@ -# Copyright (c) 2017 Pieter Wuille -# -# 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. - -"""Reference implementation for Bech32 and segwit addresses.""" - - -CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" - - -def bech32_polymod(values): - """Internal function that computes the Bech32 checksum.""" - generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] - chk = 1 - for value in values: - top = chk >> 25 - chk = (chk & 0x1ffffff) << 5 ^ value - for i in range(5): - chk ^= generator[i] if ((top >> i) & 1) else 0 - return chk - - -def bech32_hrp_expand(hrp): - """Expand the HRP into values for checksum computation.""" - return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] - - -def bech32_verify_checksum(hrp, data): - """Verify a checksum given HRP and converted data characters.""" - return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 - - -def bech32_create_checksum(hrp, data): - """Compute the checksum values given HRP and data.""" - values = bech32_hrp_expand(hrp) + data - polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 - return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] - - -def bech32_encode(hrp, data): - """Compute a Bech32 string given HRP and data values.""" - combined = data + bech32_create_checksum(hrp, data) - return hrp + '1' + ''.join([CHARSET[d] for d in combined]) - - -def bech32_decode(bech): - """Validate a Bech32 string, and determine HRP and data.""" - if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or - (bech.lower() != bech and bech.upper() != bech)): - return (None, None) - bech = bech.lower() - pos = bech.rfind('1') - if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: - return (None, None) - if not all(x in CHARSET for x in bech[pos+1:]): - return (None, None) - hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos+1:]] - if not bech32_verify_checksum(hrp, data): - return (None, None) - return (hrp, data[:-6]) - - -def convertbits(data, frombits, tobits, pad=True): - """General power-of-2 base conversion.""" - acc = 0 - bits = 0 - ret = [] - maxv = (1 << tobits) - 1 - max_acc = (1 << (frombits + tobits - 1)) - 1 - for value in data: - if value < 0 or (value >> frombits): - return None - acc = ((acc << frombits) | value) & max_acc - bits += frombits - while bits >= tobits: - bits -= tobits - ret.append((acc >> bits) & maxv) - if pad: - if bits: - ret.append((acc << (tobits - bits)) & maxv) - elif bits >= frombits or ((acc << (tobits - bits)) & maxv): - return None - return ret - - -def decode(hrp, addr): - """Decode a segwit address.""" - hrpgot, data = bech32_decode(addr) - if hrpgot != hrp: - return (None, None) - decoded = convertbits(data[1:], 5, 8, False) - if decoded is None or len(decoded) < 2 or len(decoded) > 40: - return (None, None) - if data[0] > 16: - return (None, None) - if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: - return (None, None) - return (data[0], decoded) - - -def encode(hrp, witver, witprog): - """Encode a segwit address.""" - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) - assert decode(hrp, ret) is not (None, None) - return ret diff --git a/lib/servers.json b/lib/servers.json @@ -1,304 +0,0 @@ -{ - "207.154.223.80": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "4cii7ryno5j3axe4.onion": { - "pruning": "-", - "t": "50001", - "version": "1.2" - }, - "74.222.1.20": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "88.198.43.231": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "E-X.not.fyi": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "VPS.hsmiths.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "arihancckjge66iv.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "aspinall.io": { - "pruning": "-", - "s": "50002", - "version": "1.2" - }, - "bauerjda5hnedjam.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "bauerjhejlv6di7s.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "btc.asis.io": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "btc.cihar.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "btc.smsys.me": { - "pruning": "-", - "s": "995", - "version": "1.2" - }, - "daedalus.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "de.hamster.science": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "e.keff.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "elec.luggs.co": { - "pruning": "-", - "s": "443", - "version": "1.2" - }, - "electrum-server.ninja": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.achow101.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.cutie.ga": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.hsmiths.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.leblancnet.us": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.meltingice.net": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.nute.net": { - "pruning": "-", - "s": "50002", - "version": "1.2" - }, - "electrum.poorcoding.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.qtornado.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.vom-stausee.de": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum0.snel.it": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrumx-core.1209k.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrumx.bot.nu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrumx.nmdps.net": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrumx.westeurope.cloudapp.azure.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrumxhqdsmlu.onion": { - "pruning": "-", - "t": "50001", - "version": "1.2" - }, - "elx2018.mooo.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "helicarrier.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "hsmiths4fyqlw5xw.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "hsmiths5mjk6uijs.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "j5jfrdthqt5g25xz.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "kirsche.emzy.de": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "luggscoqbymhvnkp.onion": { - "pruning": "-", - "t": "80", - "version": "1.2" - }, - "ndnd.selfhost.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "ndndword5lpb7eex.onion": { - "pruning": "-", - "t": "50001", - "version": "1.2" - }, - "node.arihanc.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "node.erratic.space": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "ozahtqwp25chjdjd.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "qtornadoklbgdyww.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "rbx.curalle.ovh": { - "pruning": "-", - "s": "50002", - "version": "1.2" - }, - "ruuxwv74pjxms3ws.onion": { - "pruning": "-", - "s": "10042", - "t": "50001", - "version": "1.2" - }, - "s7clinmo4cazmhul.onion": { - "pruning": "-", - "t": "50001", - "version": "1.2" - }, - "songbird.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "spv.48.org": { - "pruning": "-", - "s": "50002", - "t": "50003", - "version": "1.2" - }, - "tardis.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - } -} diff --git a/lib/servers_regtest.json b/lib/servers_regtest.json @@ -1,8 +0,0 @@ -{ - "127.0.0.1": { - "pruning": "-", - "s": "51002", - "t": "51001", - "version": "1.2" - } -} diff --git a/lib/servers_testnet.json b/lib/servers_testnet.json @@ -1,31 +0,0 @@ -{ - "electrumx.kekku.li": { - "pruning": "-", - "s": "51002", - "version": "1.2" - }, - "hsmithsxurybd7uh.onion": { - "pruning": "-", - "s": "53012", - "t": "53011", - "version": "1.2" - }, - "testnet.hsmiths.com": { - "pruning": "-", - "s": "53012", - "t": "53011", - "version": "1.2" - }, - "testnet.qtornado.com": { - "pruning": "-", - "s": "51002", - "t": "51001", - "version": "1.2" - }, - "testnet1.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - } -} diff --git a/lib/simple_config.py b/lib/simple_config.py @@ -1,552 +0,0 @@ -import json -import threading -import time -import os -import stat -from decimal import Decimal -from typing import Union - -from copy import deepcopy - -from . import util -from .util import (user_dir, print_error, PrintError, make_dir, - NoDynamicFeeEstimates, format_fee_satoshis, quantize_feerate) -from .i18n import _ - -FEE_ETA_TARGETS = [25, 10, 5, 2] -FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000] - -# satoshi per kbyte -FEERATE_MAX_DYNAMIC = 1500000 -FEERATE_WARNING_HIGH_FEE = 600000 -FEERATE_FALLBACK_STATIC_FEE = 150000 -FEERATE_DEFAULT_RELAY = 1000 -FEERATE_STATIC_VALUES = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000] - - -config = None - - -def get_config(): - global config - return config - - -def set_config(c): - global config - config = c - - -FINAL_CONFIG_VERSION = 3 - - -class SimpleConfig(PrintError): - """ - The SimpleConfig class is responsible for handling operations involving - configuration files. - - There are two different sources of possible configuration values: - 1. Command line options. - 2. User configuration (in the user's config directory) - They are taken in order (1. overrides config options set in 2.) - """ - - def __init__(self, options=None, read_user_config_function=None, - read_user_dir_function=None): - - if options is None: - options = {} - - # This lock needs to be acquired for updating and reading the config in - # a thread-safe way. - self.lock = threading.RLock() - - self.mempool_fees = {} - self.fee_estimates = {} - self.fee_estimates_last_updated = {} - self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees - - # The following two functions are there for dependency injection when - # testing. - if read_user_config_function is None: - read_user_config_function = read_user_config - if read_user_dir_function is None: - self.user_dir = user_dir - else: - self.user_dir = read_user_dir_function - - # The command line options - self.cmdline_options = deepcopy(options) - # don't allow to be set on CLI: - self.cmdline_options.pop('config_version', None) - - # Set self.path and read the user config - self.user_config = {} # for self.get in electrum_path() - self.path = self.electrum_path() - self.user_config = read_user_config_function(self.path) - if not self.user_config: - # avoid new config getting upgraded - self.user_config = {'config_version': FINAL_CONFIG_VERSION} - - # config "upgrade" - CLI options - self.rename_config_keys( - self.cmdline_options, {'auto_cycle': 'auto_connect'}, True) - - # config upgrade - user config - if self.requires_upgrade(): - self.upgrade() - - # Make a singleton instance of 'self' - set_config(self) - - def electrum_path(self): - # Read electrum_path from command line - # Otherwise use the user's default data directory. - path = self.get('electrum_path') - if path is None: - path = self.user_dir() - - make_dir(path, allow_symlink=False) - if self.get('testnet'): - path = os.path.join(path, 'testnet') - make_dir(path, allow_symlink=False) - elif self.get('regtest'): - path = os.path.join(path, 'regtest') - make_dir(path, allow_symlink=False) - elif self.get('simnet'): - path = os.path.join(path, 'simnet') - make_dir(path, allow_symlink=False) - - self.print_error("electrum directory", path) - return path - - def rename_config_keys(self, config, keypairs, deprecation_warning=False): - """Migrate old key names to new ones""" - updated = False - for old_key, new_key in keypairs.items(): - if old_key in config: - if new_key not in config: - config[new_key] = config[old_key] - if deprecation_warning: - self.print_stderr('Note that the {} variable has been deprecated. ' - 'You should use {} instead.'.format(old_key, new_key)) - del config[old_key] - updated = True - return updated - - def set_key(self, key, value, save=True): - if not self.is_modifiable(key): - self.print_stderr("Warning: not changing config key '%s' set on the command line" % key) - return - self._set_key_in_user_config(key, value, save) - - def _set_key_in_user_config(self, key, value, save=True): - with self.lock: - if value is not None: - self.user_config[key] = value - else: - self.user_config.pop(key, None) - if save: - self.save_user_config() - - def get(self, key, default=None): - with self.lock: - out = self.cmdline_options.get(key) - if out is None: - out = self.user_config.get(key, default) - return out - - def requires_upgrade(self): - return self.get_config_version() < FINAL_CONFIG_VERSION - - def upgrade(self): - with self.lock: - self.print_error('upgrading config') - - self.convert_version_2() - self.convert_version_3() - - self.set_key('config_version', FINAL_CONFIG_VERSION, save=True) - - def convert_version_2(self): - if not self._is_upgrade_method_needed(1, 1): - return - - self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'}) - - try: - # change server string FROM host:port:proto TO host:port:s - server_str = self.user_config.get('server') - host, port, protocol = str(server_str).rsplit(':', 2) - assert protocol in ('s', 't') - int(port) # Throw if cannot be converted to int - server_str = '{}:{}:s'.format(host, port) - self._set_key_in_user_config('server', server_str) - except BaseException: - self._set_key_in_user_config('server', None) - - self.set_key('config_version', 2) - - def convert_version_3(self): - if not self._is_upgrade_method_needed(2, 2): - return - - base_unit = self.user_config.get('base_unit') - if isinstance(base_unit, str): - self._set_key_in_user_config('base_unit', None) - map_ = {'btc':8, 'mbtc':5, 'ubtc':2, 'bits':2, 'sat':0} - decimal_point = map_.get(base_unit.lower()) - self._set_key_in_user_config('decimal_point', decimal_point) - - self.set_key('config_version', 3) - - def _is_upgrade_method_needed(self, min_version, max_version): - cur_version = self.get_config_version() - if cur_version > max_version: - return False - elif cur_version < min_version: - raise Exception( - ('config upgrade: unexpected version %d (should be %d-%d)' - % (cur_version, min_version, max_version))) - else: - return True - - def get_config_version(self): - config_version = self.get('config_version', 1) - if config_version > FINAL_CONFIG_VERSION: - self.print_stderr('WARNING: config version ({}) is higher than ours ({})' - .format(config_version, FINAL_CONFIG_VERSION)) - return config_version - - def is_modifiable(self, key): - return key not in self.cmdline_options - - def save_user_config(self): - if not self.path: - return - path = os.path.join(self.path, "config") - s = json.dumps(self.user_config, indent=4, sort_keys=True) - try: - with open(path, "w", encoding='utf-8') as f: - f.write(s) - os.chmod(path, stat.S_IREAD | stat.S_IWRITE) - except FileNotFoundError: - # datadir probably deleted while running... - if os.path.exists(self.path): # or maybe not? - raise - - def get_wallet_path(self): - """Set the path of the wallet.""" - - # command line -w option - if self.get('wallet_path'): - return os.path.join(self.get('cwd'), self.get('wallet_path')) - - # path in config file - path = self.get('default_wallet_path') - if path and os.path.exists(path): - return path - - # default path - util.assert_datadir_available(self.path) - dirpath = os.path.join(self.path, "wallets") - make_dir(dirpath, allow_symlink=False) - - new_path = os.path.join(self.path, "wallets", "default_wallet") - - # default path in pre 1.9 versions - old_path = os.path.join(self.path, "electrum.dat") - if os.path.exists(old_path) and not os.path.exists(new_path): - os.rename(old_path, new_path) - - return new_path - - def remove_from_recently_open(self, filename): - recent = self.get('recently_open', []) - if filename in recent: - recent.remove(filename) - self.set_key('recently_open', recent) - - def set_session_timeout(self, seconds): - self.print_error("session timeout -> %d seconds" % seconds) - self.set_key('session_timeout', seconds) - - def get_session_timeout(self): - return self.get('session_timeout', 300) - - def open_last_wallet(self): - if self.get('wallet_path') is None: - last_wallet = self.get('gui_last_wallet') - if last_wallet is not None and os.path.exists(last_wallet): - self.cmdline_options['default_wallet_path'] = last_wallet - - def save_last_wallet(self, wallet): - if self.get('wallet_path') is None: - path = wallet.storage.path - self.set_key('gui_last_wallet', path) - - def impose_hard_limits_on_fee(func): - def get_fee_within_limits(self, *args, **kwargs): - fee = func(self, *args, **kwargs) - if fee is None: - return fee - fee = min(FEERATE_MAX_DYNAMIC, fee) - fee = max(FEERATE_DEFAULT_RELAY, fee) - return fee - return get_fee_within_limits - - @impose_hard_limits_on_fee - def eta_to_fee(self, slider_pos) -> Union[int, None]: - """Returns fee in sat/kbyte.""" - slider_pos = max(slider_pos, 0) - slider_pos = min(slider_pos, len(FEE_ETA_TARGETS)) - if slider_pos < len(FEE_ETA_TARGETS): - target_blocks = FEE_ETA_TARGETS[slider_pos] - fee = self.fee_estimates.get(target_blocks) - else: - fee = self.fee_estimates.get(2) - if fee is not None: - fee += fee/2 - fee = int(fee) - return fee - - def fee_to_depth(self, target_fee): - depth = 0 - for fee, s in self.mempool_fees: - depth += s - if fee <= target_fee: - break - else: - return 0 - return depth - - @impose_hard_limits_on_fee - def depth_to_fee(self, slider_pos) -> int: - """Returns fee in sat/kbyte.""" - target = self.depth_target(slider_pos) - depth = 0 - for fee, s in self.mempool_fees: - depth += s - if depth > target: - break - else: - return 0 - return fee * 1000 - - def depth_target(self, slider_pos): - slider_pos = max(slider_pos, 0) - slider_pos = min(slider_pos, len(FEE_DEPTH_TARGETS)-1) - return FEE_DEPTH_TARGETS[slider_pos] - - def eta_target(self, i): - if i == len(FEE_ETA_TARGETS): - return 1 - return FEE_ETA_TARGETS[i] - - def fee_to_eta(self, fee_per_kb): - import operator - l = list(self.fee_estimates.items()) + [(1, self.eta_to_fee(4))] - dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l) - min_target, min_value = min(dist, key=operator.itemgetter(1)) - if fee_per_kb < self.fee_estimates.get(25)/2: - min_target = -1 - return min_target - - def depth_tooltip(self, depth): - return "%.1f MB from tip"%(depth/1000000) - - def eta_tooltip(self, x): - if x < 0: - return _('Low fee') - elif x == 1: - return _('In the next block') - else: - return _('Within {} blocks').format(x) - - def get_fee_status(self): - dyn = self.is_dynfee() - mempool = self.use_mempool_fees() - pos = self.get_depth_level() if mempool else self.get_fee_level() - fee_rate = self.fee_per_kb() - target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate) - return tooltip + ' [%s]'%target if dyn else target + ' [Static]' - - def get_fee_text(self, pos, dyn, mempool, fee_rate): - """Returns (text, tooltip) where - text is what we target: static fee / num blocks to confirm in / mempool depth - tooltip is the corresponding estimate (e.g. num blocks for a static fee) - """ - if fee_rate is None: - rate_str = 'unknown' - else: - rate_str = format_fee_satoshis(fee_rate/1000) + ' sat/byte' - - if dyn: - if mempool: - depth = self.depth_target(pos) - text = self.depth_tooltip(depth) - else: - eta = self.eta_target(pos) - text = self.eta_tooltip(eta) - tooltip = rate_str - else: - text = rate_str - if mempool and self.has_fee_mempool(): - depth = self.fee_to_depth(fee_rate) - tooltip = self.depth_tooltip(depth) - elif not mempool and self.has_fee_etas(): - eta = self.fee_to_eta(fee_rate) - tooltip = self.eta_tooltip(eta) - else: - tooltip = '' - return text, tooltip - - def get_depth_level(self): - maxp = len(FEE_DEPTH_TARGETS) - 1 - return min(maxp, self.get('depth_level', 2)) - - def get_fee_level(self): - maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block" - return min(maxp, self.get('fee_level', 2)) - - def get_fee_slider(self, dyn, mempool): - if dyn: - if mempool: - pos = self.get_depth_level() - maxp = len(FEE_DEPTH_TARGETS) - 1 - fee_rate = self.depth_to_fee(pos) - else: - pos = self.get_fee_level() - maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block" - fee_rate = self.eta_to_fee(pos) - else: - fee_rate = self.fee_per_kb(dyn=False) - pos = self.static_fee_index(fee_rate) - maxp = 9 - return maxp, pos, fee_rate - - def static_fee(self, i): - return FEERATE_STATIC_VALUES[i] - - def static_fee_index(self, value): - if value is None: - raise TypeError('static fee cannot be None') - dist = list(map(lambda x: abs(x - value), FEERATE_STATIC_VALUES)) - return min(range(len(dist)), key=dist.__getitem__) - - def has_fee_etas(self): - return len(self.fee_estimates) == 4 - - def has_fee_mempool(self): - return bool(self.mempool_fees) - - def has_dynamic_fees_ready(self): - if self.use_mempool_fees(): - return self.has_fee_mempool() - else: - return self.has_fee_etas() - - def is_dynfee(self): - return bool(self.get('dynamic_fees', True)) - - def use_mempool_fees(self): - return bool(self.get('mempool_fees', False)) - - def _feerate_from_fractional_slider_position(self, fee_level: float, dyn: bool, - mempool: bool) -> Union[int, None]: - fee_level = max(fee_level, 0) - fee_level = min(fee_level, 1) - if dyn: - max_pos = (len(FEE_DEPTH_TARGETS) - 1) if mempool else len(FEE_ETA_TARGETS) - slider_pos = round(fee_level * max_pos) - fee_rate = self.depth_to_fee(slider_pos) if mempool else self.eta_to_fee(slider_pos) - else: - max_pos = len(FEERATE_STATIC_VALUES) - 1 - slider_pos = round(fee_level * max_pos) - fee_rate = FEERATE_STATIC_VALUES[slider_pos] - return fee_rate - - def fee_per_kb(self, dyn: bool=None, mempool: bool=None, fee_level: float=None) -> Union[int, None]: - """Returns sat/kvB fee to pay for a txn. - Note: might return None. - - fee_level: float between 0.0 and 1.0, representing fee slider position - """ - if dyn is None: - dyn = self.is_dynfee() - if mempool is None: - mempool = self.use_mempool_fees() - if fee_level is not None: - return self._feerate_from_fractional_slider_position(fee_level, dyn, mempool) - # there is no fee_level specified; will use config. - # note: 'depth_level' and 'fee_level' in config are integer slider positions, - # unlike fee_level here, which (when given) is a float in [0.0, 1.0] - if dyn: - if mempool: - fee_rate = self.depth_to_fee(self.get_depth_level()) - else: - fee_rate = self.eta_to_fee(self.get_fee_level()) - else: - fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE) - return fee_rate - - def fee_per_byte(self): - """Returns sat/vB fee to pay for a txn. - Note: might return None. - """ - fee_per_kb = self.fee_per_kb() - return fee_per_kb / 1000 if fee_per_kb is not None else None - - def estimate_fee(self, size): - fee_per_kb = self.fee_per_kb() - if fee_per_kb is None: - raise NoDynamicFeeEstimates() - return self.estimate_fee_for_feerate(fee_per_kb, size) - - @classmethod - def estimate_fee_for_feerate(cls, fee_per_kb, size): - fee_per_kb = Decimal(fee_per_kb) - fee_per_byte = fee_per_kb / 1000 - # to be consistent with what is displayed in the GUI, - # the calculation needs to use the same precision: - fee_per_byte = quantize_feerate(fee_per_byte) - return round(fee_per_byte * size) - - def update_fee_estimates(self, key, value): - self.fee_estimates[key] = value - self.fee_estimates_last_updated[key] = time.time() - - def is_fee_estimates_update_required(self): - """Checks time since last requested and updated fee estimates. - Returns True if an update should be requested. - """ - now = time.time() - return now - self.last_time_fee_estimates_requested > 60 - - def requested_fee_estimates(self): - self.last_time_fee_estimates_requested = time.time() - - def get_video_device(self): - device = self.get("video_device", "default") - if device == 'default': - device = '' - return device - - -def read_user_config(path): - """Parse and store the user config settings in electrum.conf into user_config[].""" - if not path: - return {} - config_path = os.path.join(path, "config") - if not os.path.exists(config_path): - return {} - try: - with open(config_path, "r", encoding='utf-8') as f: - data = f.read() - result = json.loads(data) - except: - print_error("Warning: Cannot read config file.", config_path) - return {} - if not type(result) is dict: - return {} - return result diff --git a/lib/storage.py b/lib/storage.py @@ -1,647 +0,0 @@ -#!/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 os -import ast -import threading -import json -import copy -import re -import stat -import pbkdf2, hmac, hashlib -import base64 -import zlib -from collections import defaultdict - -from . import util -from .util import PrintError, profiler, InvalidPassword, WalletFileException, bfh -from .plugins import run_hook, plugin_loaders -from .keystore import bip44_derivation -from . import bitcoin -from . import ecc - - -# seed_version is now used for the version of the wallet file - -OLD_SEED_VERSION = 4 # electrum versions < 2.0 -NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 17 # electrum >= 2.7 will set this to prevent - # old versions from overwriting new format - - - -def multisig_type(wallet_type): - '''If wallet_type is mofn multi-sig, return [m, n], - otherwise return None.''' - if not wallet_type: - return None - match = re.match('(\d+)of(\d+)', wallet_type) - if match: - match = [int(x) for x in match.group(1, 2)] - return match - -def get_derivation_used_for_hw_device_encryption(): - return ("m" - "/4541509'" # ascii 'ELE' as decimal ("BIP43 purpose") - "/1112098098'") # ascii 'BIE2' as decimal - -# storage encryption version -STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW = range(0, 3) - -class WalletStorage(PrintError): - - def __init__(self, path, manual_upgrades=False): - self.print_error("wallet path", path) - self.manual_upgrades = manual_upgrades - self.lock = threading.RLock() - self.data = {} - self.path = path - self.modified = False - self.pubkey = None - if self.file_exists(): - with open(self.path, "r", encoding='utf-8') as f: - self.raw = f.read() - self._encryption_version = self._init_encryption_version() - if not self.is_encrypted(): - self.load_data(self.raw) - else: - self._encryption_version = STO_EV_PLAINTEXT - # avoid new wallets getting 'upgraded' - self.put('seed_version', FINAL_SEED_VERSION) - - def load_data(self, s): - try: - self.data = json.loads(s) - except: - try: - d = ast.literal_eval(s) - labels = d.get('labels', {}) - except Exception as e: - raise IOError("Cannot read wallet file '%s'" % self.path) - self.data = {} - for key, value in d.items(): - try: - json.dumps(key) - json.dumps(value) - except: - self.print_error('Failed to convert label to json format', key) - continue - self.data[key] = value - - # check here if I need to load a plugin - t = self.get('wallet_type') - l = plugin_loaders.get(t) - if l: l() - - if not self.manual_upgrades: - if self.requires_split(): - raise WalletFileException("This wallet has multiple accounts and must be split") - if self.requires_upgrade(): - self.upgrade() - - def is_past_initial_decryption(self): - """Return if storage is in a usable state for normal operations. - - The value is True exactly - if encryption is disabled completely (self.is_encrypted() == False), - or if encryption is enabled but the contents have already been decrypted. - """ - return bool(self.data) - - def is_encrypted(self): - """Return if storage encryption is currently enabled.""" - return self.get_encryption_version() != STO_EV_PLAINTEXT - - def is_encrypted_with_user_pw(self): - return self.get_encryption_version() == STO_EV_USER_PW - - def is_encrypted_with_hw_device(self): - return self.get_encryption_version() == STO_EV_XPUB_PW - - def get_encryption_version(self): - """Return the version of encryption used for this storage. - - 0: plaintext / no encryption - - ECIES, private key derived from a password, - 1: password is provided by user - 2: password is derived from an xpub; used with hw wallets - """ - return self._encryption_version - - def _init_encryption_version(self): - try: - magic = base64.b64decode(self.raw)[0:4] - if magic == b'BIE1': - return STO_EV_USER_PW - elif magic == b'BIE2': - return STO_EV_XPUB_PW - else: - return STO_EV_PLAINTEXT - except: - return STO_EV_PLAINTEXT - - def file_exists(self): - return self.path and os.path.exists(self.path) - - @staticmethod - def get_eckey_from_password(password): - secret = pbkdf2.PBKDF2(password, '', iterations=1024, macmodule=hmac, digestmodule=hashlib.sha512).read(64) - ec_key = ecc.ECPrivkey.from_arbitrary_size_secret(secret) - return ec_key - - def _get_encryption_magic(self): - v = self._encryption_version - if v == STO_EV_USER_PW: - return b'BIE1' - elif v == STO_EV_XPUB_PW: - return b'BIE2' - else: - raise WalletFileException('no encryption magic for version: %s' % v) - - def decrypt(self, password): - ec_key = self.get_eckey_from_password(password) - if self.raw: - enc_magic = self._get_encryption_magic() - s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic)) - else: - s = None - self.pubkey = ec_key.get_public_key_hex() - s = s.decode('utf8') - self.load_data(s) - - def check_password(self, password): - """Raises an InvalidPassword exception on invalid password""" - if not self.is_encrypted(): - return - if self.pubkey and self.pubkey != self.get_eckey_from_password(password).get_public_key_hex(): - raise InvalidPassword() - - def set_keystore_encryption(self, enable): - self.put('use_encryption', enable) - - def set_password(self, password, enc_version=None): - """Set a password to be used for encrypting this storage.""" - if enc_version is None: - enc_version = self._encryption_version - if password and enc_version != STO_EV_PLAINTEXT: - ec_key = self.get_eckey_from_password(password) - self.pubkey = ec_key.get_public_key_hex() - self._encryption_version = enc_version - else: - self.pubkey = None - self._encryption_version = STO_EV_PLAINTEXT - # make sure next storage.write() saves changes - with self.lock: - self.modified = True - - def get(self, key, default=None): - with self.lock: - v = self.data.get(key) - if v is None: - v = default - else: - v = copy.deepcopy(v) - return v - - def put(self, key, value): - try: - json.dumps(key, cls=util.MyEncoder) - json.dumps(value, cls=util.MyEncoder) - except: - self.print_error("json error: cannot save", key) - return - with self.lock: - if value is not None: - if self.data.get(key) != value: - self.modified = True - self.data[key] = copy.deepcopy(value) - elif key in self.data: - self.modified = True - self.data.pop(key) - - @profiler - def write(self): - with self.lock: - self._write() - - def _write(self): - if threading.currentThread().isDaemon(): - self.print_error('warning: daemon thread cannot write wallet') - return - if not self.modified: - return - s = json.dumps(self.data, indent=4, sort_keys=True, cls=util.MyEncoder) - if self.pubkey: - s = bytes(s, 'utf8') - c = zlib.compress(s) - enc_magic = self._get_encryption_magic() - public_key = ecc.ECPubkey(bfh(self.pubkey)) - s = public_key.encrypt_message(c, enc_magic) - s = s.decode('utf8') - - temp_path = "%s.tmp.%s" % (self.path, os.getpid()) - with open(temp_path, "w", encoding='utf-8') as f: - f.write(s) - f.flush() - os.fsync(f.fileno()) - - mode = os.stat(self.path).st_mode if os.path.exists(self.path) else stat.S_IREAD | stat.S_IWRITE - # perform atomic write on POSIX systems - try: - os.rename(temp_path, self.path) - except: - os.remove(self.path) - os.rename(temp_path, self.path) - os.chmod(self.path, mode) - self.print_error("saved", self.path) - self.modified = False - - def requires_split(self): - d = self.get('accounts', {}) - return len(d) > 1 - - def split_accounts(storage): - result = [] - # backward compatibility with old wallets - d = storage.get('accounts', {}) - if len(d) < 2: - return - wallet_type = storage.get('wallet_type') - if wallet_type == 'old': - assert len(d) == 2 - storage1 = WalletStorage(storage.path + '.deterministic') - storage1.data = copy.deepcopy(storage.data) - storage1.put('accounts', {'0': d['0']}) - storage1.upgrade() - storage1.write() - storage2 = WalletStorage(storage.path + '.imported') - storage2.data = copy.deepcopy(storage.data) - storage2.put('accounts', {'/x': d['/x']}) - storage2.put('seed', None) - storage2.put('seed_version', None) - storage2.put('master_public_key', None) - storage2.put('wallet_type', 'imported') - storage2.upgrade() - storage2.write() - result = [storage1.path, storage2.path] - elif wallet_type in ['bip44', 'trezor', 'keepkey', 'ledger', 'btchip', 'digitalbitbox']: - mpk = storage.get('master_public_keys') - for k in d.keys(): - i = int(k) - x = d[k] - if x.get("pending"): - continue - xpub = mpk["x/%d'"%i] - new_path = storage.path + '.' + k - storage2 = WalletStorage(new_path) - storage2.data = copy.deepcopy(storage.data) - # save account, derivation and xpub at index 0 - storage2.put('accounts', {'0': x}) - storage2.put('master_public_keys', {"x/0'": xpub}) - storage2.put('derivation', bip44_derivation(k)) - storage2.upgrade() - storage2.write() - result.append(new_path) - else: - raise WalletFileException("This wallet has multiple accounts and must be split") - return result - - def requires_upgrade(self): - return self.file_exists() and self.get_seed_version() < FINAL_SEED_VERSION - - @profiler - def upgrade(self): - self.print_error('upgrading wallet format') - - self.convert_imported() - self.convert_wallet_type() - self.convert_account() - self.convert_version_13_b() - self.convert_version_14() - self.convert_version_15() - self.convert_version_16() - self.convert_version_17() - - self.put('seed_version', FINAL_SEED_VERSION) # just to be sure - self.write() - - def convert_wallet_type(self): - if not self._is_upgrade_method_needed(0, 13): - return - - wallet_type = self.get('wallet_type') - if wallet_type == 'btchip': wallet_type = 'ledger' - if self.get('keystore') or self.get('x1/') or wallet_type=='imported': - return False - assert not self.requires_split() - seed_version = self.get_seed_version() - seed = self.get('seed') - xpubs = self.get('master_public_keys') - xprvs = self.get('master_private_keys', {}) - mpk = self.get('master_public_key') - keypairs = self.get('keypairs') - key_type = self.get('key_type') - if seed_version == OLD_SEED_VERSION or wallet_type == 'old': - d = { - 'type': 'old', - 'seed': seed, - 'mpk': mpk, - } - self.put('wallet_type', 'standard') - self.put('keystore', d) - - elif key_type == 'imported': - d = { - 'type': 'imported', - 'keypairs': keypairs, - } - self.put('wallet_type', 'standard') - self.put('keystore', d) - - elif wallet_type in ['xpub', 'standard']: - xpub = xpubs["x/"] - xprv = xprvs.get("x/") - d = { - 'type': 'bip32', - 'xpub': xpub, - 'xprv': xprv, - 'seed': seed, - } - self.put('wallet_type', 'standard') - self.put('keystore', d) - - elif wallet_type in ['bip44']: - xpub = xpubs["x/0'"] - xprv = xprvs.get("x/0'") - d = { - 'type': 'bip32', - 'xpub': xpub, - 'xprv': xprv, - } - self.put('wallet_type', 'standard') - self.put('keystore', d) - - elif wallet_type in ['trezor', 'keepkey', 'ledger', 'digitalbitbox']: - xpub = xpubs["x/0'"] - derivation = self.get('derivation', bip44_derivation(0)) - d = { - 'type': 'hardware', - 'hw_type': wallet_type, - 'xpub': xpub, - 'derivation': derivation, - } - self.put('wallet_type', 'standard') - self.put('keystore', d) - - elif (wallet_type == '2fa') or multisig_type(wallet_type): - for key in xpubs.keys(): - d = { - 'type': 'bip32', - 'xpub': xpubs[key], - 'xprv': xprvs.get(key), - } - if key == 'x1/' and seed: - d['seed'] = seed - self.put(key, d) - else: - raise WalletFileException('Unable to tell wallet type. Is this even a wallet file?') - # remove junk - self.put('master_public_key', None) - self.put('master_public_keys', None) - self.put('master_private_keys', None) - self.put('derivation', None) - self.put('seed', None) - self.put('keypairs', None) - self.put('key_type', None) - - def convert_version_13_b(self): - # version 13 is ambiguous, and has an earlier and a later structure - if not self._is_upgrade_method_needed(0, 13): - return - - if self.get('wallet_type') == 'standard': - if self.get('keystore').get('type') == 'imported': - pubkeys = self.get('keystore').get('keypairs').keys() - d = {'change': []} - receiving_addresses = [] - for pubkey in pubkeys: - addr = bitcoin.pubkey_to_address('p2pkh', pubkey) - receiving_addresses.append(addr) - d['receiving'] = receiving_addresses - self.put('addresses', d) - self.put('pubkeys', None) - - self.put('seed_version', 13) - - def convert_version_14(self): - # convert imported wallets for 3.0 - if not self._is_upgrade_method_needed(13, 13): - return - - if self.get('wallet_type') =='imported': - addresses = self.get('addresses') - if type(addresses) is list: - addresses = dict([(x, None) for x in addresses]) - self.put('addresses', addresses) - elif self.get('wallet_type') == 'standard': - if self.get('keystore').get('type')=='imported': - addresses = set(self.get('addresses').get('receiving')) - pubkeys = self.get('keystore').get('keypairs').keys() - assert len(addresses) == len(pubkeys) - d = {} - for pubkey in pubkeys: - addr = bitcoin.pubkey_to_address('p2pkh', pubkey) - assert addr in addresses - d[addr] = { - 'pubkey': pubkey, - 'redeem_script': None, - 'type': 'p2pkh' - } - self.put('addresses', d) - self.put('pubkeys', None) - self.put('wallet_type', 'imported') - self.put('seed_version', 14) - - def convert_version_15(self): - if not self._is_upgrade_method_needed(14, 14): - return - if self.get('seed_type') == 'segwit': - # should not get here; get_seed_version should have caught this - raise Exception('unsupported derivation (development segwit, v14)') - self.put('seed_version', 15) - - def convert_version_16(self): - # fixes issue #3193 for Imported_Wallets with addresses - # also, previous versions allowed importing any garbage as an address - # which we now try to remove, see pr #3191 - if not self._is_upgrade_method_needed(15, 15): - return - - def remove_address(addr): - def remove_from_dict(dict_name): - d = self.get(dict_name, None) - if d is not None: - d.pop(addr, None) - self.put(dict_name, d) - - def remove_from_list(list_name): - lst = self.get(list_name, None) - if lst is not None: - s = set(lst) - s -= {addr} - self.put(list_name, list(s)) - - # note: we don't remove 'addr' from self.get('addresses') - remove_from_dict('addr_history') - remove_from_dict('labels') - remove_from_dict('payment_requests') - remove_from_list('frozen_addresses') - - if self.get('wallet_type') == 'imported': - addresses = self.get('addresses') - assert isinstance(addresses, dict) - addresses_new = dict() - for address, details in addresses.items(): - if not bitcoin.is_address(address): - remove_address(address) - continue - if details is None: - addresses_new[address] = {} - else: - addresses_new[address] = details - self.put('addresses', addresses_new) - - self.put('seed_version', 16) - - def convert_version_17(self): - # delete pruned_txo; construct spent_outpoints - if not self._is_upgrade_method_needed(16, 16): - return - - self.put('pruned_txo', None) - - from .transaction import Transaction - transactions = self.get('transactions', {}) # txid -> raw_tx - spent_outpoints = defaultdict(dict) - for txid, raw_tx in transactions.items(): - tx = Transaction(raw_tx) - for txin in tx.inputs(): - if txin['type'] == 'coinbase': - continue - prevout_hash = txin['prevout_hash'] - prevout_n = txin['prevout_n'] - spent_outpoints[prevout_hash][prevout_n] = txid - self.put('spent_outpoints', spent_outpoints) - - self.put('seed_version', 17) - - def convert_imported(self): - if not self._is_upgrade_method_needed(0, 13): - return - - # '/x' is the internal ID for imported accounts - d = self.get('accounts', {}).get('/x', {}).get('imported',{}) - if not d: - return False - addresses = [] - keypairs = {} - for addr, v in d.items(): - pubkey, privkey = v - if privkey: - keypairs[pubkey] = privkey - else: - addresses.append(addr) - if addresses and keypairs: - raise WalletFileException('mixed addresses and privkeys') - elif addresses: - self.put('addresses', addresses) - self.put('accounts', None) - elif keypairs: - self.put('wallet_type', 'standard') - self.put('key_type', 'imported') - self.put('keypairs', keypairs) - self.put('accounts', None) - else: - raise WalletFileException('no addresses or privkeys') - - def convert_account(self): - if not self._is_upgrade_method_needed(0, 13): - return - - self.put('accounts', None) - - def _is_upgrade_method_needed(self, min_version, max_version): - cur_version = self.get_seed_version() - if cur_version > max_version: - return False - elif cur_version < min_version: - raise WalletFileException( - 'storage upgrade: unexpected version {} (should be {}-{})' - .format(cur_version, min_version, max_version)) - else: - return True - - def get_action(self): - action = run_hook('get_action', self) - if self.file_exists() and self.requires_upgrade(): - if action: - raise WalletFileException('Incomplete wallet files cannot be upgraded.') - return 'upgrade_storage' - if action: - return action - if not self.file_exists(): - return 'new' - - def get_seed_version(self): - seed_version = self.get('seed_version') - if not seed_version: - seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION - if seed_version > FINAL_SEED_VERSION: - raise WalletFileException('This version of Electrum is too old to open this wallet.\n' - '(highest supported storage version: {}, version of this file: {})' - .format(FINAL_SEED_VERSION, seed_version)) - if seed_version==14 and self.get('seed_type') == 'segwit': - self.raise_unsupported_version(seed_version) - if seed_version >=12: - return seed_version - if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]: - self.raise_unsupported_version(seed_version) - return seed_version - - def raise_unsupported_version(self, seed_version): - msg = "Your wallet has an unsupported seed version." - msg += '\n\nWallet file: %s' % os.path.abspath(self.path) - if seed_version in [5, 7, 8, 9, 10, 14]: - msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version - if seed_version == 6: - # version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog - msg += '\n\nThis file was created because of a bug in version 1.9.8.' - if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None: - # pbkdf2 was not included with the binaries, and wallet creation aborted. - msg += "\nIt does not contain any keys, and can safely be removed." - else: - # creation was complete if electrum was run from source - msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet." - raise WalletFileException(msg) diff --git a/lib/synchronizer.py b/lib/synchronizer.py @@ -1,213 +0,0 @@ -#!/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. -from threading import Lock -import hashlib - -# from .bitcoin import Hash, hash_encode -from .transaction import Transaction -from .util import ThreadJob, bh2u - - -class Synchronizer(ThreadJob): - '''The synchronizer keeps the wallet up-to-date with its set of - addresses and their transactions. It subscribes over the network - to wallet addresses, gets the wallet to generate new addresses - when necessary, requests the transaction history of any addresses - we don't have the full history of, and requests binary transaction - data of any transactions the wallet doesn't have. - - External interface: __init__() and add() member functions. - ''' - - def __init__(self, wallet, network): - self.wallet = wallet - self.network = network - self.new_addresses = set() - # Entries are (tx_hash, tx_height) tuples - self.requested_tx = {} - self.requested_histories = {} - self.requested_addrs = set() - self.lock = Lock() - - self.initialized = False - self.initialize() - - def parse_response(self, response): - if response.get('error'): - self.print_error("response error:", response) - return None, None - return response['params'], response['result'] - - def is_up_to_date(self): - return (not self.requested_tx and not self.requested_histories - and not self.requested_addrs) - - def release(self): - self.network.unsubscribe(self.on_address_status) - - def add(self, address): - '''This can be called from the proxy or GUI threads.''' - with self.lock: - self.new_addresses.add(address) - - def subscribe_to_addresses(self, addresses): - if addresses: - self.requested_addrs |= addresses - self.network.subscribe_to_addresses(addresses, self.on_address_status) - - def get_status(self, h): - if not h: - return None - status = '' - for tx_hash, height in h: - status += tx_hash + ':%d:' % height - return bh2u(hashlib.sha256(status.encode('ascii')).digest()) - - def on_address_status(self, response): - if self.wallet.synchronizer is None and self.initialized: - return # we have been killed, this was just an orphan callback - params, result = self.parse_response(response) - if not params: - return - addr = params[0] - history = self.wallet.history.get(addr, []) - if self.get_status(history) != result: - # note that at this point 'result' can be None; - # if we had a history for addr but now the server is telling us - # there is no history - if addr not in self.requested_histories: - self.requested_histories[addr] = result - self.network.request_address_history(addr, self.on_address_history) - # remove addr from list only after it is added to requested_histories - if addr in self.requested_addrs: # Notifications won't be in - self.requested_addrs.remove(addr) - - def on_address_history(self, response): - if self.wallet.synchronizer is None and self.initialized: - return # we have been killed, this was just an orphan callback - params, result = self.parse_response(response) - if not params: - return - addr = params[0] - try: - server_status = self.requested_histories[addr] - except KeyError: - # note: server_status can be None even if we asked for the history, - # so it is not sufficient to test that - self.print_error("receiving history (unsolicited)", addr, len(result)) - return - self.print_error("receiving history", addr, len(result)) - hashes = set(map(lambda item: item['tx_hash'], result)) - hist = list(map(lambda item: (item['tx_hash'], item['height']), result)) - # tx_fees - tx_fees = [(item['tx_hash'], item.get('fee')) for item in result] - tx_fees = dict(filter(lambda x:x[1] is not None, tx_fees)) - # Check that txids are unique - if len(hashes) != len(result): - self.print_error("error: server history has non-unique txids: %s"% addr) - # Check that the status corresponds to what was announced - elif self.get_status(hist) != server_status: - self.print_error("error: status mismatch: %s" % addr) - else: - # Store received history - self.wallet.receive_history_callback(addr, hist, tx_fees) - # Request transactions we don't have - self.request_missing_txs(hist) - # Remove request; this allows up_to_date to be True - self.requested_histories.pop(addr) - - def on_tx_response(self, response): - if self.wallet.synchronizer is None and self.initialized: - return # we have been killed, this was just an orphan callback - params, result = self.parse_response(response) - if not params: - return - tx_hash = params[0] - tx = Transaction(result) - try: - tx.deserialize() - except Exception: - self.print_msg("cannot deserialize transaction, skipping", tx_hash) - return - if tx_hash != tx.txid(): - self.print_error("received tx does not match expected txid ({} != {})" - .format(tx_hash, tx.txid())) - return - tx_height = self.requested_tx.pop(tx_hash) - self.wallet.receive_tx_callback(tx_hash, tx, tx_height) - self.print_error("received tx %s height: %d bytes: %d" % - (tx_hash, tx_height, len(tx.raw))) - # callbacks - self.network.trigger_callback('new_transaction', tx) - if not self.requested_tx: - self.network.trigger_callback('updated') - - def request_missing_txs(self, hist): - # "hist" is a list of [tx_hash, tx_height] lists - transaction_hashes = [] - for tx_hash, tx_height in hist: - if tx_hash in self.requested_tx: - continue - if tx_hash in self.wallet.transactions: - continue - transaction_hashes.append(tx_hash) - self.requested_tx[tx_hash] = tx_height - - self.network.get_transactions(transaction_hashes, self.on_tx_response) - - def initialize(self): - '''Check the initial state of the wallet. Subscribe to all its - addresses, and request any transactions in its address history - we don't have. - ''' - for history in self.wallet.history.values(): - # Old electrum servers returned ['*'] when all history for - # the address was pruned. This no longer happens but may - # remain in old wallets. - if history == ['*']: - continue - self.request_missing_txs(history) - - if self.requested_tx: - self.print_error("missing tx", self.requested_tx) - self.subscribe_to_addresses(set(self.wallet.get_addresses())) - self.initialized = True - - def run(self): - '''Called from the network proxy thread main loop.''' - # 1. Create new addresses - self.wallet.synchronize() - - # 2. Subscribe to new addresses - with self.lock: - addresses = self.new_addresses - self.new_addresses = set() - self.subscribe_to_addresses(addresses) - - # 3. Detect if situation has changed - up_to_date = self.is_up_to_date() - if up_to_date != self.wallet.is_up_to_date(): - self.wallet.set_up_to_date(up_to_date) - self.network.trigger_callback('updated') diff --git a/lib/tests/__init__.py b/lib/tests/__init__.py @@ -1,38 +0,0 @@ -import unittest -import threading - -from lib import constants - - -# Set this locally to make the test suite run faster. -# If set, unit tests that would normally test functions with multiple implementations, -# will only be run once, using the fastest implementation. -# e.g. libsecp256k1 vs python-ecdsa. pycryptodomex vs pyaes. -FAST_TESTS = False - - -# some unit tests are modifying globals; sorry. -class SequentialTestCase(unittest.TestCase): - - test_lock = threading.Lock() - - def setUp(self): - super().setUp() - self.test_lock.acquire() - - def tearDown(self): - super().tearDown() - self.test_lock.release() - - -class TestCaseForTestnet(SequentialTestCase): - - @classmethod - def setUpClass(cls): - super().setUpClass() - constants.set_testnet() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - constants.set_mainnet() diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py @@ -1,761 +0,0 @@ -import base64 -import unittest -import sys - -from lib import bitcoin -from lib.bitcoin import ( - public_key_to_p2pkh, - bip32_root, bip32_public_derivation, bip32_private_derivation, - Hash, address_from_private_key, - is_address, is_private_key, xpub_from_xprv, is_new_seed, is_old_seed, - var_int, op_push, address_to_script, - deserialize_privkey, serialize_privkey, is_segwit_address, - is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub, - xpub_type, is_xprv, is_bip32_derivation, seed_type, EncodeBase58Check, - script_num_to_hex, push_script, add_number_to_script, int_to_hex) -from lib import ecc, crypto, ecc_fast -from lib.ecc import number_to_string, string_to_number -from lib.transaction import opcodes -from lib.util import bfh, bh2u -from lib import constants -from lib.storage import WalletStorage -from lib.keystore import xtype_from_derivation - -from . import SequentialTestCase -from . import TestCaseForTestnet -from . import FAST_TESTS - - -try: - import ecdsa -except ImportError: - sys.exit("Error: python-ecdsa does not seem to be installed. Try 'sudo pip install ecdsa'") - - -def needs_test_with_all_ecc_implementations(func): - """Function decorator to run a unit test twice: - once when libsecp256k1 is not available, once when it is. - - NOTE: this is inherently sequential; - tests running in parallel would break things - """ - def run_test(*args, **kwargs): - if FAST_TESTS: # if set, only run tests once, using fastest implementation - func(*args, **kwargs) - return - ecc_fast.undo_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1() - try: - # first test without libsecp - func(*args, **kwargs) - finally: - # if libsecp is not available, we are done - if not ecc_fast._libsecp256k1: - return - ecc_fast.do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1() - # if libsecp is available, test again now - func(*args, **kwargs) - return run_test - - -def needs_test_with_all_aes_implementations(func): - """Function decorator to run a unit test twice: - once when pycryptodomex is not available, once when it is. - - NOTE: this is inherently sequential; - tests running in parallel would break things - """ - def run_test(*args, **kwargs): - if FAST_TESTS: # if set, only run tests once, using fastest implementation - func(*args, **kwargs) - return - _aes = crypto.AES - crypto.AES = None - try: - # first test without pycryptodomex - func(*args, **kwargs) - finally: - # if pycryptodomex is not available, we are done - if not _aes: - return - crypto.AES = _aes - # if pycryptodomex is available, test again now - func(*args, **kwargs) - return run_test - - -class Test_bitcoin(SequentialTestCase): - - def test_libsecp256k1_is_available(self): - # we want the unit testing framework to test with libsecp256k1 available. - self.assertTrue(bool(ecc_fast._libsecp256k1)) - - def test_pycryptodomex_is_available(self): - # we want the unit testing framework to test with pycryptodomex available. - self.assertTrue(bool(crypto.AES)) - - @needs_test_with_all_aes_implementations - @needs_test_with_all_ecc_implementations - def test_crypto(self): - for message in [b"Chancellor on brink of second bailout for banks", b'\xff'*512]: - self._do_test_crypto(message) - - def _do_test_crypto(self, message): - G = ecc.generator() - _r = G.order() - pvk = ecdsa.util.randrange(_r) - - Pub = pvk*G - pubkey_c = Pub.get_public_key_bytes(True) - #pubkey_u = point_to_ser(Pub,False) - addr_c = public_key_to_p2pkh(pubkey_c) - - #print "Private key ", '%064x'%pvk - eck = ecc.ECPrivkey(number_to_string(pvk,_r)) - - #print "Compressed public key ", pubkey_c.encode('hex') - enc = ecc.ECPubkey(pubkey_c).encrypt_message(message) - dec = eck.decrypt_message(enc) - self.assertEqual(message, dec) - - #print "Uncompressed public key", pubkey_u.encode('hex') - #enc2 = EC_KEY.encrypt_message(message, pubkey_u) - dec2 = eck.decrypt_message(enc) - self.assertEqual(message, dec2) - - signature = eck.sign_message(message, True) - #print signature - eck.verify_message_for_address(signature, message) - - @needs_test_with_all_ecc_implementations - def test_ecc_sanity(self): - G = ecc.generator() - n = G.order() - self.assertEqual(ecc.CURVE_ORDER, n) - inf = n * G - self.assertEqual(ecc.point_at_infinity(), inf) - self.assertTrue(inf.is_at_infinity()) - self.assertFalse(G.is_at_infinity()) - self.assertEqual(11 * G, 7 * G + 4 * G) - self.assertEqual((n + 2) * G, 2 * G) - self.assertEqual((n - 2) * G, -2 * G) - A = (n - 2) * G - B = (n - 1) * G - C = n * G - D = (n + 1) * G - self.assertFalse(A.is_at_infinity()) - self.assertFalse(B.is_at_infinity()) - self.assertTrue(C.is_at_infinity()) - self.assertTrue((C * 5).is_at_infinity()) - self.assertFalse(D.is_at_infinity()) - self.assertEqual(inf, C) - self.assertEqual(inf, A + 2 * G) - self.assertEqual(inf, D + (-1) * G) - self.assertNotEqual(A, B) - - @needs_test_with_all_ecc_implementations - def test_msg_signing(self): - msg1 = b'Chancellor on brink of second bailout for banks' - msg2 = b'Electrum' - - def sign_message_with_wif_privkey(wif_privkey, msg): - txin_type, privkey, compressed = deserialize_privkey(wif_privkey) - key = ecc.ECPrivkey(privkey) - return key.sign_message(msg, compressed) - - sig1 = sign_message_with_wif_privkey( - 'L1TnU2zbNaAqMoVh65Cyvmcjzbrj41Gs9iTLcWbpJCMynXuap6UN', msg1) - addr1 = '15hETetDmcXm1mM4sEf7U2KXC9hDHFMSzz' - sig2 = sign_message_with_wif_privkey( - '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', msg2) - addr2 = '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6' - - sig1_b64 = base64.b64encode(sig1) - sig2_b64 = base64.b64encode(sig2) - - self.assertEqual(sig1_b64, b'H/9jMOnj4MFbH3d7t4yCQ9i7DgZU/VZ278w3+ySv2F4yIsdqjsc5ng3kmN8OZAThgyfCZOQxZCWza9V5XzlVY0Y=') - self.assertEqual(sig2_b64, b'G84dmJ8TKIDKMT9qBRhpX2sNmR0y5t+POcYnFFJCs66lJmAs3T8A6Sbpx7KA6yTQ9djQMabwQXRrDomOkIKGn18=') - - self.assertTrue(ecc.verify_message_with_address(addr1, sig1, msg1)) - self.assertTrue(ecc.verify_message_with_address(addr2, sig2, msg2)) - - self.assertFalse(ecc.verify_message_with_address(addr1, b'wrong', msg1)) - self.assertFalse(ecc.verify_message_with_address(addr1, sig2, msg1)) - - @needs_test_with_all_aes_implementations - @needs_test_with_all_ecc_implementations - def test_decrypt_message(self): - key = WalletStorage.get_eckey_from_password('pw123') - self.assertEqual(b'me<(s_s)>age', key.decrypt_message(b'QklFMQMDFtgT3zWSQsa+Uie8H/WvfUjlu9UN9OJtTt3KlgKeSTi6SQfuhcg1uIz9hp3WIUOFGTLr4RNQBdjPNqzXwhkcPi2Xsbiw6UCNJncVPJ6QBg==')) - self.assertEqual(b'me<(s_s)>age', key.decrypt_message(b'QklFMQKXOXbylOQTSMGfo4MFRwivAxeEEkewWQrpdYTzjPhqjHcGBJwdIhB7DyRfRQihuXx1y0ZLLv7XxLzrILzkl/H4YUtZB4uWjuOAcmxQH4i/Og==')) - self.assertEqual(b'hey_there' * 100, key.decrypt_message(b'QklFMQLOOsabsXtGQH8edAa6VOUa5wX8/DXmxX9NyHoAx1a5bWgllayGRVPeI2bf0ZdWK0tfal0ap0ZIVKbd2eOJybqQkILqT6E1/Syzq0Zicyb/AA1eZNkcX5y4gzloxinw00ubCA8M7gcUjJpOqbnksATcJ5y2YYXcHMGGfGurWu6uJ/UyrNobRidWppRMW5yR9/6utyNvT6OHIolCMEf7qLcmtneoXEiz51hkRdZS7weNf9mGqSbz9a2NL3sdh1A0feHIjAZgcCKcAvksNUSauf0/FnIjzTyPRpjRDMeDC8Ci3sGiuO3cvpWJwhZfbjcS26KmBv2CHWXfRRNFYOInHZNIXWNAoBB47Il5bGSMd+uXiGr+SQ9tNvcu+BiJNmFbxYqg+oQ8dGAl1DtvY2wJVY8k7vO9BIWSpyIxfGw7EDifhc5vnOmGe016p6a01C3eVGxgl23UYMrP7+fpjOcPmTSF4rk5U5ljEN3MSYqlf1QEv0OqlI9q1TwTK02VBCjMTYxDHsnt04OjNBkNO8v5uJ4NR+UUDBEp433z53I59uawZ+dbk4v4ZExcl8EGmKm3Gzbal/iJ/F7KQuX2b/ySEhLOFVYFWxK73X1nBvCSK2mC2/8fCw8oI5pmvzJwQhcCKTdEIrz3MMvAHqtPScDUOjzhXxInQOCb3+UBj1PPIdqkYLvZss1TEaBwYZjLkVnK2MBj7BaqT6Rp6+5A/fippUKHsnB6eYMEPR2YgDmCHL+4twxHJG6UWdP3ybaKiiAPy2OHNP6PTZ0HrqHOSJzBSDD+Z8YpaRg29QX3UEWlqnSKaan0VYAsV1VeaN0XFX46/TWO0L5tjhYVXJJYGqo6tIQJymxATLFRF6AZaD1Mwd27IAL04WkmoQoXfO6OFfwdp/shudY/1gBkDBvGPICBPtnqkvhGF+ZF3IRkuPwiFWeXmwBxKHsRx/3+aJu32Ml9+za41zVk2viaxcGqwTc5KMexQFLAUwqhv+aIik7U+5qk/gEVSuRoVkihoweFzKolNF+BknH2oB4rZdPixag5Zje3DvgjsSFlOl69W/67t/Gs8htfSAaHlsB8vWRQr9+v/lxTbrAw+O0E+sYGoObQ4qQMyQshNZEHbpPg63eWiHtJJnrVBvOeIbIHzoLDnMDsWVWZSMzAQ1vhX1H5QLgSEbRlKSliVY03kDkh/Nk/KOn+B2q37Ialq4JcRoIYFGJ8AoYEAD0tRuTqFddIclE75HzwaNG7NyKW1plsa72ciOPwsPJsdd5F0qdSQ3OSKtooTn7uf6dXOc4lDkfrVYRlZ0PX')) - - @needs_test_with_all_aes_implementations - @needs_test_with_all_ecc_implementations - def test_encrypt_message(self): - key = WalletStorage.get_eckey_from_password('secret_password77') - msgs = [ - bytes([0] * 555), - b'cannot think of anything funny' - ] - for plaintext in msgs: - ciphertext1 = key.encrypt_message(plaintext) - ciphertext2 = key.encrypt_message(plaintext) - self.assertEqual(plaintext, key.decrypt_message(ciphertext1)) - self.assertEqual(plaintext, key.decrypt_message(ciphertext2)) - self.assertNotEqual(ciphertext1, ciphertext2) - - @needs_test_with_all_ecc_implementations - def test_sign_transaction(self): - eckey1 = ecc.ECPrivkey(bfh('7e1255fddb52db1729fc3ceb21a46f95b8d9fe94cc83425e936a6c5223bb679d')) - sig1 = eckey1.sign_transaction(bfh('5a548b12369a53faaa7e51b5081829474ebdd9c924b3a8230b69aa0be254cd94')) - self.assertEqual(bfh('3045022100902a288b98392254cd23c0e9a49ac6d7920f171b8249a48e484b998f1874a2010220723d844826828f092cf400cb210c4fa0b8cd1b9d1a7f21590e78e022ff6476b9'), sig1) - - eckey2 = ecc.ECPrivkey(bfh('c7ce8c1462c311eec24dff9e2532ac6241e50ae57e7d1833af21942136972f23')) - sig2 = eckey2.sign_transaction(bfh('642a2e66332f507c92bda910158dfe46fc10afbf72218764899d3af99a043fac')) - self.assertEqual(bfh('30440220618513f4cfc87dde798ce5febae7634c23e7b9254a1eabf486be820f6a7c2c4702204fef459393a2b931f949e63ced06888f35e286e446dc46feb24b5b5f81c6ed52'), sig2) - - @needs_test_with_all_aes_implementations - def test_aes_homomorphic(self): - """Make sure AES is homomorphic.""" - payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' - password = u'secret' - enc = crypto.pw_encode(payload, password) - dec = crypto.pw_decode(enc, password) - self.assertEqual(dec, payload) - - @needs_test_with_all_aes_implementations - def test_aes_encode_without_password(self): - """When not passed a password, pw_encode is noop on the payload.""" - payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' - enc = crypto.pw_encode(payload, None) - self.assertEqual(payload, enc) - - @needs_test_with_all_aes_implementations - def test_aes_deencode_without_password(self): - """When not passed a password, pw_decode is noop on the payload.""" - payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' - enc = crypto.pw_decode(payload, None) - self.assertEqual(payload, enc) - - @needs_test_with_all_aes_implementations - def test_aes_decode_with_invalid_password(self): - """pw_decode raises an Exception when supplied an invalid password.""" - payload = u"blah" - password = u"uber secret" - wrong_password = u"not the password" - enc = crypto.pw_encode(payload, password) - self.assertRaises(Exception, crypto.pw_decode, enc, wrong_password) - - def test_hash(self): - """Make sure the Hash function does sha256 twice""" - payload = u"test" - expected = b'\x95MZI\xfdp\xd9\xb8\xbc\xdb5\xd2R&x)\x95\x7f~\xf7\xfalt\xf8\x84\x19\xbd\xc5\xe8"\t\xf4' - - result = Hash(payload) - self.assertEqual(expected, result) - - def test_int_to_hex(self): - self.assertEqual('00', int_to_hex(0, 1)) - self.assertEqual('ff', int_to_hex(-1, 1)) - self.assertEqual('00000000', int_to_hex(0, 4)) - self.assertEqual('01000000', int_to_hex(1, 4)) - self.assertEqual('7f', int_to_hex(127, 1)) - self.assertEqual('7f00', int_to_hex(127, 2)) - self.assertEqual('80', int_to_hex(128, 1)) - self.assertEqual('80', int_to_hex(-128, 1)) - self.assertEqual('8000', int_to_hex(128, 2)) - self.assertEqual('ff', int_to_hex(255, 1)) - self.assertEqual('ff7f', int_to_hex(32767, 2)) - self.assertEqual('0080', int_to_hex(-32768, 2)) - self.assertEqual('ffff', int_to_hex(65535, 2)) - with self.assertRaises(OverflowError): int_to_hex(256, 1) - with self.assertRaises(OverflowError): int_to_hex(-129, 1) - with self.assertRaises(OverflowError): int_to_hex(-257, 1) - with self.assertRaises(OverflowError): int_to_hex(65536, 2) - with self.assertRaises(OverflowError): int_to_hex(-32769, 2) - - def test_var_int(self): - for i in range(0xfd): - self.assertEqual(var_int(i), "{:02x}".format(i) ) - - self.assertEqual(var_int(0xfd), "fdfd00") - self.assertEqual(var_int(0xfe), "fdfe00") - self.assertEqual(var_int(0xff), "fdff00") - self.assertEqual(var_int(0x1234), "fd3412") - self.assertEqual(var_int(0xffff), "fdffff") - self.assertEqual(var_int(0x10000), "fe00000100") - self.assertEqual(var_int(0x12345678), "fe78563412") - self.assertEqual(var_int(0xffffffff), "feffffffff") - self.assertEqual(var_int(0x100000000), "ff0000000001000000") - self.assertEqual(var_int(0x0123456789abcdef), "ffefcdab8967452301") - - def test_op_push(self): - self.assertEqual(op_push(0x00), '00') - self.assertEqual(op_push(0x12), '12') - self.assertEqual(op_push(0x4b), '4b') - self.assertEqual(op_push(0x4c), '4c4c') - self.assertEqual(op_push(0xfe), '4cfe') - self.assertEqual(op_push(0xff), '4cff') - self.assertEqual(op_push(0x100), '4d0001') - self.assertEqual(op_push(0x1234), '4d3412') - self.assertEqual(op_push(0xfffe), '4dfeff') - self.assertEqual(op_push(0xffff), '4dffff') - self.assertEqual(op_push(0x10000), '4e00000100') - self.assertEqual(op_push(0x12345678), '4e78563412') - - def test_script_num_to_hex(self): - # test vectors from https://github.com/btcsuite/btcd/blob/fdc2bc867bda6b351191b5872d2da8270df00d13/txscript/scriptnum.go#L77 - self.assertEqual(script_num_to_hex(127), '7f') - self.assertEqual(script_num_to_hex(-127), 'ff') - self.assertEqual(script_num_to_hex(128), '8000') - self.assertEqual(script_num_to_hex(-128), '8080') - self.assertEqual(script_num_to_hex(129), '8100') - self.assertEqual(script_num_to_hex(-129), '8180') - self.assertEqual(script_num_to_hex(256), '0001') - self.assertEqual(script_num_to_hex(-256), '0081') - self.assertEqual(script_num_to_hex(32767), 'ff7f') - self.assertEqual(script_num_to_hex(-32767), 'ffff') - self.assertEqual(script_num_to_hex(32768), '008000') - self.assertEqual(script_num_to_hex(-32768), '008080') - - def test_push_script(self): - # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#push-operators - self.assertEqual(push_script(''), bh2u(bytes([opcodes.OP_0]))) - self.assertEqual(push_script('07'), bh2u(bytes([opcodes.OP_7]))) - self.assertEqual(push_script('10'), bh2u(bytes([opcodes.OP_16]))) - self.assertEqual(push_script('81'), bh2u(bytes([opcodes.OP_1NEGATE]))) - self.assertEqual(push_script('11'), '0111') - self.assertEqual(push_script(75 * '42'), '4b' + 75 * '42') - self.assertEqual(push_script(76 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('4c' + 76 * '42'))) - self.assertEqual(push_script(100 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('64' + 100 * '42'))) - self.assertEqual(push_script(255 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('ff' + 255 * '42'))) - self.assertEqual(push_script(256 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA2]) + bfh('0001' + 256 * '42'))) - self.assertEqual(push_script(520 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA2]) + bfh('0802' + 520 * '42'))) - - def test_add_number_to_script(self): - # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#numbers - self.assertEqual(add_number_to_script(0), bytes([opcodes.OP_0])) - self.assertEqual(add_number_to_script(7), bytes([opcodes.OP_7])) - self.assertEqual(add_number_to_script(16), bytes([opcodes.OP_16])) - self.assertEqual(add_number_to_script(-1), bytes([opcodes.OP_1NEGATE])) - self.assertEqual(add_number_to_script(-127), bfh('01ff')) - self.assertEqual(add_number_to_script(-2), bfh('0182')) - self.assertEqual(add_number_to_script(17), bfh('0111')) - self.assertEqual(add_number_to_script(127), bfh('017f')) - self.assertEqual(add_number_to_script(-32767), bfh('02ffff')) - self.assertEqual(add_number_to_script(-128), bfh('028080')) - self.assertEqual(add_number_to_script(128), bfh('028000')) - self.assertEqual(add_number_to_script(32767), bfh('02ff7f')) - self.assertEqual(add_number_to_script(-8388607), bfh('03ffffff')) - self.assertEqual(add_number_to_script(-32768), bfh('03008080')) - self.assertEqual(add_number_to_script(32768), bfh('03008000')) - self.assertEqual(add_number_to_script(8388607), bfh('03ffff7f')) - self.assertEqual(add_number_to_script(-2147483647), bfh('04ffffffff')) - self.assertEqual(add_number_to_script(-8388608 ), bfh('0400008080')) - self.assertEqual(add_number_to_script(8388608), bfh('0400008000')) - self.assertEqual(add_number_to_script(2147483647), bfh('04ffffff7f')) - - def test_address_to_script(self): - # bech32 native segwit - # test vectors from BIP-0173 - self.assertEqual(address_to_script('BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4'), '0014751e76e8199196d454941c45d1b3a323f1433bd6') - self.assertEqual(address_to_script('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), '5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6') - self.assertEqual(address_to_script('BC1SW50QA3JX3S'), '6002751e') - self.assertEqual(address_to_script('bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), '5210751e76e8199196d454941c45d1b3a323') - - # base58 P2PKH - self.assertEqual(address_to_script('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), '76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac') - self.assertEqual(address_to_script('1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), '76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac') - - # base58 P2SH - self.assertEqual(address_to_script('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), 'a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487') - self.assertEqual(address_to_script('3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), 'a914f47c8954e421031ad04ecd8e7752c9479206b9d387') - - -class Test_bitcoin_testnet(TestCaseForTestnet): - - def test_address_to_script(self): - # bech32 native segwit - # test vectors from BIP-0173 - self.assertEqual(address_to_script('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7'), '00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262') - self.assertEqual(address_to_script('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy'), '0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433') - - # base58 P2PKH - self.assertEqual(address_to_script('mutXcGt1CJdkRvXuN2xoz2quAAQYQ59bRX'), '76a9149da64e300c5e4eb4aaffc9c2fd465348d5618ad488ac') - self.assertEqual(address_to_script('miqtaRTkU3U8rzwKbEHx3g8FSz8GJtPS3K'), '76a914247d2d5b6334bdfa2038e85b20fc15264f8e5d2788ac') - - # base58 P2SH - self.assertEqual(address_to_script('2N3LSvr3hv5EVdfcrxg2Yzecf3SRvqyBE4p'), 'a9146eae23d8c4a941316017946fc761a7a6c85561fb87') - self.assertEqual(address_to_script('2NE4ZdmxFmUgwu5wtfoN2gVniyMgRDYq1kk'), 'a914e4567743d378957cd2ee7072da74b1203c1a7a0b87') - - -class Test_xprv_xpub(SequentialTestCase): - - xprv_xpub = ( - # Taken from test vectors in https://en.bitcoin.it/wiki/BIP_0032_TestVectors - {'xprv': 'xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76', - 'xpub': 'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy', - 'xtype': 'standard'}, - {'xprv': 'yprvAJEYHeNEPcyBoQYM7sGCxDiNCTX65u4ANgZuSGTrKN5YCC9MP84SBayrgaMyZV7zvkHrr3HVPTK853s2SPk4EttPazBZBmz6QfDkXeE8Zr7', - 'xpub': 'ypub6XDth9u8DzXV1tcpDtoDKMf6kVMaVMn1juVWEesTshcX4zUVvfNgjPJLXrD9N7AdTLnbHFL64KmBn3SNaTe69iZYbYCqLCCNPZKbLz9niQ4', - 'xtype': 'p2wpkh-p2sh'}, - {'xprv': 'zprvAWgYBBk7JR8GkraNZJeEodAp2UR1VRWJTXyV1ywuUVs1awUgTiBS1ZTDtLA5F3MFDn1LZzu8dUpSKdT7ToDpvEG6PQu4bJs7zQY47Sd3sEZ', - 'xpub': 'zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8', - 'xtype': 'p2wpkh'}, - ) - - def _do_test_bip32(self, seed, sequence): - xprv, xpub = bip32_root(bfh(seed), 'standard') - self.assertEqual("m/", sequence[0:2]) - path = 'm' - sequence = sequence[2:] - for n in sequence.split('/'): - child_path = path + '/' + n - if n[-1] != "'": - xpub2 = bip32_public_derivation(xpub, path, child_path) - xprv, xpub = bip32_private_derivation(xprv, path, child_path) - if n[-1] != "'": - self.assertEqual(xpub, xpub2) - path = child_path - - return xpub, xprv - - @needs_test_with_all_ecc_implementations - def test_bip32(self): - # see https://en.bitcoin.it/wiki/BIP_0032_TestVectors - xpub, xprv = self._do_test_bip32("000102030405060708090a0b0c0d0e0f", "m/0'/1/2'/2/1000000000") - self.assertEqual("xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", xpub) - self.assertEqual("xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", xprv) - - xpub, xprv = self._do_test_bip32("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542","m/0/2147483647'/1/2147483646'/2") - self.assertEqual("xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", xpub) - self.assertEqual("xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", xprv) - - @needs_test_with_all_ecc_implementations - def test_xpub_from_xprv(self): - """We can derive the xpub key from a xprv.""" - for xprv_details in self.xprv_xpub: - result = xpub_from_xprv(xprv_details['xprv']) - self.assertEqual(result, xprv_details['xpub']) - - @needs_test_with_all_ecc_implementations - def test_is_xpub(self): - for xprv_details in self.xprv_xpub: - xpub = xprv_details['xpub'] - self.assertTrue(is_xpub(xpub)) - self.assertFalse(is_xpub('xpub1nval1d')) - self.assertFalse(is_xpub('xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG')) - - @needs_test_with_all_ecc_implementations - def test_xpub_type(self): - for xprv_details in self.xprv_xpub: - xpub = xprv_details['xpub'] - self.assertEqual(xprv_details['xtype'], xpub_type(xpub)) - - @needs_test_with_all_ecc_implementations - def test_is_xprv(self): - for xprv_details in self.xprv_xpub: - xprv = xprv_details['xprv'] - self.assertTrue(is_xprv(xprv)) - self.assertFalse(is_xprv('xprv1nval1d')) - self.assertFalse(is_xprv('xprv661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG')) - - def test_is_bip32_derivation(self): - self.assertTrue(is_bip32_derivation("m/0'/1")) - self.assertTrue(is_bip32_derivation("m/0'/0'")) - self.assertTrue(is_bip32_derivation("m/44'/0'/0'/0/0")) - self.assertTrue(is_bip32_derivation("m/49'/0'/0'/0/0")) - self.assertFalse(is_bip32_derivation("mmmmmm")) - self.assertFalse(is_bip32_derivation("n/")) - self.assertFalse(is_bip32_derivation("")) - self.assertFalse(is_bip32_derivation("m/q8462")) - - def test_xtype_from_derivation(self): - self.assertEqual('standard', xtype_from_derivation("m/44'")) - self.assertEqual('standard', xtype_from_derivation("m/44'/")) - self.assertEqual('standard', xtype_from_derivation("m/44'/0'/0'")) - self.assertEqual('standard', xtype_from_derivation("m/44'/5241'/221")) - self.assertEqual('standard', xtype_from_derivation("m/45'")) - self.assertEqual('standard', xtype_from_derivation("m/45'/56165/271'")) - self.assertEqual('p2wpkh-p2sh', xtype_from_derivation("m/49'")) - self.assertEqual('p2wpkh-p2sh', xtype_from_derivation("m/49'/134")) - self.assertEqual('p2wpkh', xtype_from_derivation("m/84'")) - self.assertEqual('p2wpkh', xtype_from_derivation("m/84'/112'/992/112/33'/0/2")) - self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/0'/0'/1'")) - self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/0'/0'/1'/52112/52'")) - self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/9'/2'/1'")) - self.assertEqual('p2wsh', xtype_from_derivation("m/48'/0'/0'/2'")) - self.assertEqual('p2wsh', xtype_from_derivation("m/48'/1'/0'/2'/77'/0")) - - def test_version_bytes(self): - xprv_headers_b58 = { - 'standard': 'xprv', - 'p2wpkh-p2sh': 'yprv', - 'p2wsh-p2sh': 'Yprv', - 'p2wpkh': 'zprv', - 'p2wsh': 'Zprv', - } - xpub_headers_b58 = { - 'standard': 'xpub', - 'p2wpkh-p2sh': 'ypub', - 'p2wsh-p2sh': 'Ypub', - 'p2wpkh': 'zpub', - 'p2wsh': 'Zpub', - } - for xtype, xkey_header_bytes in constants.net.XPRV_HEADERS.items(): - xkey_header_bytes = bfh("%08x" % xkey_header_bytes) - xkey_bytes = xkey_header_bytes + bytes([0] * 74) - xkey_b58 = EncodeBase58Check(xkey_bytes) - self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype])) - - xkey_bytes = xkey_header_bytes + bytes([255] * 74) - xkey_b58 = EncodeBase58Check(xkey_bytes) - self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype])) - - for xtype, xkey_header_bytes in constants.net.XPUB_HEADERS.items(): - xkey_header_bytes = bfh("%08x" % xkey_header_bytes) - xkey_bytes = xkey_header_bytes + bytes([0] * 74) - xkey_b58 = EncodeBase58Check(xkey_bytes) - self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) - - xkey_bytes = xkey_header_bytes + bytes([255] * 74) - xkey_b58 = EncodeBase58Check(xkey_bytes) - self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) - - -class Test_xprv_xpub_testnet(TestCaseForTestnet): - - def test_version_bytes(self): - xprv_headers_b58 = { - 'standard': 'tprv', - 'p2wpkh-p2sh': 'uprv', - 'p2wsh-p2sh': 'Uprv', - 'p2wpkh': 'vprv', - 'p2wsh': 'Vprv', - } - xpub_headers_b58 = { - 'standard': 'tpub', - 'p2wpkh-p2sh': 'upub', - 'p2wsh-p2sh': 'Upub', - 'p2wpkh': 'vpub', - 'p2wsh': 'Vpub', - } - for xtype, xkey_header_bytes in constants.net.XPRV_HEADERS.items(): - xkey_header_bytes = bfh("%08x" % xkey_header_bytes) - xkey_bytes = xkey_header_bytes + bytes([0] * 74) - xkey_b58 = EncodeBase58Check(xkey_bytes) - self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype])) - - xkey_bytes = xkey_header_bytes + bytes([255] * 74) - xkey_b58 = EncodeBase58Check(xkey_bytes) - self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype])) - - for xtype, xkey_header_bytes in constants.net.XPUB_HEADERS.items(): - xkey_header_bytes = bfh("%08x" % xkey_header_bytes) - xkey_bytes = xkey_header_bytes + bytes([0] * 74) - xkey_b58 = EncodeBase58Check(xkey_bytes) - self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) - - xkey_bytes = xkey_header_bytes + bytes([255] * 74) - xkey_b58 = EncodeBase58Check(xkey_bytes) - self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) - - -class Test_keyImport(SequentialTestCase): - - priv_pub_addr = ( - {'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6', - 'exported_privkey': 'p2pkh:KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6', - 'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997', - 'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR', - 'minikey' : False, - 'txin_type': 'p2pkh', - 'compressed': True, - 'addr_encoding': 'base58', - 'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'}, - {'priv': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD', - 'exported_privkey': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD', - 'pub': '0352d78b4b37e0f6d4e164423436f2925fa57817467178eca550a88f2821973c41', - 'address': '1GXgZ5Qi6gmXTHVSpUPZLy4Ci2nbfb3ZNb', - 'minikey': False, - 'txin_type': 'p2pkh', - 'compressed': True, - 'addr_encoding': 'base58', - 'scripthash': 'a9b2a76fc196c553b352186dfcca81fcf323a721cd8431328f8e9d54216818c1'}, - {'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', - 'exported_privkey': 'p2pkh:5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', - 'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f', - 'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6', - 'minikey': False, - 'txin_type': 'p2pkh', - 'compressed': False, - 'addr_encoding': 'base58', - 'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'}, - {'priv': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN', - 'exported_privkey': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN', - 'pub': '048f0431b0776e8210376c81280011c2b68be43194cb00bd47b7e9aa66284b713ce09556cde3fee606051a07613f3c159ef3953b8927c96ae3dae94a6ba4182e0e', - 'address': '147kiRHHm9fqeMQSgqf4k35XzuWLP9fmmS', - 'minikey': False, - 'txin_type': 'p2pkh', - 'compressed': False, - 'addr_encoding': 'base58', - 'scripthash': '6dd2e07ad2de9ba8eec4bbe8467eb53f8845acff0d9e6f5627391acc22ff62df'}, - {'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz', - 'exported_privkey': 'p2wpkh-p2sh:Kz9XebiCXL2BZzhYJViiHDzn5iup1povWV8aqstzWU4sz1K5nVva', - 'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81', - 'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7', - 'minikey': False, - 'txin_type': 'p2wpkh-p2sh', - 'compressed': True, - 'addr_encoding': 'base58', - 'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'}, - {'priv': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW', - 'exported_privkey': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW', - 'pub': '0229da20a15b3363b2c28e3c5093c180b56c439df0b968a970366bb1f38435361e', - 'address': '3C79goMwT7zSTjXnPoCg6VFGAnUpZAkyus', - 'minikey': False, - 'txin_type': 'p2wpkh-p2sh', - 'compressed': True, - 'addr_encoding': 'base58', - 'scripthash': '714bf6bfe1083e69539f40d4c7a7dca85d187471b35642e55f20d7e866494cf7'}, - {'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj', - 'exported_privkey': 'p2wpkh:Kz6SuyPM5VktY5dr2d2YqdVgBA6LCWkiHqXJaC3BzxnMPSUuYzmF', - 'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b', - 'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue', - 'minikey': False, - 'txin_type': 'p2wpkh', - 'compressed': True, - 'addr_encoding': 'bech32', - 'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'}, - {'priv': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo', - 'exported_privkey': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo', - 'pub': '038c57657171c1f73e34d5b3971d05867d50221ad94980f7e87cbc2344425e6a1e', - 'address': 'bc1qpakeeg4d9ydyjxd8paqrw4xy9htsg532xzxn50', - 'minikey': False, - 'txin_type': 'p2wpkh', - 'compressed': True, - 'addr_encoding': 'bech32', - 'scripthash': '242f02adde84ebb2a7dd778b2f3a81b3826f111da4d8960d826d7a4b816cb261'}, - # from http://bitscan.com/articles/security/spotlight-on-mini-private-keys - {'priv': 'SzavMBLoXU6kDrqtUVmffv', - 'exported_privkey': 'p2pkh:5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF', - 'pub': '04588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9f88ff2a00d7e752d44cbe16e1ebcf0890b76ec7c78886109dee76ccfc8445424', - 'address': '1CC3X2gu58d6wXUWMffpuzN9JAfTUWu4Kj', - 'minikey': True, - 'txin_type': 'p2pkh', - 'compressed': False, # this is actually ambiguous... issue #2748 - 'addr_encoding': 'base58', - 'scripthash': '5b07ddfde826f5125ee823900749103cea37808038ecead5505a766a07c34445'}, - ) - - @needs_test_with_all_ecc_implementations - def test_public_key_from_private_key(self): - for priv_details in self.priv_pub_addr: - txin_type, privkey, compressed = deserialize_privkey(priv_details['priv']) - result = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) - self.assertEqual(priv_details['pub'], result) - self.assertEqual(priv_details['txin_type'], txin_type) - self.assertEqual(priv_details['compressed'], compressed) - - @needs_test_with_all_ecc_implementations - def test_address_from_private_key(self): - for priv_details in self.priv_pub_addr: - addr2 = address_from_private_key(priv_details['priv']) - self.assertEqual(priv_details['address'], addr2) - - @needs_test_with_all_ecc_implementations - def test_is_valid_address(self): - for priv_details in self.priv_pub_addr: - addr = priv_details['address'] - self.assertFalse(is_address(priv_details['priv'])) - self.assertFalse(is_address(priv_details['pub'])) - self.assertTrue(is_address(addr)) - - is_enc_b58 = priv_details['addr_encoding'] == 'base58' - self.assertEqual(is_enc_b58, is_b58_address(addr)) - - is_enc_bech32 = priv_details['addr_encoding'] == 'bech32' - self.assertEqual(is_enc_bech32, is_segwit_address(addr)) - - self.assertFalse(is_address("not an address")) - - @needs_test_with_all_ecc_implementations - def test_is_private_key(self): - for priv_details in self.priv_pub_addr: - self.assertTrue(is_private_key(priv_details['priv'])) - self.assertTrue(is_private_key(priv_details['exported_privkey'])) - self.assertFalse(is_private_key(priv_details['pub'])) - self.assertFalse(is_private_key(priv_details['address'])) - self.assertFalse(is_private_key("not a privkey")) - - @needs_test_with_all_ecc_implementations - def test_serialize_privkey(self): - for priv_details in self.priv_pub_addr: - txin_type, privkey, compressed = deserialize_privkey(priv_details['priv']) - priv2 = serialize_privkey(privkey, compressed, txin_type) - self.assertEqual(priv_details['exported_privkey'], priv2) - - @needs_test_with_all_ecc_implementations - def test_address_to_scripthash(self): - for priv_details in self.priv_pub_addr: - sh = address_to_scripthash(priv_details['address']) - self.assertEqual(priv_details['scripthash'], sh) - - @needs_test_with_all_ecc_implementations - def test_is_minikey(self): - for priv_details in self.priv_pub_addr: - minikey = priv_details['minikey'] - priv = priv_details['priv'] - self.assertEqual(minikey, is_minikey(priv)) - - @needs_test_with_all_ecc_implementations - def test_is_compressed(self): - for priv_details in self.priv_pub_addr: - self.assertEqual(priv_details['compressed'], - is_compressed(priv_details['priv'])) - - -class Test_seeds(SequentialTestCase): - """ Test old and new seeds. """ - - mnemonics = { - ('cell dumb heartbeat north boom tease ship baby bright kingdom rare squeeze', 'old'), - ('cell dumb heartbeat north boom tease ' * 4, 'old'), - ('cell dumb heartbeat north boom tease ship baby bright kingdom rare badword', ''), - ('cElL DuMb hEaRtBeAt nOrTh bOoM TeAsE ShIp bAbY BrIgHt kInGdOm rArE SqUeEzE', 'old'), - (' cElL DuMb hEaRtBeAt nOrTh bOoM TeAsE ShIp bAbY BrIgHt kInGdOm rArE SqUeEzE ', 'old'), - # below seed is actually 'invalid old' as it maps to 33 hex chars - ('hurry idiot prefer sunset mention mist jaw inhale impossible kingdom rare squeeze', 'old'), - ('cram swing cover prefer miss modify ritual silly deliver chunk behind inform able', 'standard'), - ('cram swing cover prefer miss modify ritual silly deliver chunk behind inform', ''), - ('ostrich security deer aunt climb inner alpha arm mutual marble solid task', 'standard'), - ('OSTRICH SECURITY DEER AUNT CLIMB INNER ALPHA ARM MUTUAL MARBLE SOLID TASK', 'standard'), - (' oStRiCh sEcUrItY DeEr aUnT ClImB InNeR AlPhA ArM MuTuAl mArBlE SoLiD TaSk ', 'standard'), - ('x8', 'standard'), - ('science dawn member doll dutch real can brick knife deny drive list', '2fa'), - ('science dawn member doll dutch real ca brick knife deny drive list', ''), - (' sCience dawn member doll Dutch rEAl can brick knife deny drive lisT', '2fa'), - ('frost pig brisk excite novel report camera enlist axis nation novel desert', 'segwit'), - (' fRoSt pig brisk excIte novel rePort CamEra enlist axis nation nOVeL dEsert ', 'segwit'), - ('9dk', 'segwit'), - } - - def test_new_seed(self): - seed = "cram swing cover prefer miss modify ritual silly deliver chunk behind inform able" - self.assertTrue(is_new_seed(seed)) - - seed = "cram swing cover prefer miss modify ritual silly deliver chunk behind inform" - self.assertFalse(is_new_seed(seed)) - - def test_old_seed(self): - self.assertTrue(is_old_seed(" ".join(["like"] * 12))) - self.assertFalse(is_old_seed(" ".join(["like"] * 18))) - self.assertTrue(is_old_seed(" ".join(["like"] * 24))) - self.assertFalse(is_old_seed("not a seed")) - - self.assertTrue(is_old_seed("0123456789ABCDEF" * 2)) - self.assertTrue(is_old_seed("0123456789ABCDEF" * 4)) - - def test_seed_type(self): - for seed_words, _type in self.mnemonics: - self.assertEqual(_type, seed_type(seed_words), msg=seed_words) diff --git a/lib/tests/test_commands.py b/lib/tests/test_commands.py @@ -1,33 +0,0 @@ -import unittest -from decimal import Decimal - -from lib.commands import Commands - - -class TestCommands(unittest.TestCase): - - def test_setconfig_non_auth_number(self): - self.assertEqual(7777, Commands._setconfig_normalize_value('rpcport', "7777")) - self.assertEqual(7777, Commands._setconfig_normalize_value('rpcport', '7777')) - self.assertAlmostEqual(Decimal(2.3), Commands._setconfig_normalize_value('somekey', '2.3')) - - def test_setconfig_non_auth_number_as_string(self): - self.assertEqual("7777", Commands._setconfig_normalize_value('somekey', "'7777'")) - - def test_setconfig_non_auth_boolean(self): - self.assertEqual(True, Commands._setconfig_normalize_value('show_console_tab', "true")) - self.assertEqual(True, Commands._setconfig_normalize_value('show_console_tab', "True")) - - def test_setconfig_non_auth_list(self): - self.assertEqual(['file:///var/www/', 'https://electrum.org'], - Commands._setconfig_normalize_value('url_rewrite', "['file:///var/www/','https://electrum.org']")) - self.assertEqual(['file:///var/www/', 'https://electrum.org'], - Commands._setconfig_normalize_value('url_rewrite', '["file:///var/www/","https://electrum.org"]')) - - def test_setconfig_auth(self): - self.assertEqual("7777", Commands._setconfig_normalize_value('rpcuser', "7777")) - self.assertEqual("7777", Commands._setconfig_normalize_value('rpcuser', '7777')) - self.assertEqual("7777", Commands._setconfig_normalize_value('rpcpassword', '7777')) - self.assertEqual("2asd", Commands._setconfig_normalize_value('rpcpassword', '2asd')) - self.assertEqual("['file:///var/www/','https://electrum.org']", - Commands._setconfig_normalize_value('rpcpassword', "['file:///var/www/','https://electrum.org']")) diff --git a/lib/tests/test_dnssec.py b/lib/tests/test_dnssec.py @@ -1,41 +0,0 @@ -import dns - -from lib import dnssec - -from . import SequentialTestCase -from .test_bitcoin import needs_test_with_all_ecc_implementations - - -class TestDnsSec(SequentialTestCase): - - @needs_test_with_all_ecc_implementations - def test_python_validate_rrsig_ecdsa(self): - rrset = dns.rrset.from_text("getmonero.org.", 3599, 1, 48, - "257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0d xCjjnopKl+GqJxpVXckHAeF+KkxLbxIL fDLUT0rAK9iUzy1L53eKGQ==", - "256 3 13 koPbw9wmYZ7ggcjnQ6ayHyhHaDNMYELK TqT+qRGrZpWSccr/lBcrm10Z1PuQHB3A zhii+sb0PYFkH1ruxLhe5g==") - rrsig = dns.rdtypes.ANY.RRSIG.RRSIG.from_text(1, 46, - dns.tokenizer.Tokenizer("DNSKEY 13 2 3600 20180612115508 20180413115508 2371 getmonero.org. SSjtP2jCtXPukps7E3kum709xq2TH6Lt Ur32UhE7WKwSUfLTZ4EAoD5g22mi1fpB GDGb30kCMndDVjnHAEBDWw==")) - keys = {dns.name.Name([b'getmonero', b'org', b'']): rrset} - origin = None - now = 1527185178.7842247 - - # 'None' means it is valid - self.assertEqual(None, dnssec.python_validate_rrsig(rrset, rrsig, keys, origin, now)) - - def test_python_validate_rrsig_rsa(self): - rrset = dns.rrset.from_text("getmonero.org.", 12698, 1, 43, - "2371 13 2 3b7f818a879ecb9931dae983d4529afedeb53993759d8080735083f954d40bc8") - rrsig = dns.rdtypes.ANY.RRSIG.RRSIG.from_text(1, 46, - dns.tokenizer.Tokenizer("DS 7 2 86400 20180609010045 20180519000045 1862 org. SgdGsY4BAm7c3qpwzVLy3ua4orvrsJQO 0rUQDDrrXR6lElnbF+AS0gEEfdZfDv11 65AuNil/+kT2Qh/ExgstvhWQ88XdDnHB ouvRMf9pg3p/q5Otet/StRzf33SMPgC1 zLzkfkSBCjJkwVmwde8saGnjdcW522ra Ge/6JcsryRw=")) - - rrset2 = dns.rrset.from_text("org.", 866, 1, 48, - "256 3 7 AwEAAXxsMmN/JgpEE9Y4uFNRJm7Q9GBw mEYUCsCxuKlgBU9WrQEFRrvAeMamUBeX 4SE8s3V/TEk/TgGmPPp0pMkKD7mseluK 6Ard2HZ6O3nPAzL4i8py/UDRUmYNSCxw fdfjUWRmcB9H+NKWMsJoDhAkLFqg5HS7 f0j4Vb99Wac24Fk7", - "256 3 7 AwEAAcLdAPt3vn/ND00zZlyTx7OBko+9 YeCrSl2eGuEXjef0Lqf0tKGikoHwnmTH tT8J/aGqkZImLMVByJbknE0wKDnbvbKD oTQxPwUQZLH6k3sTdsPKESKDSBSc6VFM q35gx6CeuRYZ9KkGWiUsKqJhXPo6tyJF CBxfaNQQyrzBnv4/", - "257 3 7 AwEAAZTjbIO5kIpxWUtyXc8avsKyHIIZ +LjC2Dv8naO+Tz6X2fqzDC1bdq7HlZwt kaqTkMVVJ+8gE9FIreGJ4c8G1GdbjQgb P1OyYIG7OHTc4hv5T2NlyWr6k6QFz98Q 4zwFIGTFVvwBhmrMDYsOTtXakK6QwHov A1+83BsUACxlidpwB0hQacbD6x+I2RCD zYuTzj64Jv0/9XsX6AYV3ebcgn4hL1jI R2eJYyXlrAoWxdzxcW//5yeL5RVWuhRx ejmnSVnCuxkfS4AQ485KH2tpdbWcCopL JZs6tw8q3jWcpTGzdh/v3xdYfNpQNcPI mFlxAun3BtORPA2r8ti6MNoJEHU=", - "257 3 7 AwEAAcMnWBKLuvG/LwnPVykcmpvnntwx fshHlHRhlY0F3oz8AMcuF8gw9McCw+Bo C2YxWaiTpNPuxjSNhUlBtcJmcdkz3/r7 PIn0oDf14ept1Y9pdPh8SbIBIWx50ZPf VRlj8oQXv2Y6yKiQik7bi3MT37zMRU2k w2oy3cgrsGAzGN4s/C6SFYon5N1Q2O4h GDbeOq538kATOy0GFELjuauV9guX/431 msYu4Rgb5lLuQ3Mx5FSIxXpI/RaAn2mh M4nEZ/5IeRPKZVGydcuLBS8GZlxW4qbb 8MgRZ8bwMg0pqWRHmhirGmJIt3UuzvN1 pSFBfX7ysI9PPhSnwXCNDXk0kk0=") - keys = {dns.name.Name([b'org', b'']): rrset2} - origin = None - now = 1527191953.6527798 - - # 'None' means it is valid - self.assertEqual(None, dnssec.python_validate_rrsig(rrset, rrsig, keys, origin, now)) diff --git a/lib/tests/test_interface.py b/lib/tests/test_interface.py @@ -1,28 +0,0 @@ -import unittest - -from lib import interface - -from . import SequentialTestCase - - -class TestInterface(SequentialTestCase): - - def test_match_host_name(self): - self.assertTrue(interface._match_hostname('asd.fgh.com', 'asd.fgh.com')) - self.assertFalse(interface._match_hostname('asd.fgh.com', 'asd.zxc.com')) - self.assertTrue(interface._match_hostname('asd.fgh.com', '*.fgh.com')) - self.assertFalse(interface._match_hostname('asd.fgh.com', '*fgh.com')) - self.assertFalse(interface._match_hostname('asd.fgh.com', '*.zxc.com')) - - def test_check_host_name(self): - i = interface.TcpConnection(server=':1:', queue=None, config_path=None) - - self.assertFalse(i.check_host_name(None, None)) - self.assertFalse(i.check_host_name( - peercert={'subjectAltName': []}, name='')) - self.assertTrue(i.check_host_name( - peercert={'subjectAltName': [('DNS', 'foo.bar.com')]}, - name='foo.bar.com')) - self.assertTrue(i.check_host_name( - peercert={'subject': [('commonName', 'foo.bar.com')]}, - name='foo.bar.com')) diff --git a/lib/tests/test_mnemonic.py b/lib/tests/test_mnemonic.py @@ -1,42 +0,0 @@ -import unittest -from lib import keystore -from lib import mnemonic -from lib import old_mnemonic -from lib.util import bh2u - -from . import SequentialTestCase - - -class Test_NewMnemonic(SequentialTestCase): - - def test_to_seed(self): - seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic='foobar', passphrase='none') - self.assertEqual(bh2u(seed), - '741b72fd15effece6bfe5a26a52184f66811bd2be363190e07a42cca442b1a5b' - 'b22b3ad0eb338197287e6d314866c7fba863ac65d3f156087a5052ebc7157fce') - - def test_random_seeds(self): - iters = 10 - m = mnemonic.Mnemonic(lang='en') - for _ in range(iters): - seed = m.make_seed() - i = m.mnemonic_decode(seed) - self.assertEqual(m.mnemonic_encode(i), seed) - - -class Test_OldMnemonic(SequentialTestCase): - - def test(self): - seed = '8edad31a95e7d59f8837667510d75a4d' - result = old_mnemonic.mn_encode(seed) - words = 'hardly point goal hallway patience key stone difference ready caught listen fact' - self.assertEqual(result, words.split()) - self.assertEqual(old_mnemonic.mn_decode(result), seed) - -class Test_BIP39Checksum(SequentialTestCase): - - def test(self): - mnemonic = u'gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog' - is_checksum_valid, is_wordlist_valid = keystore.bip39_is_checksum_valid(mnemonic) - self.assertTrue(is_wordlist_valid) - self.assertTrue(is_checksum_valid) diff --git a/lib/tests/test_simple_config.py b/lib/tests/test_simple_config.py @@ -1,149 +0,0 @@ -import ast -import sys -import os -import unittest -import tempfile -import shutil - -from io import StringIO -from lib.simple_config import (SimpleConfig, read_user_config) - -from . import SequentialTestCase - - -class Test_SimpleConfig(SequentialTestCase): - - def setUp(self): - super(Test_SimpleConfig, self).setUp() - # make sure "read_user_config" and "user_dir" return a temporary directory. - self.electrum_dir = tempfile.mkdtemp() - # Do the same for the user dir to avoid overwriting the real configuration - # for development machines with electrum installed :) - self.user_dir = tempfile.mkdtemp() - - self.options = {"electrum_path": self.electrum_dir} - self._saved_stdout = sys.stdout - self._stdout_buffer = StringIO() - sys.stdout = self._stdout_buffer - - def tearDown(self): - super(Test_SimpleConfig, self).tearDown() - # Remove the temporary directory after each test (to make sure we don't - # pollute /tmp for nothing. - shutil.rmtree(self.electrum_dir) - shutil.rmtree(self.user_dir) - - # Restore the "real" stdout - sys.stdout = self._saved_stdout - - def test_simple_config_key_rename(self): - """auto_cycle was renamed auto_connect""" - fake_read_user = lambda _: {"auto_cycle": True} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options=self.options, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - self.assertEqual(config.get("auto_connect"), True) - self.assertEqual(config.get("auto_cycle"), None) - fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True} - config = SimpleConfig(options=self.options, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - self.assertEqual(config.get("auto_connect"), False) - self.assertEqual(config.get("auto_cycle"), None) - - def test_simple_config_command_line_overrides_everything(self): - """Options passed by command line override all other configuration - sources""" - fake_read_user = lambda _: {"electrum_path": "b"} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options=self.options, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - self.assertEqual(self.options.get("electrum_path"), - config.get("electrum_path")) - - def test_simple_config_user_config_is_used_if_others_arent_specified(self): - """If no system-wide configuration and no command-line options are - specified, the user configuration is used instead.""" - fake_read_user = lambda _: {"electrum_path": self.electrum_dir} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options={}, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - self.assertEqual(self.options.get("electrum_path"), - config.get("electrum_path")) - - def test_cannot_set_options_passed_by_command_line(self): - fake_read_user = lambda _: {"electrum_path": "b"} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options=self.options, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - config.set_key("electrum_path", "c") - self.assertEqual(self.options.get("electrum_path"), - config.get("electrum_path")) - - def test_can_set_options_set_in_user_config(self): - another_path = tempfile.mkdtemp() - fake_read_user = lambda _: {"electrum_path": self.electrum_dir} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options={}, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - config.set_key("electrum_path", another_path) - self.assertEqual(another_path, config.get("electrum_path")) - - def test_user_config_is_not_written_with_read_only_config(self): - """The user config does not contain command-line options when saved.""" - fake_read_user = lambda _: {"something": "a"} - read_user_dir = lambda : self.user_dir - self.options.update({"something": "c"}) - config = SimpleConfig(options=self.options, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - config.save_user_config() - contents = None - with open(os.path.join(self.electrum_dir, "config"), "r") as f: - contents = f.read() - result = ast.literal_eval(contents) - result.pop('config_version', None) - self.assertEqual({"something": "a"}, result) - - -class TestUserConfig(SequentialTestCase): - - def setUp(self): - super(TestUserConfig, self).setUp() - self._saved_stdout = sys.stdout - self._stdout_buffer = StringIO() - sys.stdout = self._stdout_buffer - - self.user_dir = tempfile.mkdtemp() - - def tearDown(self): - super(TestUserConfig, self).tearDown() - shutil.rmtree(self.user_dir) - sys.stdout = self._saved_stdout - - def test_no_path_means_no_result(self): - result = read_user_config(None) - self.assertEqual({}, result) - - def test_path_without_config_file(self): - """We pass a path but if does not contain a "config" file.""" - result = read_user_config(self.user_dir) - self.assertEqual({}, result) - - def test_path_with_reprd_object(self): - - class something(object): - pass - - thefile = os.path.join(self.user_dir, "config") - payload = something() - with open(thefile, "w") as f: - f.write(repr(payload)) - - result = read_user_config(self.user_dir) - self.assertEqual({}, result) diff --git a/lib/tests/test_storage_upgrade.py b/lib/tests/test_storage_upgrade.py @@ -1,301 +0,0 @@ -import shutil -import tempfile - -from lib.storage import WalletStorage -from lib.wallet import Wallet - -from lib.tests.test_wallet import WalletTestCase - -from . import SequentialTestCase - - -# TODO add other wallet types: 2fa, xpub-only -# TODO hw wallet with client version 2.6.x (single-, and multiacc) -class TestStorageUpgrade(WalletTestCase): - - def test_upgrade_from_client_1_9_8_seeded(self): - wallet_str = "{'addr_history':{'177hEYTccmuYH8u68pYfaLteTxwJrVgvJj':[],'15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc':[],'1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf':[],'1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs':[],'1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC':[],'1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm':[],'1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj':[],'1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa':[]},'accounts_expanded':{},'master_public_key':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb','use_encryption':False,'seed':'2605aafe50a45bdf2eb155302437e678','accounts':{0:{0:['1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC','1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj','177hEYTccmuYH8u68pYfaLteTxwJrVgvJj','1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm','15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc'],1:['1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs','1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa','1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf']}},'seed_version':4}" - self._upgrade_storage(wallet_str) - - # TODO pre-2.0 mixed wallets are not split currently - #def test_upgrade_from_client_1_9_8_mixed(self): - # wallet_str = "{'addr_history':{'15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc':[],'177hEYTccmuYH8u68pYfaLteTxwJrVgvJj':[],'1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC':[],'1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm':[],'1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj':[],'1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf':[],'1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs':[],'1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa':[]},'accounts_expanded':{},'master_public_key':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb','use_encryption':False,'seed':'2605aafe50a45bdf2eb155302437e678','accounts':{0:{0:['1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC','1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj','177hEYTccmuYH8u68pYfaLteTxwJrVgvJj','1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm','15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc'],1:['1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs','1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa','1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf'],'mpk':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb'}},'imported_keys':{'15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA':'5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq','1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6':'L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U','1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr':'L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM'},'seed_version':4}" - # self._upgrade_storage(wallet_str, accounts=2) - - def test_upgrade_from_client_2_0_4_seeded(self): - wallet_str = '{"accounts":{"0":{"change":["03d8e267e8de7769b52a8727585b3c44b4e148b86b2c90e3393f78a75bd6aab83f","03f09b3562bec870b4eb8626c20d449ee85ef17ea896a6a82b454e092eef91b296","02df953880df9284715e8199254edcf3708c635adc92a90dbf97fbd64d1eb88a36"],"receiving":["02cd4d73d5e335dafbf5c9338f88ceea3d7511ab0f9b8910745ac940ff40913a30","0243ed44278a178101e0fb14d36b68e6e13d00fe3434edb56e4504ea6f5db2e467","0367c0aa3681ec3635078f79f8c78aa339f19e38d9e1c9e2853e30e66ade02cac3","0237d0fe142cff9d254a3bdd3254f0d5f72676b0099ba799764a993a0d0ba80111","020a899fd417527b3929c8f625c93b45392244bab69ff91b582ed131977d5cd91e","039e84264920c716909b88700ef380336612f48237b70179d0b523784de28101f7","03125452df109a51be51fe21e71c3a4b0bba900c9c0b8d29b4ee2927b51f570848","0291fa554217090bab96eeff63e1c6fdec37358ed597d18fa32c60c02a48878c8c","030b6354a4365bab55e86269fb76241fd69716f02090ead389e1fce13d474aa569","023dcba431d8887ab63595f0df1e978e4a5f1c3aac6670e43d03956448a229f740","0332a61cbe04fe027033369ce7569b860c24462878bdd8c0332c22a3f5fdcc1790","021249480422d93dba2aafcd4575e6f630c4e3a2a832dd8a15f884e1052b6836e4","02516e91dede15d3a15dd648591bb92e107b3a53d5bc34b286ab389ce1af3130aa","02e1da3dddd81fa6e4895816da9d4b8ab076d6ea8034b1175169c0f247f002f4cf","0390ef1e3fdbe137767f8b5abad0088b105eee8c39e075305545d405be3154757a","03fca30eb33c6e1ffa071d204ccae3060680856ae9b93f31f13dd11455e67ee85d","034f6efdbbe1bfa06b32db97f16ff3a0dd6cf92769e8d9795c465ff76d2fbcb794","021e2901009954f23d2bf3429d4a531c8ca3f68e9598687ef816f20da08ff53848","02d3ccf598939ff7919ee23d828d229f85e3e58842582bf054491c59c8b974aa6e","03a1daffa39f42c1aaae24b859773a170905c6ee8a6dab8c1bfbfc93f09b88f4db"],"xpub":"xpub661MyMwAqRbcFsrzES8RWNiD7RxDqT4p8NjvTY9mLi8xdphQ9x1TiY8GnqCpQx4LqJBdcGeXrsAa2b2G7ZcjJcest9wHcqYfTqXmQja6vfV"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K3PnX8QbR9EmUZQ7jRzLxm9pKf9k9nNbym2NFcQhDAjonwZ39jtWLYp6qk5UHotj13p2y7w1ZhhvvyV5eCcaPUrKofs9CXQ9"},"master_public_keys":{"x/":"xpub661MyMwAqRbcFsrzES8RWNiD7RxDqT4p8NjvTY9mLi8xdphQ9x1TiY8GnqCpQx4LqJBdcGeXrsAa2b2G7ZcjJcest9wHcqYfTqXmQja6vfV"},"seed":"seven direct thunder glare prevent please fatal blush buzz artefact gate vendor above","seed_version":11,"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_0_4_importedkeys(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_0_4_watchaddresses(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_0_4_trezor_singleacc(self): - wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_0_4_trezor_multiacc(self): - wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]]},"labels":{"0":"Main account","1":"acc1"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) - - def test_upgrade_from_client_2_0_4_multisig(self): - wallet_str = '{"accounts":{"0":{"change":[["03c3a8549f35d7842192e7e00afa25ef1c779d05f1c891ba7c30de968fb29e3e78","02e191e105bccf1b4562d216684632b9ec22c87e1457b537eb27516afa75c56831"],["03793397f02b3bd3d0f6f0dafc7d42b9701234a269805d89efbbc2181683368e4b","02153705b8e4df41dc9d58bc0360c79a9209b3fc289ec54118f0b149d5a3b3546d"],["02511e8cfb39c8ce1c790f26bcab68ba5d5f79845ec1c6a92b0ac9f331648d866a","02c29c1ea70e23d866204a11ec8d8ecd70d6f51f58dd8722824cacb1985d4d1870"]],"receiving":[["0283ce4f0f12811e1b27438a3edb784aeb600ca7f4769c9c49c3e704e216421d3e","03a1bbada7401cade3b25a23e354186c772e2ba4ac0d9c0447627f7d540eb9891d"],["0286b45a0bcaa215716cbc59a22b6b1910f9ebad5884f26f55c2bb38943ee8fdb6","02799680336c6bd19005588fad12256223cb8a416649d60ea5d164860c0872b931"],["039e2bf377709e41bba49fb1f3f873b9b87d50ae3b574604cd9b96402211ea1f36","02ef9ceaaf754ba46f015e1d704f1a06157cc4441da0cfaf096563b22ec225ca5f"],["025220baaca5bff1a5ffbf4d36e9fcc6f5d05f4af750ef29f6d88d9b5f95fef79a","02350c81bebfa3a894df69302a6601175731d443948a12d8ec7860981988e3803e"],["028fd6411534d722b625482659de54dd609f5b5c935ae8885ca24bfd3266210527","03b9c7780575f17e64f9dfd5947945b1dbdb65aecef562ac076335fd7aa09844e4"],["0353066065985ec06dbef33e7a081d9240023891a51c4e9eda7b3eb1b4af165e04","028c3fa7622e4c8bac07a2c549885a045532e67a934ca10e20729d0fdfe3a75339"],["02253b4eabf2834af86b409d5ca8e671de9a75c3937bff2dac9521c377ca195668","02d5e83c445684eb502049f48e621e1ca16e07e5dc4013c84d661379635f58877b"],["030d38e4c7a5c7c9551adcace3b70dcaa02bf841febd6dc308f3abd7b7bf2bdc49","0375a0b50cd7f3af51550207a766c5db326b2294f5a4b456a90190e4fbeb720d97"],["0327280215ba4a0d8c404085c4f6091906a9e1ada7ce4202a640ac701446095954","037cd9b5e6664d28a61e01626056cdb7e008815b365c8b65fa50ac44d6c1ad126e"],["02f80a80146674da828fc67a062d1ab47fb0714cf40ec5c517ee23ea71d3033474","03fd8ab9bc9458b87e0b7b2a46ea6b46de0a5f6ecaf1a204579698bfa881ff93ce"],["034965bd56c6ca97e0e5ffa79cdc1f15772fa625b76da84cc8adb1707e2e101775","033e13cb19d930025bfc801b829e64d12934f9f19df718f4ea6160a4fb61320a9c"],["034de271009a06d733de22601c3d3c6fe8b3ec5a44f49094ac002dc1c90a3b096d","023f0b2f653c0fdbdc292040fee363ceaa5828cfd8e012abcf6cd9bad2eaa3dc72"],["022aec8931c5b17bdcdd6637db34718db6f267cb0a55a611eb6602e15deb6ed4df","021de5d4bbb73b6dfab2c0df6970862b08130902ff3160f31681f34aecf39721f6"],["02a0e3b52293ec73f89174ff6e5082fcfebc45f2fdd9cfe12a6981aa120a7c1fa7","0371d41b5f18e8e1990043c1e52f998937bc7e81b8ace4ddfc5cd0d029e4c81894"],["030bc1cbe4d750067254510148e3af9bc84925cdd17db3b54d9bbf4a409b83719a","0371c4800364a8a32bfbda7ea7724c1f5bdbd794df8a6080a3bd3b52c52cf32402"],["0318c5cd5f19ff037e3dec3ce5ac1a48026f5a58c4129271b12ae22f8542bcd718","03b5c70db71d520d04f810742e7a5f42d810e94ec6cbf4b48fa6dd7b4d425e76c1"],["0213f68b86a8c4a0840fa88d9a06904c59292ec50172813b8cca62768f3b708811","0353037209eb400ba7fcfa9f296a8b2745e1bbcbfb28c4adebf74de2e0e6a58c00"],["028decff8a7f5a7982402d95b050fbc9958e449f154990bbfe0f553a1d4882fd03","025ecd14812876e885d8f54cab30d1c2a8ae6c6ed0847e96abd65a3700148d94e2"],["0267f8dab8fdc1df4231414f31cfeb58ce96f3471ba78328cd429263d151c81fed","03e0d01df1fd9e958a7324d29afefbc76793a40447a2625c494355c577727d69ba"],["03de3c4d173b27cdfdd8e56fbf3cd6ee8729b94209c20e5558ddd7a76281a37e2e","0218ccb595d7fa559f0bae1ea76d19526980b027fb9be009b6b486d8f8eb0e00d5"]],"xpub":"xpub661MyMwAqRbcFUEYv1psxyPnjiHhTYe85AwFRs5jShbpgrfQ9UXBmxantqgGT3oAVLiHDYoR3ruT3xRGcxsmBMJxyg94FGcxF86QnzYDc6e","xpub2":"xpub661MyMwAqRbcGFd5DccFn4YW2HEdPhVZ2NEBAn416bvDFBi8HN5udmB6DkWpuXFtXaXZdq9UvMoiHxaauk6R1CZgKUR8vpng4LoudP4YVXA"}},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2zA5ozHsbqT4BgTD45vGhx1edUg7tN4qp4LFbwCwEAGK3ZVaBaCRQnuy7AJ7qbPGxKiynNtGd7CzjBXEV4mEwStnPo98Xve"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFUEYv1psxyPnjiHhTYe85AwFRs5jShbpgrfQ9UXBmxantqgGT3oAVLiHDYoR3ruT3xRGcxsmBMJxyg94FGcxF86QnzYDc6e","x2/":"xpub661MyMwAqRbcGFd5DccFn4YW2HEdPhVZ2NEBAn416bvDFBi8HN5udmB6DkWpuXFtXaXZdq9UvMoiHxaauk6R1CZgKUR8vpng4LoudP4YVXA"},"seed":"start accuse bounce inhale crucial infant october radar enforce stage dumb spot account","seed_version":11,"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_1_1_seeded(self): - wallet_str = '{"accounts":{"0":{"change":["03cbd39265f007d39045ccab5833e1ae16c357f9d35e67099d8e41940bf63ec330","03c94e9590d9bcd579caae15d062053e2820fe2a405c153dd4dca4618b7172ea6f","028a875b6f7e56f8cba66a1cec5dc1dfca9df79b7c92702d0a551c6c1b49d0f59b"],"receiving":["02fa100994f912df3e9538c244856828531f84e707f4d9eccfdd312c2e3ef7cf10","02fe230740aa27ace4f4b2e8b330cd57792051acf03652ae1622704d7eb7d4e5e4","03e3f65a991f417d69a732e040090c8c2f18baf09c3a9dc8aa465949aeb0b3271f","0382aa34a9cb568b14ebae35e69b3be6462d9ed8f30d48e0a6983e5af74fa441d3","03dfd8638e751e48fd42bf020874f49fbb5f54e96eff67d72eeeda3aa2f84f01c6","033904139de555bdf978e45931702c27837312ed726736eeff340ca6e0a439d232","03c6ca845d5bd9055f8889edcd53506cf714ac1042d9e059db630ec7e1af34133d","030b3bafc8a4ff8822951d4983f65b9bc43552c8181937188ba8c26e4c1d1be3ab","03828c371d3984ca5a248997a3e096ce21f9aeeb2f2a16457784b92a55e2aef288","033f42b4fbc434a587f6c6a0d10ac401f831a77c9e68453502a50fe278b6d9265c","0384e2c23268e2eb88c674c860519217af42fd6816273b299f0a6c39ddcc05bfa2","0257c60adde9edca8c14b6dd804004abc66bac17cc2acbb0490fcab8793289b921","02e2a67b1618a3a449f45296ea72a8fa9d8be6c58759d11d038c2fe034981efa73","02a9ef53a502b3a38c2849b130e2b20de9e89b023274463ea1a706ed92719724eb","037fc8802a11ba7ef06682908c24bcaedca1e2240111a1dd229bf713e2aa1d65a1","03ea0685fbd134545869234d1f219fff951bc3ec9e3e7e41d8b90283cd3f445470","0296bbe06cdee522b6ee654cc3592fce1795e9ff4dc0e2e2dea8acaf6d2d6b953b","036beac563bc85f9bc479a15d1937ea8e2c20637825a134c01d257d43addab217a","03389a4a6139de61a2e0e966b07d7b25b0c5f3721bf6fdcad20e7ae11974425bd9","026cffa2321319433518d75520c3a852542e0fa8b95e2cf4af92932a7c48ee9dbd"],"xpub":"xpub661MyMwAqRbcGDxKhL5YS1kaB5B7q8H6xPZwCrgZ1iE2XXaiUeqD9MFEYRAuX7UNfdAED9yhAZdCB4ZS8dFrGDVU3x9ZK8uej8u8Pa2DLMq"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K3jsrbJYY4soqd3LdRfZFbAeLQUGwTNh3ejFZw7WxbYvkhAmPM88Swt1JwFX6DVGjPXeUcGcqa1XFuJPeiQaC9wiZ16PTKgQ"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGDxKhL5YS1kaB5B7q8H6xPZwCrgZ1iE2XXaiUeqD9MFEYRAuX7UNfdAED9yhAZdCB4ZS8dFrGDVU3x9ZK8uej8u8Pa2DLMq"},"pruned_txo":{},"seed":"flat toe story egg tide casino leave liquid strike cat busy knife absorb","seed_version":11,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_1_1_importedkeys(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_1_1_watchaddresses(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_1_1_trezor_singleacc(self): - wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_1_1_trezor_multiacc(self): - wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) - - def test_upgrade_from_client_2_1_1_multisig(self): - wallet_str = '{"accounts":{"0":{"change":[["03b5ca15f87baa1bb9d2508a9cf7cb596915a2749a6932bd71a5f353d72e2ff51e","03069d12bb7dc9fe7b8dab9ab2c7828173a4a4a5bacb10b9004854aef2ada2e440"],["036d7aeef82d50520f7d30d20a6b58a5e61c40949af4c147a105a8724478ba6339","021208a4a6c76934fbc2eed72a4a71713a5a093fb203ec3197edd1e4be8d9fb342"],["03ee5bd2bc7f9800b85f6f0a3fe8c23c797fa90d832f0332dfc72532e298dce54e","03474b76f33036673e1df73800b06d2df4b3617768c2b6a4f8a7f7d17c2b08cec3"]],"receiving":[["0288d4cc7e83b7028b8d2197c4efb490cb3dd248ee8683c715d9c59eb1884b2696","02c8ffee4ef168237f4a303dfe4957e328a8163c827cbe8ad07dcc24304b343869"],["022770e608e45981a31bad39a747a827ff4ce1eb28348fbe29ab776bdbf39346b4","03ebd247971aced7e2f49c495658ac5c32f764ebc4df5d033505e665f8d3f87b56"],["0256ede358326a99878d9de6c2c6a156548c266195fecea7906ddbb170da740f8d","02a500e7438d672c374713a9179fef03cbf075dd4c854566d6d9f4d899c01a4cf4"],["03fe2f59f10f6703bd3a43d0ae665ab72fb8b73b14f3a389b92e735e825fffdbe9","0255dd91624ba62481e432b9575729757b046501b8310b1dee915df6c4472f7979"],["0262c7c02f83196f6e3b9dd29e1bcad4834891b69ece12f628eea4379af6e701f8","0319ce2894fdf42bc87d45167a64b24ee2acdb5d45b6e4aadce4154a1479c8c58a"],["03bfb9ca9edab6650a908ffdcc0514f784aaccac466ba26c15340bc89a158d0b4c","03bcce80eed7b494f793b38b55cc25ae62e462ec7bf4d8ff6e4d583e8d04a4ac6d"],["0301dc9a41a44189e40c786048a0b6c13cc8865f3674fdf8e6cb2ab041eb71c0c7","020ded564880e7298068cf1498efcfb0f2306c6003e3de09f89030477ff7d02e18"],["03baffd970ecba170c31f48a95694a1063d14c834ccf2fdce0df46c3b81ab8edfb","0243ec650fc7c6642f7fb3b98e1df62f8b28b2e8722e79ccb271badba3545e8fc2"],["024be204a4bd321a727fb4a427189ae2f761f2a2c9898e9c37072e8a01026736d4","0239dc233c3e9e7c32287fdd7932c248650a36d8ab033875d272281297fadf292a"],["02197190b214c0215511d17e54e3e82cbe09f08e5ba2fb47aeafe01d8a88a8cb25","034a13cf01e26e9aa574f9ba37e75f6df260958154b0f6425e0242eacd5a3979c5"],["0226660fce4351019be974959b6b7dcf18d5aa280c6315af362ab60374b5283746","0304e49d2337a529ed8a647eceb555cd82e7e2546073568e30254530a61c174100"],["0324bb7d892dbe30930eb8de4b021f6d5d7e7da0c4ac9e3b95e1a2c684258d5d6c","02487aa272f0d3a86358064e080daf209ee501654e083f0917ad2aff3bbeb43424"],["03678b52056416da4baa8d51dca8eea534e38bd1d9328c8d01d5774c7107a0f9c1","0331deff043d709fc8171e08625a9adffba1bb614417b589a206c3a80eff86eddd"],["023a94d91c08c8c574199bc16e12789630c97cb990aeb5a54d938ff3c86786aabf","02d139837e34858f733e7e1b7d61b51d2730c57c274ed644ab80aff6e9e2fdef73"],["032f92dc11020035cd16995cfdc4bc6bef92bc4a06eb70c43474e6f7a782c9c0e1","0307d2c32713f010a0d0186e47670c6e46d7a7e623026f9ed99eb27cdae2ae4b49"],["02f66a91a024628d6f6969af2ed9ded087a88e9be86e4b3e5830868643244ec1ae","02f2a83ebb1fbbd04e59a93284e35320c74347176c0592512411a15efa7bf5fa44"],["03585bae6f04f2d3f927d79321b819cccf2bcd1d28d616aac9407c6c13d590dfbd","021f48f02b485b9b3223fca4fbc4dd823a8151053b8640b3766c37dfa99ba78006"],["02b28e2d6f1ac3fde4b34c938e83c0ef0d85fd540d8c33b33a109f4ebbc4a36a4d","030a25a960e28e751a95d3c0167fad496f9ec4bc307637c69b3bd6682930532736"],["03782c0dee8d279c547d26853e31d90bc7d098e16015c2cc334f2cc2a2964f2118","021fe4d6392dba40f1aa35fa9ec3ebfde710423f036482f6a5b3c47d0e149dfe47"],["0379b464b4f9cced0c71ee66c4fca1e61190bac9a6294242aabd4108f6a986a029","030a5802c5997ebae590147cb5eeba1690455c5d2a87306345586e808167072b50"]],"xpub":"xpub661MyMwAqRbcErzzVC45mcZaZM7gpxh4iwfsQVuyTma3qpWuRi9ZRdL8ACqu25LP2jssmKmpEbnGohH9XnoZ1etW3TKaiy5dWnUuiN6LvD9","xpub2":"xpub661MyMwAqRbcH4DqLo2tRYzSnnqjXk21aqNz3oAuYkr66YxucWgc2X8oLdS2KLPSKqrfZwStQYEpUp5jVxQfTBmEwtw3JaPRf6mq6JLD3Qr"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2NvXPAX5QUcr1KHCRVyDMikGc7WMuS34y2BktAqJsq1eJvk7JWroKM8PdGa2FHWiTpAvH9nj6BkQos5XhJU5mfS12tdtBYy"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcErzzVC45mcZaZM7gpxh4iwfsQVuyTma3qpWuRi9ZRdL8ACqu25LP2jssmKmpEbnGohH9XnoZ1etW3TKaiy5dWnUuiN6LvD9","x2/":"xpub661MyMwAqRbcH4DqLo2tRYzSnnqjXk21aqNz3oAuYkr66YxucWgc2X8oLdS2KLPSKqrfZwStQYEpUp5jVxQfTBmEwtw3JaPRf6mq6JLD3Qr"},"pruned_txo":{},"seed":"snack oxygen clock very envelope staff table bus sense fiscal cereal pilot abuse","seed_version":11,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_2_0_seeded(self): - wallet_str = '{"accounts":{"0":{"change":["038f4bae4a901fe5f2a30a06a09681fff6678e8efda4e881f71dcdc0fdb36dd1b8","032c628bec66fe98c3921b4fea6f18d241e6b23f4baf9e56c78b7a5262cd4cc412","0232b68a11cde50a49fb3155fe2c9e9cf7aa9f4bcb0f51c3963b13c997e40de40d"],"receiving":["0237246e68c6916c43c7c5aca1031df0c442439b80ceda07eaf72645a0597ed6aa","03f35bee973012909d839c9999137b7f2f3296c02791764da3f55561425bb1d53c","02fdbe9f95e2279045e6ef5f04172c6fe9476ba09d70aa0a8483347bfc10dee65e","026bc52dc91445594bb639c7a996d682ac74a4564381874b9d36cc5feea103d7a4","0319182796c6377447234eeee9fe62ce6b25b83a9c46965d9a02c579a23f9fa57a","02e23d202a45515ce509c8b9548a251de3ad8e64c92b24bb74b354c8d4d0dc85af","0307d7ccb51aa6860606bcbe008acc1aae5b53d19d0752a20a327b6ec164399b52","038a2362fde711e1a4b9c5f8fe1090a0a38aec3643c0c3d69b00660b213dc4bfb8","0396255ef7b75e5d8ffc18d01b9012a98141ee5458a68cde8b25c492c569a22ab8","02c7edf03d215b7d3478fb26e9375d541440f4a8b5c562c0eb98fab6215dbea731","024286902b95da3daf6ffb571d5465537dae5b4e00139e6465e440d6a26892158e","03aa0d3fa1fe190a24e14d6aabd9c163c7fe70707b00f7e0f9fa6b4d3a4e441149","03995d433093a2ae9dc305fe8664f6ab9143b2f7eaf6f31bc5fefdacb183699808","033c5da7c4c7a3479ddb569fecbcbb8725867370746c04ff5d2a84d1706607bbab","036a097331c285c83c4dab7d454170b60a94d8d9daa152b0af6af81dbd7f0cc440","033ed002ddf99c1e21cb8468d0f5512d71466ac5ba4003b33d71a181e3a696e3c5","02a6a0f30d1a341063a57a0549a3d16d9487b1d4e0d4bffadabdc62d1ad1a43f8f","02dcae71fc2e31013cf12ad78f9e16672eeb7c75e536f4f7d36adb54f9682884eb","028ef32bc57b95697dacdb29b724e3d0fa860ffdc33c295962b680d31b23232090","0314afd1ac2a4bf324d6e73f466a60f511d59088843f93c895507e7af1ccdb5a3b"],"xpub":"xpub661MyMwAqRbcEuc5dCRqgPpGX2bKStk4g2cbZ96SSmKsQmLUrhaQEtrfnBMsXSUSyKWuCtjLiZ8zXrxcNeC2LR8gnZPrQJdmUEeofS2yuux"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2RXcXAtqKFsXxzkq3S2DJogzkkgptRntXy1LKAG9h6YBvw8JjSUogF1UNneyYgS5uYshMBemqr41XsC7bTr8Fjx1uAyLbPC"},"master_public_keys":{"x/":"xpub661MyMwAqRbcEuc5dCRqgPpGX2bKStk4g2cbZ96SSmKsQmLUrhaQEtrfnBMsXSUSyKWuCtjLiZ8zXrxcNeC2LR8gnZPrQJdmUEeofS2yuux"},"pruned_txo":{},"seed":"agree tongue gas total hollow clip wasp slender dolphin rebel ozone omit achieve","seed_version":11,"stored_height":0,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_2_0_importedkeys(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":489714,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_2_0_watchaddresses(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_2_0_trezor_singleacc(self): - wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_2_0_trezor_multiacc(self): - wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490006,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) - - def test_upgrade_from_client_2_2_0_multisig(self): - wallet_str = '{"accounts":{"0":{"change":[["037ba2d9d7446d54f1b46c902427e58a4b63915745de40f31db52e95e2eb8c559c","03aab9d4cb98fec92e1a9fc93b93f439b30cdb47cb3fae113779d0d26e85ceca7b"],["036c6cb5ed99f4d3c8d2dd594c0a791e266a443d57a51c3c7320e0e90cf040dad0","03f777561f36c795911e1e42b3b4babe473bcce32463eb9340b48d86fded8a226a"],["03de4acea515b1b3b6a2b574d08539ced475f86fdf00b43bff16ec43f6f8efc8b7","036ebfdd8ba75c94e0cb1819ecba464d04a77bab11c8fc2b7e90dd952092c01f0e"]],"receiving":[["03e768d9de027e4edaf0685abb240dde9af1188f5b5d2aa08773b0083972bdec74","0280eccb8edec0e6de521abba3831f51900e9d0655c59cddf054b72a70b520ddae"],["02f9c0b7e8fe426a45540027abca63c27109db47b5c86886b99db63450444bb460","03cb5cdcc26b0aa326bc895fcc38b63416880cdc404efbeab3ff14f849e4f4bd63"],["024d6267b9348a64f057b8e094649de36e45da586ef8ca5ecb7137f6294f6fd9e3","034c14b014eb28abfeaa0676b195bde158ab9b4c3806428e587a8a3c3c0f2d38bb"],["02bc3d5456aa836e9a155296be6a464dfa45eb2164dd0691c53c8a7a05b2cb7c42","03a374129009d7e407a5f185f74100554937c118faf3bbe4fe1cac31547f46effa"],["024808c2d17387cd6d466d13b278f76d4d04a7d31734f0708a8baf20ae8c363f9a","02e18dfc7f5ea9e8b6afe0853a9aba55861208b32f22c81aa4be0e6aee7951963d"],["0331bef7adca60ae484a12cc3c4b788d4296e0b52500731bf5dff1b935973d4768","025774c45aeac2ae87b7a67e79517ffb8264bdf1b56905a76e7e7579f875cbed55"],["020566e7351b4bfe6c0d7bda3af24267245a856af653dd00c482555f305b71a8e3","036545f66ad2fe95eeb0ec1feb501d552773e0910ec6056d6b827bc0bb970a1ecc"],["038dc34e68a49d2205f4934b739e510dca95961d0f8ab6f6cd9279d68048cfd93b","03810c50d1e2ff0e39179788e8506784bc214768884f6f71dc4323f6c29e25c888"],["035059ff052ab044fd807905067ec79b19177edcf1b1b969051dc0e6957b1e1eab","03d790376a0144860017bea5b5f2f0a9f184a55623e9a1e8f3670bf6aba273f4fb"],["02bb730d880b90e421d9ac97313b3c0eec6b12a8c778388d52a188af7dc026db43","030ae3ae865b805c3c11668b46ec4f324d50f6b5fbc2bb3a9ae0ddc4aea0d1487a"],["0306eeb93a37b7dcbb5c20146cfd3036e9a16e5b35ecfe77261a6e257ee0a7b178","03fb49f5f1d843ca6d62cee86fd4f79b6cc861f692e54576a9c937fdff13714be9"],["03f4c358e03bd234055c1873e77f451bea6b54167d36c005abeb704550fbe7bee1","03fc36f11d726fd4321f99177a0fff9b924ec6905d581a16436417d2ea884d3c80"],["024d68322a93f2924d6a0290ebe7481e29215f1c182bd8fdeb514ade8563321c87","02aa5502de7b402e064dfebc28cb09316a0f90eec333104c981f571b8bc69279e2"],["03cbda5b33a72be05b0e50ef7a9872e28d82d5a883e78a73703f53e40a5184f7a5","02ebf10a631436aa0fdef9c61e1f7d645aa149c67d3cb8d94d673eb3a994c36f86"],["0285891a0f1212efff208baf289fd6316f08615bee06c0b9385cc0baad60ebc08a","0356a6c4291f26a5b0c798f3d0b9837d065a50c9af7708f928c540017f150c40b6"],["02403988346d00e9b949a230647edbe5c03ce36b06c4c64da774a13aca0f49ce92","02717944f0bb32067fb0f858f7a7b422984c33d42fd5de9a055d00c33b72731426"],["02161a510f42bcc7cdd24e7541a0bdbcac08b1c63b491df1974c6d5cd977d57750","03006d73c0ab9fdd8867690d9282031995cfd094b5bdc3ff66f3832c5b8a9ca7f9"],["03d80ea710e1af299f1079dd528d6cdc5797faa310bafa90ca7c45ea44d5ba64f3","02b29e1170d6bec16ace70536565f1dff1480cba2a7545cfec7b522568a6ab5c38"],["02c3f6e8dea3cace7aab89d8258751827cb5791424c71fa82ae30192251ca11a28","02a43d2d952e1f3fb58c56dadabb39cf5ed437c566f504a79f2ade243abd2c9139"],["0308e96e38eb89ca5abaa6776a1968a1cbb33197ec91d40bb44bede61cb11a517f","034d0545444e5a5410872a3384cedd3fb198a8211bb391107e8e2c0b0b67932b20"]],"xpub":"xpub661MyMwAqRbcFCKg479EAwb6KLrQNcFSQKNjQRJpRFSiFRnp87cpntXkDUEvRtFTEARirm9584ML8sLBkF3gDBcyYgknnxCCrBMwPDDMQwC","xpub2":"xpub661MyMwAqRbcFaEDoCANCiY9dhXvA8GgXFSLXYADmxmatLidGTxnVL6vuoFAMg9ugX8MTKjZPiP9uUPXusUji11LnWWLCw8Lzgx7pM5sg1s"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2iFCx5cDooeMmK1uy9Xb36T8c2uCruujNdTfaaJaF6DGNDcDKkX1U4V1XiEcvCqoNsQhMQUnp8ZvMgxDBDErtMACo2HtGgQ"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFCKg479EAwb6KLrQNcFSQKNjQRJpRFSiFRnp87cpntXkDUEvRtFTEARirm9584ML8sLBkF3gDBcyYgknnxCCrBMwPDDMQwC","x2/":"xpub661MyMwAqRbcFaEDoCANCiY9dhXvA8GgXFSLXYADmxmatLidGTxnVL6vuoFAMg9ugX8MTKjZPiP9uUPXusUji11LnWWLCw8Lzgx7pM5sg1s"},"pruned_txo":{},"seed":"such duck column calm verb sock used message army suffer humble olive abstract","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_3_2_seeded(self): - wallet_str = '{"accounts":{"0":{"change":["03b37d18c0c52da686e8fd3cc5d242e62036ac2b38f101439227f9e15b46f88c42","026f946e309e64dcb4e62b00a12aee9ee14d26989880e690d8c307f45385958875","03c75552e48d1d44f966fb9cfe483b9479cc882edcf81e2faf92fba27c7bbecbc1","020965e9f1468ebda183fea500856c7e2afcc0ccdc3da9ccafc7548658d35d1fb3","03da778470ee52e0e22b34505a7cc4a154e67de67175e609a6466db4833a4623ed","0243f6bbb6fea8e0da750645b18973bc4bd107c224d136f26c7219aab6359c2705"],"receiving":["0376bf85c1bf8960947fe575adc0a3f3ba08f6172336a1099793efd0483b19e089","03f0fe0412a3710a5a8a1c2e01fe6065b7a902f1ccbf38cd7669806423860ad111","03eacb81482ba01a741b5ee8d52bb6e48647107ef9a638ca9a7b09f6d98964a456","03c8b598f6153a87fc37f693a148a7c1d32df30597404e6a162b3b5198d0f2ba33","03fefef3ee4f918e9cd3e56501018bcededc48090b33c15bf1a4c3155c8059610a","0390562881078a8b0d54d773d6134091e2da43c8a97f4f3088a92ca64d21fcf549","0366a0977bb35903390e6b86bbb6faa818e603954042e98fe954a4b8d81d815311","025d176af6047d959cfdd9842f35d31837034dd4269324dc771c698d28ad9ae3d6","02667adce009891ee872612f31cd23c5e94604567140b81d0eae847f5539c906d6","03de40832017ba85e8131c2af31079ab25a72646d28c8d2b6a39c98c4d1253ae2f","02854c17fdef156b1681f494dfc7a10c6a8033d0c577b287947b72ecada6e6386b","0283ff8f775ba77038f787b9bf667f538f186f861b003833600065b4ad8fd84362","03b0a4e9a6ffecd955bd0e2b169113b544a7cba1688dca6fce204552403dc28391","02445465cf40603506dbe7fa853bc1aae0d79ca90e57b6a7af6ffc1341c4ca8e2d","0220ea678e2541f809da75552c07f9e64863a254029446d6270e433a4434be2bd7","02640e87aab83bd84fe964eac72657b34d5ad924026f8d2222557c56580607808e","020fa9a0c3b335c6cdc6588b14c596dfae242547dd68e5c6bce6a9347152ff4021","03f7f052076dc35483c91033edef2cc93b54fb054fe3b36546800fa1a76b1d321a","030fd12243e1ffe1fc6ec3cdb7e020a467d3146d55d52af915552f2481a91657cd","02dd1a2becbc344a297b104e4bb41f7de4f5fcff1f3244e4bb124fbb6a70b5eb18"],"xpub":"xpub661MyMwAqRbcEnd8FGgkz7V8iJZ2FvDcg669i7NSS7h7nmq5k5WeHohNqosRSjx9CKiRxMgTidPWA5SJYsjrXhr1azR3boubNp24gZHUeY4"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2JYf9F9kcyYQAGiXrTVmJsAYuixpsnA8uyVwCYCPk1NtzYuNmeLRLKcMYb3UoPgTocYsHsAje3mSjX4jp3Ci17VhuESjsBU"},"master_public_keys":{"x/":"xpub661MyMwAqRbcEnd8FGgkz7V8iJZ2FvDcg669i7NSS7h7nmq5k5WeHohNqosRSjx9CKiRxMgTidPWA5SJYsjrXhr1azR3boubNp24gZHUeY4"},"pruned_txo":{},"seed":"scheme grape nephew hen song purity pizza syrup must dentist bright grit accuse","seed_version":11,"stored_height":0,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_3_2_importedkeys(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":489715,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_3_2_watchaddresses(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_3_2_trezor_singleacc(self): - wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_3_2_trezor_multiacc(self): - wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490008,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) - - def test_upgrade_from_client_2_3_2_multisig(self): - wallet_str = '{"accounts":{"0":{"change":[["03083942fe75c1345833faa4d31a635e088ca173047ddd6ef5b7f1395892ef339d","03c02f486ed1f0e6d1aefbdea293c8cb44b34a3c719849c45e52ef397e6540bbda"],["0326d9adb5488c6aba8238e26c6185f4d2f1b072673e33fb6b495d62dc800ff988","023634ebe9d7448af227be5c85e030656b353df81c7cf9d23bc2c7403b9af7509b"],["0223728d8dd019e2bd2156754c2136049a3d2a39bf2cb65965945f4c598fdb6db6","037b6d4df2dde500789f79aa2549e8a6cb421035cda485581f7851175e0c95d00e"],["03c47ade02def712ebbf142028d304971bec99ca53be8e668e9cf15ff0ef186e19","02e212ad25880f2c9be7dfd1966e4b6ae8b3ea40e09d482378b942ca2e716397b0"],["03dab42b0eaee6b0e0d982fbf03364b378f39a1b3a80e980460ae96930a10bff6c","02baf8778e83fbad7148f3860ce059b3d27002c323eab5957693fb8e529f2d757f"],["02fc3019e886b0ce171242ddedb5f8dcde87d80ad9f707edb8e6db66a4389bea49","0241b4e9394698af006814acf09bf301f79d6feb2e1831a7bc3e8097311b1a96dd"]],"receiving":[["023e2bf49bc40aeed95cb1697d8542354df8572a8f93f5abe1bcec917778cc9fc6","03cf4e80c4bf3779e402b85f268ada2384932651cc41e324e51fc69d6af55ae593"],["02d9ba257aa3aba2517bb889d1d5a2e435d10c9352b2330600decab8c8082db242","03de9e91769733f6943483167602dd3d439e34b7078186066af8e90ec58076c2a7"],["02ccdd5b486cefa658af0c49d85aefa3ab62f808335ffcd4b8d4197a3c50ab073c","03e80dbbd0fb93d01d6446d0af1c18c16d26bdbb2538d8bf7f2f68ce95ba857667"],["031605867287fe3b1fee55e07b2f513792374bb5baf30f316970c5bc095651a789","02c0802b96cee67d6acec5266eb3b491c303cea009d57a6bb7aee83cc602206ad5"],["037d07d30dec97da4ea09d568f96f0eb6cd86d02781a7adff16c1647e1bcd23260","03d856a53bc90be84810ce94c8aac0791c9a63379fd61790c11dae926647aa4eec"],["028887f2d54ffefc98e5a605c83bedba79367c2a4fe11b98ec6582896ffad79216","0259dab6dafe52306fe6e3686f27a36e0650c99789bb19cbcd0907db00957030a9"],["039d83064dd37681eaf7babe333b210685ba9fe63627e2f2d525c1fb9c4d84d772","03381011299678d6b72ff82d6a47ed414b9e35fcf97fc391b3ff1607fb0bf18617"],["03ace6ceb95c93a446ae9ff5211385433c9bbf5785d52b4899e80623586f354004","0369de6b20b87219b3a56ea8007c33091f090698301b89dd6132cf6ef24b7889a0"],["031ec2b1d53da6a162138fb8f4a1ec27d62c45c13dddecebbd55ad8a5d05397382","02417a3320e15c2a5f0345ac927a10d7218883170a9e64837e629d14f8f3de7c78"],["02b85c8b2f33b6a8a882c383368be8e0a91491ea57595b6a690f01041be5bef4fb","0383ad57c7899284e9497e9dccb1de5bf8559b87157f13fee5677dcf2fbeb7b782"],["03eaa9e3ea81b2fa6e636373d860c0014e67ac6363c9284e465384986c2ec77ee2","03b1bd0d6355d99e8cab6d177f10f05eb8ddd3e762871f176d78a79f14ae037826"],["03ecd1b458e7c2b71a6542f8e64c750358c1421542ffe7630cc3ecc6866d379dfe","02d5c5432ca5e4243430f73a69c180c23bda8c7c269d7b824a4463e3ac58850984"],["028098ae6e772460047cdd6694230dcfc44da8ceabcae0624225f2452be7ae26c4","02add86858446c8a59ed3132264a8141292cd4ece6653bf3605895cceb00ba30b9"],["02f580882255cda6fae954294164b26f2c4b6b2744c0930daaa7a9953275f2f410","02c09c5e369910d84057637157bdf1fb721387bb2867c3c2adb2d91711498bbe5e"],["025e628f78c95135669ab8b9178f4396b0b513cbeae9ca631ba5e5e8321a4a05bc","03476f35b4defcc67334a0ff2ce700fb55df39b0f7f4ff993907e21091f6a29a31"],["026fa6f3214dce2ad2325dae3cd8d6728ce62af1903e308797ff071129fe111eca","03d07eb26749caceca56ffe77d9837aaf2f657c028bd3575724b7e2f1a8b3261a5"],["03894311c920ef03295c3f1c8851f5dc9c77e903943940820b084953a0a92efcc3","0368b0b3774f9de81b9f10e884d819ccf22b3c0ed507d12ce2a13efc36d06cdc17"],["024f8a61c23aa4a13a3a9eb9519ed3ec734f54c5e71d55f1805e873c31a125c467","039e9c6708767bd563fcdca049c4d8a1acab4a051d4f804ae31b5e9de07942570f"],["038f9b8f4b9fe6af5ced879a16bb6d56d81831f11987d23b32716ca4331f6cbabf","035453374f020646f6eda9528543ec0363923a3b7bbb40bc9db34740245d0132e7"],["02e30cd68ae23b3b3239d4e98745660b08d7ce30f2f6296647af977268a23b6c86","02ee5e33d164f0ad6b63f0c412734c1960507286ad675a343df9f0479d21a86ecc"]],"xpub":"xpub661MyMwAqRbcGAPwDuNBFdPguAcMFDrUFznD8RsCFkjQqtMPE66H5CDpecMJ9giZ1GVuZUpxhX4nFh1R3fzzr4hjDoxDSHymXVXQa7H1TjG","xpub2":"xpub661MyMwAqRbcFMKuZtmYryCNiNvHAki74TizX3b6dxaREtjLMoqnLJbd1zQKjWwKEThmB4VRtwePAWHNk9G5nHvAEvMHDYemERPQ7bMjQE3"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3gKU7sqAtVSxM8mrqm8ctmrcL3TahRCRy62EgYn2XPuLoJAGbBGvL4ArbPoAay5jo7L1UbBv15SsmrSKdTQSgDE351WSkm6"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcGAPwDuNBFdPguAcMFDrUFznD8RsCFkjQqtMPE66H5CDpecMJ9giZ1GVuZUpxhX4nFh1R3fzzr4hjDoxDSHymXVXQa7H1TjG","x2/":"xpub661MyMwAqRbcFMKuZtmYryCNiNvHAki74TizX3b6dxaREtjLMoqnLJbd1zQKjWwKEThmB4VRtwePAWHNk9G5nHvAEvMHDYemERPQ7bMjQE3"},"pruned_txo":{},"seed":"brick huge enforce behave cabin cram okay friend sketch actor casual barrel abuse","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_4_3_seeded(self): - wallet_str = '{"accounts":{"0":{"change":["02707eb483e51d859b52605756aee6773ea74c148d415709467f0b2a965cd78648","0321cddfb60d7ac41fdf866b75e4ad0b85cc478a3a84dc2e8db17d9a2b9f61c3b5","0368b237dea621f6e1d580a264580380da95126e46c7324b601c403339e25a6de9","02334d75548225b421f556e39f50425da8b8a36960cce564db8001f7508fef49f6","02990b264de812802743a378e7846338411c3afab895cff35fb24a430fa6b43733","02bc3b39ca00a777e95d89f773428bad5051272b0df582f52eb8d6ebb5bb849383"],"receiving":["0286c9d9b59daa3845b2d96ce13ac0312baebaf318251bac6d634bcac5ff815d9d","0220b65829b3a030972be34559c4bb1fc91f8dfd7e1703ddb43da9aa28aa224864","02fe34b26938c29faee00d8d704eae92b7c97d487825892290309073dc85ae5374","03ea255ae2ba7169802543cf7af135783f4fca91924fd0285bdbe386d78a0ab87e","027115aeea786e2745812f2ec2ae8fee3d038d96c9556b1324ac50c913b83a9e6a","03627439bb701352e35d0cf8e00617d8e9bf329697e430b0a5d999370097e025b4","034120249c6b15d051525156845aefaa83988adf9ed1dd18b796217dcf9824b617","02dfeb0c89eee66026d7650ee618c2172551f97fdd9ed249e696c54734d26e39a3","037e031bb4e51beb5c739ba6ab64aa696e85457ea63cc56698b7d9b731fd1e8e61","0302ea6818525492adc5ed8cfd2966efd704915199559fe1c06d6651fd36533012","0349394140560d685d455595f697d17b44e832ec453b5a2f02a3f5ed66205f3d30","036815bf2437df00440b15cfa7123544648cf266247989e82540d6b1cae1589892","02f98568e8f0f4b780f005e538a7452a60b2c06a5d2e3a23fa26d88459d118ef56","02e36ccb8b05a2762a08f60541d1a5a136afd6a73119eea8c7c377cc8b07eb2e2f","031566539feb6f0a212cca2604906b1c1f5cfc5bf5d5206e0c695e37ef3a141fd2","025754e770bedeef6f4e932fa231b858b49d28183e1be6da23e597c67dd7785f19","03a29961f5fb9c197cffe743081a761442a3cf9ded0be2fa07ab67023a74c08d28","023184c1995a9f51af566c9c0b4da92d7fd4a5c59ff93c34a323e94671ddbe414a","029efdb15d3aec708b3af2aee34a9157ff731bec94e4f19f634ab43d3101e47bd8","03e16b13fe6bb9aa6dc4e331e19ab4d3d291a2670b97e6040e87a7c7309b243af9"],"xpub":"xpub661MyMwAqRbcF1KGEGXxFTupKQHTTUan1qZMTp4yUxiwF2uRRum7u1TCnaJRjaSBW4d42Fwfi6xfLvfRfgtDixekGDWK9CPWguR7YzXKKeV"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2XEo8EzwtKy5mNSy41rvecdkfRfMvdBxNEaGtNSsMD8iwHsc91UxKtSrDHXex53NkMRRDwnm4PmqS7N35K8BR1KCD2qm5iE"},"master_public_keys":{"x/":"xpub661MyMwAqRbcF1KGEGXxFTupKQHTTUan1qZMTp4yUxiwF2uRRum7u1TCnaJRjaSBW4d42Fwfi6xfLvfRfgtDixekGDWK9CPWguR7YzXKKeV"},"seed":"smart fish version ocean category disagree hospital mystery survey chef kid latin about","seed_version":11,"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_4_3_importedkeys(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"stored_height":477636,"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_4_3_watchaddresses(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_4_3_trezor_singleacc(self): - wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":485855,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_4_3_trezor_multiacc(self): - wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]]},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490009,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) - - def test_upgrade_from_client_2_4_3_multisig(self): - wallet_str = '{"accounts":{"0":{"change":[["03467a8bae231aff83aa01999ee4d3834894969df7a3b0753e23ae7a3aae089f6b","02180c539980494b4e59edbda5e5340be2f5fbf07e7c3898b0488950dda04f3476"],["03d8e18a428837e707f35d8e2da106da2e291b8acbf40ca0e7bf1ac102cda1de11","03fad368e3eb468a7fe721805c89f4405581854a58dcef7205a0ab9b903fd39c23"],["0331c9414d3eee5bee3c2dcab911537376148752af83471bf3b623c184562815d9","02dcd25d2752a6303f3a8366fae2d62a9ff46519d70da96380232fc9818ee7029e"],["03bb18a304533086e85782870413688eabef6a444a620bf679f77095b9d06f5a16","02f089ed84b0f7b6cb0547741a18517f2e67d7b5d4d4dd050490345831ce2aef9e"],["02dc6ebde88fdfeb2bcd69fce5c5c76db6409652c347d766b91671e37d0747e423","038086a75e36ac0d6e321b581464ea863ab0be9c77098b01d9bc8561391ed0c695"],["02a0b30b12f0c4417a4bef03cb64aa55e4de52326cf9ebe0714613b7375d48a22e","02c149adda912e8dc060e3bbe4020c96cff1a32e0c95098b2573e67b330e714df0"]],"m":2,"receiving":[["0254281a737060e919b071cb58cc16a3865e36ea65d08a7a50ba2e10b80ff326d5","0257421fa90b0f0bc75b67dd54ffa61dc421d583f307c58c48b719dd59078023e4"],["03854ce9bbc7813d535099658bcc6c671a2c25a269fdb044ee0ed5deb95da0d7e0","025379ca82313dde797e5aa3f222dddf0f7223cb271f79ecce2c8178bea3e33c62"],["03ae6ad5ffc75d71adc2ab87e3adc63fa8696a8656e1135adb5ae88ddb6d39089f","025ed8821f8b37aef69b1aabf89e4e405f09206c330c78e94206b21139ddafcc4f"],["033ea4d8b88d36d14a52983ae30d486254af2dfa1c7f8e04bc9d8e34b3ffe4b32a","02b441a3e47a338d89027755b81724219362b8d9b66142d32fcb91c9c7829d8c9f"],["029195704b9bbc3014452bbf07baa7bf6277dfefd9721aea8438f2671ba57b898b","022264503140f99b41c0269666ab6d16b2dad72865dbd2bf6153d45f5d11978e4d"],["037e3caa2d151123821dff34fd8a76ac0d56fa97c41127e9b330a115bf12d76674","02a4ae28e2011537de4cce0c47af4ac0484b38d408befcb731c3d752922fcd3c5b"],["02226853ca32e72b4771ccc47c0aae27c65ed0d25c525c1f673b913b97dca46cc5","027a9c855fc4e6b3f8495e77347a1e03c0298c6a86bd5a89800195bd445ae3e3bd"],["02890f7eee0766d2dde92f3146cd461ae0fa9caf07e1f3559d023a20349bae5e44","0380249f30829b3656c32064ddf657311159cecb36f9dbbf8e50e3d7279b70c57e"],["02ab9613fd5a67a3fdf6b6241d757ce92b2640d9d436e968742cb7c4ec4bb3e6e9","0204b29cc980b18dfb3a4f9ca6796c6be3e0aee2462719b4a787e31c8c5d79c8cf"],["029103b50ecc0cc818c1c97e8acb8ce3e1d86f67e49f60c8496683f15e753c3eed","0247abb2c5e4cde22eb59a203557c0bbe87e9c449e6c2973e693ac14d0d9cf3f28"],["02817c935c971e6e318ba9e25402df26ca016a4e532459be5841c2d83a5aa8a967","03331fe3a2e4aa3e2dc1d8d4afc5a88c57350806b905e593b5876c6b9cef71fd4d"],["03023c6797af5c9c3d7db2fbeb9d7236601fe5438036200f2f59d9b997d29ec123","023b1084f008cf2e9632967095958bb0bbd59e60a0537e6003d780c7ebccb2d4f5"],["0245e0bdebe483fef984e4e023eb34641e65909cd566eb6bd6c0bce592296265a1","0363bad4b477d551f46b19afcc10decf6a4c1200becb5b22c032c62e6d90b373b8"],["0379ba2f8c5e8e5e3f358615d230348fe8d7855ef9c0e1cf97aac4ec09dfe690aa","02ecda86ff40b286a3faadf9a5b361ab7a5beb50426296a8c0e3d222f404ae4380"],["02e090227c22efa7f60f290408ce9f779e27b39d4acec216111cc3a8b9594ab451","02144954ddabb55abcfe49ea703a4e909ab86db2f971a2e85fc006dffbdf85af52"],["025dc4bd1c4809470b5a14cf741519ad7f5f2ccd331b42e0afd2ce182cdf25f82d","03d292524190af850665c2255a785d66c59fea2b502d4037bb31fdde10ad9b043f"],["027e7c549f613ae9ba1d806c8c8256f870e1c7912e3e91cbb326d61fb20ac3a096","03fbbf15ee2b49878c022d0b30478b6a3acb61f24af6754b3f8bcb4d2e71968099"],["02c188eaf5391e52fdcd66f8522df5ae996e20c524577ac9ffa7a9a9af54508f7c","03fe28f1ea4a0f708fa2539988758efd5144a128cc12aed28285e4483382a6636a"],["03bea51abacd82d971f1ef2af58dcbd1b46cdfa5a3a107af526edf40ca3097b78d","02267d2c8d43034d03219bb5bc0af842fb08f028111fc363ec43ab3b631134228a"],["03c3a0ecdbf8f0a162434b0db53b3b51ce02886cbc20c52e19a42b5f681dac6ffb","02d1ede70e7b1520a6ccabd91488af24049f1f1cf2661c07d8d87aee31d5aec7c9"]],"xpubs":["xpub661MyMwAqRbcFafkG2opdo3ou3zUEpFK3eKpWWYkdA5kfvootPkZzqvUV1rtLYRLdUxvXBZApzZpwyR2mXBd1hRtnc4LoaLTQWDFcPKnKiQ","xpub661MyMwAqRbcFrxPbuWkHdMeaZMjb4jKpm51RHxQ3czEDmyK3Qa3Z43niVzVjFyhJs6SrdXgQg56DHMDcC94a7MCtn9Pwh2bafhHGJbLWeH"]}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3NsvVsyjvVQv2XXFBc1UTY9QcuYnVHTFLyeAVsFo1FjJsBk48XK16jZLqRs1B5Sa6SCqYdA2XFvB9riBca2GyGccYGKKP6t"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFrxPbuWkHdMeaZMjb4jKpm51RHxQ3czEDmyK3Qa3Z43niVzVjFyhJs6SrdXgQg56DHMDcC94a7MCtn9Pwh2bafhHGJbLWeH","x2/":"xpub661MyMwAqRbcFafkG2opdo3ou3zUEpFK3eKpWWYkdA5kfvootPkZzqvUV1rtLYRLdUxvXBZApzZpwyR2mXBd1hRtnc4LoaLTQWDFcPKnKiQ"},"pruned_txo":{},"seed":"angry work entry banana taste climb script fold level rate organ edge account","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_5_4_seeded(self): - wallet_str = '{"accounts":{"0":{"change":["0253e61683b66ebf5a4916334adf1409ffe031016717868c9600d313e87538e745","021762e47578385ecedc03c7055da1713971c82df242920e7079afaf153cc37570","0303a8d6a35956c228aa95a17aab3dee0bca255e8b4f7e8155b23acef15cf4a974","02e881bc60018f9a6c566e2eb081a670f48d89b4a6615466788a4e2ce20246d4c6","02f0090e29817ef64c17f27bf6cdebc1222f7e11d7112073f45708e8d218340777","035b9c53b85fd0c2b434682675ac862bfcc7c5bb6993aee8e542f01d96ff485d67"],"receiving":["024fbc610bd51391794c40a7e04b0e4d4adeb6b0c0cc84ac0b3dad90544e428c47","024a2832afb0a366b149b6a64b648f0df0d28c15caa77f7bbf62881111d6915fe9","028cd24716179906bee99851a9062c6055ec298a3956b74631e30f5239a50cb328","039761647d7584ba83386a27875fe3d7715043c2817f4baca91e7a0c81d164d73d","02606fc2f0ce90edc495a617329b3c5c5cc46e36d36e6c66015b1615137278eabd","02191cc2986e33554e7b155f9eddcc3904fdba43a5a3638499d3b7b5452692b740","024b5bf755b2f65cab1f7e5505febc1db8b91781e5aac352902e79bc96ad7d9ad0","0309816cb047402b84133f4f3c5e56c215e860204513278beef54a87254e44c14a","03f53d34337c12ddb94950b1fee9e4a9cf06ad591db66194871d31a17ec7b59ac7","0325ede4b08073d7f288741c2c577878919fd5d832a9e6e04c9eac5563ae13aa83","02eca43081b04f68d6c8b81781acd59e5b8d2ba44dba195369afc40790fd9edef7","029a8ca96c64d3a98345be1594208908f2be5e6af6bcc6ff3681f271e75fcf232e","02fbe0804980750163a216cc91cfe86e907addf0e80797a8ea5067977eb4897c1b","0344f32fc1ee8b2eb08f419325529f495d77a3b5ea683bbce7a44178705ab59302","021dd62bdf18256bd5316ce3cbcca58785378058a41ba2d1c58f4cc76449b3c424","035e61cdbdb4306e58a816a19ad92c7ca3a392b67ac6d7257646868ffe512068c5","0326a4db82f21787d0246f8144abe6cda124383b7d93a6536f36c05af530ea262a","02b352a27a8f9c57b8e5c89b357ba9d0b5cb18bf623509b34cd881fcf8b89a819a","02a59188edef1ed29c158a0adb970588d2031cfe53e72e83d35b7e8dd0c0c77525","02e8b9e42a54d072c8887542c405f6c99cfabf41bdde639944b44ba7408837afd1"],"xpub":"xpub661MyMwAqRbcGh7ywNf1BYoFCs8mht2YnvkMYUJTazrAWbnbvkrisvSvrKGjRTDtw324xzprbDgphsmPv2pB6K5Sux3YNHC8pnJANCBY6vG"}},"accounts_expanded":{},"addr_history":{"12LXoVHUnAXn6BVBpshjwd7sSTwp5nsd7W":[],"12iXPYBErR6ZMESB9Nv74S4pVxdGMNLiW2":[],"13jmb5Vc2qh29tPhg637BwCJN7hStGWYXE":[],"14dHBBbwFVC7niSCqrb5HCHRK5K8rrgaW6":[],"14xsHuYGs4gKpRK3deuYwhMBTAwUeu2dpB":[],"15MpWMUasNVPTpzC5hK2AuVFwQ3AHd8fkv":[],"17nmvao3F84ebPrcv1LUxPUSS94U9EvCUt":[],"17yotEc8oUgJVQUnkjZSQjcqqZEbFFnXx8":[],"1A3c1rCCS2MYYobffyUHwPqkqE5ZpvG8Um":[],"1AtCzmcth79q6HgeyDnM3NLfr29hBHcfcg":[],"1AufJhUsMbqwbLK9JzUGQ9tTwphCQiVCwD":[],"1B77DkhJ8qHcwPQC2c1HyuNcYu5TzxxaJ7":[],"1D4bgjc4MDtEPWNTVfqG5bAodVu3D1Gjft":[],"1DefMPXdeCSQC5ieu8kR7hNGAXykNzWXpm":[],"1E673RESY1SvTWwUr5hQ1E7dGiRiSgkYFP":[],"1Ex6hnmpgp3FQrpR5aYvp9zpXemFiH7vky":[],"1FH2iAc5YgJKj1KcpJ1djuW3wJ2GbQezAv":[],"1GpjShJMGrLQGP6nZFDEswU7qUUgJbNRKi":[],"1H4BtV4Grfq2azQgHSNziN7MViQMDR9wxd":[],"1HnWq29dPuDRA7gx9HQLySGdwGWiNx4UP1":[],"1LMuebyhm8vnuw5qX3tqU2BhbacegeaFuE":[],"1LTJK8ffwJzRaNR5dDEKqJt6T8b4oVbaZx":[],"1LtXYvRr4j1WpLLA398nbmKhzhqq4abKi8":[],"1NfsUmibBxnuA3ir8GJvPUtY5czuiCfuYK":[],"1Q3cZjzADnnx5pcc1NN2ekJjLijNjXMXfr":[],"1okpBWorqo5WsBf5KmocsfhBCEDhNstW2":[]},"master_private_keys":{"x/":"xprv9s21ZrQH143K4D3WqM7zpQrWeqJHJRJhRhpkk5tr2fKBdoTTPDYUL88T12Ad9RHwViugcMbngkMDY626vD5syaFDoUB2cpLeraBaHvZHWFn"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGh7ywNf1BYoFCs8mht2YnvkMYUJTazrAWbnbvkrisvSvrKGjRTDtw324xzprbDgphsmPv2pB6K5Sux3YNHC8pnJANCBY6vG"},"pruned_txo":{},"seed":"tent alien genius panic stage below spoon swap merge hammer gorilla squeeze ability","seed_version":11,"stored_height":489715,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard","winpos-qt":[100,100,840,400]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_5_4_importedkeys(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported","winpos-qt":[595,261,840,400]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_5_4_watchaddresses(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported","winpos-qt":[406,393,840,400]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_5_4_trezor_singleacc(self): - wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"addr_history":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":490046,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor","winpos-qt":[522,328,840,400]}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_5_4_trezor_multiacc(self): - wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12bBPWWDwvtXrR9ntSgaQ7AnGyVJr16m5q":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"13853om3ye5c8x6K1LfT3uCWEnG14Z82ML":[],"13BGVmizH8fk3qNm1biNZxAaQY3vPwurjZ":[],"13Tvp2DLQFpUxvc7JxAD3TXfAUWvjhwUiL":[],"15EQcTGzduGXSaRihKy1FY99EQQco8k2UW":[],"15paDwtQ33jJmJhjoBJhpWYGJDFCZppEF9":[],"17X8K766zBYLTjSNvHB9hA6SWRPMTcT556":[],"17zSo4aveNaE5DiTmwNZtxrJmS5ymzvwqj":[],"19BRVkUFfrAcxW9poaBSEUA2yv7SwN3SXh":[],"19gPT2mb9FQCiiPdAmMAaberShzNRiAtTB":[],"1A3vopoUcrWn7JbiAzGZactQz8HbnC1MoD":[],"1D1bn2Jzcx4D2GXbxzrJ1GwP4eNq98Q948":[],"1DvytpRGLJujPtSLYTRABzpy2r6hKJBYQd":[],"1EGg2acXNhJfv1bU3ixrbrmgxFtAUWpdY":[],"1Ev3S9YWxS7KWT8kyLmEuKV5sexNKcMUKV":[],"1FfpRnukxbfBnoudWvw9sdmc86YbVs7eGb":[],"1GBxNE82WLgd38CzoFTEkz6QS9EwLj1ym7":[],"1JFDe97zENNUiKeizcFUHss13vS2AcrVdE":[],"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ":[],"1JQqX3yg6VYxL6unuRArDQaBZYo3ktSCCP":[],"1JUbrr4grE71ZgWNqm9z9ZHHJDcCzFYM4V":[],"1JuHUVbYfBLDUhTHx5tkDDyDbCnMsF8C9w":[],"1KZu7p244ETkdB5turRP4vhG2QJskARYWS":[],"1LE7jioE7y24m3MMZayRKpvdCy2Dz2LQae":[],"1LVr2pTU7LPQu8o8DqsxcGrvwu5rZADxfi":[],"1LmugnVryiuMbgdUAv3LucnRMLvqg8AstU":[],"1MPN5vptDZCXc11fZjpW1pvAgUZ5Ksh3ky":[]},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490009,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor","winpos-qt":[757,469,840,400]}''' - self._upgrade_storage(wallet_str, accounts=2) - - def test_upgrade_from_client_2_5_4_multisig(self): - wallet_str = '{"accounts":{"0":{"change":[["02a63209b49df0bb98d8a262e9891fe266ffdce4be09d5e1ffaf269a10d7e7a17c","02a074035006ed8ee8f200859c004c073b687140f7d40bd333cdbbe43bad1e50bc"],["0280e2367142669e08e27fb9fd476076a7f34f596e130af761aef54ec54954a64d","02719a66c59f76c36921cf7b330fca7aaa4d863ee367828e7d89cd2f1aad98c3ac"],["0332083e80df509d3bd8a06538ca20030086c9ed3313300f7313ed98421482020f","032f336744f53843d8a007990fa909e35e42e1e32460fae2e0fc1aef7c2cff2180"],["03fe014e5816497f9e27d26ce3ae8d374edadec410227b2351e9e65eb4c5d32ab7","0226edd8c3af9e339631145fd8a9f6d321fdc52fe0dc8e30503541c348399dd52a"],["03e6717b18d7cbe264c6f5d0ad80f915163f6f6c08c121ac144a7664b95aedfdf3","03d69a074eba3bc2c1c7b1f6f85822be39aee20341923e406c2b445c255545394a"],["023112f87a5b9b2eadc73b8d5657c137b50609cd83f128d130172a0ed9e3fea9bc","029a81fd5ba57a2c2c6cfbcb34f369d87af8759b66364d5411eddd28e8a65f67fa"]],"m":2,"receiving":[["03c35c3da2c864ee3192a847ffd3f67fa59c095d8c2c0f182ed9556308ec37231e","03cfcb6d1774bfd916bd261232645f6c765da3401bf794ab74e84a6931d8318786"],["03973c83f84a4cf5d7b21d1e8b29d6cbd4cb40d7460166835cd1e1fd2418cfcf2e","03596801e66976959ac1bdb4025d65a412d95d320ed9d1280ac3e89b041e663cf4"],["02b78ac89bfdf90559f24313d7393af272092827efc33ba3a0d716ee8b75fd08ff","038e21fae8a033459e15a700551c1980131eb555bbb8b23774f8851aa10dcac6b8"],["0288e9695bb24f336421d5dcf16efb799e7d1f8284413fe08e9569588bc116567e","027123ba3314f77a8eb8bb57ba1015dd6d61b709420f6a3320ba4571b728ef2d91"],["0312e1483f7f558aef1a14728cc125bb4ee5cff0e7fa916ba8edd25e3ebceb05e9","02dad92a9893ad95d3be5ebc40828cef080e4317e3a47af732127c3fee41451356"],["03a694e428a74d37194edc9e231e68399767fdb38a20eca7b72caf81b7414916a8","03129a0cef4ed428031972050f00682974b3d9f30a571dc3917377595923ac41d8"],["026ed41491a6d0fb3507f3ca7de7fb2fbfdfb28463ae2b91f2ab782830d8d5b32c","03211b3c30c41d54734b3f13b8c9354dac238d82d012839ee0199b2493d7e7b6fc"],["03480e87ffa55a96596be0af1d97bca86987741eb5809675952a854d59f5e8adc2","0215f04df467d411e2a9ed8883a21860071ab721314503019a10ed30e225e522e7"],["0389fce63841e9231d5890b1a0c19479f8f40f4f463ef8e54ef306641abe545ac8","02396961d498c2dcb3c7081b50c5a4df15fda31300285a4c779a59c9abc98ea20d"],["03d4a3053e9e08dc21a334106b5f7d9ac93e42c9251ceb136b83f1a614925eb1fb","025533963c22b4f5fbfe75e6ee5ad7ee1c7bff113155a7695a408049e0b16f1c52"],["038a07c8d2024b9118651474bd881527e8b9eb85fc90fdcb04c1e38688d498de4b","03164b188eb06a3ea96039047d0db1c8f9be34bfd454e35471b1c2f429acd40afb"],["0214070cd393f39c062ce1e982a8225e5548dbbbd654aeba6d36bfcc7a685c7b12","029c6a9fb61705cc39bef34b09c684a362d4862b16a3b0b39ca4f94d75cd72290c"],["027b3497f72f581fea0a678bc20482b6fc7b4b507f7263d588001d73fdf5fe314e","021b80b159d19b6978a41c2a6bf7d3448bc73001885f933f7854f450b5873091f3"],["0303e9d76e4fe7336397c760f6fdfd5fb7500f83e491efb604fa2442db6e1da417","03a8d1b22a73d4c181aecd8cfe8bb2ee30c5dd386249d2a5a3b071b7a25b9da73a"],["0298e472b74832af856fb68eed02ff00a235fd0424d833bc305613e9f44087d0ee","03bb9bc2e4aaa9b022b35c8d122dfccb6c28ae8f0996a8fb4a021af8ec96a7beaf"],["02e933a4afb354500da03373514247e1be12e67cc4683e0cb82f508878cc3cc048","02c07a57b071bc449a95dd80308e53b26e4ebf4d523f620eecb17f96ae3aa814e9"],["03f73476951078b3ccc549bc7e6362797aaaacb1ea0edc81404b4d16cb321255a3","03b3a825fb9fc497e568fba69f70e2c3dcdc793637e242fce578546fcbd33cb312"],["03bbdf99fddeea64a96bbb9d1e6d7ced571c9c7757045dcbd8c40137125b017dc5","03aedf4452afefb1c3da25e698f621cb3a3a0130aa299488e018b93a45b5e6c21d"],["03b85891edb147d43c0a5935a20d6bbf8d32c542bfecccf3ae0158b65bd639b34e","03b34713c636a1c103b82d6cec917d442c59522ddc5a60bf7412266dd9790e7760"],["028ddf53b85f6c01122a96bd6c181ee17ca222ee9eca85bdeeb25c4b5315005e3b","02f4821995bfd5d0adb7a78d6e3a967ac72ace9d9a4f9392aff2711533893e017b"]],"xpubs":["xpub661MyMwAqRbcGHtCYBSGGVgMSihroMkuyE25GPyzfQvS2vSFG7SgJYf7rtXJjMh7srBJj8WddLtjapHnUQLwJ7kxsy5HiNZnGvF9pm2du7b","xpub661MyMwAqRbcEdd7bzA86LbhMoTv8NeyqcNP5z1Tiz9ajCRQDzdeXHw3h5ucDNGWr6mPFCZBcBE31VNKyR3vWM7WEeisu5m4VsCyuA6H8fp"]}},"accounts_expanded":{},"addr_history":{"32JvbwfEGJwZHGm3nwYiXyfsnGCb3L8hMX":[],"32pWy5sKkQsjyDz45tog47cA8vQyzC3UUZ":[],"334yqX1WtS6mY2vize7znTaL64HspwVkGF":[],"33GY9w6a4XmLAWxNgNFFRXTTRxbu3Nz8ip":[],"33geBcyW8Bw53EgAv3qwMVkVnvxZWj5J1X":[],"35BneogkCNxSiSN1YLmhKLP8giDbGkZiTX":[],"37U4J5b9B7rQnQXYstMoQnb6i9aWpptnLi":[],"37gqbHdbrCcGyrNF21AiDkofVCie5LpFmQ":[],"37t1Q5R92co4by2aagtLcqdWTDEzFuAuwZ":[],"37z3ruAHCxnzeJeLz96ZpkbwS3CLbtXtPc":[],"39qePsKaeviFEMC6CWX37DqaQda4jA2E6A":[],"3A5eratrDWu4SqsoHpuqswNsQmp9k8TXR2":[],"3B1N3PG5dNPYsTAuHFbVfkwXeZqqNS1CuP":[],"3BABbvd3eAuwiqJwppm54dJauKnRUieQU8":[],"3CAsH7BJnNT4kmwrbG8XZMMwW6ue8w4auJ":[],"3CX2GLCTfpFHSgAmbGRmuDKGHMbWY8tCp7":[],"3CrLUTVHuG1Y3swny9YDmkfJ89iHHU93NB":[],"3CxRa6yAQ2N2rpDHyUTaViGG4XVASAqwAN":[],"3DLTrsdYabso7QpxoLSW5ZFjLxBwrLEqqW":[],"3GG3APgrdDCTmC9tTwWu3sNV9aAnpFcddA":[],"3JDWpTxnsKoKut9WdG4k933qmPE5iJ8hRR":[],"3LdHoahj7rHRrQVe38D4iN43ySBpW5HQRZ":[],"3Lt56BqiJwZ1um1FtXJXzbY5uk32GVBa8K":[],"3MM9417myjN7ubMDkaK1wQ9RbjEc1zHCRH":[],"3NTivFVXva4DCjPmsf5p5Gt1dmuV39qD2v":[],"3QCwtjMywMtT3Vg6BwS146LcQjJnZPAPHZ":[]},"master_private_keys":{"x1/":"xprv9s21ZrQH143K29YeVxd7jCexomdRiuw8UPSnHbbrAecbrQ6FgTKPyVcZqp2256L5DSTdb8UepPVaDwJecswTrEhdyZiaNGERJpfzWV5FcN5"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcEdd7bzA86LbhMoTv8NeyqcNP5z1Tiz9ajCRQDzdeXHw3h5ucDNGWr6mPFCZBcBE31VNKyR3vWM7WEeisu5m4VsCyuA6H8fp","x2/":"xpub661MyMwAqRbcGHtCYBSGGVgMSihroMkuyE25GPyzfQvS2vSFG7SgJYf7rtXJjMh7srBJj8WddLtjapHnUQLwJ7kxsy5HiNZnGvF9pm2du7b"},"pruned_txo":{},"seed":"park dash merit trend life field acid wrap dinosaur kit bar hotel abuse","seed_version":11,"stored_height":490034,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2","winpos-qt":[564,329,840,400]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_6_4_seeded(self): - wallet_str = '{"accounts":{"0":{"change":["03236a8ce6fd3d343358f92d3686b33fd6e7301bf9f635e94c21825780ab79c93d","0393e39f6b4a3651013fca3352b89f1ae31751d4268603f1423c71ff79cbb453a1","033d9722ecf50846527037295736708b20857b4dd7032fc02317f9780d6715e8ff","03f1d56d2ade1daae5706ea945cab2af719060a955c8ad78153693d8d08ed6b456","029260d935322dd3188c3c6b03a7b82e174f11ca7b4d332521740c842c34649137","0266e8431b49f129b892273ab4c8834a19c6432d5ed0a72f6e88be8c629c731ede"],"receiving":["0350f41cfac3fa92310bb4f36e4c9d45ec39f227a0c6e7555748dff17e7a127f67","02f997d3ed0e460961cdfa91dec4fa09f6a7217b2b14c91ed71d208375914782ba","029a498e2457744c02f4786ac5f0887619505c1dae99de24cf500407089d523414","03b15b06044de7935a0c1486566f0459f5e66c627b57d2cda14b418e8b9017aca1","026e9c73bdf2160630720baa3da2611b6e34044ad52519614d264fbf4adc5c229a","0205184703b5a8df9ae622ea0e8326134cbeb92e1f252698bc617c9598aff395a1","02af55f9af0e46631cb7fde6d1df6715dc6018df51c2370932507e3d6d41c19eec","0374e0c89aa4ecf1816f374f6de8750b9c6648d67fe0316a887a132c608af5e7c0","0321bb62f5b5c393aa82750c5512703e39f4824f4c487d1dc130f690360c0e5847","0338ea6ebb2ed80445f64b2094b290c81d0e085e6000367eb64b1dc5049f11c2e9","020c3371a9fd283977699c44a205621dea8abfc8ebc52692a590c60e22202fa49b","0395555e4646f94b10af7d9bc57e1816895ad2deddef9d93242d6d342cea3d753b","02ffa4495d020d17b54da83eaf8fbe489d81995577021ade3a340a39f5a0e2d45c","030f0e16b2d55c3b40b64835f87ab923d58bcdbb1195fadc2f05b6714d9331e837","02f70041fc4b1155785784a7c23f35d5d6490e300a7dd5b7053f88135fc1f14dfd","03b39508c6f9c7b8c3fb8a1b91e61a0850c3ac76ccd1a53fbc5b853a94979cffa8","03b02aa869aa14b0ec03c4935cc12f221c3f204f44d64146d468e07370c040bfe7","02b7d246a721e150aaf0e0e60a30ad562a32ef76a450101f3f772fef4d92b212d9","037cd5271b31466a75321d7c9e16f995fd0a2b320989c14bee82e161c83c714321","03d4ad77e15be312b29987630734d27ca6e9ee418faa6a8d6a50581eca40662829"],"xpub":"xpub661MyMwAqRbcGwHDovebbFy19vHfW2Cqtyf2TaJkAwhFWsLYfHHYcCnM7smpvntxJP1YMVT5triFbWiCGXPRPhqdCxFumA77MuQB1CeWHpE"}},"accounts_expanded":{},"addr_history":{"12qKnKuhCZ1Q9XBi1N6SnxYEUtb5XZXuY5":[],"1321ddunxShHmF4cjh3v5yqR7uatvSNndK":[],"13Ji3kGWn9qxLcWGhd46xjV6hg8SRw8x2P":[],"145q5ZDXuFi6v9dA2t8HyD8ysorfb81NRt":[],"14gB2wLy2DMkBVtuU6HHP3kQYNFYPzAguU":[],"16VGRwtZwp4yapQN5fS8CprK6mmnEicCEj":[],"16ahKVzCviRi24rwkoKgiSVSkvRNiQudE1":[],"16wjKZ1CWAMEzSR4UxQTWqXRm9jcJ9Dbuf":[],"18ReWGJBq1XkJaPAirVdT6RqDskcFeD5Ho":[],"1A1ECMMJU4NicWNwfMBn3XJriB4WHAcPUC":[],"1Bvxbfc2wXB8z8kyz2uyKw2Ps8JeGQM9FP":[],"1EDWUz4kPq8ZbCdQq8rLhFc3qSZ6Fpt1TD":[],"1EsvTarawMm5BfF44hpRtE4GfZFfZZ1JG3":[],"1JgaekD2ETMJm6oRNnwTWRK9ZxXeUcbi18":[],"1KHdLodsSWj1LrrD9d1RbApfqzpxRs5sxu":[],"1KgGwpKhruHWpMNtrpRExDWLLk5qHCHBdg":[],"1LFf8d3XD9atZvMVMAiq9ygaeZbphbKzSo":[],"1N3XncDQsWE2qff1EVyQEmR6JLLzD3mEL7":[],"1NUtLcVQNmY5TJCieM1cUmBmv18AafY1vq":[],"1NYFsm7PpneT65byRtm8niyvtzKsbEeuXA":[],"1NvEcSvfCe8LPvPkK4ZxhjzaUncTPqe9jX":[],"1PV8xdkYKxeMpnzeeA4eYEpL24j1G9ApV2":[],"1PdiGtznaW1mok6ETffeRvPP5f4ekBRAfq":[],"1QApNe4DtK7HAbJrn5kYkYxZMt86U5ChSb":[],"1QnH7F6RBXFe7LtszQ6KTRUPkQKRtXTnm":[],"1ekukhMNSWCfnRsmpkuTRuLMbz6cstkrq":[]},"master_private_keys":{"x/":"xprv9s21ZrQH143K4TCkhu7bE82GbtTB6ZUzXkjRfBu8ccAGe51Q7jyJ4QTsGbWxpHxnatKeYV7Ad83m7KC81THBm2xmyxA1q8BuuRXSGnmhhR8"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGwHDovebbFy19vHfW2Cqtyf2TaJkAwhFWsLYfHHYcCnM7smpvntxJP1YMVT5triFbWiCGXPRPhqdCxFumA77MuQB1CeWHpE"},"pruned_txo":{},"seed":"heart cabbage scout rely square census satoshi home purpose legal replace move able","seed_version":11,"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard","winpos-qt":[582,394,840,400]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_6_4_importedkeys(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported","winpos-qt":[510,338,840,400]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_6_4_watchaddresses(self): - wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported","winpos-qt":[582,425,840,400]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_6_4_multisig(self): - wallet_str = '{"accounts":{"0":{"change":[["03d0bcdc86a64cc2024c84853e88985f6f30d3dc3f219b432680c338a3996a89ed","024f326d48aa0a62310590b10522b69d250a2439544aa4dc496f7ba6351e6ebbfe"],["03c0416928528a9aaaee558590447ee63fd33fa497deebefcf363b1af90d867762","03db7de16cd6f3dcd0329a088382652bc3e6b21ee1a732dd9655e192c887ed88a7"],["0291790656844c9d9c24daa344c0b426089eadd3952935c58ce6efe00ef1369828","02c2a5493893643102f77f91cba709f11aaab3e247863311d6fc3d3fc82624c3cc"],["023dc976bd1410a7e9f34c230051db58a3f487763f00df1f529b10f55ee85b931c","036c318a7530eedf3584fd8b24c4024656508e35057a0e7654f21e89e121d0bd30"],["02c8820711b39272e9730a1c5c5c78fe39a642b8097f8724b2592cc987017680ce","0380e3ebe0ea075e33acb3f796ad6548fde86d37c62fe8e4f6ab5d2073c1bb1d43"],["0369a32ddd213677a0509c85af514537d5ee04c68114da3bc720faeb3adb45e6f8","0370e85ac01af5e3fd5a5c3969c8bca3e4fc24efb9f82d34d5790e718a507cecb6"]],"m":2,"receiving":[["0207739a9ff4a643e1d4adb03736ec43d13ec897bdff76b40a25d3a16e19e464aa","02372ea4a291aeb1fadb26f36976348fc169fc70514797e53b789a87c9b27cc568"],["0248ae7671882ec87dd6bacf7eb2ff078558456cf5753952cddb5dde08f471f3d6","035bac54828b383545d7b70824a8be2f2d9584f656bfdc680298a38e9383ed9e51"],["02cb99ba41dfbd510cd25491c12bd0875fe8155b5a6694ab781b42bd949252ff26","03b520feba42149947f8b2bbc7e8c03f9376521f20ac7b7f122dd44ab27309d7c6"],["0395902d5ebb4905edd7c4aedecf17be0675a2ffeb27d85af25451659c05cc5198","02b4a01d4bd25cadcbf49900005e8d5060ed9cdc35eb33f2cd65cc45cc7ebc00c5"],["02f9d06c136f05acc94e4572399f17238bb56fa15271e3cb816ae7bb9be24b00b6","035516437612574b2b563929c49308911651205e7cebb621940742e570518f1c50"],["0376a7de3abaee6631bd4441658987c27e0c7eee2190a86d44841ae718a014ee43","03cb702364ffd59cb92b2e2128c18d8a5a255be2b95eb950641c5f17a5a900eecb"],["03240c5e868ecb02c4879ae5f5bad809439fdbd2825769d75be188e34f6e533a67","026b0d05784e4b4c8193443ce60bea162eee4d99f9dfa94a53ae3bc046a8574eeb"],["02d087cccb7dc457074aa9decc04de5a080757493c6aa12fa5d7d3d389cfdb5b8e","0293ab7d0d8bbb2d433e7521a1100a08d75a32a02be941f731d5809b22d86edb33"],["03d1b83ab13c5b35701129bed42c1f1fbe86dd503181ad66af3f4fb729f46a277e","0382ec5e920bc5c60afa6775952760668af42b67d36d369cd0e9acc17e6d0a930d"],["03f1737db45f3a42aebd813776f179d5724fce9985e715feb54d836020b8517bfe","0287a9dfb8ee2adab81ef98d52acd27c25f558d2a888539f7d583ef8c00c34d6dc"],["038eb8804e433023324c1d439cd5fbbd641ca85eadcfc5a8b038cb833a755dac21","0361a7c80f0d9483c416bc63d62506c3c8d34f6233b6d100bb43b6fe8ec39388b9"],["0336437ada4cd35bec65469afce298fe49e846085949d93ef59bf77e1a1d804e4a","0321898ed89df11fcfb1be44bb326e4bb3272464f000a9e51fb21d25548619d377"],["0260f0e59d6a80c49314d5b5b857d1df64d474aba48a37c95322292786397f3dc6","03acd6c9aeac54c9510304c2c97b7e206bbf5320c1e268a2757d400356a30c627b"],["0373dc423d6ee57fac3b9de5e2b87cf36c21f2469f17f32f5496e9e7454598ba8e","031ddc1f40c8b8bf68117e790e2d18675b57166e9521dff1da44ba368be76555b3"],["031878b39bc6e35b33ceac396b429babd02d15632e4a926be0220ccbd710c7d7b9","025a71cc5009ae07e3e991f78212e99dd5be7adf941766d011197f331ce8c1bed0"],["032d3b42ed4913a134145f004cf105b66ae97a9914c35fb73d37170d37271acfcd","0322adeb83151937ddcd32d5bf2d3ed07c245811d0f7152716f82120f21fb25426"],["0312759ff0441c59cb477b5ec1b22e76a794cd821c13b8900d72e34e9848f088c2","02d868626604046887d128388e86c595483085f86a395d68920e244013b544ef3b"],["038c4d5f49ab08be619d4fed7161c339ea37317f92d36d4b3487f7934794b79df4","03f4afb40ae7f4a886f9b469a81168ad549ad341390ff91ebf043c4e4bfa05ecc1"],["02378b36e9f84ba387f0605a738288c159a5c277bbea2ea70191ade359bc597dbb","029fd6f0ee075a08308c0ccda7ace4ad9107573d2def988c2e207ac1d69df13355"],["02cfecde7f415b0931fc1ec06055ff127e9c3bec82af5e3affb15191bf995ffc1a","02abb7481504173a7aa1b9860915ef62d09a323425f680d71746be6516f0bb4acf"]],"xpubs":["xpub661MyMwAqRbcF4mZnFnBRYGBaiD9aQRp9w2jaPUrDg3Eery5gywV7eFMzQKmNyY1W4m4fUwsinMw1tFhMNEZ9KjNtkUSBHPXdcXBwCg5ctV","xpub661MyMwAqRbcGHU5H41miJ2wXBLYYk4psK7pB5pWyxK6m5EARwLrKtmpnMzP52qGsKZEtjJCyohVEaZTFXbohjVdfpDFifgMBT82EvkFpsW"]}},"accounts_expanded":{},"addr_history":{"329Ju5tiAr4vHZExAT4KydYEkfKiHraY2N":[],"32HJ13iTVh3sCWyXzipcGb1e78ZxcHrQ7v":[],"32cAdiAapUzNVRYXmDud5J5vEDcGsPHjD8":[],"33fKLmoCo8oFfeV987P6KrNTghSHjJM251":[],"34cE6ZcgXvHEyKbEP2Jpz5C3aEWhvPoPG2":[],"36xsnTKKBojYRHEApVR6bCFbDLp9oqNAxU":[],"372PG6D3chr8tWF3J811dKSpPS84MPU6SE":[],"378nVF8daT4r3jfX1ebKRheUVZX5zaa9wd":[],"392ZtXKp2THrk5VtbandXxFLB8yr2g14aA":[],"39cCrU3Zz3SsHiQUDiyPS1Qd5ZL3Rh1GhQ":[],"3A2cRoBdem5tdRjq514Pp7ZvaxydgZiaNG":[],"3Ceoi3MKdh2xiziHDAzmriwjDx4dvxxLzm":[],"3FcXdG8mh1YeQCYVib8Aw7zwnKpComimLH":[],"3J4b31yAbQkKhejSW7Qz54qNJDEy3t9uSe":[],"3JpJrSxE1GP1X5h82zvLA2TbMZ8nUsGW6z":[],"3K1dzpbcop1MotuqyFQyEuXbvQehaKnGVM":[],"3L8Us8SN22Hj6GnZPRCLaowA1ZtbptXxxL":[],"3LANyoJyShQ8w55tvopoGiZ2BTVjLfChiP":[],"3LoJGQdXTzVaDYudUguP4jNJYy4gNDaRpN":[],"3MD8jVH7Crp5ucFomDnWqB6kQrEQ9VF5xv":[],"3ME8DemkFJSn2tHS23yuk2WfaMP86rd3s7":[],"3MFNr17oSZpFtH16hGPgXz2em2hJkd3SZn":[],"3QHRTYnW2HWCWoeisVcy3xsAFC5xb6UYAK":[],"3QKwygVezHFBthudRUh8V7wwtWjZk3whpB":[],"3QNPY3dznFwRv6VMcKgmn8FGJdsuSRRjco":[],"3QNwwD8dp6kvS8Fys4ZxVJYZAwCXdXQBKo":[]},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3oPcB2UmMA6Cy9W49HLyW6CDNhQuRcn7tGu1tQ2bn6TLw8HFWbu5oP38Z2fFCo5Q4n3fog4DTqywYqfSDWhYbDgVD1TGZoP"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcGHU5H41miJ2wXBLYYk4psK7pB5pWyxK6m5EARwLrKtmpnMzP52qGsKZEtjJCyohVEaZTFXbohjVdfpDFifgMBT82EvkFpsW","x2/":"xpub661MyMwAqRbcF4mZnFnBRYGBaiD9aQRp9w2jaPUrDg3Eery5gywV7eFMzQKmNyY1W4m4fUwsinMw1tFhMNEZ9KjNtkUSBHPXdcXBwCg5ctV"},"pruned_txo":{},"seed":"turkey weapon legend tower style multiply tomorrow wet like frame leave cash achieve","seed_version":11,"stored_height":490035,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2","winpos-qt":[610,418,840,400]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_7_18_seeded(self): - wallet_str = '{"addr_history":{"12nzqpb4vxiFmcvypswSWK1f4cvGwhYAE8":[],"13sapXcP5Wq25PiXh5Zr9mLhyjdfrppWyi":[],"14EzC5y5eFCXg4T7cH4hXoivzysEpGXBTM":[],"15PUQBi2eEzprCZrS8dkfXuoNv8TuqwoBm":[],"16NvXzjxHbiNAULoRRTBjSmecMgF87FAtb":[],"16oyPjLM4R96aZCnSHqBBkDMgbE2ehDWFe":[],"1BfhL8ZPcaZkXTZKASQYcFJsPfXNwCwVMV":[],"1Bn3vun14mDWBDkx4PvK2SyWK1nqB9MSmM":[],"1BrCEnhf763JhVNcZsjGcNmmisBfRkrdcn":[],"1BvXCwXAdaSTES4ENALv3Tw6TJcZbMzu5o":[],"1C2vzgDyPqtvzFRYUgavoLvk3KGujkUUjg":[],"1CN22zUHuX5SxGTmGvPTa2X6qiCJZjDUAW":[],"1CUT9Su42c4MFxrfbrouoniuhVuvRjsKYS":[],"1DLaXDPng4wWXW7AdDG3cLkuKXgEUpjFHq":[],"1DTLcXN6xPUVXP1ZQmt2heXe2KHDSdvRNv":[],"1F1zYJag8yXVnDgGGy7waQT3Sdyp7wLZm3":[],"1Fim67c46NHTcSUu329uF8brTmkoiz6Ej8":[],"1Go6JcgkfZuA7fyQFKuLddee9hzpo31uvL":[],"1J6mhetXo9Eokq7NGjwbKnHryxUCpgbCDn":[],"1K9sFmS7qM2P5JpVGQhHMqQgAnNiujS5jZ":[],"1KBdFn9tGPYEqXnHyJAHxBfCQFF9v3mq95":[],"1LRWRLWHE2pdMviVeTeJBa8nFbUTWSCvrg":[],"1LpXAktoSKbRx7QFkyb2KkSNJXSGLtTg9T":[],"1LtxCQLTqD1q5Q5BReP932t5D7pKx5wiap":[],"1MX5AS3pA5jBhmg4DDuDQEuNhPGS4cGU4F":[],"1Pz9bYFMeqZkXahx9yPjXtJwL69zB3xCp2":[]},"keystore":{"seed":"giraffe tuition frog desk airport rural since dizzy regular victory mind coconut","type":"bip32","xprv":"xprv9s21ZrQH143K28Jvnpm7hU3xPt18neaDpcpoMKTyi9ewNRg6puJ2RAE5gZNPQ73bbmU9WsagxLQ3a6i2t1M9W289HY9Q5sEzFsLaYq3ZQf3","xpub":"xpub661MyMwAqRbcEcPPtrJ84bzgwuqdC7J5BqkQ9hsbGVBvFE1FNScGxxYZXpC9ncowEe7EZVbAerSypw3wCjrmLmsHeG3RzySw5iEJhAfZaZT"},"pruned_txo":{},"pubkeys":{"change":["033e860b0823ed2bf143594b07031d9d95d35f6e4ad6093ddc3071b8d2760f133f","03f51e8798a1a46266dee899bada3e1517a7a57a8402deeef30300a8918c81889a","0308168b05810f62e3d08c61e3c545ccbdce9af603adbdf23dcc366c47f1c5634c","03d7eddff48be72310347efa93f6022ac261cc33ee0704cdad7b6e376e9f90f574","0287e34a1d3fd51efdc83f946f2060f13065e39e587c347b65a579b95ef2307d45","02df34e258a320a11590eca5f0cb0246110399de28186011e8398ce99dd806854a"],"receiving":["031082ff400cbe517cc2ae37492a6811d129b8fb0a8c6bd083313f234e221527ae","03fac4d7402c0d8b290423a05e09a323b51afebd4b5917964ba115f48ab280ef07","03c0a8c4ab604634256d3cfa350c4b6ca294a4374193055195a46626a6adea920f","03b0bc3112231a9bea6f5382f4324f23b4e2deb5f01a90b0fe006b816367e43958","03a59c08c8e2d66523c888416e89fa1aaec679f7043aa5a9145925c7a80568e752","0346fefc07ab2f38b16c8d979a8ffe05bc9f31dd33291b4130797fa7d78f6e4a35","025eb34724546b3c6db2ee8b59fbc4731bafadac5df51bd9bbb20b456d550ef56e","02b79c26e2eac48401d8a278c63eec84dc5bef7a71fa7ce01a6e333902495272e2","03a3a212462a2b12dc33a89a3e85684f3a02a647db3d7eaae18c029a6277c4f8ac","02d13fc5b57c4d057accf42cc918912221c528907a1474b2c6e1b9ca24c9655c1a","023c87c3ca86f25c282d9e6b8583b0856a4888f46666b413622d72baad90a25221","030710e320e9911ebfc89a6b377a5c2e5ae0ab16b9a3df54baa9dbd3eb710bf03c","03406b5199d34be50725db2fcd440e487d13d1f7611e604db81bb06cdd9077ffa5","0378139461735db84ff4d838eb408b9c124e556cfb6bac571ed6b2d0ec671abd0c","030538379532c476f664d8795c0d8e5d29aea924d964c685ea5c2343087f055a82","02d1b93fa37b824b4842c46ef36e5c50aadbac024a6f066b482be382bec6b41e5a","02d64e92d12666cde831eb21e00079ecfc3c4f64728415cc38f899aca32f1a5558","0347480bf4d321f5dce2fcd496598fbdce19825de6ed5b06f602d66de7155ac1c0","03242e3dfd8c4b6947b0fbb0b314620c0c3758600bb842f0848f991e9a2520a81c","021acadf6300cb7f2cca11c6e1c7e59e3cf923a786f6371c3b85dd6f8b65c68470"]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[709,314,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_7_18_importedkeys(self): - wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"pubkeys":{"change":[],"receiving":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2"]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[420,312,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_7_18_watchaddresses(self): - wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[553,402,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_7_18_trezor_singleacc(self): - wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"pubkeys":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"]},"seed_version":13,"stored_height":490013,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[631,410,840,405]}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_7_18_multisig(self): - wallet_str = '{"addr_history":{"32WKXQ6BWtGJDVTpdcUMhtRZWzgk5eKnhD":[],"33rvo2pxaccCV7jLwvth36sdLkdEqhM8B8":[],"347kG9dzt2M1ZPTa2zzcmVrAE75LuZs9A2":[],"34BBeAVEe5AM6xkRebddFG8JH6Vx1M5hHH":[],"34MAGbxxCHPX8ASfKsyNkzpqPEUTZ5i1Kx":[],"36uNpoPSgUhN5Cc1wRQyL77aD1RL3a9X6f":[],"384xygkfYsSuXN478zhN4jmNcky1bPo7Cq":[],"39GBGaGpp1ePBsjjaw8NmbZNZkMzhfmZ3W":[],"3BRhw13g9ShGcuHbHExxtFfvhjrxiSiA7J":[],"3BboKZc2VgjKVxoC5gndLGpwEkPJuQrZah":[],"3C3gKJ2UQNNHY2SG4h43zRS1faSLhnqQEr":[],"3CEY1V5WvCTxjHEPG5BY4eXpcYhakTvULJ":[],"3DJyQ94H9g18PR6hfzZNxwwdU6773JaYHd":[],"3Djb7sWog5ANggPWHm4xT5JiTrTSCmVQ8N":[],"3EfgjpUeJBhp3DcgP9wz3EhHNdkCbiJe2L":[],"3FWgjvaL8xN6ne19WCEeD5xxryyKAQ5tn1":[],"3H4ZtDFovXxwWXCpRo8mrCczjTrtbT6eYL":[],"3HvnjPzpaE3VGWwGTALZBguT8p9fyAcfHS":[],"3JGuY9EpzuZkDLR7vVGhqK7zmX9jhYEfmD":[],"3JvrP4gpCUeQzqgPyDt2XePXn3kpqFTo9i":[],"3K3TVvsfo52gdwz7gk84hfP77gRmpc3hkf":[],"3K5uh5viV4Dac267Q3eNurQQBnpEbYck5G":[],"3KaoWE1m3QrtvxTQLFfvNs8gwQH8kQDpFM":[],"3Koo71MC4wBfiDKTsck7qCrRjtGx2SwZqT":[],"3L8XBt8KxwqNX1vJprp6C9YfNW4hkYrC6d":[],"3QmZjxPwcsHZgVUR2gQ6wdbGJBbFro8KLJ":[]},"pruned_txo":{},"pubkeys":{"change":[["031bfbbfb36b5e526bf4d94bfc59f170177b2c821f7d4d4c0e1ee945467fe031a0","03c4664d68e3948e2017c5c55f7c1aec72c1c15686b07875b0f20d5f856ebeb703"],["03c515314e4b695a809d3ba08c20bef00397a0e2df729eaf17b8e082825395e06b","032391d8ab8cad902e503492f1051129cee42dc389231d3cdba60541d70e163244"],["035934f55c09ecec3e8f2aa72407ee7ba3c2f077be08b92a27bc4e81b5e27643fe","0332b121ed13753a1f573feaf4d0a94bf5dd1839b94018844a30490dd501f5f5fb"],["02b1367f7f07cbe1ef2c75ac83845c173770e42518da20efde3239bf988dbff5ac","03f3a8b9033b3545fbe47cab10a6f42c51393ed6e525371e864109f0865a0af43c"],["02e7c25f25ecc17969a664d5225c37ec76184a8843f7a94655f5ed34b97c52445d","030ae4304923e6d8d6cd67324fa4c8bc44827918da24a05f9240df7c91c8e8db8f"],["02deb653a1d54372dbc8656fe0a461d91bcaec18add290ccaa742bdaefdb9ec69b","023c1384f90273e3fc8bc551e71ace8f34831d4a364e56a6e778cd802b7f7965a6"]],"receiving":[["02d978f23dc1493db4daf066201f25092d91d60c4b749ca438186764e6d80e6aa1","02912a8c05d16800589579f08263734957797d8e4bc32ad7411472d3625fd51f10"],["024a4b4f2553d7f4cc2229922387aad70e5944a5266b2feb15f453cedbb5859b13","03f8c6751ee93a0f4afb7b2263982b849b3d4d13c2e30b3f8318908ad148274b4b"],["03cd88a88aabc4b833b4631f4ffb4b9dc4a0845bb7bc3309fab0764d6aa08c4f25","03568901b1f3fb8db05dd5c2092afc90671c3eb8a34b03f08bcfb6b20adf98f1cd"],["030530ffe2e4a41312a41f708febab4408ca8e431ce382c1eedb837901839b550d","024d53412197fc609a6ca6997c6634771862f2808c155723fac03ea89a5379fdcc"],["02de503d2081b523087ca195dbae55bafb27031a918a1cfedbd2c4c0da7d519902","03f4a27a98e41bddb7543bf81a9c53313bf9cfb2c2ebdb6bf96551221d8aecb01a"],["03504bc595ac0d947299759871bfdcf46bcdd8a0590c44a78b8b69f1b152019418","0291f188301773dbc7c1d12e88e3aa86e6d4a88185a896f02852141e10e7e986ab"],["0389c3ab262b7994d2202e163632a264f49dd5f78517e01c9210b6d0a29f524cd4","034bdfa9cc0c6896cb9488329d14903cfe60a2879771c5568adfc452f8dba1b2cb"],["02c55a517c162aae2cb5b36eef78b51aa15040e7293033a5b55ba299e375da297d","027273faf29e922d95987a09c2554229becb857a68112bd139409eb111e7cdb45e"],["02401e62d645dc64d43f77ba1f360b529a4c644ed3fc15b35932edafbaf741e844","02c44cbffc13cb53134354acd18c54c59fa78ec61307e147fa0f6f536fb030a675"],["02194a538f37b388b2b138f73a37d7fbb9a3e62f6b5a00bad2420650adc4fb44d9","03e5cc15d47fcdcf815baa0e15227bc5e6bd8af6cae6add71f724e95bc29714ce5"],["037ebf7b2029c8ea0c1861f98e0952c544a38b9e7caebbf514ff58683063cd0e78","022850577856c810dead8d3d44f28a3b71aaf21cdc682db1beb8056408b1d57d52"],["02aea7537611754fdafd98f341c5a6827f8301eaf98f5710c02f17a07a8938a30e","032fa37659a8365fdae3b293a855c5a692faca687b0875e9720219f9adf4bdb6c2"],["0224b0b8d200238495c58e1bc83afd2b57f9dbb79f9a1fdb40747bebb51542c8d3","03b88cd2502e62b69185b989abb786a57de27431ece4eabb26c934848d8426cbd6"],["032802b0be2a00a1e28e1e29cfd2ad79d36ef936a0ef1c834b0bbe55c1b2673bff","032669b2d80f9110e49d49480acf696b74ecca28c21e7d9c1dd2743104c54a0b13"],["03fcfa90eac92950dd66058bbef0feb153e05a114af94b6843d15200ef7cf9ea4a","023246268fbe8b9a023d9a3fa413f666853bbf92c4c0af47731fdded51751e0c3a"],["020cf5fffe70b174e242f6193930d352c54109578024677c1a13ffce5e1f9e6a29","03cb996663b9c895c3e04689f0cf1473974023fa0d59416be2a0b01ccdaa3cc484"],["03467e4fff9b33c73b0140393bde3b35a3f804bce79eccf9c53a1f76c59b7452bd","03251c2a041e953c8007d9ee838569d6be9eacfbf65857e875d87c32a8123036d8"],["02192e19803bfa6f55748aada33f778f0ebb22a1c573e5e49cba14b6a431ef1c37","02224ce74f1ee47ba6eaaf75618ce2d4768a041a553ee5eb60b38895f3f6de11dc"],["032679be8a73fa5f72d438d6963857bd9e49aef6134041ca950c70b017c0c7d44f","025a8463f1c68e85753bd2d37a640ab586d8259f21024f6173aeed15a23ad4287b"],["03ab0355c95480f0157ae48126f893a6d434aa1341ad04c71517b104f3eda08d3d","02ba4aadba99ae8dc60515b15a087e8763496fcf4026f5a637d684d0d0f8a5f76c"]]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[523,230,840,405],"x1/":{"seed":"pudding sell evoke crystal try order supply chase fine drive nurse double","type":"bip32","xprv":"xprv9s21ZrQH143K2MK5erSSgeaPA1H7gENYS6grakohkaK2M4tzqo6XAjLoRPcBRW9NbGNpaZN3pdoSKLeiQJwmqdSi3GJWZLnK1Txpbn3zinV","xpub":"xpub661MyMwAqRbcEqPYksyT3nX7i37c5h6PoKcTP9DKJur1DsE9PLQmiXfHGe8RmN538Pj8t3qUQcZXCMrkS5z1uWJ6jf9EptAFbC4Z2nKaEQE"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcGYXvLgWjW91feK49GajmPdEarB3Ny8JDduUhzTcEThc8Xs1GyqMR4S7xPHvSq4sbDEFzQh3hjJJFEksUzvnjYnap5RX9o4j"}}' - self._upgrade_storage(wallet_str) - - # seed_version 13 is ambiguous - # client 2.7.18 created wallets with an earlier "v13" structure - # client 2.8.3 created wallets with a later "v13" structure - # client 2.8.3 did not do a proper clean-slate upgrade - # the wallet here was created in 2.7.18 with a couple privkeys imported - # then opened in 2.8.3, after which a few other new privkeys were imported - # it's in some sense in an "inconsistent" state - def test_upgrade_from_client_2_8_3_importedkeys_flawed_previous_upgrade_from_2_7_18(self): - wallet_str = '{"addr_history":{"15VBrfYwoXvDWyXHq1myxDv4h36qUmCHcE":[],"179vRrzjT9k7k5oCNCx6eodYCaLKPy9UQn":[],"18o6WCBWdAaM5kjKnyEL4HysoT324rvJu7":[],"1A9F6ZEqmfKeuLeEq5eWFxajgiJfGCc7ar":[],"1BTjGNUmeMSPBTuXTdwD3DLyCugAZaFb7w":[],"1CjW4KM38acCRw3spiFKiZsj7xmmQqqwd8":[],"1EaDNLPwHRraX1N3ecPWJ2mm7NRgdtvpCj":[],"1PYtQBkjXHQX6YtMzEgehN638o784pK3ce":[],"1yT2T4ha3i1GZoK2iP8EpcgSNG34R2ufM":[]},"addresses":{"change":[],"receiving":["1PYtQBkjXHQX6YtMzEgehN638o784pK3ce","1yT2T4ha3i1GZoK2iP8EpcgSNG34R2ufM","1CjW4KM38acCRw3spiFKiZsj7xmmQqqwd8","1A9F6ZEqmfKeuLeEq5eWFxajgiJfGCc7ar","18o6WCBWdAaM5kjKnyEL4HysoT324rvJu7","1EaDNLPwHRraX1N3ecPWJ2mm7NRgdtvpCj","179vRrzjT9k7k5oCNCx6eodYCaLKPy9UQn","1BTjGNUmeMSPBTuXTdwD3DLyCugAZaFb7w","15VBrfYwoXvDWyXHq1myxDv4h36qUmCHcE"]},"keystore":{"keypairs":{"0206b77fd06f212ad7d85f4a054c231ba4e7894b1773dcbb449671ee54618ff5e9":"L52LWS2hB5ev9JYiisFewJH9Q16U7yYcSNt3M8UKLmL5p1q3v2H2","028cda4a0f03cbcbc695d9cac0858081fd5458acfd29564127d329553245afca42":"KzRhkN9Psm9BobcPx3X3VykVA8yhCBrVvE4tTyq6NE283sL6uvYG","02ba4117a24d7e38ae14c429fce0d521aa1fb6bb97558a13f1ef2bc0a476a1741f":"KySXfvidmMBf8iw6m3R9WtdfKcQPWXenwMZtpno5XpfLMNHH8PMn","031bb44462038b97010624a8f8cb15a10fd0d277f12aba3ccf5ce0d36fc6df3112":"KxmcmCvNrZFgy2jyz9W353XbMwCYWHzYTQVzbaDfZM4FLxemgmKh","0339081c4a0ce22c01aa78a5d025e7a109100d1a35ef0f8f06a0d4c5f9ffefc042":"L53Ks569m3H1dRzua3nGzBE3AaEV8dMvBoHDeSJGnWEDeL775mJ5","0339ea71aba2805238e636c2f1b3e5a8308b1dbdbb335787c51f2f6bf3f6218643":"KwHDUpfvnSC58bs3nGy7YpducXkbmo6UUHrydBHy6sT1mRJcVvBo","04e7dc460c87267cf0958d6904d9cd99a4af0d64d61858636aec7a02e5f9a578d27c1329d5ddc45a937130ed4a59e4147cb4907724321baa6a976f9972a17f79ba":"5JECca5E7r1eNgME7NsPdE29XiVCVwXSzEihnhAQXuMdsJ4VL8S","04e9ad0bf70c51c06c2459961175c47cfec59d58ebef4ffcd9836904ef11230afce03ab5eaac5958b538382195b5aea9bf057c0486079869bb72ef9c958f33f1ed":"5Jt9rGLWgxoJUo4eoYEECskLmRA4BkZqHPHg7DdghKBaWarKuxW","04f8cbd67830ab37138c92898a64a4edf836a60aa5b36956547788bd205c635d6a3056fa6a079961384ae336e737d4c45835821c8915dbc5e18a7def88df83946b":"5KRjCNThRDP8aQTJ3Hq9HUSVNRNUB2e69xwLfMUsrXYLXT7U8b9"},"type":"imported"},"pruned_txo":{},"pubkeys":{"change":[],"receiving":["04e9ad0bf70c51c06c2459961175c47cfec59d58ebef4ffcd9836904ef11230afce03ab5eaac5958b538382195b5aea9bf057c0486079869bb72ef9c958f33f1ed","0339081c4a0ce22c01aa78a5d025e7a109100d1a35ef0f8f06a0d4c5f9ffefc042","0339ea71aba2805238e636c2f1b3e5a8308b1dbdbb335787c51f2f6bf3f6218643","02ba4117a24d7e38ae14c429fce0d521aa1fb6bb97558a13f1ef2bc0a476a1741f","028cda4a0f03cbcbc695d9cac0858081fd5458acfd29564127d329553245afca42","04e7dc460c87267cf0958d6904d9cd99a4af0d64d61858636aec7a02e5f9a578d27c1329d5ddc45a937130ed4a59e4147cb4907724321baa6a976f9972a17f79ba","04f8cbd67830ab37138c92898a64a4edf836a60aa5b36956547788bd205c635d6a3056fa6a079961384ae336e737d4c45835821c8915dbc5e18a7def88df83946b"]},"seed_version":13,"stored_height":492756,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_8_3_seeded(self): - wallet_str = '{"addr_history":{"13sNgoAhqDUTB3YSzWYcKKvP2EczG5JGmt":[],"14C6nXs2GRaK3o5U5e8dJSpVRCoqTsyAkJ":[],"14fH7oRM4bqJtJkgJEynTShcUXQwdxH6mw":[],"16FECc7nP2wor1ijXKihGofUoCkoJnq6XR":[],"16cMJC5ZAtPnvLQBzfHm9YR9GoDxUseMEk":[],"17CbQhK3gutqgWt2iLX69ZeSCvw8yFxPLz":[],"17jEaAyekE8BHPvPmkJqFUh1v1GSi6ywoV":[],"19F5SjaWYVCKMPWR8q1Freo4RGChSmFztL":[],"19snysSPZEbgjmeMtuT7qDMTLH2fa7zrWW":[],"1AFgvLGNHP3nZDNrZ4R2BZKnbwDVAEUP4q":[],"1AwWgUbjQfRhKVLKm1o7qfpXnqeN3cu7Ms":[],"1B4FU2WEd2NQzd2MkWBLHw87uJhBxoVghh":[],"1BEBouVJFihDmEQMTAv4bNV2Q7dZh5iJzv":[],"1BdB7ahc8TSR9RJDmWgGSgsWji2BgzcVvC":[],"1DGhQ1up6dMieEwFdsQQFHRriyyR59rYVq":[],"1HBAAqFVndXBcWdWQNYVYSDK9kdUu8ZRU3":[],"1HMrRJkTayNRBZdXZKVb7oLZKj24Pq65T6":[],"1HiB2QCfNem8b4cJaZ2Rt9T4BbUCPXvTpT":[],"1HkbtbyocwHWjKBmzKmq8szv3cFgSGy7dL":[],"1K5CWjgZEYcKTsJWeQrH6NcMPzFUAikD8z":[],"1KMDUXdqpthH1XZU4q5kdSoMZmCW9yDMcN":[],"1KmHNiNmeS7tWRLYTFDMrTbKR6TERYicst":[],"1NQwmHYdxU1pFTTWyptn8vPW1hsSWJBRTn":[],"1NuPofeK8yNEjtVAu9Rc2pKS9kw8YWUatL":[],"1Q3eTNJWTnfxPkUJXQkeCqPh1cBQjjEXFn":[],"1QEuVTdenchPn9naMhakYx8QwGUXE6JYp":[]},"addresses":{"change":["1K5CWjgZEYcKTsJWeQrH6NcMPzFUAikD8z","19snysSPZEbgjmeMtuT7qDMTLH2fa7zrWW","1DGhQ1up6dMieEwFdsQQFHRriyyR59rYVq","17CbQhK3gutqgWt2iLX69ZeSCvw8yFxPLz","1Q3eTNJWTnfxPkUJXQkeCqPh1cBQjjEXFn","17jEaAyekE8BHPvPmkJqFUh1v1GSi6ywoV"],"receiving":["1KMDUXdqpthH1XZU4q5kdSoMZmCW9yDMcN","1HkbtbyocwHWjKBmzKmq8szv3cFgSGy7dL","1HiB2QCfNem8b4cJaZ2Rt9T4BbUCPXvTpT","14fH7oRM4bqJtJkgJEynTShcUXQwdxH6mw","1NuPofeK8yNEjtVAu9Rc2pKS9kw8YWUatL","16FECc7nP2wor1ijXKihGofUoCkoJnq6XR","19F5SjaWYVCKMPWR8q1Freo4RGChSmFztL","1NQwmHYdxU1pFTTWyptn8vPW1hsSWJBRTn","1HBAAqFVndXBcWdWQNYVYSDK9kdUu8ZRU3","1B4FU2WEd2NQzd2MkWBLHw87uJhBxoVghh","1HMrRJkTayNRBZdXZKVb7oLZKj24Pq65T6","1KmHNiNmeS7tWRLYTFDMrTbKR6TERYicst","1BdB7ahc8TSR9RJDmWgGSgsWji2BgzcVvC","14C6nXs2GRaK3o5U5e8dJSpVRCoqTsyAkJ","1AFgvLGNHP3nZDNrZ4R2BZKnbwDVAEUP4q","13sNgoAhqDUTB3YSzWYcKKvP2EczG5JGmt","1AwWgUbjQfRhKVLKm1o7qfpXnqeN3cu7Ms","1QEuVTdenchPn9naMhakYx8QwGUXE6JYp","1BEBouVJFihDmEQMTAv4bNV2Q7dZh5iJzv","16cMJC5ZAtPnvLQBzfHm9YR9GoDxUseMEk"]},"keystore":{"seed":"novel clay width echo swing blanket absorb salute asset under ginger final","type":"bip32","xprv":"xprv9s21ZrQH143K2jfFF6ektPj6zCCsDGGjQxhD2FQ21j6yrA1piWWEjch2kf1smzB2rzm8rPkdJuHf3vsKqMX9ogtE2A7JF49qVUHrgtjRymM","xpub":"xpub661MyMwAqRbcFDjiM8BmFXfqYE3McizanBcopdoda4dxixLyG3pVHR1WbwgjLo9RL882KRfpfpxh7a7zXPogDdR4xj9TpJWJGsbwaodLSKe"},"pruned_txo":{},"seed_type":"standard","seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_8_3_importedkeys(self): - wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_8_3_watchaddresses(self): - wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[535,380,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_8_3_trezor_singleacc(self): - wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"addresses":{"change":["1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ","14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM","1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG","15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6","1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL","1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs"],"receiving":["1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu","18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw","17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH","12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC","15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ","1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid","1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz","1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj","146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz","1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC","1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo","1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb","1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe","1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv","1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp","15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S","1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX","1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp","1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk","1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD"]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[744,390,840,405]}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_8_3_multisig(self): - wallet_str = '{"addr_history":{"32Qk6Q7XYD2v3et9g5fA97ky8XRAJNDZCS":[],"339axnadPaQg3ngChNBKap2dndUWrSwjk6":[],"34FG8qzA6UYLxrnkpVkM9mrGYix3ZyePJZ":[],"35CR3h2dFF3EkRX5yK47NGuF2FcLtJvpUM":[],"35zrocLBQbHfEqysgv2v5z3RH7BRGQzSMJ":[],"36uBJPkgiQwav23ybewbgkQ2zEzJDY2EX1":[],"37nSiBvGXm1PNYseymaJn5ERcU4mSMueYc":[],"39r4XCmfU4J3N98YQ8Fwvm8VN1Fukfj7QW":[],"3BDqFoYMxyy7nWCpRChYV6YCGh9qnWDmav":[],"3CGCLSHU8ZjeXv6oukJ3eAQN4fqEQ7wuyX":[],"3DCNnfh7oWLsnS3p5QdWfW3hvcFF8qAPFq":[],"3DPheE9uany9ET2qBnWF1wh3zDtptGP6Ts":[],"3EeNJHgSYVJPxYR2NaYv2M2ZnXkPRWSHQh":[],"3FWZ7pJPxZhGr8p6HNr9LLsHA8sABcP7cF":[],"3FZbzEF9HdRqzif2cKUFnwW9AFTJcibjVK":[],"3GEhQHTrWykC6Jfu923qtpxJECsEGVdhUc":[],"3HJ95uxwW6rMoEhYgUfcgpd3ExU3fjkfNb":[],"3HbdMVgKRqadNiHRNGizUCyTQYpJ1aXFav":[],"3J6xRF9d16QNsvoXkYkeTwTU8L5N3Y8f7c":[],"3JBbS3GvhvoLgtLcuMvHCtqjE7dnbpTMkz":[],"3KNWZasWDBuVzzp5Y5cbEgjeYn3NKHZKso":[],"3KQ5tTEbkQSkKiccKFDPrhLnBjSMey6CQM":[],"3KrFHcAzNJYjukGDDZm2HeV5Mok4NGQaD6":[],"3LNZbX9wenL3bLxJTQnPidSvVt3EtDrnUg":[],"3LzjAqqfiN8w4TSiW8Up7bKLD5CicBUC3a":[],"3Nro51wauHugv72NMtY9pmLnwX3FXWU1eE":[]},"addresses":{"change":["34FG8qzA6UYLxrnkpVkM9mrGYix3ZyePJZ","3LzjAqqfiN8w4TSiW8Up7bKLD5CicBUC3a","3GEhQHTrWykC6Jfu923qtpxJECsEGVdhUc","3Nro51wauHugv72NMtY9pmLnwX3FXWU1eE","3JBbS3GvhvoLgtLcuMvHCtqjE7dnbpTMkz","3CGCLSHU8ZjeXv6oukJ3eAQN4fqEQ7wuyX"],"receiving":["35zrocLBQbHfEqysgv2v5z3RH7BRGQzSMJ","3FWZ7pJPxZhGr8p6HNr9LLsHA8sABcP7cF","3DPheE9uany9ET2qBnWF1wh3zDtptGP6Ts","3HbdMVgKRqadNiHRNGizUCyTQYpJ1aXFav","3KQ5tTEbkQSkKiccKFDPrhLnBjSMey6CQM","35CR3h2dFF3EkRX5yK47NGuF2FcLtJvpUM","3HJ95uxwW6rMoEhYgUfcgpd3ExU3fjkfNb","3FZbzEF9HdRqzif2cKUFnwW9AFTJcibjVK","39r4XCmfU4J3N98YQ8Fwvm8VN1Fukfj7QW","3LNZbX9wenL3bLxJTQnPidSvVt3EtDrnUg","32Qk6Q7XYD2v3et9g5fA97ky8XRAJNDZCS","339axnadPaQg3ngChNBKap2dndUWrSwjk6","3EeNJHgSYVJPxYR2NaYv2M2ZnXkPRWSHQh","3BDqFoYMxyy7nWCpRChYV6YCGh9qnWDmav","3DCNnfh7oWLsnS3p5QdWfW3hvcFF8qAPFq","3KNWZasWDBuVzzp5Y5cbEgjeYn3NKHZKso","37nSiBvGXm1PNYseymaJn5ERcU4mSMueYc","3KrFHcAzNJYjukGDDZm2HeV5Mok4NGQaD6","36uBJPkgiQwav23ybewbgkQ2zEzJDY2EX1","3J6xRF9d16QNsvoXkYkeTwTU8L5N3Y8f7c"]},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[671,238,840,405],"x1/":{"seed":"property play install hill hunt follow trash comic pulse consider canyon limit","type":"bip32","xprv":"xprv9s21ZrQH143K46tCjDh5i4H9eSJpnMrYyLUbVZheTbNjiamdxPiffMEYLgxuYsMFokFrNEZ6S6z5wSXXszXaCVQWf6jzZvn14uYZhsnM9Sb","xpub":"xpub661MyMwAqRbcGaxfqFE65CDtCU9KBpaQLZQCHx7G1vuibP6nVw2vD9Z2Bz2DsH43bDZGXjmcvx2TD9wq3CmmFcoT96RCiDd1wMSUB2UH7Gu"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcEncvVc1zrPFZSKe7iAP1LTRhzxuXpmztu1kTtnfj8XNFzzmGH1X1gcGxczBZ3MmYKkxXgZKJCsNXXdasNaQJKJE4KcUjn1L"}}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_9_3_seeded(self): - wallet_str = '{"addr_history":{"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes":[],"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1":[],"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB":[],"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c":[],"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz":[],"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA":[],"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV":[],"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z":[],"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv":[],"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B":[],"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz":[],"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G":[],"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq":[],"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d":[],"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs":[],"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado":[],"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z":[],"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52":[],"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP":[],"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv":[],"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb":[],"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ":[],"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G":[],"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN":[],"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J":[],"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt":[]},"addresses":{"change":["1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP","1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z","15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV","1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq","19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G","1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb"],"receiving":["14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA","13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB","19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz","1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv","1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt","13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c","1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ","12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes","12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1","14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz","1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN","17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z","1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado","18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv","1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G","18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B","1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d","1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs","1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52","1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J"]},"keystore":{"seed":"cereal wise two govern top pet frog nut rule sketch bundle logic","type":"bip32","xprv":"xprv9s21ZrQH143K29XjRjUs6MnDB9wXjXbJP2kG1fnRk8zjdDYWqVkQYUqaDtgZp5zPSrH5PZQJs8sU25HrUgT1WdgsPU8GbifKurtMYg37d4v","xpub":"xpub661MyMwAqRbcEdcCXm1sTViwjBn28zK9kFfrp4C3JUXiW1sfP34f6HA45B9yr7EH5XGzWuTfMTdqpt9XPrVQVUdgiYb5NW9m8ij1FSZgGBF"},"pruned_txo":{},"seed_type":"standard","seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[619,310,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_9_3_importedkeys(self): - wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_9_3_watchaddresses(self): - wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":490039,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[499,386,840,405]}' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_9_3_trezor_singleacc(self): - wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"addresses":{"change":["1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ","14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM","1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG","15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6","1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL","1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs"],"receiving":["1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu","18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw","17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH","12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC","15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ","1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid","1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz","1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj","146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz","1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC","1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo","1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb","1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe","1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv","1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp","15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S","1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX","1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp","1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk","1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD"]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"seed_version":13,"stored_height":490014,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[753,486,840,405]}''' - self._upgrade_storage(wallet_str) - - def test_upgrade_from_client_2_9_3_multisig(self): - wallet_str = '{"addr_history":{"31uiqKhw4PQSmZWnCkqpeh6moB8B1jXEt3":[],"32PBjkXmwRoEQt8HBZcAEUbNwaHw5dR5fe":[],"33FQMD675LMRLZDLYLK7QV6TMYA1uYW1sw":[],"33MQEs6TCgxmAJhZvUEXYr6gCkEoEYzUfm":[],"33vuhs2Wor9Xkax66ucDkscPcU6nQHw8LA":[],"35tbMt1qBGmy5RNcsdGZJgs7XVbf5gEgPs":[],"36zhHEtGA33NjHJdxCMjY6DLeU2qxhiLUE":[],"37rZuTsieKVpRXshwrY8qvFBn6me42mYr5":[],"38A2KDXYRmRKZRRCGgazrj19i22kDr8d4V":[],"38GZH5GhxLKi5so9Aka6orY2EDZkvaXdxm":[],"3AEtxrCwiYv5Y5CRmHn1c5nZnV3Hpfh5BM":[],"3AaHWprY1MytygvQVDLp6i63e9o5CwMSN5":[],"3DAD19hHXNxAfZjCtUbWjZVxw1fxQqCbY7":[],"3GK4CBbgwumoeR9wxJjr1QnfnYhGUEzHhN":[],"3H18xmkyX3XAb5MwucqKpEhTnh3qz8V4Mn":[],"3JhkakvHAyFvukJ3cyaVgiyaqjYNo2gmsS":[],"3JtA4x1AKW4BR5YAEeLR5D157Nd92NHArC":[],"3KQosfGFGsUniyqsidE2Y4Bz1y4iZUkGW6":[],"3KXe1z2Lfk22zL6ggQJLpHZfc9dKxYV95p":[],"3KZiENj4VHdUycv9UDts4ojVRsaMk8LC5c":[],"3KeTKHJbkZN1QVkvKnHRqYDYP7UXsUu6va":[],"3L5aZKtDKSd65wPLMRooNtWHkKd5Mz6E3i":[],"3LAPqjqW4C2Se9HNziUhNaJQS46X1r9p3M":[],"3P3JJPoyNFussuyxkDbnYevYim5XnPGmwZ":[],"3PgNdMYSaPRymskby885DgKoTeA1uZr6Gi":[],"3Pm7DaUzaDMxy2mW5WzHp1sE9hVWEpdf7J":[]},"addresses":{"change":["31uiqKhw4PQSmZWnCkqpeh6moB8B1jXEt3","3JhkakvHAyFvukJ3cyaVgiyaqjYNo2gmsS","3GK4CBbgwumoeR9wxJjr1QnfnYhGUEzHhN","3LAPqjqW4C2Se9HNziUhNaJQS46X1r9p3M","33MQEs6TCgxmAJhZvUEXYr6gCkEoEYzUfm","3AEtxrCwiYv5Y5CRmHn1c5nZnV3Hpfh5BM"],"receiving":["3P3JJPoyNFussuyxkDbnYevYim5XnPGmwZ","33FQMD675LMRLZDLYLK7QV6TMYA1uYW1sw","3DAD19hHXNxAfZjCtUbWjZVxw1fxQqCbY7","3AaHWprY1MytygvQVDLp6i63e9o5CwMSN5","3H18xmkyX3XAb5MwucqKpEhTnh3qz8V4Mn","36zhHEtGA33NjHJdxCMjY6DLeU2qxhiLUE","37rZuTsieKVpRXshwrY8qvFBn6me42mYr5","38A2KDXYRmRKZRRCGgazrj19i22kDr8d4V","38GZH5GhxLKi5so9Aka6orY2EDZkvaXdxm","33vuhs2Wor9Xkax66ucDkscPcU6nQHw8LA","3L5aZKtDKSd65wPLMRooNtWHkKd5Mz6E3i","3KXe1z2Lfk22zL6ggQJLpHZfc9dKxYV95p","3KQosfGFGsUniyqsidE2Y4Bz1y4iZUkGW6","3KZiENj4VHdUycv9UDts4ojVRsaMk8LC5c","32PBjkXmwRoEQt8HBZcAEUbNwaHw5dR5fe","3KeTKHJbkZN1QVkvKnHRqYDYP7UXsUu6va","3JtA4x1AKW4BR5YAEeLR5D157Nd92NHArC","3PgNdMYSaPRymskby885DgKoTeA1uZr6Gi","3Pm7DaUzaDMxy2mW5WzHp1sE9hVWEpdf7J","35tbMt1qBGmy5RNcsdGZJgs7XVbf5gEgPs"]},"pruned_txo":{},"seed_version":13,"stored_height":485855,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[617,227,840,405],"x1/":{"seed":"speed cruise market wasp ability alarm hold essay grass coconut tissue recipe","type":"bip32","xprv":"xprv9s21ZrQH143K48ig2wcAuZoEKaYdNRaShKFR3hLrgwsNW13QYRhXH6gAG1khxim6dw2RtAzF8RWbQxr1vvWUJFfEu2SJZhYbv6pfreMpuLB","xpub":"xpub661MyMwAqRbcGco98y9BGhjxscP7mtJJ4YB1r5kUFHQMNoNZ5y1mptze7J37JypkbrmBdnqTvSNzxL7cE1FrHg16qoj9S12MUpiYxVbTKQV"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcGrCDZaVs9VC7Z6579tsGvpqyDYZEHKg2MXoDkxhrWoukqvwDPXKdxVkYA6Hv9XHLETptfZfNpcJZmsUThdXXkTNGoBjQv1o"}}' - self._upgrade_storage(wallet_str) - -########## - - @classmethod - def setUpClass(cls): - super().setUpClass() - from lib.plugins import Plugins - from lib.simple_config import SimpleConfig - - cls.electrum_path = tempfile.mkdtemp() - config = SimpleConfig({'electrum_path': cls.electrum_path}) - - gui_name = 'cmdline' - # TODO it's probably wasteful to load all plugins... only need Trezor - Plugins(config, True, gui_name) - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - shutil.rmtree(cls.electrum_path) - - def _upgrade_storage(self, wallet_json, accounts=1): - storage = self._load_storage_from_json_string(wallet_json, manual_upgrades=True) - - if accounts == 1: - self.assertFalse(storage.requires_split()) - if storage.requires_upgrade(): - storage.upgrade() - self._sanity_check_upgraded_storage(storage) - else: - self.assertTrue(storage.requires_split()) - new_paths = storage.split_accounts() - self.assertEqual(accounts, len(new_paths)) - for new_path in new_paths: - new_storage = WalletStorage(new_path, manual_upgrades=False) - self._sanity_check_upgraded_storage(new_storage) - - def _sanity_check_upgraded_storage(self, storage): - self.assertFalse(storage.requires_split()) - self.assertFalse(storage.requires_upgrade()) - w = Wallet(storage) - - def _load_storage_from_json_string(self, wallet_json, manual_upgrades=True): - with open(self.wallet_path, "w") as f: - f.write(wallet_json) - storage = WalletStorage(self.wallet_path, manual_upgrades=manual_upgrades) - return storage diff --git a/lib/tests/test_transaction.py b/lib/tests/test_transaction.py @@ -1,813 +0,0 @@ -import unittest - -from lib import transaction -from lib.bitcoin import TYPE_ADDRESS -from lib.keystore import xpubkey_to_address -from lib.util import bh2u, bfh - -from . import SequentialTestCase, TestCaseForTestnet -from .test_bitcoin import needs_test_with_all_ecc_implementations - -unsigned_blob = '45505446ff0001000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000005701ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' -signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' -v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700" -signed_segwit_blob = "01000000000101b66d722484f2db63e827ebf41d02684fed0c6550e85015a6c9d41ef216a8a6f00000000000fdffffff0280c3c90100000000160014b65ce60857f7e7892b983851c2a8e3526d09e4ab64bac30400000000160014c478ebbc0ab2097706a98e10db7cf101839931c4024730440220789c7d47f876638c58d98733c30ae9821c8fa82b470285dcdf6db5994210bf9f02204163418bbc44af701212ad42d884cc613f3d3d831d2d0cc886f767cca6e0235e012103083a6dc250816d771faa60737bfe78b23ad619f6b458e0a1f1688e3a0605e79c00000000" - -signed_blob_signatures = ['3046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501', ] - -class TestBCDataStream(SequentialTestCase): - - def test_compact_size(self): - s = transaction.BCDataStream() - values = [0, 1, 252, 253, 2**16-1, 2**16, 2**32-1, 2**32, 2**64-1] - for v in values: - s.write_compact_size(v) - - with self.assertRaises(transaction.SerializationError): - s.write_compact_size(-1) - - self.assertEqual(bh2u(s.input), - '0001fcfdfd00fdfffffe00000100feffffffffff0000000001000000ffffffffffffffffff') - for v in values: - self.assertEqual(s.read_compact_size(), v) - - with self.assertRaises(transaction.SerializationError): - s.read_compact_size() - - def test_string(self): - s = transaction.BCDataStream() - with self.assertRaises(transaction.SerializationError): - s.read_string() - - msgs = ['Hello', ' ', 'World', '', '!'] - for msg in msgs: - s.write_string(msg) - for msg in msgs: - self.assertEqual(s.read_string(), msg) - - with self.assertRaises(transaction.SerializationError): - s.read_string() - - def test_bytes(self): - s = transaction.BCDataStream() - s.write(b'foobar') - self.assertEqual(s.read_bytes(3), b'foo') - self.assertEqual(s.read_bytes(2), b'ba') - self.assertEqual(s.read_bytes(4), b'r') - self.assertEqual(s.read_bytes(1), b'') - -class TestTransaction(SequentialTestCase): - - @needs_test_with_all_ecc_implementations - def test_tx_unsigned(self): - expected = { - 'inputs': [{ - 'type': 'p2pkh', - 'address': '1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD', - 'num_sig': 1, - 'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a', - 'prevout_n': 0, - 'pubkeys': ['02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6'], - 'scriptSig': '01ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000', - 'sequence': 4294967295, - 'signatures': [None], - 'x_pubkeys': ['ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000']}], - 'lockTime': 0, - 'outputs': [{ - 'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', - 'prevout_n': 0, - 'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac', - 'type': TYPE_ADDRESS, - 'value': 1000000}], - 'partial': True, - 'segwit_ser': False, - 'version': 1, - } - tx = transaction.Transaction(unsigned_blob) - self.assertEqual(tx.deserialize(), expected) - self.assertEqual(tx.deserialize(), None) - - self.assertEqual(tx.as_dict(), {'hex': unsigned_blob, 'complete': False, 'final': True}) - self.assertEqual(tx.get_outputs(), [('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', 1000000)]) - self.assertEqual(tx.get_output_addresses(), ['14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs']) - - self.assertTrue(tx.has_address('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs')) - self.assertTrue(tx.has_address('1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD')) - self.assertFalse(tx.has_address('1CQj15y1N7LDHp7wTt28eoD1QhHgFgxECH')) - - self.assertEqual(tx.serialize(), unsigned_blob) - - tx.update_signatures(signed_blob_signatures) - self.assertEqual(tx.raw, signed_blob) - - tx.update(unsigned_blob) - tx.raw = None - blob = str(tx) - self.assertEqual(transaction.deserialize(blob), expected) - - @needs_test_with_all_ecc_implementations - def test_tx_signed(self): - expected = { - 'inputs': [{'address': None, - 'num_sig': 0, - 'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a', - 'prevout_n': 0, - 'scriptSig': '493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6', - 'sequence': 4294967295, - 'type': 'unknown'}], - 'lockTime': 0, - 'outputs': [{ - 'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', - 'prevout_n': 0, - 'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac', - 'type': TYPE_ADDRESS, - 'value': 1000000}], - 'partial': False, - 'segwit_ser': False, - 'version': 1 - } - tx = transaction.Transaction(signed_blob) - self.assertEqual(tx.deserialize(), expected) - self.assertEqual(tx.deserialize(), None) - self.assertEqual(tx.as_dict(), {'hex': signed_blob, 'complete': True, 'final': True}) - - self.assertEqual(tx.serialize(), signed_blob) - - tx.update_signatures(signed_blob_signatures) - - self.assertEqual(tx.estimated_total_size(), 193) - self.assertEqual(tx.estimated_base_size(), 193) - self.assertEqual(tx.estimated_witness_size(), 0) - self.assertEqual(tx.estimated_weight(), 772) - self.assertEqual(tx.estimated_size(), 193) - - def test_estimated_output_size(self): - estimated_output_size = transaction.Transaction.estimated_output_size - self.assertEqual(estimated_output_size('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), 34) - self.assertEqual(estimated_output_size('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), 32) - self.assertEqual(estimated_output_size('bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af'), 31) - self.assertEqual(estimated_output_size('bc1qnvks7gfdu72de8qv6q6rhkkzu70fqz4wpjzuxjf6aydsx7wxfwcqnlxuv3'), 43) - - # TODO other tests for segwit tx - def test_tx_signed_segwit(self): - tx = transaction.Transaction(signed_segwit_blob) - - self.assertEqual(tx.estimated_total_size(), 222) - self.assertEqual(tx.estimated_base_size(), 113) - self.assertEqual(tx.estimated_witness_size(), 109) - self.assertEqual(tx.estimated_weight(), 561) - self.assertEqual(tx.estimated_size(), 141) - - def test_errors(self): - with self.assertRaises(TypeError): - transaction.Transaction.pay_script(output_type=None, addr='') - - with self.assertRaises(BaseException): - xpubkey_to_address('') - - def test_parse_xpub(self): - res = xpubkey_to_address('fe4e13b0f311a55b8a5db9a32e959da9f011b131019d4cebe6141b9e2c93edcbfc0954c358b062a9f94111548e50bde5847a3096b8b7872dcffadb0e9579b9017b01000200') - self.assertEqual(res, ('04ee98d63800824486a1cf5b4376f2f574d86e0a3009a6448105703453f3368e8e1d8d090aaecdd626a45cc49876709a3bbb6dc96a4311b3cac03e225df5f63dfc', '19h943e4diLc68GXW7G75QNe2KWuMu7BaJ')) - - def test_version_field(self): - tx = transaction.Transaction(v2_blob) - self.assertEqual(tx.txid(), "b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe") - - def test_get_address_from_output_script(self): - # the inverse of this test is in test_bitcoin: test_address_to_script - addr_from_script = lambda script: transaction.get_address_from_output_script(bfh(script)) - ADDR = transaction.TYPE_ADDRESS - - # bech32 native segwit - # test vectors from BIP-0173 - self.assertEqual((ADDR, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6')) - self.assertEqual((ADDR, 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')) - self.assertEqual((ADDR, 'bc1sw50qa3jx3s'), addr_from_script('6002751e')) - self.assertEqual((ADDR, 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), addr_from_script('5210751e76e8199196d454941c45d1b3a323')) - - # base58 p2pkh - self.assertEqual((ADDR, '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac')) - self.assertEqual((ADDR, '1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac')) - - # base58 p2sh - self.assertEqual((ADDR, '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487')) - self.assertEqual((ADDR, '3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387')) - -##### - - def _run_naive_tests_on_tx(self, raw_tx, txid): - tx = transaction.Transaction(raw_tx) - self.assertEqual(txid, tx.txid()) - self.assertEqual(raw_tx, tx.serialize()) - self.assertTrue(tx.estimated_size() >= 0) - - def test_txid_coinbase_to_p2pk(self): - raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4103400d0302ef02062f503253482f522cfabe6d6dd90d39663d10f8fd25ec88338295d4c6ce1c90d4aeb368d8bdbadcc1da3b635801000000000000000474073e03ffffffff013c25cf2d01000000434104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac00000000' - txid = 'dbaf14e1c476e76ea05a8b71921a46d6b06f0a950f17c5f9f1a03b8fae467f10' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_coinbase_to_p2pkh(self): - raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff25033ca0030400001256124d696e656420627920425443204775696c640800000d41000007daffffffff01c00d1298000000001976a91427a1f12771de5cc3b73941664b2537c15316be4388ac00000000' - txid = '4328f9311c6defd9ae1bd7f4516b62acf64b361eb39dfcf09d9925c5fd5c61e8' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_segwit_coinbase_to_p2pk(self): - raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502cd010101ffffffff0240be402500000000232103f4e686cdfc96f375e7c338c40c9b85f4011bb843a3e62e46a1de424ef87e9385ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000' - txid = 'fb5a57c24e640a6d8d831eb6e41505f3d54363c507da3733b098d820e3803301' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_segwit_coinbase_to_p2pkh(self): - raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502c3010101ffffffff0240be4025000000001976a9141ea896d897483e0eb33dd6423f4a07970d0a0a2788ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000' - txid = 'ed3d100577477d799107eba97e76770b3efa253c7200e9abfb43da5d2b33513e' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_segwit_coinbase_to_p2sh(self): - raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff050214030101ffffffff02902f50090000000017a914ba582096f8647ca4195f55c8ef7e7e6e120e88b1870000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000' - txid = 'e28ee5866ec0535fe5efac5ad350cbf4960ed981b471a0c4a6baad1d8168d3d7' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2pk_to_p2pkh(self): - raw_tx = '010000000118231a31d2df84f884ced6af11dc24306319577d4d7c340124a7e2dd9c314077000000004847304402200b6c45891aed48937241907bc3e3868ee4c792819821fcde33311e5a3da4789a02205021b59692b652a01f5f009bd481acac2f647a7d9c076d71d85869763337882e01fdffffff016c95052a010000001976a9149c4891e7791da9e622532c97f43863768264faaf88ac00000000' - txid = '90ba90a5b115106d26663fce6c6215b8699c5d4b2672dd30756115f3337dddf9' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2pk_to_p2sh(self): - raw_tx = '0100000001e4643183d6497823576d17ac2439fb97eba24be8137f312e10fcc16483bb2d070000000048473044022032bbf0394dfe3b004075e3cbb3ea7071b9184547e27f8f73f967c4b3f6a21fa4022073edd5ae8b7b638f25872a7a308bb53a848baa9b9cc70af45fcf3c683d36a55301fdffffff011821814a0000000017a9143c640bc28a346749c09615b50211cb051faff00f8700000000' - txid = '172bdf5a690b874385b98d7ab6f6af807356f03a26033c6a65ab79b4ac2085b5' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2pk_to_p2wpkh(self): - raw_tx = '01000000015e5e2bf15f5793fdfd01e0ccd380033797ed2d4dba9498426ca84904176c26610000000049483045022100c77aff69f7ab4bb148f9bccffc5a87ee893c4f7f7f96c97ba98d2887a0f632b9022046367bdb683d58fa5b2e43cfc8a9c6d57724a27e03583942d8e7b9afbfeea5ab01fdffffff017289824a00000000160014460fc70f208bffa9abf3ae4abbd2f629d9cdcf5900000000' - txid = 'ca554b1014952f900aa8cf6e7ab02137a6fdcf933ad6a218de3891a2ef0c350d' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2pkh_to_p2pkh(self): - raw_tx = '0100000001f9dd7d33f315617530dd72264b5d9c69b815626cce3f66266d1015b1a590ba90000000006a4730440220699bfee3d280a499daf4af5593e8750b54fef0557f3c9f717bfa909493a84f60022057718eec7985b7796bb8630bf6ea2e9bf2892ac21bd6ab8f741a008537139ffe012103b4289890b40590447b57f773b5843bf0400e9cead08be225fac587b3c2a8e973fdffffff01ec24052a010000001976a914ce9ff3d15ed5f3a3d94b583b12796d063879b11588ac00000000' - txid = '24737c68f53d4b519939119ed83b2a8d44d716d7f3ca98bcecc0fbb92c2085ce' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2pkh_to_p2sh(self): - raw_tx = '010000000195232c30f6611b9f2f82ec63f5b443b132219c425e1824584411f3d16a7a54bc000000006b4830450221009f39ac457dc8ff316e5cc03161c9eff6212d8694ccb88d801dbb32e85d8ed100022074230bb05e99b85a6a50d2b71e7bf04d80be3f1d014ea038f93943abd79421d101210317be0f7e5478e087453b9b5111bdad586038720f16ac9658fd16217ffd7e5785fdffffff0200e40b540200000017a914d81df3751b9e7dca920678cc19cac8d7ec9010b08718dfd63c2c0000001976a914303c42b63569ff5b390a2016ff44651cd84c7c8988acc7010000' - txid = '155e4740fa59f374abb4e133b87247dccc3afc233cb97c2bf2b46bba3094aedc' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2pkh_to_p2wpkh(self): - raw_tx = '0100000001ce85202cb9fbc0ecbc98caf3d716d7448d2a3bd89e113999514b3df5687c7324000000006b483045022100adab7b6cb1179079c9dfc0021f4db0346730b7c16555fcc4363059dcdd95f653022028bcb816f4fb98615fb8f4b18af3ad3708e2d72f94a6466cc2736055860422cf012102a16a25148dd692462a691796db0a4a5531bcca970a04107bf184a2c9f7fd8b12fdffffff012eb6042a010000001600147d0170de18eecbe84648979d52b666dddee0b47400000000' - txid = 'ed29e100499e2a3a64a2b0cb3a68655b9acd690d29690fa541be530462bf3d3c' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2sh_to_p2pkh(self): - raw_tx = '01000000000101f9823f87af35d158e7dc81a67011f4e511e3f6cab07ac108e524b0ff8b950b39000000002322002041f0237866eb72e4a75cd6faf5ccd738703193907d883aa7b3a8169c636706a9fdffffff020065cd1d000000001976a9148150cd6cf729e7e262699875fec1f760b0aab3cc88acc46f9a3b0000000017a91433ccd0f95a7b9d8eef68be40bb59c64d6e14d87287040047304402205ca97126a5956c2deaa956a2006d79a348775d727074a04b71d9c18eb5e5525402207b9353497af15881100a2786adab56c8930c02d46cc1a8b55496c06e22d3459b01483045022100b4fa898057927c2d920ae79bca752dda58202ea8617d3e6ed96cbd5d1c0eb2fc02200824c0e742d1b4d643cec439444f5d8779c18d4f42c2c87cce24044a3babf2df0147522102db78786b3c214826bd27010e3c663b02d67144499611ee3f2461c633eb8f1247210377082028c124098b59a5a1e0ea7fd3ebca72d59c793aecfeedd004304bac15cd52aec9010000' - txid = '17e1d498ba82503e3bfa81ac4897a57e33f3d36b41bcf4765ba604466c478986' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2sh_to_p2sh(self): - raw_tx = '01000000000101b58520acb479ab656a3c03263af0567380aff6b67a8db98543870b695adf2b170000000017160014cfd2b9f7ed9d4d4429ed6946dbb3315f75e85f14fdffffff020065cd1d0000000017a91485f5681bec38f9f07ae9790d7f27c2bb90b5b63c87106ab32c0000000017a914ff402e164dfce874435641ae9ac41fc6fb14c4e18702483045022100b3d1c89c7c92151ed1df78815924569446782776b6a2c170ca5d74c5dd1ad9b102201d7bab1974fd2aa66546dd15c1f1e276d787453cec31b55a2bd97b050abf20140121024a1742ece86df3dbce4717c228cf51e625030cef7f5e6dde33a4fffdd17569eac7010000' - txid = 'ead0e7abfb24ddbcd6b89d704d7a6091e43804a458baa930adf6f1cb5b6b42f7' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2sh_to_p2wpkh(self): - raw_tx = '010000000001018689476c4604a65b76f4bc416bd3f3337ea59748ac81fa3b3e5082ba98d4e1170100000023220020ae40340707f9726c0f453c3d47c96e7f3b7b4b85608eb3668b69bbef9c7ab374fdffffff0218b2cc1d0000000017a914f2fdd81e606ff2ab804d7bb46bf8838a711c277b870065cd1d0000000016001496ad8959c1f0382984ecc4da61c118b4c8751e5104004730440220387b9e7d402fbcada9ba55a27a8d0563eafa9904ebd2f8f7e3d86e4b45bc0ec202205f37fa0e2bf8cbd384f804562651d7c6f69adce5db4c1a5b9103250a47f73e6b01473044022074903f4dd4fd6b32289be909eb5109924740daa55e79be6dbd728687683f9afa02205d934d981ca12cbec450611ca81dc4127f8da5e07dd63d41049380502de3f15401475221025c3810b37147105106cef970f9b91d3735819dee4882d515c1187dbd0b8f0c792103e007c492323084f1c103beff255836408af89bb9ae7f2fcf60502c28ff4b0c9152aeca010000' - txid = '6f294c84cbd0241650931b4c1be3dfb2f175d682c7a9538b30b173e1083deed3' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2wpkh_to_p2pkh(self): - raw_tx = '0100000000010197e6bf4a70bc118e3a8d9842ed80422e335679dfc29b5ba0f9123f6a5863b8470000000000fdffffff02402bca7f130000001600146f579c953d9e7e7719f2baa20bde22eb5f24119200e87648170000001976a9140cd8fa5fd81c3acf33f93efd179b388de8dd693388ac0247304402204ff33b3ea8fb270f62409bfc257457ca5eb1fec5e4d3a7c11aa487207e131d4d022032726b998e338e5245746716e5cd0b40d32b69d1535c3d841f049d98a5d819b1012102dc3ce3220363aff579eb2c45c973e8b186a829c987c3caea77c61975666e7d1bc8010000' - txid = 'c721ed35767a3a209b688e68e3bb136a72d2b631fe81c56be8bdbb948c343dbc' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2wpkh_to_p2sh(self): - raw_tx = '010000000001013c3dbf620453be41a50f69290d69cd9a5b65683acbb0a2643a2a9e4900e129ed0000000000fdffffff02002f68590000000017a914c7c4dcd0ddf70f15c6df13b4a4d56e9f13c49b2787a0429cd000000000160014e514e3ecf89731e7853e4f3a20983484c569d3910247304402205368cc548209303db5a8f2ebc282bd0f7af0d080ce0f7637758587f94d3971fb0220098cec5752554758bc5fa4de332b980d5e0054a807541581dc5e4de3ed29647501210233717cd73d95acfdf6bd72c4fb5df27cd6bd69ce947daa3f4a442183a97877efc8010000' - txid = '390b958bffb024e508c17ab0caf6e311e5f41170a681dce758d135af873f82f9' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_p2wpkh_to_p2wpkh(self): - raw_tx = '010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000' - txid = '51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_input_p2wsh_p2sh_not_multisig(self): - raw_tx = '0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000' - txid = 'e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d' - self._run_naive_tests_on_tx(raw_tx, txid) - - # input: p2sh, not multisig - def test_txid_regression_issue_3899(self): - raw_tx = '0100000004328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c010000000b0009630330472d5fae685bffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c020000000b0009630359646d5fae6858ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c030000000b000963034bd4715fae6854ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c040000000b000963036de8705fae6860ffffffff0130750000000000001976a914b5abca61d20f9062fb1fdbb880d9d93bac36675188ac00000000' - txid = 'f570d5d1e965ee61bcc7005f8fefb1d3abbed9d7ddbe035e2a68fa07e5fc4a0d' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_negative_version_num(self): - raw_tx = 'f0b47b9a01ecf5e5c3bbf2cf1f71ecdc7f708b0b222432e914b394e24aad1494a42990ddfc000000008b483045022100852744642305a99ad74354e9495bf43a1f96ded470c256cd32e129290f1fa191022030c11d294af6a61b3da6ed2c0c296251d21d113cfd71ec11126517034b0dcb70014104a0fe6e4a600f859a0932f701d3af8e0ecd4be886d91045f06a5a6b931b95873aea1df61da281ba29cadb560dad4fc047cf47b4f7f2570da4c0b810b3dfa7e500ffffffff0240420f00000000001976a9147eeacb8a9265cd68c92806611f704fc55a21e1f588ac05f00d00000000001976a914eb3bd8ccd3ba6f1570f844b59ba3e0a667024a6a88acff7f0000' - txid = 'c659729a7fea5071361c2c1a68551ca2bf77679b27086cc415adeeb03852e369' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_regression_issue_4333(self): - raw_tx = '0100000001a300499298b3f03200c05d1a15aa111a33c769aff6fb355c6bf52ebdb58ca37100000000171600756161616161616161616161616161616161616151fdffffff01c40900000000000017a914001975d5f07f3391674416c1fcd67fd511d257ff871bc71300' - txid = '9b9f39e314662a7433aadaa5c94a2f1e24c7e7bf55fc9e1f83abd72be933eb95' - self._run_naive_tests_on_tx(raw_tx, txid) - - -# these transactions are from Bitcoin Core unit tests ---> -# https://github.com/bitcoin/bitcoin/blob/11376b5583a283772c82f6d32d0007cdbf5b8ef0/src/test/data/tx_valid.json - - def test_txid_bitcoin_core_0001(self): - raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' - txid = '23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0002(self): - raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' - txid = 'fcabc409d8e685da28536e1e5ccc91264d755cd4c57ed4cae3dbaa4d3b93e8ed' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0003(self): - raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' - txid = 'c9aa95f2c48175fdb70b34c23f1c3fc44f869b073a6f79b1343fbce30c3cb575' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0004(self): - raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' - txid = 'da94fda32b55deb40c3ed92e135d69df7efc4ee6665e0beb07ef500f407c9fd2' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0005(self): - raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000' - txid = 'f76f897b206e4f78d60fe40f2ccb542184cfadc34354d3bb9bdc30cc2f432b86' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0006(self): - raw_tx = '01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d00000000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac00000000' - txid = 'c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0007(self): - raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000' - txid = 'e41ffe19dff3cbedb413a2ca3fbbcd05cb7fd7397ffa65052f8928aa9c700092' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0008(self): - raw_tx = '01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000' - txid = 'f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0009(self): - raw_tx = '01000000020002000000000000000000000000000000000000000000000000000000000000000000000151ffffffff0001000000000000000000000000000000000000000000000000000000000000000000006b483045022100c9cdd08798a28af9d1baf44a6c77bcc7e279f47dc487c8c899911bc48feaffcc0220503c5c50ae3998a733263c5c0f7061b483e2b56c4c41b456e7d2f5a78a74c077032102d5c25adb51b61339d2b05315791e21bbe80ea470a49db0135720983c905aace0ffffffff010000000000000000015100000000' - txid = 'b56471690c3ff4f7946174e51df68b47455a0d29344c351377d712e6d00eabe5' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0010(self): - raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000' - txid = '99517e5b47533453cc7daa332180f578be68b80370ecfe84dbfff7f19d791da4' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0011(self): - raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100c66c9cdf4c43609586d15424c54707156e316d88b0a1534c9e6b0d4f311406310221009c0fe51dbc9c4ab7cc25d3fdbeccf6679fe6827f08edf2b4a9f16ee3eb0e438a0123210338e8034509af564c62644c07691942e0c056752008a173c89f60ab2a88ac2ebfacffffffff010000000000000000015100000000' - txid = 'ab097537b528871b9b64cb79a769ae13c3c3cd477cc9dddeebe657eabd7fdcea' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0012(self): - raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010040075af0750700015100000000' - txid = '4d163e00f1966e9a1eab8f9374c3e37f4deb4857c247270e25f7d79a999d2dc9' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0013(self): - raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510000000000000000015100000000' - txid = '9fe2ef9dde70e15d78894a4800b7df3bbfb1addb9a6f7d7c204492fdb6ee6cc4' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0014(self): - raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025151ffffffff010000000000000000015100000000' - txid = '99d3825137602e577aeaf6a2e3c9620fd0e605323dc5265da4a570593be791d4' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0015(self): - raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6451515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151ffffffff010000000000000000015100000000' - txid = 'c0d67409923040cc766bbea12e4c9154393abef706db065ac2e07d91a9ba4f84' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0016(self): - raw_tx = '010000000200010000000000000000000000000000000000000000000000000000000000000000000049483045022100d180fd2eb9140aeb4210c9204d3f358766eb53842b2a9473db687fa24b12a3cc022079781799cd4f038b85135bbe49ec2b57f306b2bb17101b17f71f000fcab2b6fb01ffffffff0002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000' - txid = 'c610d85d3d5fdf5046be7f123db8a0890cee846ee58de8a44667cfd1ab6b8666' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0017(self): - raw_tx = '01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df101010000000002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000' - txid = 'a647a7b3328d2c698bfa1ee2dd4e5e05a6cea972e764ccb9bd29ea43817ca64f' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0018(self): - raw_tx = '010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c59da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbea097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf950650802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e18217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a3788dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9cfdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa34b26f62db4a88ac00000000' - txid = 'afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0019(self): - raw_tx = '01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe0000483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa0148304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f4014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000' - txid = 'f4b05f978689c89000f729cae187dcfbe64c9819af67a4f05c0b4d59e717d64d' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0020(self): - raw_tx = '0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000' - txid = 'cc60b1f899ec0a69b7c3f25ddf32c4524096a9c5b01cbd84c6d0312a0c478984' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0021(self): - raw_tx = '01000000012c651178faca83be0b81c8c1375c4b0ad38d53c8fe1b1c4255f5e795c25792220000000049483045022100d6044562284ac76c985018fc4a90127847708c9edb280996c507b28babdc4b2a02203d74eca3f1a4d1eea7ff77b528fde6d5dc324ec2dbfdb964ba885f643b9704cd01ffffffff010100000000000000232102c2410f8891ae918cab4ffc4bb4a3b0881be67c7a1e7faa8b5acf9ab8932ec30cac00000000' - txid = '1edc7f214659d52c731e2016d258701911bd62a0422f72f6c87a1bc8dd3f8667' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0022(self): - raw_tx = '0100000001f725ea148d92096a79b1709611e06e94c63c4ef61cbae2d9b906388efd3ca99c000000000100ffffffff0101000000000000002321028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02ac00000000' - txid = '018adb7133fde63add9149a2161802a1bcf4bdf12c39334e880c073480eda2ff' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0023(self): - raw_tx = '0100000001be599efaa4148474053c2fa031c7262398913f1dc1d9ec201fd44078ed004e44000000004900473044022022b29706cb2ed9ef0cb3c97b72677ca2dfd7b4160f7b4beb3ba806aa856c401502202d1e52582412eba2ed474f1f437a427640306fd3838725fab173ade7fe4eae4a01ffffffff010100000000000000232103ac4bba7e7ca3e873eea49e08132ad30c7f03640b6539e9b59903cf14fd016bbbac00000000' - txid = '1464caf48c708a6cc19a296944ded9bb7f719c9858986d2501cf35068b9ce5a2' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0024(self): - raw_tx = '010000000112b66d5e8c7d224059e946749508efea9d66bf8d0c83630f080cf30be8bb6ae100000000490047304402206ffe3f14caf38ad5c1544428e99da76ffa5455675ec8d9780fac215ca17953520220779502985e194d84baa36b9bd40a0dbd981163fa191eb884ae83fc5bd1c86b1101ffffffff010100000000000000232103905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9fac00000000' - txid = '1fb73fbfc947d52f5d80ba23b67c06a232ad83fdd49d1c0a657602f03fbe8f7a' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0025(self): - raw_tx = '0100000001b0ef70cc644e0d37407e387e73bfad598d852a5aa6d691d72b2913cebff4bceb000000004a00473044022068cd4851fc7f9a892ab910df7a24e616f293bcb5c5fbdfbc304a194b26b60fba022078e6da13d8cb881a22939b952c24f88b97afd06b4c47a47d7f804c9a352a6d6d0100ffffffff0101000000000000002321033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194ac00000000' - txid = '24cecfce0fa880b09c9b4a66c5134499d1b09c01cc5728cd182638bea070e6ab' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0026(self): - raw_tx = '0100000001c188aa82f268fcf08ba18950f263654a3ea6931dabc8bf3ed1d4d42aaed74cba000000004b0000483045022100940378576e069aca261a6b26fb38344e4497ca6751bb10905c76bb689f4222b002204833806b014c26fd801727b792b1260003c55710f87c5adbd7a9cb57446dbc9801ffffffff0101000000000000002321037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1ac00000000' - txid = '9eaa819e386d6a54256c9283da50c230f3d8cd5376d75c4dcc945afdeb157dd7' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0027(self): - raw_tx = '01000000012432b60dc72cebc1a27ce0969c0989c895bdd9e62e8234839117f8fc32d17fbc000000004a493046022100a576b52051962c25e642c0fd3d77ee6c92487048e5d90818bcf5b51abaccd7900221008204f8fb121be4ec3b24483b1f92d89b1b0548513a134e345c5442e86e8617a501ffffffff010000000000000000016a00000000' - txid = '46224764c7870f95b58f155bce1e38d4da8e99d42dbb632d0dd7c07e092ee5aa' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0028(self): - raw_tx = '01000000014710b0e7cf9f8930de259bdc4b84aa5dfb9437b665a3e3a21ff26e0bf994e183000000004a493046022100a166121a61b4eeb19d8f922b978ff6ab58ead8a5a5552bf9be73dc9c156873ea02210092ad9bc43ee647da4f6652c320800debcf08ec20a094a0aaf085f63ecb37a17201ffffffff010000000000000000016a00000000' - txid = '8d66836045db9f2d7b3a75212c5e6325f70603ee27c8333a3bce5bf670d9582e' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0029(self): - raw_tx = '01000000015ebaa001d8e4ec7a88703a3bcf69d98c874bca6299cca0f191512bf2a7826832000000004948304502203bf754d1c6732fbf87c5dcd81258aefd30f2060d7bd8ac4a5696f7927091dad1022100f5bcb726c4cf5ed0ed34cc13dadeedf628ae1045b7cb34421bc60b89f4cecae701ffffffff010000000000000000016a00000000' - txid = 'aab7ef280abbb9cc6fbaf524d2645c3daf4fcca2b3f53370e618d9cedf65f1f8' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0030(self): - raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a900000000924830450221009c0a27f886a1d8cb87f6f595fbc3163d28f7a81ec3c4b252ee7f3ac77fd13ffa02203caa8dfa09713c8c4d7ef575c75ed97812072405d932bd11e6a1593a98b679370148304502201e3861ef39a526406bad1e20ecad06be7375ad40ddb582c9be42d26c3a0d7b240221009d0a3985e96522e59635d19cc4448547477396ce0ef17a58e7d74c3ef464292301ffffffff010000000000000000016a00000000' - txid = '6327783a064d4e350c454ad5cd90201aedf65b1fc524e73709c52f0163739190' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0031(self): - raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0100ffffffff010000000000000000016a00000000' - txid = '892464645599cc3c2d165adcc612e5f982a200dfaa3e11e9ce1d228027f46880' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0032(self): - raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510151ffffffff010000000000000000016a00000000' - txid = '578db8c6c404fec22c4a8afeaf32df0e7b767c4dda3478e0471575846419e8fc' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0033(self): - raw_tx = '0100000001e0be9e32f1f89c3d916c4f21e55cdcd096741b895cc76ac353e6023a05f4f7cc00000000d86149304602210086e5f736a2c3622ebb62bd9d93d8e5d76508b98be922b97160edc3dcca6d8c47022100b23c312ac232a4473f19d2aeb95ab7bdf2b65518911a0d72d50e38b5dd31dc820121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac4730440220508fa761865c8abd81244a168392876ee1d94e8ed83897066b5e2df2400dad24022043f5ee7538e87e9c6aef7ef55133d3e51da7cc522830a9c4d736977a76ef755c0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000' - txid = '974f5148a0946f9985e75a240bb24c573adbbdc25d61e7b016cdbb0a5355049f' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0034(self): - raw_tx = '01000000013c6f30f99a5161e75a2ce4bca488300ca0c6112bde67f0807fe983feeff0c91001000000e608646561646265656675ab61493046022100ce18d384221a731c993939015e3d1bcebafb16e8c0b5b5d14097ec8177ae6f28022100bcab227af90bab33c3fe0a9abfee03ba976ee25dc6ce542526e9b2e56e14b7f10121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac493046022100c3b93edcc0fd6250eb32f2dd8a0bba1754b0f6c3be8ed4100ed582f3db73eba2022100bf75b5bd2eff4d6bf2bda2e34a40fcc07d4aa3cf862ceaa77b47b81eff829f9a01ab21038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000' - txid = 'b0097ec81df231893a212657bf5fe5a13b2bff8b28c0042aca6fc4159f79661b' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0035(self): - raw_tx = '01000000016f3dbe2ca96fa217e94b1017860be49f20820dea5c91bdcb103b0049d5eb566000000000fd1d0147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac47304402203757e937ba807e4a5da8534c17f9d121176056406a6465054bdd260457515c1a02200f02eccf1bec0f3a0d65df37889143c2e88ab7acec61a7b6f5aa264139141a2b0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000' - txid = 'feeba255656c80c14db595736c1c7955c8c0a497622ec96e3f2238fbdd43a7c9' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0036(self): - raw_tx = '01000000012139c555ccb81ee5b1e87477840991ef7b386bc3ab946b6b682a04a621006b5a01000000fdb40148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f2204148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390175ac4830450220646b72c35beeec51f4d5bc1cbae01863825750d7f490864af354e6ea4f625e9c022100f04b98432df3a9641719dbced53393022e7249fb59db993af1118539830aab870148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a580039017521038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000' - txid = 'a0c984fc820e57ddba97f8098fa640c8a7eb3fe2f583923da886b7660f505e1e' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0037(self): - raw_tx = '0100000002f9cbafc519425637ba4227f8d0a0b7160b4e65168193d5af39747891de98b5b5000000006b4830450221008dd619c563e527c47d9bd53534a770b102e40faa87f61433580e04e271ef2f960220029886434e18122b53d5decd25f1f4acb2480659fea20aabd856987ba3c3907e0121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffff42e7988254800876b69f24676b3e0205b77be476512ca4d970707dd5c60598ab00000000fd260100483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a53034930460221008431bdfa72bc67f9d41fe72e94c88fb8f359ffa30b33c72c121c5a877d922e1002210089ef5fc22dd8bfc6bf9ffdb01a9862d27687d424d1fefbab9e9c7176844a187a014c9052483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c7153aeffffffff01a08601000000000017a914d8dacdadb7462ae15cd906f1878706d0da8660e68700000000' - txid = '5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0038(self): - raw_tx = '0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000' - txid = 'ded7ff51d89a4e1ec48162aee5a96447214d93dfb3837946af2301a28f65dbea' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0039(self): - raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000' - txid = '3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0040(self): - raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d' - txid = 'abd62b4627d8d9b2d95fcfd8c87e37d2790637ce47d28018e3aece63c1d62649' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0041(self): - raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d' - txid = '58b6de8413603b7f556270bf48caedcf17772e7105f5419f6a80be0df0b470da' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0042(self): - raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff' - txid = '5f99c0abf511294d76cbe144d86b77238a03e086974bc7a8ea0bdb2c681a0324' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0043(self): - raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000' - txid = '25d35877eaba19497710666473c50d5527d38503e3521107a3fc532b74cd7453' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0044(self): - raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff' - txid = '1b9aef851895b93c62c29fbd6ca4d45803f4007eff266e2f96ff11e9b6ef197b' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0045(self): - raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000' - txid = '3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0046(self): - raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000' - txid = 'f53761038a728b1f17272539380d96e93f999218f8dcb04a8469b523445cd0fd' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0047(self): - raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000' - txid = 'd193f0f32fceaf07bb25c897c8f99ca6f69a52f6274ca64efc2a2e180cb97fc1' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0048(self): - raw_tx = '010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000' - txid = '50a1e0e6a134a564efa078e3bd088e7e8777c2c0aec10a752fd8706470103b89' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0049(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000' - txid = 'e2207d1aaf6b74e5d98c2fa326d2dc803b56b30a3f90ce779fa5edb762f38755' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0050(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000' - txid = 'f335864f7c12ec7946d2c123deb91eb978574b647af125a414262380c7fbd55c' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0051(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000' - txid = 'd1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0052(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000' - txid = '3a13e1b6371c545147173cc4055f0ed73686a9f73f092352fb4b39ca27d360e6' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0053(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000' - txid = 'bffda23e40766d292b0510a1b556453c558980c70c94ab158d8286b3413e220d' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0054(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000' - txid = '01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0055(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000' - txid = 'f6d2359c5de2d904e10517d23e7c8210cca71076071bbf46de9fbd5f6233dbf1' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0056(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000' - txid = '19c2b7377229dae7aa3e50142a32fd37cef7171a01682f536e9ffa80c186f6c9' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0057(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000' - txid = 'c9dda3a24cc8a5acb153d1085ecd2fecf6f87083122f8cdecc515b1148d4c40d' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0058(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000' - txid = 'd1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0059(self): - raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000' - txid = '01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0060(self): - raw_tx = '02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2010000000100000000000000000000000000' - txid = '4b5e0aae1251a9dc66b4d5f483f1879bf518ea5e1765abc5a9f2084b43ed1ea7' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0061(self): - raw_tx = '0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000' - txid = '5f16eb3ca4581e2dfb46a28140a4ee15f85e4e1c032947da8b93549b53c105f5' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0062(self): - raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000' - txid = 'b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0063(self): - raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000' - txid = 'b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0064(self): - raw_tx = '01000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000' - txid = 'fee125c6cd142083fabd0187b1dd1f94c66c89ec6e6ef6da1374881c0c19aece' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0065(self): - raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000023220020ff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000' - txid = '5f32557914351fee5f89ddee6c8983d476491d29e601d854e3927299e50450da' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0066(self): - raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff05540b0000000000000151d0070000000000000151840300000000000001513c0f00000000000001512c010000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71000000000000' - txid = '07dfa2da3d67c8a2b9f7bd31862161f7b497829d5da90a88ba0f1a905e7a43f7' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0067(self): - raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0068(self): - raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff0484030000000000000151d0070000000000000151540b0000000000000151c800000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = 'f92bb6e4f3ff89172f23ef647f74c13951b665848009abb5862cdf7a0412415a' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0069(self): - raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0070(self): - raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff04b60300000000000001519e070000000000000151860b00000000000001009600000000000000015100000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = 'e657e25fc9f2b33842681613402759222a58cf7dd504d6cdc0b69a0b8c2e7dcb' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0071(self): - raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0072(self): - raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = '4ede5e22992d43d42ccdf6553fb46e448aa1065ba36423f979605c1e5ab496b8' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0073(self): - raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0074(self): - raw_tx = '01000000000103000100000000000000000000000000000000000000000000000000000000000000000000000200000000010000000000000000000000000000000000000000000000000000000000000100000000ffffffff000100000000000000000000000000000000000000000000000000000000000002000000000200000003e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = 'cfe9f4b19f52b8366860aec0d2b5815e329299b2e9890d477edd7f1182be7ac8' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0075(self): - raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = 'aee8f4865ca40fa77ff2040c0d7de683bea048b103d42ca406dc07dd29d539cb' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0076(self): - raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0077(self): - raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0078(self): - raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd08020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000' - txid = 'd93ab9e12d7c29d2adc13d5cdf619d53eec1f36eb6612f55af52be7ba0448e97' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0079(self): - raw_tx = '0100000000010c00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff0001000000000000000000000000000000000000000000000000000000000000020000006a473044022026c2e65b33fcd03b2a3b0f25030f0244bd23cc45ae4dec0f48ae62255b1998a00220463aa3982b718d593a6b9e0044513fd67a5009c2fdccc59992cffc2b167889f4012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000030000006a4730440220008bd8382911218dcb4c9f2e75bf5c5c3635f2f2df49b36994fde85b0be21a1a02205a539ef10fb4c778b522c1be852352ea06c67ab74200977c722b0bc68972575a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000040000006b483045022100d9436c32ff065127d71e1a20e319e4fe0a103ba0272743dbd8580be4659ab5d302203fd62571ee1fe790b182d078ecfd092a509eac112bea558d122974ef9cc012c7012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000050000006a47304402200e2c149b114ec546015c13b2b464bbcb0cdc5872e6775787527af6cbc4830b6c02207e9396c6979fb15a9a2b96ca08a633866eaf20dc0ff3c03e512c1d5a1654f148012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000060000006b483045022100b20e70d897dc15420bccb5e0d3e208d27bdd676af109abbd3f88dbdb7721e6d6022005836e663173fbdfe069f54cde3c2decd3d0ea84378092a5d9d85ec8642e8a41012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff00010000000000000000000000000000000000000000000000000000000000000700000000ffffffff00010000000000000000000000000000000000000000000000000000000000000800000000ffffffff00010000000000000000000000000000000000000000000000000000000000000900000000ffffffff00010000000000000000000000000000000000000000000000000000000000000a00000000ffffffff00010000000000000000000000000000000000000000000000000000000000000b0000006a47304402206639c6e05e3b9d2675a7f3876286bdf7584fe2bbd15e0ce52dd4e02c0092cdc60220757d60b0a61fc95ada79d23746744c72bac1545a75ff6c2c7cdb6ae04e7e9592012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0ce8030000000000000151e9030000000000000151ea030000000000000151eb030000000000000151ec030000000000000151ed030000000000000151ee030000000000000151ef030000000000000151f0030000000000000151f1030000000000000151f2030000000000000151f30300000000000001510248304502210082219a54f61bf126bfc3fa068c6e33831222d1d7138c6faa9d33ca87fd4202d6022063f9902519624254d7c2c8ea7ba2d66ae975e4e229ae38043973ec707d5d4a83012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022017fb58502475848c1b09f162cb1688d0920ff7f142bed0ef904da2ccc88b168f02201798afa61850c65e77889cbcd648a5703b487895517c88f85cdd18b021ee246a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000000247304402202830b7926e488da75782c81a54cd281720890d1af064629ebf2e31bf9f5435f30220089afaa8b455bbeb7d9b9c3fe1ed37d07685ade8455c76472cda424d93e4074a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022026326fcdae9207b596c2b05921dbac11d81040c4d40378513670f19d9f4af893022034ecd7a282c0163b89aaa62c22ec202cef4736c58cd251649bad0d8139bcbf55012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71024730440220214978daeb2f38cd426ee6e2f44131a33d6b191af1c216247f1dd7d74c16d84a02205fdc05529b0bc0c430b4d5987264d9d075351c4f4484c16e91662e90a72aab24012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402204a6e9f199dc9672cf2ff8094aaa784363be1eb62b679f7ff2df361124f1dca3302205eeb11f70fab5355c9c8ad1a0700ea355d315e334822fa182227e9815308ee8f012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000' - txid = 'b83579db5246aa34255642768167132a0c3d2932b186cd8fb9f5490460a0bf91' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0080(self): - raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000' - txid = '2b1e44fff489d09091e5e20f9a01bbc0e8d80f0662e629fd10709cdb4922a874' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0081(self): - raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff01d00700000000000001510003483045022100e078de4e96a0e05dcdc0a414124dd8475782b5f3f0ed3f607919e9a5eeeb22bf02201de309b3a3109adb3de8074b3610d4cf454c49b61247a2779a0bcbf31c889333032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc711976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac00000000' - txid = '60ebb1dd0b598e20dd0dd462ef6723dd49f8f803b6a2492926012360119cfdd7' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0082(self): - raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff02e8030000000000000151e90300000000000001510247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000' - txid = 'ed0c7f4163e275f3f77064f471eac861d01fdf55d03aa6858ebd3781f70bf003' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0083(self): - raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff02e9030000000000000151e80300000000000001510248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000' - txid = 'f531ddf5ce141e1c8a7fdfc85cc634e5ff686f446a5cf7483e9dbe076b844862' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0084(self): - raw_tx = '01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000' - txid = '98229b70948f1c17851a541f1fe532bf02c408267fecf6d7e174c359ae870654' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0085(self): - raw_tx = '01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000' - txid = '570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0086(self): - raw_tx = '01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000' - txid = 'e0b8142f587aaa322ca32abce469e90eda187f3851043cc4f2a0fff8c13fc84e' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0087(self): - raw_tx = '0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000' - txid = 'b9ecf72df06b8f98f8b63748d1aded5ffc1a1186f8a302e63cf94f6250e29f4d' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0088(self): - raw_tx = '0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000' - txid = '27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0089(self): - raw_tx = '010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000' - txid = '22d020638e3b7e1f2f9a63124ac76f5e333c74387862e3675f64b25e960d3641' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0090(self): - raw_tx = '0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9781d66b61fb5a7ef00ac5ad5bc6ffc78be7b44a566e3c87870e1079368df4c4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000' - txid = '2862bc0c69d2af55da7284d1b16a7cddc03971b77e5a97939cca7631add83bf5' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0091(self): - raw_tx = '01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601522102cd74a2809ffeeed0092bc124fd79836706e41f048db3f6ae9df8708cefb83a1c2102e615999372426e46fd107b76eaf007156a507584aa2cc21de9eee3bdbd26d36c4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000' - txid = '1aebf0c98f01381765a8c33d688f8903e4d01120589ac92b78f1185dc1f4119c' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_bitcoin_core_0092(self): - raw_tx = '010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960101022102966f109c54e85d3aee8321301136cedeb9fc710fdef58a9de8a73942f8e567c021034ffc99dd9a79dd3cb31e2ab3e0b09e0e67db41ac068c625cd1f491576016c84e9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000' - txid = '45d17fb7db86162b2b6ca29fa4e163acf0ef0b54110e49b819bda1f948d423a3' - self._run_naive_tests_on_tx(raw_tx, txid) - -# txns from Bitcoin Core ends <--- - - -class TestTransactionTestnet(TestCaseForTestnet): - - def _run_naive_tests_on_tx(self, raw_tx, txid): - tx = transaction.Transaction(raw_tx) - self.assertEqual(txid, tx.txid()) - self.assertEqual(raw_tx, tx.serialize()) - self.assertTrue(tx.estimated_size() >= 0) - -# partial txns using our partial format ---> - # NOTE: our partial format contains xpubs, and xpubs have version bytes, - # and version bytes encode the network as well; so these are network-sensitive! - - def test_txid_partial_segwit_p2wpkh(self): - raw_tx = '45505446ff000100000000010115a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff02f6fd1200000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600140f9de573bc679d040e763d13f0250bd03e625f6ffeffffffff9095ab000000000000000201ff53ff045f1cf6014af5fa07800000002fa3f450ba41799b9b62642979505817783a9b6c656dc11cd0bb4fa362096808026adc616c25a4d0a877d1741eb1db9cef65c15118bd7d5f31bf65f319edda81840100c8000f391400' - txid = '63ff7e99d85d8e33f683e6ec84574bdf8f5111078a5fe900893e019f9a7f95c3' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_partial_segwit_p2wpkh_p2sh_simple(self): - raw_tx = '45505446ff0001000000000101d0d23a6fbddb21cc664cb81cca96715baa4d6dbe5b7b9bcc6632f1005a7b0b840100000017160014a78a91261e71a681b6312cd184b14503a21f856afdffffff0134410f000000000017a914d6514ca17ecc31952c990daf96e307fbc58529cd87feffffffff40420f000000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c30000020011391400' - txid = '2739f2e7fde9b8ec73fce4aee53722cc7683312d1321ded073284c51fadf44df' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_partial_segwit_p2wpkh_p2sh_mixed_outputs(self): - raw_tx = '45505446ff00010000000001011dcac788f24b84d771b60c44e1f9b6b83429e50f06e1472d47241922164013b00100000017160014801d28ca6e2bde551112031b6cb75de34f10851ffdffffff0440420f00000000001600140f9de573bc679d040e763d13f0250bd03e625f6fc0c62d000000000017a9142899f6484e477233ce60072fc185ef4c1f2c654487809698000000000017a914d40f85ba3c8fa0f3615bcfa5d6603e36dfc613ef87712d19040000000017a914e38c0cffde769cb65e72cda1c234052ae8d2254187feffffffff6ad1ee040000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c301000c000f391400' - txid = 'ba5c88e07a4025a39ad3b85247cbd4f556a70d6312b18e04513c7cec9d45d6ac' - self._run_naive_tests_on_tx(raw_tx, txid) - -# end partial txns <--- - - -class NetworkMock(object): - - def __init__(self, unspent): - self.unspent = unspent - - def synchronous_send(self, arg): - return self.unspent diff --git a/lib/tests/test_util.py b/lib/tests/test_util.py @@ -1,109 +0,0 @@ -import unittest -from lib.util import format_satoshis, parse_URI - -from . import SequentialTestCase - - -class TestUtil(SequentialTestCase): - - def test_format_satoshis(self): - result = format_satoshis(1234) - expected = "0.00001234" - self.assertEqual(expected, result) - - def test_format_satoshis_negative(self): - result = format_satoshis(-1234) - expected = "-0.00001234" - self.assertEqual(expected, result) - - def test_format_fee(self): - result = format_satoshis(1700/1000, 0, 0) - expected = "1.7" - self.assertEqual(expected, result) - - def test_format_fee_precision(self): - result = format_satoshis(1666/1000, 0, 0, precision=6) - expected = "1.666" - self.assertEqual(expected, result) - - result = format_satoshis(1666/1000, 0, 0, precision=1) - expected = "1.7" - self.assertEqual(expected, result) - - def test_format_satoshis_whitespaces(self): - result = format_satoshis(12340, whitespaces=True) - expected = " 0.0001234 " - self.assertEqual(expected, result) - - result = format_satoshis(1234, whitespaces=True) - expected = " 0.00001234" - self.assertEqual(expected, result) - - def test_format_satoshis_whitespaces_negative(self): - result = format_satoshis(-12340, whitespaces=True) - expected = " -0.0001234 " - self.assertEqual(expected, result) - - result = format_satoshis(-1234, whitespaces=True) - expected = " -0.00001234" - self.assertEqual(expected, result) - - def test_format_satoshis_diff_positive(self): - result = format_satoshis(1234, is_diff=True) - expected = "+0.00001234" - self.assertEqual(expected, result) - - def test_format_satoshis_diff_negative(self): - result = format_satoshis(-1234, is_diff=True) - expected = "-0.00001234" - self.assertEqual(expected, result) - - def _do_test_parse_URI(self, uri, expected): - result = parse_URI(uri) - self.assertEqual(expected, result) - - def test_parse_URI_address(self): - self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', - {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma'}) - - def test_parse_URI_only_address(self): - self._do_test_parse_URI('15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', - {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma'}) - - - def test_parse_URI_address_label(self): - self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?label=electrum%20test', - {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'label': 'electrum test'}) - - def test_parse_URI_address_message(self): - self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?message=electrum%20test', - {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'message': 'electrum test', 'memo': 'electrum test'}) - - def test_parse_URI_address_amount(self): - self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003', - {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'amount': 30000}) - - def test_parse_URI_address_request_url(self): - self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?r=http://domain.tld/page?h%3D2a8628fc2fbe', - {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'r': 'http://domain.tld/page?h=2a8628fc2fbe'}) - - def test_parse_URI_ignore_args(self): - self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?test=test', - {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'test': 'test'}) - - def test_parse_URI_multiple_args(self): - self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.00004&label=electrum-test&message=electrum%20test&test=none&r=http://domain.tld/page', - {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'amount': 4000, 'label': 'electrum-test', 'message': u'electrum test', 'memo': u'electrum test', 'r': 'http://domain.tld/page', 'test': 'none'}) - - def test_parse_URI_no_address_request_url(self): - self._do_test_parse_URI('bitcoin:?r=http://domain.tld/page?h%3D2a8628fc2fbe', - {'r': 'http://domain.tld/page?h=2a8628fc2fbe'}) - - def test_parse_URI_invalid_address(self): - self.assertRaises(BaseException, parse_URI, 'bitcoin:invalidaddress') - - def test_parse_URI_invalid(self): - self.assertRaises(BaseException, parse_URI, 'notbitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma') - - def test_parse_URI_parameter_polution(self): - self.assertRaises(Exception, parse_URI, 'bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003&label=test&amount=30.0') diff --git a/lib/tests/test_wallet.py b/lib/tests/test_wallet.py @@ -1,71 +0,0 @@ -import shutil -import tempfile -import sys -import unittest -import os -import json - -from io import StringIO -from lib.storage import WalletStorage, FINAL_SEED_VERSION - -from . import SequentialTestCase - - -class FakeSynchronizer(object): - - def __init__(self): - self.store = [] - - def add(self, address): - self.store.append(address) - - -class WalletTestCase(SequentialTestCase): - - def setUp(self): - super(WalletTestCase, self).setUp() - self.user_dir = tempfile.mkdtemp() - - self.wallet_path = os.path.join(self.user_dir, "somewallet") - - self._saved_stdout = sys.stdout - self._stdout_buffer = StringIO() - sys.stdout = self._stdout_buffer - - def tearDown(self): - super(WalletTestCase, self).tearDown() - shutil.rmtree(self.user_dir) - # Restore the "real" stdout - sys.stdout = self._saved_stdout - - -class TestWalletStorage(WalletTestCase): - - def test_read_dictionary_from_file(self): - - some_dict = {"a":"b", "c":"d"} - contents = json.dumps(some_dict) - with open(self.wallet_path, "w") as f: - contents = f.write(contents) - - storage = WalletStorage(self.wallet_path, manual_upgrades=True) - self.assertEqual("b", storage.get("a")) - self.assertEqual("d", storage.get("c")) - - def test_write_dictionary_to_file(self): - - storage = WalletStorage(self.wallet_path) - - some_dict = { - u"a": u"b", - u"c": u"d", - u"seed_version": FINAL_SEED_VERSION} - - for key, value in some_dict.items(): - storage.put(key, value) - storage.write() - - contents = "" - with open(self.wallet_path, "r") as f: - contents = f.read() - self.assertEqual(some_dict, json.loads(contents)) diff --git a/lib/tests/test_wallet_vertical.py b/lib/tests/test_wallet_vertical.py @@ -1,1604 +0,0 @@ -import unittest -from unittest import mock -import shutil -import tempfile -from typing import Sequence - -import lib -from lib import storage, bitcoin, keystore, constants -from lib.transaction import Transaction -from lib.simple_config import SimpleConfig -from lib.wallet import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT, sweep -from lib.util import bfh, bh2u - -from plugins.trustedcoin import trustedcoin - -from . import TestCaseForTestnet -from . import SequentialTestCase -from .test_bitcoin import needs_test_with_all_ecc_implementations - - -class WalletIntegrityHelper: - - gap_limit = 1 # make tests run faster - - @classmethod - def check_seeded_keystore_sanity(cls, test_obj, ks): - test_obj.assertTrue(ks.is_deterministic()) - test_obj.assertFalse(ks.is_watching_only()) - test_obj.assertFalse(ks.can_import()) - test_obj.assertTrue(ks.has_seed()) - - @classmethod - def check_xpub_keystore_sanity(cls, test_obj, ks): - test_obj.assertTrue(ks.is_deterministic()) - test_obj.assertTrue(ks.is_watching_only()) - test_obj.assertFalse(ks.can_import()) - test_obj.assertFalse(ks.has_seed()) - - @classmethod - def create_standard_wallet(cls, ks, gap_limit=None): - store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') - store.put('keystore', ks.dump()) - store.put('gap_limit', gap_limit or cls.gap_limit) - w = lib.wallet.Standard_Wallet(store) - w.synchronize() - return w - - @classmethod - def create_imported_wallet(cls, privkeys=False): - store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') - if privkeys: - k = keystore.Imported_KeyStore({}) - store.put('keystore', k.dump()) - w = lib.wallet.Imported_Wallet(store) - return w - - @classmethod - def create_multisig_wallet(cls, keystores: Sequence, multisig_type: str, gap_limit=None): - """Creates a multisig wallet.""" - store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') - for i, ks in enumerate(keystores): - cosigner_index = i + 1 - store.put('x%d/' % cosigner_index, ks.dump()) - store.put('wallet_type', multisig_type) - store.put('gap_limit', gap_limit or cls.gap_limit) - w = lib.wallet.Multisig_Wallet(store) - w.synchronize() - return w - - -# TODO passphrase/seed_extension -class TestWalletKeystoreAddressIntegrityForMainnet(SequentialTestCase): - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_standard(self, mock_write): - seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song' - self.assertEqual(bitcoin.seed_type(seed_words), 'standard') - - ks = keystore.from_seed(seed_words, '', False) - - WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks) - self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) - - self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K32jECVM729vWgGq4mUDJCk1ozqAStTphzQtCTuoFmFafNoG1g55iCnBTXUzz3zWnDb5CVLGiFvmaZjuazHDL8a81cPQ8KL6') - self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U') - - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(w.txin_type, 'p2pkh') - - self.assertEqual(w.get_receiving_addresses()[0], '1NNkttn1YvVGdqBW4PR6zvc3Zx3H5owKRf') - self.assertEqual(w.get_change_addresses()[0], '1KSezYMhAJMWqFbVFB2JshYg69UpmEXR4D') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_segwit(self, mock_write): - seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' - self.assertEqual(bitcoin.seed_type(seed_words), 'segwit') - - ks = keystore.from_seed(seed_words, '', False) - - WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks) - self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) - - self.assertEqual(ks.xprv, 'zprvAZswDvNeJeha8qZ8g7efN3FXYVJLaEUsE9TW6qXDEbVe74AZ75c2sZFZXPNFzxnhChDQ89oC8C5AjWwHmH1HeRKE1c4kKBQAmjUDdKDUZw2') - self.assertEqual(ks.xpub, 'zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ') - - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(w.txin_type, 'p2wpkh') - - self.assertEqual(w.get_receiving_addresses()[0], 'bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af') - self.assertEqual(w.get_change_addresses()[0], 'bc1qdy94n2q5qcp0kg7v9yzwe6wvfkhnvyzje7nx2p') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_old(self, mock_write): - seed_words = 'powerful random nobody notice nothing important anyway look away hidden message over' - self.assertEqual(bitcoin.seed_type(seed_words), 'old') - - ks = keystore.from_seed(seed_words, '', False) - - WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks) - self.assertTrue(isinstance(ks, keystore.Old_KeyStore)) - - self.assertEqual(ks.mpk, 'e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3') - - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(w.txin_type, 'p2pkh') - - self.assertEqual(w.get_receiving_addresses()[0], '1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo') - self.assertEqual(w.get_change_addresses()[0], '1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_2fa(self, mock_write): - seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove' - self.assertEqual(bitcoin.seed_type(seed_words), '2fa') - - xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '') - - ks1 = keystore.from_xprv(xprv1) - self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) - self.assertEqual(ks1.xprv, 'xprv9uraXy9F3HP7i8QDqwNTBiD8Jf4bPD4Epif8cS8qbUbgeidUesyZpKmzfcSeHutsGfFnjgih7kzwTB5UQVRNB5LoXaNc8pFusKYx3KVVvYR') - self.assertEqual(ks1.xpub, 'xpub68qvwUg8sewQvcUgwxuTYr9rrgu5nfn6BwajQpYT9p8fXWxdCRHpN86UWruWJAD1ede8Sv8ERrTa22Gyc4SBfm7zFpcyoVWVBKCVwnw6s1J') - self.assertEqual(ks1.xpub, xpub1) - - ks2 = keystore.from_xprv(xprv2) - self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) - self.assertEqual(ks2.xprv, 'xprv9uraXy9F3HP7kKSiRAvLV7Nrjj7YzspDys7dvGLLu4tLZT49CEBxPWp88dHhVxvZ69SHrPQMUCWjj4Ka2z9kNvs1HAeEf3extGGeSWqEVqf') - self.assertEqual(ks2.xpub, 'xpub68qvwUg8sewQxoXBXCTLrFKbHkx3QLY5M63EiejxTQRKSFPHjmWCwK8byvZMM2wZNYA3SmxXoma3M1zxhGESHZwtB7SwrxRgKXAG8dCD2eS') - self.assertEqual(ks2.xpub, xpub2) - - long_user_id, short_id = trustedcoin.get_user_id( - {'x1/': {'xpub': xpub1}, - 'x2/': {'xpub': xpub2}}) - xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(), long_user_id) - ks3 = keystore.from_xpub(xpub3) - WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3) - self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore)) - - w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3') - self.assertEqual(w.txin_type, 'p2sh') - - self.assertEqual(w.get_receiving_addresses()[0], '35L8XmCDoEBKeaWRjvmZvoZvhp8BXMMMPV') - self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_seed_bip44_standard(self, mock_write): - seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' - self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) - - ks = keystore.from_bip39_seed(seed_words, '', "m/44'/0'/0'") - - self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) - - self.assertEqual(ks.xprv, 'xprv9zGLcNEb3cHUKizLVBz6RYeE9bEZAVPjH2pD1DEzCnPcsemWc3d3xTao8sfhfUmDLMq6e3RcEMEvJG1Et8dvfL8DV4h7mwm9J6AJsW9WXQD') - self.assertEqual(ks.xpub, 'xpub6DFh1smUsyqmYD4obDX6ngaxhd53Zx7aeFjoobebm7vbkT6f9awJWFuGzBT9FQJEWFBL7UyhMXtYzRcwDuVbcxtv9Ce2W9eMm4KXLdvdbjv') - - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(w.txin_type, 'p2pkh') - - self.assertEqual(w.get_receiving_addresses()[0], '16j7Dqk3Z9DdTdBtHcCVLaNQy9MTgywUUo') - self.assertEqual(w.get_change_addresses()[0], '1GG5bVeWgAp5XW7JLCphse14QaC4qiHyWn') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_seed_bip49_p2sh_segwit(self, mock_write): - seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' - self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) - - ks = keystore.from_bip39_seed(seed_words, '', "m/49'/0'/0'") - - self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) - - self.assertEqual(ks.xprv, 'yprvAJEYHeNEPcyBoQYM7sGCxDiNCTX65u4ANgZuSGTrKN5YCC9MP84SBayrgaMyZV7zvkHrr3HVPTK853s2SPk4EttPazBZBmz6QfDkXeE8Zr7') - self.assertEqual(ks.xpub, 'ypub6XDth9u8DzXV1tcpDtoDKMf6kVMaVMn1juVWEesTshcX4zUVvfNgjPJLXrD9N7AdTLnbHFL64KmBn3SNaTe69iZYbYCqLCCNPZKbLz9niQ4') - - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(w.txin_type, 'p2wpkh-p2sh') - - self.assertEqual(w.get_receiving_addresses()[0], '35ohQTdNykjkF1Mn9nAVEFjupyAtsPAK1W') - self.assertEqual(w.get_change_addresses()[0], '3KaBTcviBLEJajTEMstsA2GWjYoPzPK7Y7') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_seed_bip84_native_segwit(self, mock_write): - # test case from bip84 - seed_words = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' - self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) - - ks = keystore.from_bip39_seed(seed_words, '', "m/84'/0'/0'") - - self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore)) - - self.assertEqual(ks.xprv, 'zprvAdG4iTXWBoARxkkzNpNh8r6Qag3irQB8PzEMkAFeTRXxHpbF9z4QgEvBRmfvqWvGp42t42nvgGpNgYSJA9iefm1yYNZKEm7z6qUWCroSQnE') - self.assertEqual(ks.xpub, 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs') - - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(w.txin_type, 'p2wpkh') - - self.assertEqual(w.get_receiving_addresses()[0], 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu') - self.assertEqual(w.get_change_addresses()[0], 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_multisig_seed_standard(self, mock_write): - seed_words = 'blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure' - self.assertEqual(bitcoin.seed_type(seed_words), 'standard') - - ks1 = keystore.from_seed(seed_words, '', True) - WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks1) - self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) - self.assertEqual(ks1.xprv, 'xprv9s21ZrQH143K3t9vo23J3hajRbzvkRLJ6Y1zFrUFAfU3t8oooMPfb7f87cn5KntgqZs5nipZkCiBFo5ZtaSD2eDo7j7CMuFV8Zu6GYLTpY6') - self.assertEqual(ks1.xpub, 'xpub661MyMwAqRbcGNEPu3aJQqXTydqR9t49Tkwb4Esrj112kw8xLthv8uybxvaki4Ygt9xiwZUQGeFTG7T2TUzR3eA4Zp3aq5RXsABHFBUrq4c') - - # electrum seed: ghost into match ivory badge robot record tackle radar elbow traffic loud - ks2 = keystore.from_xpub('xpub661MyMwAqRbcGfCPEkkyo5WmcrhTq8mi3xuBS7VEZ3LYvsgY1cCFDbenT33bdD12axvrmXhuX3xkAbKci3yZY9ZEk8vhLic7KNhLjqdh5ec') - WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) - self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) - - w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') - self.assertEqual(w.txin_type, 'p2sh') - - self.assertEqual(w.get_receiving_addresses()[0], '32ji3QkAgXNz6oFoRfakyD3ys1XXiERQYN') - self.assertEqual(w.get_change_addresses()[0], '36XWwEHrrVCLnhjK5MrVVGmUHghr9oWTN1') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_multisig_seed_segwit(self, mock_write): - seed_words = 'snow nest raise royal more walk demise rotate smooth spirit canyon gun' - self.assertEqual(bitcoin.seed_type(seed_words), 'segwit') - - ks1 = keystore.from_seed(seed_words, '', True) - WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks1) - self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) - self.assertEqual(ks1.xprv, 'ZprvAjxLRqPiDfPDxXrm8JvcoCGRAW6xUtktucG6AMtdzaEbTEJN8qcECvujfhtDU3jLJ9g3Dr3Gz5m1ypfMs8iSUh62gWyHZ73bYLRWyeHf6y4') - self.assertEqual(ks1.xpub, 'Zpub6xwgqLvc42wXB1wEELTdALD9iXwStMUkGqBgxkJFYumaL2dWgNvUkjEDWyDFZD3fZuDWDzd1KQJ4NwVHS7hs6H6QkpNYSShfNiUZsgMdtNg') - - # electrum seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool - ks2 = keystore.from_xpub('Zpub6y4oYeETXAbzLNg45wcFDGwEG3vpgsyMJybiAfi2pJtNF3i3fJVxK2BeZJaw7VeKZm192QHvXP3uHDNpNmNDbQft9FiMzkKUhNXQafUMYUY') - WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) - self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) - - w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') - self.assertEqual(w.txin_type, 'p2wsh') - - self.assertEqual(w.get_receiving_addresses()[0], 'bc1qvzezdcv6vs5h45ugkavp896e0nde5c5lg5h0fwe2xyfhnpkxq6gq7pnwlc') - self.assertEqual(w.get_change_addresses()[0], 'bc1qxqf840dqswcmu7a8v82fj6ej0msx08flvuy6kngr7axstjcaq6us9hrehd') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_multisig_seed_bip45_standard(self, mock_write): - seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' - self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) - - ks1 = keystore.from_bip39_seed(seed_words, '', "m/45'/0") - self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) - self.assertEqual(ks1.xprv, 'xprv9vyEFyXf7pYVv4eDU3hhuCEAHPHNGuxX73nwtYdpbLcqwJCPwFKknAK8pHWuHHBirCzAPDZ7UJHrYdhLfn1NkGp9rk3rVz2aEqrT93qKRD9') - self.assertEqual(ks1.xpub, 'xpub69xafV4YxC6o8Yiga5EiGLAtqR7rgNgNUGiYgw3S9g9pp6XYUne1KxdcfYtxwmA3eBrzMFuYcNQKfqsXCygCo4GxQFHfywxpUbKNfYvGJka') - - # bip39 seed: tray machine cook badge night page project uncover ritual toward person enact - # der: m/45'/0 - ks2 = keystore.from_xpub('xpub6B26nSWddbWv7J3qQn9FbwPPQktSBdPQfLfHhRK4375QoZq8fvM8rQey1koGSTxC5xVoMzNMaBETMUmCqmXzjc8HyAbN7LqrvE4ovGRwNGg') - WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) - self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) - - w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') - self.assertEqual(w.txin_type, 'p2sh') - - self.assertEqual(w.get_receiving_addresses()[0], '3JPTQ2nitVxXBJ1yhMeDwH6q417UifE3bN') - self.assertEqual(w.get_change_addresses()[0], '3FGyDuxgUDn2pSZe5xAJH1yUwSdhzDMyEE') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_multisig_seed_p2sh_segwit(self, mock_write): - # bip39 seed: pulse mixture jazz invite dune enrich minor weapon mosquito flight fly vapor - # der: m/49'/0'/0' - # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh - ks1 = keystore.from_xprv('YprvAUXFReVvDjrPerocC3FxVH748sJUTvYjkAhtKop5VnnzVzMEHr1CHrYQKZwfJn1As3X4LYMav6upxd5nDiLb6SCjRZrBH76EFvyQAG4cn79') - self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) - self.assertEqual(ks1.xpub, 'Ypub6hWbqA2p47QgsLt5J4nxrR3ngu8xsPGb7PdV8CDh48KyNngNqPKSqertAqYhQ4umELu1UsZUCYfj9XPA6AdSMZWDZQobwF7EJ8uNrECaZg1') - - # bip39 seed: slab mixture skin evoke harsh tattoo rare crew sphere extend balcony frost - # der: m/49'/0'/0' - ks2 = keystore.from_xpub('Ypub6iNDhL4WWq5kFZcdFqHHwX4YTH4rYGp8xbndpRrY7WNZFFRfogSrL7wRTajmVHgR46AT1cqUG1mrcRd7h1WXwBsgX2QvT3zFbBCDiSDLkau') - WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) - self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) - - w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') - self.assertEqual(w.txin_type, 'p2wsh-p2sh') - - self.assertEqual(w.get_receiving_addresses()[0], '35LeC45QgCVeRor1tJD6LiDgPbybBXisns') - self.assertEqual(w.get_change_addresses()[0], '39RhtDchc6igmx5tyoimhojFL1ZbQBrXa6') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip32_extended_version_bytes(self, mock_write): - seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant' - self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) - bip32_seed = keystore.bip39_to_seed(seed_words, '') - self.assertEqual('0df68c16e522eea9c1d8e090cfb2139c3b3a2abed78cbcb3e20be2c29185d3b8df4e8ce4e52a1206a688aeb88bfee249585b41a7444673d1f16c0d45755fa8b9', - bh2u(bip32_seed)) - - def create_keystore_from_bip32seed(xtype): - ks = keystore.BIP32_KeyStore({}) - ks.add_xprv_from_seed(bip32_seed, xtype=xtype, derivation='m/') - return ks - - ks = create_keystore_from_bip32seed(xtype='standard') - self.assertEqual('033a05ec7ae9a9833b0696eb285a762f17379fa208b3dc28df1c501cf84fe415d0', ks.derive_pubkey(0, 0)) - self.assertEqual('02bf27f41683d84183e4e930e66d64fc8af5508b4b5bf3c473c505e4dbddaeed80', ks.derive_pubkey(1, 0)) - - ks = create_keystore_from_bip32seed(xtype='standard') # p2pkh - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K3nyWMZVjzGL4KKAE1zahmhTHuV5pdw4eK3o3igC5QywgQG7UTRe6TGBniPDpPFWzXMeMUFbBj8uYsfXGjyMmF54wdNt8QBm') - self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcGH3yTb2kMQGnsLziRTJZ8vNthsVSCGbdBr8CGDWKxnGAFYgyKTzBtwvPPmfVAWJuFmxRXjSbUTg87wDkWQ5GmzpfUcN9t8Z') - self.assertEqual(w.get_receiving_addresses()[0], '19fWEVaXqgJFFn7JYNr6ouxyjZy3uK7CdK') - self.assertEqual(w.get_change_addresses()[0], '1EEX7da31qndYyeKdbM665w1ze5gbkkAZZ') - - ks = create_keystore_from_bip32seed(xtype='p2wpkh-p2sh') - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(ks.xprv, 'yprvABrGsX5C9janu6AdBvHNCMRZVHJfxcaCgoyWgsyi1wSXN9cGyLMe33bpRU54TLJ1ruJbTrpNqusYQeFvBx1CXNb9k1DhKtBFWo8b1sLbXhN') - self.assertEqual(ks.xpub, 'ypub6QqdH2c5z7967aF6HwpNZVNJ3K9AN5J442u7VGPKaGyWEwwRWsftaqvJGkeZKNe7Jb3C9FG3dAfT94ZzFRrcGhMizGvB6Jtm3itJsEFhxMC') - self.assertEqual(w.get_receiving_addresses()[0], '34SAT5gGF5UaBhhSZ8qEuuxYvZ2cm7Zi23') - self.assertEqual(w.get_change_addresses()[0], '38unULZaetSGSKvDx7Krukh8zm8NQnxGiA') - - ks = create_keystore_from_bip32seed(xtype='p2wpkh') - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(ks.xprv, 'zprvAWgYBBk7JR8GkPMk2H4zQSX4fFT7uEZhbvVjUGsbPwpQRFRWDzXCf7FxSg2eTEwwGYRQDLQwJaE6HvsUueRDKcGkcLv7unzjnXCEQVWhrF9') - self.assertEqual(ks.xpub, 'zpub6jftahH18ngZxsSD8JbzmaToDHHcJhHYy9RLGfHCxHMPJ3kemXqTCuaSHxc9KHJ2iE9ztirc5q212MBYy8Gd4w3KrccbgDiFKSwxFpYKEH6') - self.assertEqual(w.get_receiving_addresses()[0], 'bc1qtuynwzd0d6wptvyqmc6ehkm70zcamxpshyzu5e') - self.assertEqual(w.get_change_addresses()[0], 'bc1qjy5zunxh6hjysele86qqywfa437z4xwmleq8wk') - - ks = create_keystore_from_bip32seed(xtype='standard') # p2sh - w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') - self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K3nyWMZVjzGL4KKAE1zahmhTHuV5pdw4eK3o3igC5QywgQG7UTRe6TGBniPDpPFWzXMeMUFbBj8uYsfXGjyMmF54wdNt8QBm') - self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcGH3yTb2kMQGnsLziRTJZ8vNthsVSCGbdBr8CGDWKxnGAFYgyKTzBtwvPPmfVAWJuFmxRXjSbUTg87wDkWQ5GmzpfUcN9t8Z') - self.assertEqual(w.get_receiving_addresses()[0], '3F4nm8Vunb7mxVvqhUP238PYge2hpU5qYv') - self.assertEqual(w.get_change_addresses()[0], '3N8jvKGmxzVHENn6B4zTdZt3N9bmRKjj96') - - ks = create_keystore_from_bip32seed(xtype='p2wsh-p2sh') - w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') - self.assertEqual(ks.xprv, 'YprvANkMzkodih9AKfL18akM2RmND5LwAyFo15dBc9FFPiGvzLBBjjjv8ATkEB2Y1mWv6NNaLSpVj8G3XosgVBA9frhpaUL6jHeFQXQTbqVPcv2') - self.assertEqual(ks.xpub, 'Ypub6bjiQGLXZ4hTY9QUEcHMPZi6m7BRaRyeNJYnQXerx3ous8WLHH4AfxnE5Tc2sos1Y47B1qGAWP3xGEBkYf1ZRBUPpk2aViMkwTABT6qoiBb') - self.assertEqual(w.get_receiving_addresses()[0], '3L1BxLLASGKE3DR1ruraWm3hZshGCKqcJx') - self.assertEqual(w.get_change_addresses()[0], '3NDGcbZVXTpaQWRhiuVPpXsNt4g2JiCX4E') - - ks = create_keystore_from_bip32seed(xtype='p2wsh') - w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') - self.assertEqual(ks.xprv, 'ZprvAhadJRUYsNgeAxX7xwXyEWrsP3VP7bFHvC9QPY98miep3RzQzPuUkE7tFNz81gAqW1VP5vR4BncbR6VFCsaAU6PRSp2XKCTjgFU6zRpk6Xp') - self.assertEqual(ks.xpub, 'Zpub6vZyhw1ShkEwPSbb4y4ybeobw5KsX3y9HR51BvYkL4BnvEKZXwDjJ2SN6fZcsiWvwhDymJriy3QW9WoKGMRaDR9zh5j15dBFDBDpqjK1ekQ') - self.assertEqual(w.get_receiving_addresses()[0], 'bc1q84x0yrztvcjg88qef4d6978zccxulcmc9y88xcg4ghjdau999x7q7zv2qe') - self.assertEqual(w.get_change_addresses()[0], 'bc1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3tsrkg3gs') - - -class TestWalletKeystoreAddressIntegrityForTestnet(TestCaseForTestnet): - - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_write): - # bip39 seed: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose - # der: m/49'/1'/0' - # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh - ks1 = keystore.from_xprv('Uprv9BEixD3As2LK5h6G2SNT3cTqbZpsWYPceKTSuVAm1yuSybxSvQz2MV1o8cHTtctQmj4HAenb3eh5YJv4YRZjv35i8fofVnNbs4Dd2B4i5je') - self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) - self.assertEqual(ks1.xpub, 'Upub5QE5Mia4hPtcJBAj8TuTQkQa9bfMv17U1YP3hsaNaKSRrQHbTxJGuHLGyv3MbKZixuPyjfXGUdbTjE4KwyFcX8YD7PX5ybTDbP11UT8UpZR') - - # bip39 seed: square page wood spy oil story rebel give milk screen slide shuffle - # der: m/49'/1'/0' - ks2 = keystore.from_xpub('Upub5QRzUGRJuWJe5MxGzwgQAeyJjzcdGTXkkq77w6EfBkCyf5iWppSaZ4caY2MgWcU9LP4a4uE5apUFN4wLoENoe9tpu26mrUxeGsH84dN3JFh') - WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2) - self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) - - w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2') - self.assertEqual(w.txin_type, 'p2wsh-p2sh') - - self.assertEqual(w.get_receiving_addresses()[0], '2MzsfTfTGomPRne6TkctMmoDj6LwmVkDrMt') - self.assertEqual(w.get_change_addresses()[0], '2NFp9w8tbYYP9Ze2xQpeYBJQjx3gbXymHX7') - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip32_extended_version_bytes(self, mock_write): - seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant' - self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) - bip32_seed = keystore.bip39_to_seed(seed_words, '') - self.assertEqual('0df68c16e522eea9c1d8e090cfb2139c3b3a2abed78cbcb3e20be2c29185d3b8df4e8ce4e52a1206a688aeb88bfee249585b41a7444673d1f16c0d45755fa8b9', - bh2u(bip32_seed)) - - def create_keystore_from_bip32seed(xtype): - ks = keystore.BIP32_KeyStore({}) - ks.add_xprv_from_seed(bip32_seed, xtype=xtype, derivation='m/') - return ks - - ks = create_keystore_from_bip32seed(xtype='standard') - self.assertEqual('033a05ec7ae9a9833b0696eb285a762f17379fa208b3dc28df1c501cf84fe415d0', ks.derive_pubkey(0, 0)) - self.assertEqual('02bf27f41683d84183e4e930e66d64fc8af5508b4b5bf3c473c505e4dbddaeed80', ks.derive_pubkey(1, 0)) - - ks = create_keystore_from_bip32seed(xtype='standard') # p2pkh - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(ks.xprv, 'tprv8ZgxMBicQKsPecD328MF9ux3dSaSFWci7FNQmuWH7uZ86eY8i3XpvjK8KSH8To2QphiZiUqaYc6nzDC6bTw8YCB9QJjaQL5pAApN4z7vh2B') - self.assertEqual(ks.xpub, 'tpubD6NzVbkrYhZ4Y5Epun1qZKcACU6NQqocgYyC4RYaYBMWw8nuLSMR7DvzVamkqxwRgrTJ1MBMhc8wwxT2vbHqMu8RBXy4BvjWMxR5EdZroxE') - self.assertEqual(w.get_receiving_addresses()[0], 'mpBTXYfWehjW2tavFwpUdqBJbZZkup13k2') - self.assertEqual(w.get_change_addresses()[0], 'mtkUQgf1psDtL67wMAKTv19LrdgPWy6GDQ') - - ks = create_keystore_from_bip32seed(xtype='p2wpkh-p2sh') - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(ks.xprv, 'uprv8tXDerPXZ1QsVuQ9rV8sN13YoQitC8cD2MtdZJQAVuw19kMMxhhPYnyGLeEiThgLELqNTxS91GTLsVofKAM9LRrkGeRzzEuJRtt1Tcostr7') - self.assertEqual(ks.xpub, 'upub57Wa4MvRPNyAiPUcxWfsj8zHMSZNbbL4PapEMgon4FTz2YgWWF1e6bHkBvpDKk2Rg2Zy9LsonXFFbv7jNeCZ5kdKWv8UkfcoxpdjJrZuBX6') - self.assertEqual(w.get_receiving_addresses()[0], '2MuzNWpcHrXyvPVKzEGT7Xrwp8uEnXXjWnK') - self.assertEqual(w.get_change_addresses()[0], '2MzTzY5VcGLwce7YmdEwjXhgQD7LYEKLJTm') - - ks = create_keystore_from_bip32seed(xtype='p2wpkh') - w = WalletIntegrityHelper.create_standard_wallet(ks) - self.assertEqual(ks.xprv, 'vprv9DMUxX4ShgxMMCbGgqvVa693yNsL8kbhwUQrLhJ3svJtCrAbDMrxArdQMrCJTcLFdyxBDS2hTvotknRE2rmA8fYM8z8Ra9inhcwerEsG6Ev') - self.assertEqual(ks.xpub, 'vpub5SLqN2bLY4WeZgfjnsTVwE5nXQhpYDKZJhLT95hfSFqs5eVjkuBCiewtD8moKegM5fgmtpUNFBboVCjJ6LcZszJvPFpuLaSJEYhNhUAnrCS') - self.assertEqual(w.get_receiving_addresses()[0], 'tb1qtuynwzd0d6wptvyqmc6ehkm70zcamxpsaze002') - self.assertEqual(w.get_change_addresses()[0], 'tb1qjy5zunxh6hjysele86qqywfa437z4xwm4lm549') - - ks = create_keystore_from_bip32seed(xtype='standard') # p2sh - w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') - self.assertEqual(ks.xprv, 'tprv8ZgxMBicQKsPecD328MF9ux3dSaSFWci7FNQmuWH7uZ86eY8i3XpvjK8KSH8To2QphiZiUqaYc6nzDC6bTw8YCB9QJjaQL5pAApN4z7vh2B') - self.assertEqual(ks.xpub, 'tpubD6NzVbkrYhZ4Y5Epun1qZKcACU6NQqocgYyC4RYaYBMWw8nuLSMR7DvzVamkqxwRgrTJ1MBMhc8wwxT2vbHqMu8RBXy4BvjWMxR5EdZroxE') - self.assertEqual(w.get_receiving_addresses()[0], '2N6czpsRwQ3d8AHZPNbztf5NotzEsaZmVQ8') - self.assertEqual(w.get_change_addresses()[0], '2NDgwz4CoaSzdSAQdrCcLFWsJaVowCNgiPA') - - ks = create_keystore_from_bip32seed(xtype='p2wsh-p2sh') - w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') - self.assertEqual(ks.xprv, 'Uprv95RJn67y7xyEvUZXo9brC5PMXCm9QVHoLdYJUZfhsgmQmvvGj75fduqC9MCC28uETouMLYSFtUqqzfRRcPW6UuyR77YQPeNJKd9t3XutF8b') - self.assertEqual(ks.xpub, 'Upub5JQfBberxLXY8xdzuB8rZDL65Ebdox1ehrTuGx5KS2JPejFRGePvBi9fzdmgtBFKuVdx1vsvfjdkj5jVfsMWEEjzMPEtA55orYubtrCZmRr') - self.assertEqual(w.get_receiving_addresses()[0], '2NBZQ25GC3ipaF13ZY3UT8i2xnDuS17pJqx') - self.assertEqual(w.get_change_addresses()[0], '2NDmUgLVX8vKvcJ4FQ37GSUre6QtBzKkb6k') - - ks = create_keystore_from_bip32seed(xtype='p2wsh') - w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1') - self.assertEqual(ks.xprv, 'Vprv16YtLrHXxePM6noKqtFtMtmUgBE9bEpF3fPLmpvuPksssLostujtdHBwqhEeVuzESz22UY8hyPx9ed684SQpCmUKSVhpxPFbvVNY7qnviNR') - self.assertEqual(ks.xpub, 'Vpub5dEvVGKn7251zFq7jXvUmJRbFCk5ka19cxz84LyCp2gGhq4eXJZUomop1qjGt5uFK8kkmQUV8PzJcNM4PZmX2URbDiwJjyuJ8GyFHRrEmmG') - self.assertEqual(w.get_receiving_addresses()[0], 'tb1q84x0yrztvcjg88qef4d6978zccxulcmc9y88xcg4ghjdau999x7qf2696k') - self.assertEqual(w.get_change_addresses()[0], 'tb1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3ts5777jl') - - -class TestWalletSending(TestCaseForTestnet): - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.electrum_path = tempfile.mkdtemp() - cls.config = SimpleConfig({'electrum_path': cls.electrum_path}) - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - shutil.rmtree(cls.electrum_path) - - def create_standard_wallet_from_seed(self, seed_words): - ks = keystore.from_seed(seed_words, '', False) - return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=2) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_write): - wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver') - wallet2 = self.create_standard_wallet_from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song') - - # bootstrap wallet1 - funding_tx = Transaction('01000000014576dacce264c24d81887642b726f5d64aa7825b21b350c7b75a57f337da6845010000006b483045022100a3f8b6155c71a98ad9986edd6161b20d24fad99b6463c23b463856c0ee54826d02200f606017fd987696ebbe5200daedde922eee264325a184d5bbda965ba5160821012102e5c473c051dae31043c335266d0ef89c1daab2f34d885cc7706b267f3269c609ffffffff0240420f00000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c42a2ddb90e000000001976a914c384950342cb6f8df55175b48586838b03130fad88ac00000000') - funding_txid = funding_tx.txid() - funding_output_value = 1000000 - self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_txid) - wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # wallet1 -> wallet2 - outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 250000)] - tx = wallet1.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet1.is_mine(wallet1.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('010000000001010392c1940e2ec9f2372919ca3887327fe5b98b866022cc79bab5cbed5a53d2ad0000000000feffffff0290d00300000000001976a914ea7804a2c266063572cc009a63dc25dcc0e9d9b588ac285e0b0000000000160014690b59a8140602fb23cc2904ece9cc4daf361052024730440220608a5339ca894592da82119e1e4a1d09335d70a552c683687223b8ed724465e902201b3f0feccf391b1b6257e4b18970ae57d7ca060af2dae519b3690baad2b2a34e0121030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c00000000', - str(tx_copy)) - self.assertEqual('3c06ae4d9be8226a472b3e7f7c127c7e3016f525d658d26106b80b4c7e3228e2', tx_copy.txid()) - self.assertEqual('d8d930ae91dce73118c3fffabbdfcfb87f5d91673fb4c7dfd0fbe7cf03bf426b', tx_copy.wtxid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - - wallet1.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # TX_HEIGHT_UNCONF_PARENT but nvm - wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - - # wallet2 -> wallet1 - outputs = [(bitcoin.TYPE_ADDRESS, wallet1.get_receiving_address(), 100000)] - tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('0100000001e228327e4c0bb80661d258d625f516307e7c127c7f3e2b476a22e89b4dae063c000000006b483045022100d3895b31e7c9766987c6f53794c7394f534f4acecefda5479d963236f9703d0b022026dd4e40700ceb788f136faf54bf85b966648dc7c2a608d8110604f2d22d59070121030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cffeffffff02a0860100000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c4268360200000000001976a914ca4c60999c46c2108326590b125aefd476dcb11888ac00000000', - str(tx_copy)) - self.assertEqual('5f25707571eb776bdf14142f9966bf2a681906e0a79501edbb99a972c2ceb972', tx_copy.txid()) - self.assertEqual('5f25707571eb776bdf14142f9966bf2a681906e0a79501edbb99a972c2ceb972', tx_copy.wtxid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - - wallet1.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - - # wallet level checks - self.assertEqual((0, funding_output_value - 250000 - 5000 + 100000, 0), wallet1.get_balance()) - self.assertEqual((0, 250000 - 5000 - 100000, 0), wallet2.get_balance()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_write): - wallet1a = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', '', True), - keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'), - keystore.from_xpub('tpubD6NzVbkrYhZ4XJzYkhsCbDCcZRmDAKSD7bXi9mdCni7acVt45fxbTVZyU6jRGh29ULKTjoapkfFsSJvQHitcVKbQgzgkkYsAmaovcro7Mhf') - ], - '2of3', gap_limit=2 - ) - wallet1b = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song', '', True), - keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'), - keystore.from_xpub('tpubD6NzVbkrYhZ4YARFMEZPckrqJkw59GZD1PXtQnw14ukvWDofR7Z1HMeSCxfYEZVvg4VdZ8zGok5VxHwdrLqew5cMdQntWc5mT7mh1CSgrnX') - ], - '2of3', gap_limit=2 - ) - # ^ third seed: ghost into match ivory badge robot record tackle radar elbow traffic loud - wallet2 = self.create_standard_wallet_from_seed('powerful random nobody notice nothing important anyway look away hidden message over') - - # bootstrap wallet1 - funding_tx = Transaction('010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300') - funding_txid = funding_tx.txid() - funding_output_value = 12000000 - self.assertEqual('b25cd55687c9e528c2cfd546054f35fb6741f7cf32d600f07dfecdf2e1d42071', funding_txid) - wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # wallet1 -> wallet2 - outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 370000)] - tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners - self.assertFalse(tx.is_complete()) - wallet1b.sign_transaction(tx, password=None) - - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('01000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb201000000fdfe0000483045022100f9ce5616683e613ae14b98d56436454b003348a8172e2ed598018e3d206e57d7022030c65c6551e839f9e9409812be624dbb4e36bd4152c9ed9b0988c10fd8201d1401483045022100d5cb94d4d1dcf01bb9e9280e8178a7e9ada3ad14378ca543afcc9f5667b27cb2022018e76b74800a21934e73b226b34cbbe45c877fba64693da8a20d3cb330f2eafd014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefeffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000', - str(tx_copy)) - self.assertEqual('26f3bdd0402e1cff19126244ebe3d32722cef0db507c7229ca8754f5e06ef25d', tx_copy.txid()) - self.assertEqual('26f3bdd0402e1cff19126244ebe3d32722cef0db507c7229ca8754f5e06ef25d', tx_copy.wtxid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - - wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - - # wallet2 -> wallet1 - outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] - tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('01000000015df26ee0f55487ca29727c50dbf0ce2227d3e3eb44621219ff1c2e40d0bdf326000000008b483045022100bd9f61ba82507d3a28922fb8be129e14699dfa54ddd03cc9494f696d38ac4121022071afca6fad5bc5c09b0a675e6444be3e97dbbdbc283764ee5f4e27a032d933d80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff02a08601000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887280b0400000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac00000000', - str(tx_copy)) - self.assertEqual('c573b3f8464a4ed40dfc79d0889a780f44e917beef7a75883b2427c2987f3e95', tx_copy.txid()) - self.assertEqual('c573b3f8464a4ed40dfc79d0889a780f44e917beef7a75883b2427c2987f3e95', tx_copy.wtxid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - - wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - - # wallet level checks - self.assertEqual((0, funding_output_value - 370000 - 5000 + 100000, 0), wallet1a.get_balance()) - self.assertEqual((0, 370000 - 5000 - 100000, 0), wallet2.get_balance()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_write): - wallet1a = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', '', True), - keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'), - keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra') - ], - '2of3', gap_limit=2 - ) - wallet1b = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('snow nest raise royal more walk demise rotate smooth spirit canyon gun', '', True), - keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra'), - keystore.from_xpub('Vpub5gSKXzxK7FeKQedu2q1z9oJWxqvX72AArW3HSWpEhc8othDH8xMDu28gr7gf17sp492BuJod8Tn7anjvJrKpETwqnQqX7CS8fcYyUtedEMk') - ], - '2of3', gap_limit=2 - ) - # ^ third seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool - wallet2a = WalletIntegrityHelper.create_multisig_wallet( - [ - # bip39: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose, der: m/1234'/1'/0', p2wsh-p2sh multisig - keystore.from_xprv('Uprv9CvELvByqm8k2dpecJVjgLMX1z5DufEjY4fBC5YvdGF5WjGCa7GVJJ2fYni1tyuF7Hw83E6W2ZBjAhaFLZv2ri3rEsubkCd5avg4EHKoDBN'), - keystore.from_xpub('Upub5Qb8ik4Cnu8g97KLXKgVXHqY6tH8emQvqtBncjSKsyfTZuorPtTZgX7ovKKZHuuVGBVd1MTTBkWez1XXt2weN1sWBz6SfgRPQYEkNgz81QF') - ], - '2of2', gap_limit=2 - ) - wallet2b = WalletIntegrityHelper.create_multisig_wallet( - [ - # bip39: square page wood spy oil story rebel give milk screen slide shuffle, der: m/1234'/1'/0', p2wsh-p2sh multisig - keystore.from_xprv('Uprv9BbnKEXJxXaNvdEsRJ9VA9toYrSeFJh5UfGBpM2iKe8Uh7UhrM9K8ioL53s8gvCoGfirHHaqpABDAE7VUNw8LNU1DMJKVoWyeNKu9XcDC19'), - keystore.from_xpub('Upub5RuakRisg8h3F7u7iL2k3UJFa1uiK7xauHamzTxYBbn4PXbM7eajr6M9Q2VCr6cVGhfhqWQqxnABvtSATuVM1xzxk4nA189jJwzaMn1QX7V') - ], - '2of2', gap_limit=2 - ) - - # bootstrap wallet1 - funding_tx = Transaction('01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c11300') - funding_txid = funding_tx.txid() - funding_output_value = 200000 - self.assertEqual('d2bd6c9d332db8e2c50aa521cd50f963fba214645aab2f7556e061a412103e21', funding_txid) - wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # wallet1 -> wallet2 - outputs = [(bitcoin.TYPE_ADDRESS, wallet2a.get_receiving_address(), 165000)] - tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - txid = tx.txid() - tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners - self.assertEqual(txid, tx.txid()) - self.assertFalse(tx.is_complete()) - wallet1b.sign_transaction(tx, password=None) - - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400483045022100ea2fbd3d8681cfafdcae1bdaaa64f92fb9872fb8f6bf03a2b7effcf7390b66c8022021a79eff7975479934f958f3766d6ac61d708c79b785e398b3bcd84b1039e9b501483045022100dbc4f1ec18f0e0deb4ff88d7d5b3d3b7b500a80d0c0f33efbd3262f0c8689095022074fd226c0b52e3716ad907d14cba9c79aca482a8f4a51662ca83a5b9db49e15b016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000', - str(tx_copy)) - self.assertEqual('6e9c3cd8788bdb970a124ea06136d52bc01cec4f9b1e217627d5e90ebe77d049', tx_copy.txid()) - self.assertEqual('c58650fb77d04577fccb3e201deecbf691ab52ffb61cd2e57996c4d51f7e980b', tx_copy.wtxid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - self.assertEqual(txid, tx_copy.txid()) - - wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - - # wallet2 -> wallet1 - outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] - tx = wallet2a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - txid = tx.txid() - tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners - self.assertEqual(txid, tx.txid()) - self.assertFalse(tx.is_complete()) - wallet2b.sign_transaction(tx, password=None) - - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2a.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet2a.is_mine(wallet2a.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('0100000000010149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e01000000232200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163cfeffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a0860100000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0400483045022100c254468bbe6b8bd1c8c01b6a223e46cc5c6b56fbba87d59575385ad249133b0e02207139688f8d6ae8076c92a266d98454d25c040d04c8e513a37bf7c32dad3e48210147304402204af5edbab2d674f6a9edef8c97b2f7fdf8ababedc7b287710cc7a64d4699358b022064e2d07f4bb32373be31b2003dc56b7b831a7c01419326efb3011c64b898b3f00147522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae00000000', - str(tx_copy)) - self.assertEqual('84b0dcb43022385f7a10e2710e5625a2be3cd6e390387b6100b55500d5eea8f6', tx_copy.txid()) - self.assertEqual('7e561e25da843326e61fd20a40b72fcaeb8690176fc7c3fcbadb3a0146c8396c', tx_copy.wtxid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - self.assertEqual(txid, tx_copy.txid()) - - wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - - # wallet level checks - self.assertEqual((0, funding_output_value - 165000 - 5000 + 100000, 0), wallet1a.get_balance()) - self.assertEqual((0, 165000 - 5000 - 100000, 0), wallet2a.get_balance()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_write): - wallet1a = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('phone guilt ancient scan defy gasp off rotate approve ill word exchange', '', True), - keystore.from_xpub('tpubD6NzVbkrYhZ4YPZ3ntVjqSCxiUUv2jikrUBU73Q3iJ7Y8iR41oYf991L5fanv7ciHjbjokdK2bjYqg1BzEUDxucU9qM5WRdBiY738wmgLP4') - ], - '1of2', gap_limit=2 - ) - # ^ second seed: kingdom now gift initial age right velvet exotic harbor enforce kingdom kick - wallet2 = WalletIntegrityHelper.create_standard_wallet( - # bip39: uniform tank success logic lesson awesome stove elegant regular desert drip device, der: m/49'/1'/0' - keystore.from_xprv('uprv91HGbrNZTK4x8u22nbdYGzEuWPxjaHMREUi7CNhY64KsG5ZGnVM99uCa16EMSfrnaPTFxjbRdBZ2WiBkokoM8anzAy3Vpc52o88WPkitnxi'), - gap_limit=2 - ) - - # bootstrap wallet1 - funding_tx = Transaction('010000000001027e20990282eb29588375ad04936e1e991af3bc5b9c6f1ab62eca8c25becaef6a01000000171600140e6a17fadc8bafba830f3467a889f6b211d69a00fdffffff51847fd6bcbdfd1d1ea2c2d95c2d8de1e34c5f2bd9493e88a96a4e229f564e800100000017160014ecdf9fa06856f9643b1a73144bc76c24c67774a6fdffffff021e8501000000000017a91451991bfa68fbcb1e28aa0b1e060b7d24003352e38700093d000000000017a914b0b9f31bace76cdfae2c14abc03e223403d7dc4b870247304402205e19721b92c6afd70cd932acb50815a36ee32ab46a934147d62f02c13aeacf4702207289c4a4131ef86e27058ff70b6cb6bf0e8e81c6cbab6dddd7b0a9bc732960e4012103fe504411c21f7663caa0bbf28931f03fae7e0def7bc54851e0194dfb1e2c85ef02483045022100e969b65096fba4f8b24eb5bc622d2282076241621f3efe922cc2067f7a8a6be702203ec4047dd2a71b9c83eb6a0875a6d66b4d65864637576c06ed029d3d1a8654b0012102bbc8100dca67ba0297aba51296a4184d714204a5fc2eda34708360f37019a3dccfcc1300') - funding_txid = funding_tx.txid() - funding_output_value = 4000000 - self.assertEqual('1137c12de4ce0f5b08de8846ba14c0814351a7f0f31457c8ea51a5d4b3c891a3', funding_txid) - wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # wallet1 -> wallet2 - outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 1000000)] - tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('0100000001a391c8b3d4a551eac85714f3f0a7514381c014ba4688de085b0fcee42dc13711010000009200483045022100fcf03aeb97b66791372c18aa0dd651817cf458d941dd628c966f0305a023360f022016c534530e267b6a52f90e62aa9fb50ace609ffb21e472d3ba7b29db9b30050e014751210245c90e040d4f9d1fc136b3d4d6b7535bbb5df2bd27666c21977042cc1e05b5b02103c9a6bebfce6294488315e58137a279b2efe09f1f528ecf93b40675ded3cf0e5f52aefeffffff0240420f000000000017a9149573eb50f3136dff141ac304190f41c8becc92ce8738b32d000000000017a914b815d1b430ae9b632e3834ed537f7956325ee2a98700000000', - str(tx_copy)) - self.assertEqual('1b7e94860b9681d4e371928d40fdbd4641e991aa74f1a211f239c887047e4a2a', tx_copy.txid()) - self.assertEqual('1b7e94860b9681d4e371928d40fdbd4641e991aa74f1a211f239c887047e4a2a', tx_copy.wtxid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - - wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - - # wallet2 -> wallet1 - outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 300000)] - tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('010000000001012a4a7e0487c839f211a2f174aa91e94146bdfd408d9271e3d481960b86947e1b00000000171600149fad840ed174584ee054bd26f3e411817338c5edfeffffff02e09304000000000017a914b0b9f31bace76cdfae2c14abc03e223403d7dc4b87d89a0a000000000017a9148ccd0efb2be5b412c4033715f560ed8f446c8ceb87024830450221009c816c3e0c40b37085244f0976f65635b8d711952bad9843c5f51e386fd37cc402202c34a4a7227182742d9f93e9f28c4bd30ded6514550f39614cb5ad00e46690070121038362bbf0b4918b37e9d7c75930ed3a78e3d445724cb5c37ade4a59b6e411fe4e00000000', - str(tx_copy)) - self.assertEqual('f65edb0843ff44436dc5964fb6b298e157502b9b4a83dac6b82dd2d2a3247d0a', tx_copy.txid()) - self.assertEqual('63efc09db4c7445eaaca9a5e7732202f42aec81a53b05d819f1918ce0cf3b84d', tx_copy.wtxid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - - wallet1a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - - # wallet level checks - self.assertEqual((0, funding_output_value - 1000000 - 5000 + 300000, 0), wallet1a.get_balance()) - self.assertEqual((0, 1000000 - 5000 - 300000, 0), wallet2.get_balance()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bump_fee_p2pkh(self, mock_write): - wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') - - # bootstrap wallet - funding_tx = Transaction('010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400') - funding_txid = funding_tx.txid() - funding_output_value = 10000000 - self.assertEqual('03052739fcfa2ead5f8e57e26021b0c2c546bcd3d74c6e708d5046dc58d90762', funding_txid) - wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create tx - outputs = [(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] - coins = wallet.get_spendable_coins(domain=None, config=self.config) - tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) - tx.set_rbf(True) - tx.locktime = 1325501 - wallet.sign_transaction(tx, password=None) - - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006b483045022100df74e6a88085be1ff3a3fd96cf2ef03b5e33fa06788f56aa71649f0177d1bfc402206e36a7e6124863ac746d5288d6d47c1d1eac5d4ac3818e561a7a0f2c0a269429012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d7200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400', - str(tx_copy)) - self.assertEqual('44e6dd9529a253181112fc40cadd8ebb4c4359aacb91aa24c45556a1d00839b0', tx_copy.txid()) - self.assertEqual('44e6dd9529a253181112fc40cadd8ebb4c4359aacb91aa24c45556a1d00839b0', tx_copy.wtxid()) - - wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) - - # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) - tx.locktime = 1325501 - self.assertFalse(tx.is_complete()) - - wallet.sign_transaction(tx, password=None) - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - tx_copy = Transaction(tx.serialize()) - self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006a473044022055b7e6b7e89a55740f7aa2ad1ffcd4b5c913f0de63cf512438921534bc9c3a8d022043b3b27bdc2da4cc6265e4cc9673a3780ccd5cd6f0ee2eaedb51720c15b7a00a012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987d0497200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400', - str(tx_copy)) - self.assertEqual('f26edcf20991dccedf16058adbee923db7057c9b102db660156b8142b6a59bc7', tx_copy.txid()) - self.assertEqual('f26edcf20991dccedf16058adbee923db7057c9b102db660156b8142b6a59bc7', tx_copy.wtxid()) - - wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual((0, funding_output_value - 2500000 - 10000, 0), wallet.get_balance()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_cpfp_p2pkh(self, mock_write): - wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') - - # bootstrap wallet - funding_tx = Transaction('010000000001010f40064d66d766144e17bb3276d96042fd5aee2196bcce7e415f839e55a83de800000000171600147b6d7c7763b9185b95f367cf28e4dc6d09441e73fdffffff02404b4c00000000001976a9141df43441a3a3ee563e560d3ddc7e07cc9f9c3cdb88ac009871000000000017a9143873281796131b1996d2f94ab265327ee5e9d6e28702473044022029c124e5a1e2c6fa12e45ccdbdddb45fec53f33b982389455b110fdb3fe4173102203b3b7656bca07e4eae3554900aa66200f46fec0af10e83daaa51d9e4e62a26f4012103c8f0460c245c954ef563df3b1743ea23b965f98b120497ac53bd6b8e8e9e0f9bbe391400') - funding_txid = funding_tx.txid() - funding_output_value = 5000000 - self.assertEqual('9973bf8918afa349b63934432386f585613b51034db6c8628b61ba2feb8a3668', funding_txid) - wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # cpfp tx - tx = wallet.cpfp(funding_tx, fee=50000) - tx.set_rbf(True) - tx.locktime = 1325502 - wallet.sign_transaction(tx, password=None) - - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - - self.assertEqual(tx.txid(), tx_copy.txid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - self.assertEqual('010000000168368aeb2fba618b62c8b64d03513b6185f58623433439b649a3af1889bf7399000000006a47304402203a0b369e46c5fbacb83044b7ab9d69ff7998774041d6870993504915bc495d210220272833b870d8abca516adb7dc4cb27892b1b6e4b52fbfeb592a72c3e795eb213012102a7536f0bfbc60c5a8e86e2b9df26431fc062f9f454016dbc26f2467e0bc98b3ffdffffff01f0874b00000000001976a9141df43441a3a3ee563e560d3ddc7e07cc9f9c3cdb88acbe391400', - str(tx_copy)) - self.assertEqual('47500a425518b5542d94db1157f473b8cf322d31ea97a1a642fec19386cdb761', tx_copy.txid()) - self.assertEqual('47500a425518b5542d94db1157f473b8cf322d31ea97a1a642fec19386cdb761', tx_copy.wtxid()) - - wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bump_fee_p2wpkh(self, mock_write): - wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') - - # bootstrap wallet - funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') - funding_txid = funding_tx.txid() - funding_output_value = 10000000 - self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid) - wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create tx - outputs = [(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] - coins = wallet.get_spendable_coins(domain=None, config=self.config) - tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) - tx.set_rbf(True) - tx.locktime = 1325499 - wallet.sign_transaction(tx, password=None) - - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402205442705e988abe74bf391b293bb1b886674284a92ed0788c33024f9336d60aef022013a93049d3bed693254cd31a704d70bb988a36750f0b74d0a5b4d9e29c54ca9d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400', - str(tx_copy)) - self.assertEqual('b019bbad45a46ed25365e46e4cae6428fb12ae425977eb93011ffb294cb4977e', tx_copy.txid()) - self.assertEqual('ba87313e2b3b42f1cc478843d4d53c72d6e06f6c66ac8cfbe2a59cdac2fd532d', tx_copy.wtxid()) - - wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) - - # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) - tx.locktime = 1325500 - self.assertFalse(tx.is_complete()) - - wallet.sign_transaction(tx, password=None) - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - tx_copy = Transaction(tx.serialize()) - self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987d049720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5024730440220517fed3a902b5b41fa718ffd5f229b835b8ed26f23433c4ea437d24eff66d15b0220526854a6ebcd351ab2373d0e7c4e20f17c420520b5d570c2df7ca1d773d6a55d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400', - str(tx_copy)) - self.assertEqual('9a1c0ef7e871798b86074c7f8dd1e81b6d9a758ff07e0059eee31dc6fbf4f438', tx_copy.txid()) - self.assertEqual('59144d30c911ac33359b0a32d5a3fdd2ca806982c85838e193eb95f5d315e813', tx_copy.wtxid()) - - wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual((0, funding_output_value - 2500000 - 10000, 0), wallet.get_balance()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_cpfp_p2wpkh(self, mock_write): - wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') - - # bootstrap wallet - funding_tx = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400') - funding_txid = funding_tx.txid() - funding_output_value = 5000000 - self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid) - wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # cpfp tx - tx = wallet.cpfp(funding_tx, fee=50000) - tx.set_rbf(True) - tx.locktime = 1325501 - wallet.sign_transaction(tx, password=None) - - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - - self.assertEqual(tx.txid(), tx_copy.txid()) - self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - self.assertEqual('010000000001014a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff01f0874b000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900248304502210098fbe458a9f1c595d6bf63962fad00300a7b60c6dd8b2e7625f3804a3bf1086602204bc8a46fb162be8f85a23644eccf9f4223fa092f5c861144676a34dc83a7c39d012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbd391400', - str(tx_copy)) - self.assertEqual('38a21c67336232c88ae15311f329197c69ee70e872f8acb5bc9c2b6417c35ad8', tx_copy.txid()) - self.assertEqual('b5b8264ed5f3e03d48ef82fa2a25278cd9c0563fa78e557f370b7e0558293172', tx_copy.wtxid()) - - wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) - - @needs_test_with_all_ecc_implementations - def test_sweep_p2pk(self): - - class NetworkMock: - relay_fee = 1000 - def get_local_height(self): return 1325785 - def listunspent_for_scripthash(self, scripthash): - if scripthash == '460e4fb540b657d775d84ff4955c9b13bd954c2adc26a6b998331343f85b6a45': - return [{'tx_hash': 'ac24de8b58e826f60bd7b9ba31670bdfc3e8aedb2f28d0e91599d741569e3429', 'tx_pos': 1, 'height': 1325785, 'value': 1000000}] - else: - return [] - - privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu', ] - network = NetworkMock() - dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2' - tx = sweep(privkeys, network, config=None, recipient=dest_addr, fee=5000) - - tx_copy = Transaction(tx.serialize()) - self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400', - str(tx_copy)) - self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.txid()) - self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid()) - - -class TestWalletOfflineSigning(TestCaseForTestnet): - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.electrum_path = tempfile.mkdtemp() - cls.config = SimpleConfig({'electrum_path': cls.electrum_path}) - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - shutil.rmtree(cls.electrum_path) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_write): - wallet_offline = WalletIntegrityHelper.create_standard_wallet( - # bip39: "qwe", der: m/44'/1'/0' - keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), - gap_limit=4 - ) - wallet_online = WalletIntegrityHelper.create_standard_wallet( - keystore.from_xpub('tpubDDMN69wQjDZxaJz9afZQGa48hZS7X5oSegF2hg67yddNvqfpuTN9DqvDEp7YyVf7AzXnqBqHdLhzTAStHvsoMDDb8WoJQzNrcHgDJHVYgQF'), - gap_limit=4 - ) - - # bootstrap wallet_online - funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400') - funding_txid = funding_tx.txid() - self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325340 - - self.assertFalse(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.txid()) - self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_write): - wallet_offline = WalletIntegrityHelper.create_standard_wallet( - # bip39: "qwe", der: m/49'/1'/0' - keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), - gap_limit=4 - ) - wallet_online = WalletIntegrityHelper.create_standard_wallet( - keystore.from_xpub('upub5DGeFrwFEPfD711qQ6tKPaUYjBY6BRqfxcWPT77hiHz7VMo7oNGeom5EdXoKXEazePyoN3ueJMqHBfp3MwmsaD8k9dFHoa8KGeVXev7Pbg2'), - gap_limit=4 - ) - - # bootstrap wallet_online - funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400') - funding_txid = funding_tx.txid() - self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325341 - - self.assertFalse(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('3f0d188519237478258ad2bf881643618635d11c2bb95512e830fcf2eda3c522', tx_copy.txid()) - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual('3f0d188519237478258ad2bf881643618635d11c2bb95512e830fcf2eda3c522', tx.txid()) - self.assertEqual('27b78ec072a403b0545258e7a1a8d494e4b6fd48bf77f4251a12160c92207cbc', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_write): - wallet_offline = WalletIntegrityHelper.create_standard_wallet( - # bip39: "qwe", der: m/84'/1'/0' - keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), - gap_limit=4 - ) - wallet_online = WalletIntegrityHelper.create_standard_wallet( - keystore.from_xpub('vpub5Y941QgusZGvuD5nXTpUvVWohm8q41uftcRNronjRWs9jB2iVr4BbxqbRfAoQjWHgJtDCQEXChgfsPbEuBnidtkFztZSD3zDKTrtwXa2LCa'), - gap_limit=4 - ) - - # bootstrap wallet_online - funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400') - funding_txid = funding_tx.txid() - self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325341 - - self.assertFalse(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx_copy.txid()) - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid()) - self.assertEqual('729c2e40a2fccd6b731407c01ed304119c1ac329bdf9baae5b642d916c5f3272', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_wif_online_addr_p2pkh(self, mock_write): # compressed pubkey - wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', pw=None) - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG') - - # bootstrap wallet_online - funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') - funding_txid = funding_tx.txid() - self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325340 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid()) - self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write): - wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', pw=None) - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8') - - # bootstrap wallet_online - funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') - funding_txid = funding_tx.txid() - self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325340 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid()) - self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write): - wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', pw=None) - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529') - - # bootstrap wallet_online - funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') - funding_txid = funding_tx.txid() - self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325340 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx.txid()) - self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_addr_p2pkh(self, mock_write): # compressed pubkey - wallet_offline = WalletIntegrityHelper.create_standard_wallet( - # bip39: "qwe", der: m/44'/1'/0' - keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), - gap_limit=4 - ) - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG') - - # bootstrap wallet_online - funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') - funding_txid = funding_tx.txid() - self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325340 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertFalse(tx.is_segwit()) - self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid()) - self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_write): - wallet_offline = WalletIntegrityHelper.create_standard_wallet( - # bip39: "qwe", der: m/49'/1'/0' - keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), - gap_limit=4 - ) - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8') - - # bootstrap wallet_online - funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') - funding_txid = funding_tx.txid() - self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325340 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid()) - self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_write): - wallet_offline = WalletIntegrityHelper.create_standard_wallet( - # bip39: "qwe", der: m/84'/1'/0' - keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), - gap_limit=4 - ) - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529') - - # bootstrap wallet_online - funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400') - funding_txid = funding_tx.txid() - self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325340 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - tx = wallet_offline.sign_transaction(tx_copy, password=None) - self.assertTrue(tx.is_complete()) - self.assertTrue(tx.is_segwit()) - self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx.txid()) - self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_write): - # 2-of-3 legacy p2sh multisig - wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', '', True), - keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'), - keystore.from_xpub('tpubD6NzVbkrYhZ4XJzYkhsCbDCcZRmDAKSD7bXi9mdCni7acVt45fxbTVZyU6jRGh29ULKTjoapkfFsSJvQHitcVKbQgzgkkYsAmaovcro7Mhf') - ], - '2of3', gap_limit=2 - ) - wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song', '', True), - keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'), - keystore.from_xpub('tpubD6NzVbkrYhZ4YARFMEZPckrqJkw59GZD1PXtQnw14ukvWDofR7Z1HMeSCxfYEZVvg4VdZ8zGok5VxHwdrLqew5cMdQntWc5mT7mh1CSgrnX') - ], - '2of3', gap_limit=2 - ) - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('2N4z38eTKcWTZnfugCCfRyXtXWMLnn8HDfw') - - # bootstrap wallet_online - funding_tx = Transaction('010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400') - funding_txid = funding_tx.txid() - self.assertEqual('c59913a1fa9b1ef1f6928f0db490be67eeb9d7cb05aa565ee647e859642f3532', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, '2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325503 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - first - tx = wallet_offline1.sign_transaction(tx_copy, password=None) - self.assertFalse(tx.is_complete()) - tx = Transaction(tx.serialize()) - - # sign tx - second - tx = wallet_offline2.sign_transaction(tx, password=None) - self.assertTrue(tx.is_complete()) - tx = Transaction(tx.serialize()) - - self.assertEqual('010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c500000000fdfe0000483045022100cfe41e783629a2ad0b1f17cd2dbd69db05763fa7a22691131fa321ba3140d7cb02203fbda2ccc6212315464cd814d4e909b4f80a2361e3af0f9deda06478f91a0f3901483045022100b84fd63e957f2409558f63962fc91ba58334efde8b88ff53ca71da3d0fe7219702206001c6caeb30e18a7525fc72de0003e12646bf815b12fb132c1aadd6ffa1989c014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400', - str(tx)) - self.assertEqual('bb4c28af28b970522c56ff0482cd98c2b78a90bec578bcede8a9e5cbec6ef5e7', tx.txid()) - self.assertEqual('bb4c28af28b970522c56ff0482cd98c2b78a90bec578bcede8a9e5cbec6ef5e7', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_write): - # 2-of-2 p2sh-embedded segwit multisig - wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( - [ - # bip39: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose, der: m/1234'/1'/0', p2wsh-p2sh multisig - keystore.from_xprv('Uprv9CvELvByqm8k2dpecJVjgLMX1z5DufEjY4fBC5YvdGF5WjGCa7GVJJ2fYni1tyuF7Hw83E6W2ZBjAhaFLZv2ri3rEsubkCd5avg4EHKoDBN'), - keystore.from_xpub('Upub5Qb8ik4Cnu8g97KLXKgVXHqY6tH8emQvqtBncjSKsyfTZuorPtTZgX7ovKKZHuuVGBVd1MTTBkWez1XXt2weN1sWBz6SfgRPQYEkNgz81QF') - ], - '2of2', gap_limit=2 - ) - wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet( - [ - # bip39: square page wood spy oil story rebel give milk screen slide shuffle, der: m/1234'/1'/0', p2wsh-p2sh multisig - keystore.from_xprv('Uprv9BbnKEXJxXaNvdEsRJ9VA9toYrSeFJh5UfGBpM2iKe8Uh7UhrM9K8ioL53s8gvCoGfirHHaqpABDAE7VUNw8LNU1DMJKVoWyeNKu9XcDC19'), - keystore.from_xpub('Upub5RuakRisg8h3F7u7iL2k3UJFa1uiK7xauHamzTxYBbn4PXbM7eajr6M9Q2VCr6cVGhfhqWQqxnABvtSATuVM1xzxk4nA189jJwzaMn1QX7V') - ], - '2of2', gap_limit=2 - ) - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('2MsHQRm1pNi6VsmXYRxYMcCTdPu7Xa1RyFe') - - # bootstrap wallet_online - funding_tx = Transaction('0100000000010118d494d28e5c3bf61566ca0313e22c3b561b888a317d689cc8b47b947adebd440000000017160014aec84704ea8508ddb94a3c6e53f0992d33a2a529fdffffff020f0925000000000017a91409f7aae0265787a02de22839d41e9c927768230287809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf989870247304402206b906369f4075ebcfc149f7429dcfc34e11e1b7bbfc85d1185d5e9c324be0d3702203ce7fc12fd3131920fbcbb733250f05dbf7d03e18a4656232ee69d5c54dd46bd0121028a4b697a37f3f57f6e53f90db077fa9696095b277454fda839c211d640d48649c0391400') - funding_txid = funding_tx.txid() - self.assertEqual('54356de9e156b85c8516fd4d51bdb68b5513f58b4a6147483978ae254627ee3e', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, '2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325504 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - first - tx = wallet_offline1.sign_transaction(tx_copy, password=None) - self.assertFalse(tx.is_complete()) - self.assertEqual('6a58a51591142429203b62b6ddf6b799a6926882efac229998c51bee6c3573eb', tx.txid()) - tx = Transaction(tx.serialize()) - - # sign tx - second - tx = wallet_offline2.sign_transaction(tx, password=None) - self.assertTrue(tx.is_complete()) - tx = Transaction(tx.serialize()) - - self.assertEqual('010000000001013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d355401000000232200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987040047304402205a9dd9eb5676196893fb08f60079a2e9f567ee39614075d8c5d9fab0f11cbbc7022039640855188ebb7bccd9e3f00b397a888766d42d00d006f1ca7457c15449285f014730440220234f6648c5741eb195f0f4cd645298a10ce02f6ef557d05df93331e21c4f58cb022058ce2af0de1c238c4a8dd3b3c7a9a0da6e381ddad7593cddfc0480f9fe5baadf0147522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52aec0391400', - str(tx)) - self.assertEqual('6a58a51591142429203b62b6ddf6b799a6926882efac229998c51bee6c3573eb', tx.txid()) - self.assertEqual('96d0bca1001778c54e4c3a07929fab5562c5b5a23fd1ca3aa3870cc5df2bf97d', tx.wtxid()) - - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_write): - # 2-of-3 p2wsh multisig - wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', '', True), - keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'), - keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra') - ], - '2of3', gap_limit=2 - ) - wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet( - [ - keystore.from_seed('snow nest raise royal more walk demise rotate smooth spirit canyon gun', '', True), - keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra'), - keystore.from_xpub('Vpub5gSKXzxK7FeKQedu2q1z9oJWxqvX72AArW3HSWpEhc8othDH8xMDu28gr7gf17sp492BuJod8Tn7anjvJrKpETwqnQqX7CS8fcYyUtedEMk') - ], - '2of3', gap_limit=2 - ) - # ^ third seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool - wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) - wallet_online.import_address('tb1q83p6eqxkuvq4eumcha46crpzg4nj84s9p0hnynkxg8nhvfzqcc7q4erju6') - - # bootstrap wallet_online - funding_tx = Transaction('0100000000010132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c501000000171600142e5d579693b2a7679622935df94d9f3c84909b24fdffffff0280969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c83717d010000000017a91441b772909ad301b41b76f4a3c5058888a7fe6f9a8702483045022100de54689f74b8efcce7fdc91e40761084686003bcd56c886ee97e75a7e803526102204dea51ae5e7d01bd56a8c336c64841f7fe02a8b101fa892e13f2d079bb14e6bf012102024e2f73d632c49f4b821ccd3b6da66b155427b1e5b1c4688cefd5a4b4bfa404c1391400') - funding_txid = funding_tx.txid() - self.assertEqual('643a7ab9083d0227dd9df314ce56b18d279e6018ff975079dfaab82cd7a66fa3', funding_txid) - wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) - - # create unsigned tx - outputs = [(bitcoin.TYPE_ADDRESS, '2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] - tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) - tx.set_rbf(True) - tx.locktime = 1325505 - - self.assertFalse(tx.is_complete()) - self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) - self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) - - self.assertEqual(tx.txid(), tx_copy.txid()) - - # sign tx - first - tx = wallet_offline1.sign_transaction(tx_copy, password=None) - self.assertFalse(tx.is_complete()) - self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx.txid()) - tx = Transaction(tx.serialize()) - - # sign tx - second - tx = wallet_offline2.sign_transaction(tx, password=None) - self.assertTrue(tx.is_complete()) - tx = Transaction(tx.serialize()) - - self.assertEqual('01000000000101a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c04004730440220629d89626585f563202e6b38ceddc26ccd00737e0b7ee4239b9266ef9174ea2f02200b74828399a2e35ed46c9b484af4817438d5fea890606ebb201b821944db1fdc0147304402205d1a59c84c419992069e9764a7992abca6a812cc5dfd4f0d6515d4283e660ce802202597a38899f31545aaf305629bd488f36bf54e4a05fe983932cafbb3906efb8f016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153aec1391400', - str(tx)) - self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx.txid()) - self.assertEqual('4376fa5f1f6cb37b1f3956175d3bd4ef6882169294802b250a3c672f3ff431c1', tx.wtxid()) - - -class TestWalletHistory_SimpleRandomOrder(TestCaseForTestnet): - transactions = { - "0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67": "01000000029d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2000000008b483045022100a146a2078a318c1266e42265a369a8eef8993750cb3faa8dd80754d8d541d5d202207a6ab8864986919fd1a7fd5854f1e18a8a0431df924d7a878ec3dc283e3d75340141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff9d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2010000008a47304402201c7fa37b74a915668b0244c01f14a9756bbbec1031fb69390bcba236148ab37e02206151581f9aa0e6758b503064c1e661a726d75c6be3364a5a121a8c12cf618f64014104dc28da82e141416aaf771eb78128d00a55fdcbd13622afcbb7a3b911e58baa6a99841bfb7b99bcb7e1d47904fda5d13fdf9675cdbbe73e44efcc08165f49bac6feffffff02b0183101000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac005a6202000000001976a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac54c51200", - "2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d": "010000000132201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a050000006a47304402201d20bb5629a35b84ff9dd54788b98e265623022894f12152ac0e6158042550fe02204e98969e1f7043261912dd0660d3da64e15acf5435577fc02a00eccfe76b323f012103a336ad86546ab66b6184238fe63bb2955314be118b32fa45dd6bd9c4c5875167fdffffff0254959800000000001976a9148d2db0eb25b691829a47503006370070bc67400588ac80969800000000001976a914f96669095e6df76cfdf5c7e49a1909f002e123d088ace8ca1200", - "2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd": "010000000001036cdf8d2226c57d7cc8485636d8e823c14790d5f24e6cf38ba9323babc7f6db2901000000171600143fc0dbdc2f939c322aed5a9c3544468ec17f5c3efdffffff507dce91b2a8731636e058ccf252f02b5599489b624e003435a29b9862ccc38c0200000017160014c50ff91aa2a790b99aa98af039ae1b156e053375fdffffff6254162cf8ace3ddfb3ec242b8eade155fa91412c5bde7f55decfac5793743c1010000008b483045022100de9599dcd7764ca8d4fcbe39230602e130db296c310d4abb7f7ae4d139c4d46402200fbfd8e6dc94d90afa05b0c0eab3b84feb465754db3f984fbf059447282771c30141045eecefd39fabba7b0098c3d9e85794e652bdbf094f3f85a3de97a249b98b9948857ea1e8209ee4f196a6bbcfbad103a38698ee58766321ba1cdee0cbfb60e7b2fdffffff01e85af70100000000160014e8d29f07cd5f813317bec4defbef337942d85d74024730440220218049aee7bbd34a7fa17f972a8d24a0469b0131d943ef3e30860401eaa2247402203495973f006e6ee6ae74a83228623029f238f37390ee4b587d95cdb1d1aaee9901210392ba263f3a2b260826943ff0df25e9ca4ef603b98b0a916242c947ae0626575f02473044022002603e5ceabb4406d11aedc0cccbf654dd391ce68b6b2228a40e51cf8129310d0220533743120d93be8b6c1453973935b911b0a2322e74708d23e8b5f90e74b0f192012103221b4ee0f508ba595fc1b9c2252ed9d03e99c73b97344dae93263c68834f034800ed161300", - "31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f": "0100000000010454022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a000000008b483045022100ea8fe74db2aba23ad36ac66aaa481bad2b4d1b3c331869c1d60a28ce8cfad43c02206fa817281b33fbf74a6dd7352bdc5aa1d6d7966118a4ad5b7e153f37205f1ae80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a01000000171600146dfe07e12af3db7c715bf1c455f8517e19c361e7fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a020000006a47304402200b1fb89e9a772a8519294acd61a53a29473ce76077165447f49a686f1718db5902207466e2e8290f84114dc9d6c56419cb79a138f03d7af8756de02c810f19e4e03301210222bfebe09c2638cfa5aa8223fb422fe636ba9675c5e2f53c27a5d10514f49051fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a0300000000fdffffff018793140d000000001600144b3e27ddf4fc5f367421ee193da5332ef351b700000247304402207ba52959938a3853bcfd942d8a7e6a181349069cde3ea73dbde43fa9669b8d5302207a686b92073863203305cb5d5550d88bdab0d21b9e9761ba4a106ea3970e08d901210265c1e014112ed19c9f754143fb6a2ff89f8630d62b33eb5ae708c9ea576e61b50002473044022029e868a905aa3ecae6eafcbd5959aefff0e5f39c1fc7a131a174828806e74e5202202f0aaa7c3cb3d9a9d526e5428ce37c0f0af0d774aa30b09ded8bc2230e7ffaf2012102fe0104455dc52b1689bba130664e452642180eb865217acfc6997260b7d946ae22c71200", - "336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa": "0100000000010232201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a020000006a4730440220198c0ba2b2aefa78d8cca01401d408ecdebea5ac05affce36f079f6e5c8405ca02200eabb1b9a01ff62180cf061dfacedba6b2e07355841b9308de2d37d83489c7b80121031c663e5534fe2a6de816aded6bb9afca09b9e540695c23301f772acb29c64a05fdfffffffb28ff16811d3027a2405be68154be8fdaff77284dbce7a2314c4107c2c941600000000000fdffffff015e104f01000000001976a9146dfd56a0b5d0c9450d590ad21598ecfeaa438bd788ac000247304402207d6dc521e3a4577685535f098e5bac4601aa03658b924f30bf7afef1850e437e022045b76771d8b6ca1939352d6b759fca31029e5b2edffa44dc747fe49770e746cd012102c7f36d4ceed353b90594ebaf3907972b6d73289bdf4707e120de31ec4e1eb11679f31200", - "3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308": "010000000168091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0000000008a47304402202f683a63af571f405825066bd971945a35e7142a75c9a5255d364b25b7115d5602206c59a7214ae729a519757e45fdc87061d357813217848cf94df74125221267ac014104aecb9d427e10f0c370c32210fe75b6e72ccc4f415076cf1a6318fbed5537388862c914b29269751ab3a04962df06d96f5f4f54e393a0afcbfa44b590385ae61afdffffff0240420f00000000001976a9145f917fd451ca6448978ebb2734d2798274daf00b88aca8063d00000000001976a914e1232622a96a04f5e5a24ca0792bb9c28b089d6e88ace9ca1200", - "475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d": "01000000013a7e6f19a963adc7437d2f3eb0936f1fc9ef4ba7e083e19802eb1111525a59c2000000008b483045022100958d3931051306489d48fe69b32561e0a16e82a2447c07be9d1069317084b5e502202f70c2d9be8248276d334d07f08f934ffeea83977ad241f9c2de954a2d577f94014104d950039cec15ad10ad4fb658873bc746148bc861323959e0c84bf10f8633104aa90b64ce9f80916ab0a4238e025dcddf885b9a2dd6e901fe043a433731db8ab4fdffffff02a086010000000000160014bbfab2cc3267cea2df1b68c392cb3f0294978ca922940d00000000001976a914760f657c67273a06cad5b1d757a95f4ed79f5a4b88ac4c8d1300", - "56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216": "01000000000101614b142aeeb827d35d2b77a5b11f16655b6776110ddd9f34424ff49d85706cf90200000000fdffffff02784a4c00000000001600148464f47f35cbcda2e4e5968c5a3a862c43df65a1404b4c00000000001976a914c9efecf0ecba8b42dce0ae2b28e3ea0573d351c988ac0247304402207d8e559ed1f56cb2d02c4cb6c95b95c470f4b3cb3ce97696c3a58e39e55cd9b2022005c9c6f66a7154032a0bb2edc1af1f6c8f488bec52b6581a3a780312fb55681b0121024f83b87ac3440e9b30cec707b7e1461ecc411c2f45520b45a644655528b0a68ae9ca1200", - "6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254": "0100000000010496941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e000000001716001441aec99157d762708339d7faf7a63a8c479ed84cfdffffff96941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e0100000000fdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f000000006a4730440220652145460092ef42452437b942cb3f563bf15ad90d572d0b31d9f28449b7a8dd022052aae24f58b8f76bd2c9cf165cc98623f22870ccdbef1661b6dbe01c0ef9010f01210375b63dd8e93634bbf162d88b25d6110b5f5a9638f6fe080c85f8b21c2199a1fdfdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f010000008a47304402207517c52b241e6638a84b05385e0b3df806478c2e444f671ca34921f6232ee2e70220624af63d357b83e3abe7cdf03d680705df0049ec02f02918ee371170e3b4a73d014104de408e142c00615294813233cdfe9e7774615ae25d18ba4a1e3b70420bb6666d711464518457f8b947034076038c6f0cfc8940d85d3de0386e0ad88614885c7cfdffffff0480969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac809698000000000017a914f2a76207d7b54bd34282281205923841341d9e1f87002d3101000000001976a914b8d4651937cd7db5bcf5fc98e6d2d8cfa131e85088ac743db20a00000000160014c7d0df09e03173170aed0247243874c6872748ed02483045022100b932cda0aeb029922e126568a48c05d79317747dcd77e61dce44e190e140822002202d13f84338bb272c531c4086277ac11e166c59612f4aefa6e20f78455bdc09970121028e6808a8ac1e9ede621aaabfcad6f86662dbe0ace0236f078eb23c24bc88bd5e02483045022100d74a253262e3898626c12361ba9bb5866f9303b42eec0a55ced0578829e2e61e022059c08e61d90cd63c84de61c796c9d1bc1e2f8217892a7c07b383af357ddd7a730121028641e89822127336fc12ff99b1089eb1a124847639a0e98d17ff03a135ad578b000020c71200", - "72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112": "0100000002677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f000000008b4830450221009c50c0f56f34781dfa7b3d540ac724436c67ffdc2e5b2d5a395c9ebf72116ef802205a94a490ea14e4824f36f1658a384aeaecadd54839600141eb20375a49d476d1014104c291245c2ee3babb2a35c39389df56540867f93794215f743b9aa97f5ba114c4cdee8d49d877966728b76bc649bb349efd73adef1d77452a9aac26f8c51ae1ddfdffffff677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f010000008b483045022100ae0b286493491732e7d3f91ab4ac4cebf8fe8a3397e979cb689e62d350fdcf2802206cf7adf8b29159dd797905351da23a5f6dab9b9dbf5028611e86ccef9ff9012e014104c62c4c4201d5c6597e5999f297427139003fdb82e97c2112e84452d1cfdef31f92dd95e00e4d31a6f5f9af0dadede7f6f4284b84144e912ff15531f36358bda7fdffffff019f7093030000000022002027ce908c4ee5f5b76b4722775f23e20c5474f459619b94040258290395b88afb6ec51200", - "76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4": "0100000001f4ba9948cdc4face8315c7f0819c76643e813093ffe9fbcf83d798523c7965db000000006a473044022061df431a168483d144d4cffe1c5e860c0a431c19fc56f313a899feb5296a677c02200208474cc1d11ad89b9bebec5ec00b1e0af0adaba0e8b7f28eed4aaf8d409afb0121039742bf6ab70f12f6353e9455da6ed88f028257950450139209b6030e89927997fdffffff01d4f84b00000000001976a9140b93db89b6bf67b5c2db3370b73d806f458b3d0488ac0a171300", - "7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a": "01000000000102681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a00000000232200209adfa712053a06cc944237148bcefbc48b16eb1dbdc43d1377809bcef1bea9affdffffff681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a0100000023220020f40ed2e3fbffd150e5b74f162c3ce5dae0dfeba008a7f0f8271cf1cf58bfb442fdffffff02801d2c04000000001976a9140cc01e19090785d629cdcc98316f328df554de4f88ac6d455d05000000001976a914b9e828990a8731af4527bcb6d0cddf8d5ffe90ce88ac040047304402206eb65bd302eefae24eea05781e8317503e68584067d35af028a377f0751bb55b0220226453d00db341a4373f1bcac2391f886d3a6e4c30dd15133d1438018d2aad24014730440220343e578591fab0236d28fb361582002180d82cb1ba79eec9139a7a9519fca4260220723784bd708b4a8ed17bb4b83a5fd2e667895078e80eec55119015beb3592fd2016952210222eca5665ed166d090a5241d9a1eb27a92f85f125aaf8df510b2b5f701f3f534210227bca514c22353a7ae15c61506522872afecf10df75e599aabe4d562d0834fce2103601d7d49bada5a57a4832eafe4d1f1096d7b0b051de4a29cd5fc8ad62865e0a553ae0400483045022100b15ea9daacd809eb4d783a1449b7eb33e2965d4229e1a698db10869299dddc670220128871ffd27037a3e9dac6748ce30c14b145dd7f9d56cc9dcde482461fb6882601483045022100cb659e1de65f8b87f64d1b9e62929a5d565bbd13f73a1e6e9dd5f4efa024b6560220667b13ce2e1a3af2afdcedbe83e2120a6e8341198a79efb855b8bc5f93b4729f0169522102d038600af253cf5019f9d5637ca86763eca6827ed7b2b7f8cc6326dffab5eb68210315cdb32b7267e9b366fb93efe29d29705da3db966e8c8feae0c8eb51a7cf48e82103f0335f730b9414acddad5b3ee405da53961796efd8c003e76e5cd306fcc8600c53ae1fc71200", - "9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb": "010000000001013409c10fd732d9e4b3a9a1c4beb511fa5eb32bc51fd169102a21aa8519618f800000000000fdffffff0640420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac80841e00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac64064a000000000016001469825d422ca80f2a5438add92d741c7df45211f280969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac02483045022100b4369b18bccb74d72b6a38bd6db59122a9e8af3356890a5ecd84bdb8c7ffe317022076a5aa2b817be7b3637d179106fccebb91acbc34011343c8e8177acc2da4882e0121033c8112bbf60855f4c3ae489954500c4b8f3408665d8e1f63cf3216a76125c69865281300", - "a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d": "010000000400899af3606e93106a5d0f470e4e2e480dfc2fd56a7257a1f0f4d16fd5961a0f000000006a47304402205b32a834956da303f6d124e1626c7c48a30b8624e33f87a2ae04503c87946691022068aa7f936591fb4b3272046634cf526e4f8a018771c38aff2432a021eea243b70121034bb61618c932b948b9593d1b506092286d9eb70ea7814becef06c3dfcc277d67fdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753000000006b483045022100de775a580c6cb47061d5a00c6739033f468420c5719f9851f32c6992610abd3902204e6b296e812bb84a60c18c966f6166718922780e6344f243917d7840398eb3db0121025d7317c6910ad2ad3d29a748c7796ddf01e4a8bc5e3bf2a98032f0a20223e4aafdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753010000006a4730440220615a26f38bf6eb7043794c08fb81f273896b25783346332bec4de8dfaf7ed4d202201c2bc4515fc9b07ded5479d5be452c61ce785099f5e33715e9abd4dbec410e11012103caa46fcb1a6f2505bf66c17901320cc2378057c99e35f0630c41693e97ebb7cffdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753030000006b483045022100c8fba762dc50041ee3d5c7259c01763ed913063019eefec66678fb8603624faa02200727783ccbdbda8537a6201c63e30c0b2eb9afd0e26cb568d885e6151ef2a8540121027254a862a288cfd98853161f575c49ec0b38f79c3ef0bf1fb89986a3c36a8906fdffffff0240787d01000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac3bfc1502000000001976a914c30f2af6a79296b6531bf34dba14c8419be8fb7d88ac52c51200", - "c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462": "0100000003aabec9cb99096073ae47cfb84bfd5b0063ae7f157956fd37c5d1a79d74ee6e33000000008b4830450221008136fc880d5e24fdd9d2a43f5085f374fef013b814f625d44a8075104981d92a0220744526ec8fc7887c586968f22403f0180d54c9b7ff8db9b553a3c4497982e8250141047b8b4c91c5a93a1f2f171c619ca41770427aa07d6de5130c3ba23204b05510b3bd58b7a1b35b9c4409104cfe05e1677fc8b51c03eac98b206e5d6851b31d2368fdffffff16d23bdc750c7023c085a6fc76e3e468944919783535ea2c13826f181058a656010000008a47304402204148410f2d796b1bb976b83904167d28b65dcd7c21b3876022b4fa70abc86280022039ea474245c3dc8cd7e5a572a155df7a6a54496e50c73d9fed28e76a1cf998c00141044702781daed201e35aa07e74d7bda7069e487757a71e3334dc238144ad78819de4120d262e8488068e16c13eea6092e3ab2f729c13ef9a8c42136d6365820f7dfdffffff68091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0010000008b4830450221008228af51b61a4ee09f58b4a97f204a639c9c9d9787f79b2fc64ea54402c8547902201ed81fca828391d83df5fbd01a3fa5dd87168c455ed7451ba8ccb5bf06942c3b0141046fcdfab26ac08c827e68328dbbf417bbe7577a2baaa5acc29d3e33b3cc0c6366df34455a9f1754cb0952c48461f71ca296b379a574e33bcdbb5ed26bad31220bfdffffff0210791c00000000001976a914a4b991e7c72996c424fe0215f70be6aa7fcae22c88ac80c3c901000000001976a914b0f6e64ea993466f84050becc101062bb502b4e488ac7af31200", - "c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a": "01000000018557003cb450f53922f63740f0f77db892ef27e15b2614b56309bfcee96a0ad3010000006a473044022041923c905ae4b5ed9a21aa94c60b7dbcb8176d58d1eb1506d9fb1e293b65ce01022015d6e9d2e696925c6ad46ce97cc23dec455defa6309b839abf979effc83b8b160121029332bf6bed07dcca4be8a5a9d60648526e205d60c75a21291bffcdefccafdac3fdffffff01c01c0f00000000001976a914a2185918aa1006f96ed47897b8fb620f28a1b09988ac01171300", - "e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968": "01000000016d445091b7b4fa19cbbee30141071b2202d0c27d195b9d6d2bcc7085c9cd9127010000008b483045022100daf671b52393af79487667eddc92ebcc657e8ae743c387b25d1c1a2e19c7a4e7022015ef2a52ea7e94695de8898821f9da539815775516f18329896e5fc52a3563b30141041704a3daafaace77c8e6e54cf35ed27d0bf9bb8bcd54d1b955735ff63ec54fe82a80862d455c12e739108b345d585014bf6aa0cbd403817c89efa18b3c06d6b5fdffffff02144a4c00000000001976a9148942ac692ace81019176c4fb0ac408b18b49237f88ac404b4c00000000001976a914dd36d773acb68ac1041bc31b8a40ee504b164b2e88ace9ca1200", - "e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306": "010000000125af87b0c2ebb9539d644e97e6159ccb8e1aa80fe986d01f60d2f3f37f207ae8010000008b483045022100baed0747099f7b28a5624005d50adf1069120356ac68c471a56c511a5bf6972b022046fbf8ec6950a307c3c18ca32ad2955c559b0d9bbd9ec25b64f4806f78cadf770141041ea9afa5231dc4d65a2667789ebf6806829b6cf88bfe443228f95263730b7b70fb8b00b2b33777e168bcc7ad8e0afa5c7828842794ce3814c901e24193700f6cfdffffff02a0860100000000001976a914ade907333744c953140355ff60d341cedf7609fd88ac68830a00000000001976a9145d48feae4c97677e4ca7dcd73b0d9fd1399c962b88acc9cc1300", - "e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25": "01000000010db780fff7dfcef6dba9268ecf4f6df45a1a86b86cad6f59738a0ce29b145c47010000008a47304402202887ec6ec200e4e2b4178112633011cbdbc999e66d398b1ff3998e23f7c5541802204964bd07c0f18c48b7b9c00fbe34c7bc035efc479e21a4fa196027743f06095f0141044f1714ed25332bb2f74be169784577d0838aa66f2374f5d8cbbf216063626822d536411d13cbfcef1ff3cc1d58499578bc4a3c4a0be2e5184b2dd7963ef67713fdffffff02a0860100000000001600145bbdf3ba178f517d4812d286a40c436a9088076e6a0b0c00000000001976a9143fc16bef782f6856ff6638b1b99e4d3f863581d388acfbcb1300" - } - txid_list = sorted(list(transactions)) - - @classmethod - def create_old_wallet(cls): - ks = keystore.from_old_mpk('e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3') - # seed words: powerful random nobody notice nothing important anyway look away hidden message over - w = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=20) - # some txns are beyond gap limit: - w.create_new_address(for_change=True) - return w - - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_old_wallet_txorder1(self, mock_write): - w = self.create_old_wallet() - for i in [2, 12, 7, 9, 11, 10, 16, 6, 17, 1, 13, 15, 5, 8, 4, 0, 14, 18, 3]: - tx = Transaction(self.transactions[self.txid_list[i]]) - w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual(27633300, sum(w.get_balance())) - - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_old_wallet_txorder2(self, mock_write): - w = self.create_old_wallet() - for i in [9, 18, 2, 0, 13, 3, 1, 11, 4, 17, 7, 14, 12, 15, 10, 8, 5, 6, 16]: - tx = Transaction(self.transactions[self.txid_list[i]]) - w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual(27633300, sum(w.get_balance())) - - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_old_wallet_txorder3(self, mock_write): - w = self.create_old_wallet() - for i in [5, 8, 17, 0, 9, 10, 12, 3, 15, 18, 2, 11, 14, 7, 16, 1, 4, 6, 13]: - tx = Transaction(self.transactions[self.txid_list[i]]) - w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual(27633300, sum(w.get_balance())) - - -class TestWalletHistory_EvilGapLimit(TestCaseForTestnet): - transactions = { - # txn A: - "511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4": "010000000001018aacc3c8f98964232ebb74e379d8ff4e800991eecfcf64bd1793954f5e50a8790100000000fdffffff0340420f0000000000160014dbf321e905d544b54b86a2f3ed95b0ac66a3ddb0ff0514000000000016001474f1c130d3db22894efb3b7612b2c924628d0d7e80841e000000000016001488492707677190c073b6555fb08d37e91bbb75d802483045022100cf2904e09ea9d2670367eccc184d92fcb8a9b9c79a12e4efe81df161077945db02203530276a3401d944cf7a292e0660f36ee1df4a1c92c131d2c0d31d267d52524901210215f523a412a5262612e1a5ef9842dc864b0d73dc61fb4c6bfd480a867bebb1632e181400", - # txn B: - "fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f": "01000000000101a317998ac6cc717de17213804e1459900fe257b9f4a3b9b9edd29806728277530100000000fdffffff03c0c62d00000000001600149543301687b1ca2c67718d55fbe10413c73ddec200093d00000000001600141bc12094a4475dcfbf24f9920dafddf9104ca95b3e4a4c0000000000160014b226a59f2609aa7da4026fe2c231b5ae7be12ac302483045022100f1082386d2ce81612a3957e2801803938f6c0066d76cfbd853918d4119f396df022077d05a2b482b89707a8a600013cb08448cf211218a462f2a23c2c0d80a8a0ca7012103f4aac7e189de53d95e0cb2e45d3c0b2be18e93420734934c61a6a5ad88dd541033181400", - # txn C: - "268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656": "010000000001014f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0100000000fdffffff0260e316000000000016001445e9879cf7cd5b4a15df7ddcaf5c6dca0e1508bacc242600000000001600141bc12094a4475dcfbf24f9920dafddf9104ca95b02483045022100ae3618912f341fefee11b67e0047c47c88c4fa031561c3fafe993259dd14d846022056fa0a5b5d8a65942fa68bcc2f848fd71fa455ba42bc2d421b67eb49ba62aa4e01210394d8f4f06c2ea9c569eb050c897737a7315e7f2104d9b536b49968cc89a1f11033181400", - } - - @classmethod - def create_wallet(cls): - ks = keystore.from_xpub('vpub5Vhmk4dEJKanDTTw6immKXa3thw45u3gbd1rPYjREB6viP13sVTWcH6kvbR2YeLtGjradr6SFLVt9PxWDBSrvw1Dc1nmd3oko3m24CQbfaJ') - # seed words: nephew work weather maze pyramid employ check permit garment scene kiwi smooth - w = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=20) - return w - - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_wallet_txorder1(self, mock_write): - w = self.create_wallet() - w.storage.put('stored_height', 1316917 + 100) - for txid in self.transactions: - tx = Transaction(self.transactions[txid]) - w.transactions[tx.txid()] = tx - # txn A is an external incoming txn paying to addr (3) and (15) - # txn B is an external incoming txn paying to addr (4) and (25) - # txn C is an internal transfer txn from addr (25) -- to -- (1) and (25) - w.receive_history_callback('tb1qgh5c088he4d559wl0hw27hrdeg8p2z96pefn4q', # HD index 1 - [('268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656', 1316917)], - {}) - w.synchronize() - w.receive_history_callback('tb1qm0ejr6g964zt2jux5te7m9ds43n28hdsdz9ull', # HD index 3 - [('511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4', 1316912)], - {}) - w.synchronize() - w.receive_history_callback('tb1qj4pnq958k89zcem3342lhcgyz0rnmhkzl6x0cl', # HD index 4 - [('fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f', 1316917)], - {}) - w.synchronize() - w.receive_history_callback('tb1q3pyjwpm8wxgvquak240mprfhaydmkawcsl25je', # HD index 15 - [('511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4', 1316912)], - {}) - w.synchronize() - w.receive_history_callback('tb1qr0qjp99ygawul0eylxfqmt7alygye22mj33vej', # HD index 25 - [('fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f', 1316917), - ('268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656', 1316917)], - {}) - w.synchronize() - self.assertEqual(9999788, sum(w.get_balance())) diff --git a/lib/transaction.py b/lib/transaction.py @@ -1,1229 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - lightweight Bitcoin client -# Copyright (C) 2011 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. - - - -# Note: The deserialization code originally comes from ABE. - -from typing import Sequence, Union - -from .util import print_error, profiler - -from . import ecc -from . import bitcoin -from .bitcoin import * -import struct -import traceback -import sys - -# -# Workalike python implementation of Bitcoin's CDataStream class. -# -from .keystore import xpubkey_to_address, xpubkey_to_pubkey - -NO_SIGNATURE = 'ff' -PARTIAL_TXN_HEADER_MAGIC = b'EPTF\xff' - - -class SerializationError(Exception): - """ Thrown when there's a problem deserializing or serializing """ - - -class UnknownTxinType(Exception): - pass - - -class NotRecognizedRedeemScript(Exception): - pass - - -class BCDataStream(object): - def __init__(self): - self.input = None - self.read_cursor = 0 - - def clear(self): - self.input = None - self.read_cursor = 0 - - def write(self, _bytes): # Initialize with string of _bytes - if self.input is None: - self.input = bytearray(_bytes) - else: - self.input += bytearray(_bytes) - - def read_string(self, encoding='ascii'): - # Strings are encoded depending on length: - # 0 to 252 : 1-byte-length followed by bytes (if any) - # 253 to 65,535 : byte'253' 2-byte-length followed by bytes - # 65,536 to 4,294,967,295 : byte '254' 4-byte-length followed by bytes - # ... and the Bitcoin client is coded to understand: - # greater than 4,294,967,295 : byte '255' 8-byte-length followed by bytes of string - # ... but I don't think it actually handles any strings that big. - if self.input is None: - raise SerializationError("call write(bytes) before trying to deserialize") - - length = self.read_compact_size() - - return self.read_bytes(length).decode(encoding) - - def write_string(self, string, encoding='ascii'): - string = to_bytes(string, encoding) - # Length-encoded as with read-string - self.write_compact_size(len(string)) - self.write(string) - - def read_bytes(self, length): - try: - result = self.input[self.read_cursor:self.read_cursor+length] - self.read_cursor += length - return result - except IndexError: - raise SerializationError("attempt to read past end of buffer") - - def can_read_more(self) -> bool: - if not self.input: - return False - return self.read_cursor < len(self.input) - - def read_boolean(self): return self.read_bytes(1)[0] != chr(0) - def read_int16(self): return self._read_num('<h') - def read_uint16(self): return self._read_num('<H') - def read_int32(self): return self._read_num('<i') - def read_uint32(self): return self._read_num('<I') - def read_int64(self): return self._read_num('<q') - def read_uint64(self): return self._read_num('<Q') - - def write_boolean(self, val): return self.write(chr(1) if val else chr(0)) - def write_int16(self, val): return self._write_num('<h', val) - def write_uint16(self, val): return self._write_num('<H', val) - def write_int32(self, val): return self._write_num('<i', val) - def write_uint32(self, val): return self._write_num('<I', val) - def write_int64(self, val): return self._write_num('<q', val) - def write_uint64(self, val): return self._write_num('<Q', val) - - def read_compact_size(self): - try: - size = self.input[self.read_cursor] - self.read_cursor += 1 - if size == 253: - size = self._read_num('<H') - elif size == 254: - size = self._read_num('<I') - elif size == 255: - size = self._read_num('<Q') - return size - except IndexError: - raise SerializationError("attempt to read past end of buffer") - - def write_compact_size(self, size): - if size < 0: - raise SerializationError("attempt to write size < 0") - elif size < 253: - self.write(bytes([size])) - elif size < 2**16: - self.write(b'\xfd') - self._write_num('<H', size) - elif size < 2**32: - self.write(b'\xfe') - self._write_num('<I', size) - elif size < 2**64: - self.write(b'\xff') - self._write_num('<Q', size) - - def _read_num(self, format): - try: - (i,) = struct.unpack_from(format, self.input, self.read_cursor) - self.read_cursor += struct.calcsize(format) - except Exception as e: - raise SerializationError(e) - return i - - def _write_num(self, format, num): - s = struct.pack(format, num) - self.write(s) - - -# enum-like type -# From the Python Cookbook, downloaded from http://code.activestate.com/recipes/67107/ -class EnumException(Exception): - pass - - -class Enumeration: - def __init__(self, name, enumList): - self.__doc__ = name - lookup = { } - reverseLookup = { } - i = 0 - uniqueNames = [ ] - uniqueValues = [ ] - for x in enumList: - if isinstance(x, tuple): - x, i = x - if not isinstance(x, str): - raise EnumException("enum name is not a string: " + x) - if not isinstance(i, int): - raise EnumException("enum value is not an integer: " + i) - if x in uniqueNames: - raise EnumException("enum name is not unique: " + x) - if i in uniqueValues: - raise EnumException("enum value is not unique for " + x) - uniqueNames.append(x) - uniqueValues.append(i) - lookup[x] = i - reverseLookup[i] = x - i = i + 1 - self.lookup = lookup - self.reverseLookup = reverseLookup - - def __getattr__(self, attr): - if attr not in self.lookup: - raise AttributeError - return self.lookup[attr] - def whatis(self, value): - return self.reverseLookup[value] - - -# This function comes from bitcointools, bct-LICENSE.txt. -def long_hex(bytes): - return bytes.encode('hex_codec') - -# This function comes from bitcointools, bct-LICENSE.txt. -def short_hex(bytes): - t = bytes.encode('hex_codec') - if len(t) < 11: - return t - return t[0:4]+"..."+t[-4:] - - - -opcodes = Enumeration("Opcodes", [ - ("OP_0", 0), ("OP_PUSHDATA1",76), "OP_PUSHDATA2", "OP_PUSHDATA4", "OP_1NEGATE", "OP_RESERVED", - "OP_1", "OP_2", "OP_3", "OP_4", "OP_5", "OP_6", "OP_7", - "OP_8", "OP_9", "OP_10", "OP_11", "OP_12", "OP_13", "OP_14", "OP_15", "OP_16", - "OP_NOP", "OP_VER", "OP_IF", "OP_NOTIF", "OP_VERIF", "OP_VERNOTIF", "OP_ELSE", "OP_ENDIF", "OP_VERIFY", - "OP_RETURN", "OP_TOALTSTACK", "OP_FROMALTSTACK", "OP_2DROP", "OP_2DUP", "OP_3DUP", "OP_2OVER", "OP_2ROT", "OP_2SWAP", - "OP_IFDUP", "OP_DEPTH", "OP_DROP", "OP_DUP", "OP_NIP", "OP_OVER", "OP_PICK", "OP_ROLL", "OP_ROT", - "OP_SWAP", "OP_TUCK", "OP_CAT", "OP_SUBSTR", "OP_LEFT", "OP_RIGHT", "OP_SIZE", "OP_INVERT", "OP_AND", - "OP_OR", "OP_XOR", "OP_EQUAL", "OP_EQUALVERIFY", "OP_RESERVED1", "OP_RESERVED2", "OP_1ADD", "OP_1SUB", "OP_2MUL", - "OP_2DIV", "OP_NEGATE", "OP_ABS", "OP_NOT", "OP_0NOTEQUAL", "OP_ADD", "OP_SUB", "OP_MUL", "OP_DIV", - "OP_MOD", "OP_LSHIFT", "OP_RSHIFT", "OP_BOOLAND", "OP_BOOLOR", - "OP_NUMEQUAL", "OP_NUMEQUALVERIFY", "OP_NUMNOTEQUAL", "OP_LESSTHAN", - "OP_GREATERTHAN", "OP_LESSTHANOREQUAL", "OP_GREATERTHANOREQUAL", "OP_MIN", "OP_MAX", - "OP_WITHIN", "OP_RIPEMD160", "OP_SHA1", "OP_SHA256", "OP_HASH160", - "OP_HASH256", "OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG", - "OP_CHECKMULTISIGVERIFY", - ("OP_NOP1", 0xB0), - ("OP_CHECKLOCKTIMEVERIFY", 0xB1), ("OP_CHECKSEQUENCEVERIFY", 0xB2), - "OP_NOP4", "OP_NOP5", "OP_NOP6", "OP_NOP7", "OP_NOP8", "OP_NOP9", "OP_NOP10", - ("OP_INVALIDOPCODE", 0xFF), -]) - - -def script_GetOp(_bytes : bytes): - i = 0 - while i < len(_bytes): - vch = None - opcode = _bytes[i] - i += 1 - - if opcode <= opcodes.OP_PUSHDATA4: - nSize = opcode - if opcode == opcodes.OP_PUSHDATA1: - nSize = _bytes[i] - i += 1 - elif opcode == opcodes.OP_PUSHDATA2: - (nSize,) = struct.unpack_from('<H', _bytes, i) - i += 2 - elif opcode == opcodes.OP_PUSHDATA4: - (nSize,) = struct.unpack_from('<I', _bytes, i) - i += 4 - vch = _bytes[i:i + nSize] - i += nSize - - yield opcode, vch, i - - -def script_GetOpName(opcode): - return (opcodes.whatis(opcode)).replace("OP_", "") - - -def decode_script(bytes): - result = '' - for (opcode, vch, i) in script_GetOp(bytes): - if len(result) > 0: result += " " - if opcode <= opcodes.OP_PUSHDATA4: - result += "%d:"%(opcode,) - result += short_hex(vch) - else: - result += script_GetOpName(opcode) - return result - - -def match_decoded(decoded, to_match): - if len(decoded) != len(to_match): - return False; - for i in range(len(decoded)): - if to_match[i] == opcodes.OP_PUSHDATA4 and decoded[i][0] <= opcodes.OP_PUSHDATA4 and decoded[i][0]>0: - continue # Opcodes below OP_PUSHDATA4 all just push data onto stack, and are equivalent. - if to_match[i] != decoded[i][0]: - return False - return True - - -def parse_sig(x_sig): - return [None if x == NO_SIGNATURE else x for x in x_sig] - -def safe_parse_pubkey(x): - try: - return xpubkey_to_pubkey(x) - except: - return x - -def parse_scriptSig(d, _bytes): - try: - decoded = [ x for x in script_GetOp(_bytes) ] - except Exception as e: - # coinbase transactions raise an exception - print_error("parse_scriptSig: cannot find address in input script (coinbase?)", - bh2u(_bytes)) - return - - match = [ opcodes.OP_PUSHDATA4 ] - if match_decoded(decoded, match): - item = decoded[0][1] - if item[0] == 0: - # segwit embedded into p2sh - # witness version 0 - d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) - if len(item) == 22: - d['type'] = 'p2wpkh-p2sh' - elif len(item) == 34: - d['type'] = 'p2wsh-p2sh' - else: - print_error("unrecognized txin type", bh2u(item)) - elif opcodes.OP_1 <= item[0] <= opcodes.OP_16: - # segwit embedded into p2sh - # witness version 1-16 - pass - else: - # assert item[0] == 0x30 - # pay-to-pubkey - d['type'] = 'p2pk' - d['address'] = "(pubkey)" - d['signatures'] = [bh2u(item)] - d['num_sig'] = 1 - d['x_pubkeys'] = ["(pubkey)"] - d['pubkeys'] = ["(pubkey)"] - return - - # p2pkh TxIn transactions push a signature - # (71-73 bytes) and then their public key - # (33 or 65 bytes) onto the stack: - match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ] - if match_decoded(decoded, match): - sig = bh2u(decoded[0][1]) - x_pubkey = bh2u(decoded[1][1]) - try: - signatures = parse_sig([sig]) - pubkey, address = xpubkey_to_address(x_pubkey) - except: - print_error("parse_scriptSig: cannot find address in input script (p2pkh?)", - bh2u(_bytes)) - return - d['type'] = 'p2pkh' - d['signatures'] = signatures - d['x_pubkeys'] = [x_pubkey] - d['num_sig'] = 1 - d['pubkeys'] = [pubkey] - d['address'] = address - return - - # p2sh transaction, m of n - match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1) - if match_decoded(decoded, match): - x_sig = [bh2u(x[1]) for x in decoded[1:-1]] - redeem_script_unsanitized = decoded[-1][1] # for partial multisig txn, this has x_pubkeys - try: - m, n, x_pubkeys, pubkeys, redeem_script = parse_redeemScript_multisig(redeem_script_unsanitized) - except NotRecognizedRedeemScript: - print_error("parse_scriptSig: cannot find address in input script (p2sh?)", - bh2u(_bytes)) - # we could still guess: - # d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1])) - return - # write result in d - d['type'] = 'p2sh' - d['num_sig'] = m - d['signatures'] = parse_sig(x_sig) - d['x_pubkeys'] = x_pubkeys - d['pubkeys'] = pubkeys - d['redeem_script'] = redeem_script - d['address'] = hash160_to_p2sh(hash_160(bfh(redeem_script))) - return - - # custom partial format for imported addresses - match = [ opcodes.OP_INVALIDOPCODE, opcodes.OP_0, opcodes.OP_PUSHDATA4 ] - if match_decoded(decoded, match): - x_pubkey = bh2u(decoded[2][1]) - pubkey, address = xpubkey_to_address(x_pubkey) - d['type'] = 'address' - d['address'] = address - d['num_sig'] = 1 - d['x_pubkeys'] = [x_pubkey] - d['pubkeys'] = None # get_sorted_pubkeys will populate this - d['signatures'] = [None] - return - - print_error("parse_scriptSig: cannot find address in input script (unknown)", - bh2u(_bytes)) - - -def parse_redeemScript_multisig(redeem_script: bytes): - dec2 = [ x for x in script_GetOp(redeem_script) ] - try: - m = dec2[0][0] - opcodes.OP_1 + 1 - n = dec2[-2][0] - opcodes.OP_1 + 1 - except IndexError: - raise NotRecognizedRedeemScript() - op_m = opcodes.OP_1 + m - 1 - op_n = opcodes.OP_1 + n - 1 - match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] - if not match_decoded(dec2, match_multisig): - raise NotRecognizedRedeemScript() - x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] - pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] - redeem_script2 = bfh(multisig_script(x_pubkeys, m)) - if redeem_script2 != redeem_script: - raise NotRecognizedRedeemScript() - redeem_script_sanitized = multisig_script(pubkeys, m) - return m, n, x_pubkeys, pubkeys, redeem_script_sanitized - - -def get_address_from_output_script(_bytes, *, net=None): - decoded = [x for x in script_GetOp(_bytes)] - - # The Genesis Block, self-payments, and pay-by-IP-address payments look like: - # 65 BYTES:... CHECKSIG - match = [ opcodes.OP_PUSHDATA4, opcodes.OP_CHECKSIG ] - if match_decoded(decoded, match): - return TYPE_PUBKEY, bh2u(decoded[0][1]) - - # Pay-by-Bitcoin-address TxOuts look like: - # DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG - match = [ opcodes.OP_DUP, opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG ] - if match_decoded(decoded, match): - return TYPE_ADDRESS, hash160_to_p2pkh(decoded[2][1], net=net) - - # p2sh - match = [ opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUAL ] - if match_decoded(decoded, match): - return TYPE_ADDRESS, hash160_to_p2sh(decoded[1][1], net=net) - - # segwit address - possible_witness_versions = [opcodes.OP_0] + list(range(opcodes.OP_1, opcodes.OP_16 + 1)) - for witver, opcode in enumerate(possible_witness_versions): - match = [ opcode, opcodes.OP_PUSHDATA4 ] - if match_decoded(decoded, match): - return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=witver, net=net) - - return TYPE_SCRIPT, bh2u(_bytes) - - -def parse_input(vds, full_parse: bool): - d = {} - prevout_hash = hash_encode(vds.read_bytes(32)) - prevout_n = vds.read_uint32() - scriptSig = vds.read_bytes(vds.read_compact_size()) - sequence = vds.read_uint32() - d['prevout_hash'] = prevout_hash - d['prevout_n'] = prevout_n - d['scriptSig'] = bh2u(scriptSig) - d['sequence'] = sequence - d['type'] = 'unknown' if prevout_hash != '00'*32 else 'coinbase' - d['address'] = None - d['num_sig'] = 0 - if not full_parse: - return d - d['x_pubkeys'] = [] - d['pubkeys'] = [] - d['signatures'] = {} - if d['type'] != 'coinbase' and scriptSig: - try: - parse_scriptSig(d, scriptSig) - except BaseException: - traceback.print_exc(file=sys.stderr) - print_error('failed to parse scriptSig', bh2u(scriptSig)) - return d - - -def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: - """Constructs a witness from the given stack items.""" - witness = var_int(len(items)) - for item in items: - if type(item) is int: - item = bitcoin.script_num_to_hex(item) - elif type(item) is bytes: - item = bh2u(item) - witness += bitcoin.witness_push(item) - return witness - - -def parse_witness(vds, txin, full_parse: bool): - n = vds.read_compact_size() - if n == 0: - txin['witness'] = '00' - return - if n == 0xffffffff: - txin['value'] = vds.read_uint64() - txin['witness_version'] = vds.read_uint16() - n = vds.read_compact_size() - # now 'n' is the number of items in the witness - w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n)) - txin['witness'] = construct_witness(w) - if not full_parse: - return - - try: - if txin.get('witness_version', 0) != 0: - raise UnknownTxinType() - if txin['type'] == 'coinbase': - pass - elif txin['type'] == 'address': - pass - elif txin['type'] == 'p2wsh-p2sh' or n > 2: - witness_script_unsanitized = w[-1] # for partial multisig txn, this has x_pubkeys - try: - m, n, x_pubkeys, pubkeys, witness_script = parse_redeemScript_multisig(bfh(witness_script_unsanitized)) - except NotRecognizedRedeemScript: - raise UnknownTxinType() - txin['signatures'] = parse_sig(w[1:-1]) - txin['num_sig'] = m - txin['x_pubkeys'] = x_pubkeys - txin['pubkeys'] = pubkeys - txin['witness_script'] = witness_script - if not txin.get('scriptSig'): # native segwit script - txin['type'] = 'p2wsh' - txin['address'] = bitcoin.script_to_p2wsh(witness_script) - elif txin['type'] == 'p2wpkh-p2sh' or n == 2: - txin['num_sig'] = 1 - txin['x_pubkeys'] = [w[1]] - txin['pubkeys'] = [safe_parse_pubkey(w[1])] - txin['signatures'] = parse_sig([w[0]]) - if not txin.get('scriptSig'): # native segwit script - txin['type'] = 'p2wpkh' - txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0])) - else: - raise UnknownTxinType() - except UnknownTxinType: - txin['type'] = 'unknown' - except BaseException: - txin['type'] = 'unknown' - traceback.print_exc(file=sys.stderr) - print_error('failed to parse witness', txin.get('witness')) - - -def parse_output(vds, i): - d = {} - d['value'] = vds.read_int64() - if d['value'] > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: - raise SerializationError('invalid output amount (too large)') - if d['value'] < 0: - raise SerializationError('invalid output amount (negative)') - scriptPubKey = vds.read_bytes(vds.read_compact_size()) - d['type'], d['address'] = get_address_from_output_script(scriptPubKey) - d['scriptPubKey'] = bh2u(scriptPubKey) - d['prevout_n'] = i - return d - - -def deserialize(raw: str, force_full_parse=False) -> dict: - raw_bytes = bfh(raw) - d = {} - if raw_bytes[:5] == PARTIAL_TXN_HEADER_MAGIC: - d['partial'] = is_partial = True - partial_format_version = raw_bytes[5] - if partial_format_version != 0: - raise SerializationError('unknown tx partial serialization format version: {}' - .format(partial_format_version)) - raw_bytes = raw_bytes[6:] - else: - d['partial'] = is_partial = False - full_parse = force_full_parse or is_partial - vds = BCDataStream() - vds.write(raw_bytes) - d['version'] = vds.read_int32() - n_vin = vds.read_compact_size() - is_segwit = (n_vin == 0) - if is_segwit: - marker = vds.read_bytes(1) - if marker != b'\x01': - raise ValueError('invalid txn marker byte: {}'.format(marker)) - n_vin = vds.read_compact_size() - d['segwit_ser'] = is_segwit - d['inputs'] = [parse_input(vds, full_parse=full_parse) for i in range(n_vin)] - n_vout = vds.read_compact_size() - d['outputs'] = [parse_output(vds, i) for i in range(n_vout)] - if is_segwit: - for i in range(n_vin): - txin = d['inputs'][i] - parse_witness(vds, txin, full_parse=full_parse) - d['lockTime'] = vds.read_uint32() - if vds.can_read_more(): - raise SerializationError('extra junk at the end') - return d - - -# pay & redeem scripts - - - -def multisig_script(public_keys: Sequence[str], m: int) -> str: - n = len(public_keys) - assert n <= 15 - assert m <= n - op_m = format(opcodes.OP_1 + m - 1, 'x') - op_n = format(opcodes.OP_1 + n - 1, 'x') - keylist = [op_push(len(k)//2) + k for k in public_keys] - return op_m + ''.join(keylist) + op_n + 'ae' - - - - -class Transaction: - - def __str__(self): - if self.raw is None: - self.raw = self.serialize() - return self.raw - - def __init__(self, raw): - if raw is None: - self.raw = None - elif isinstance(raw, str): - self.raw = raw.strip() if raw else None - elif isinstance(raw, dict): - self.raw = raw['hex'] - else: - raise Exception("cannot initialize transaction", raw) - self._inputs = None - self._outputs = None - self.locktime = 0 - self.version = 1 - # by default we assume this is a partial txn; - # this value will get properly set when deserializing - self.is_partial_originally = True - self._segwit_ser = None # None means "don't know" - - def update(self, raw): - self.raw = raw - self._inputs = None - self.deserialize() - - def inputs(self): - if self._inputs is None: - self.deserialize() - return self._inputs - - def outputs(self): - if self._outputs is None: - self.deserialize() - return self._outputs - - @classmethod - def get_sorted_pubkeys(self, txin): - # sort pubkeys and x_pubkeys, using the order of pubkeys - if txin['type'] == 'coinbase': - return [], [] - x_pubkeys = txin['x_pubkeys'] - pubkeys = txin.get('pubkeys') - if pubkeys is None: - pubkeys = [xpubkey_to_pubkey(x) for x in x_pubkeys] - pubkeys, x_pubkeys = zip(*sorted(zip(pubkeys, x_pubkeys))) - txin['pubkeys'] = pubkeys = list(pubkeys) - txin['x_pubkeys'] = x_pubkeys = list(x_pubkeys) - return pubkeys, x_pubkeys - - def update_signatures(self, signatures: Sequence[str]): - """Add new signatures to a transaction - - `signatures` is expected to be a list of sigs with signatures[i] - intended for self._inputs[i]. - This is used by the Trezor and KeepKey plugins. - """ - if self.is_complete(): - return - if len(self.inputs()) != len(signatures): - raise Exception('expected {} signatures; got {}'.format(len(self.inputs()), len(signatures))) - for i, txin in enumerate(self.inputs()): - pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) - sig = signatures[i] - if sig in txin.get('signatures'): - continue - pre_hash = Hash(bfh(self.serialize_preimage(i))) - sig_string = ecc.sig_string_from_der_sig(bfh(sig[:-2])) - for recid in range(4): - try: - public_key = ecc.ECPubkey.from_sig_string(sig_string, recid, pre_hash) - except ecc.InvalidECPointException: - # the point might not be on the curve for some recid values - continue - pubkey_hex = public_key.get_public_key_hex(compressed=True) - if pubkey_hex in pubkeys: - try: - public_key.verify_message_hash(sig_string, pre_hash) - except Exception: - traceback.print_exc(file=sys.stderr) - continue - j = pubkeys.index(pubkey_hex) - print_error("adding sig", i, j, pubkey_hex, sig) - self.add_signature_to_txin(i, j, sig) - #self._inputs[i]['x_pubkeys'][j] = pubkey - break - # redo raw - self.raw = self.serialize() - - def add_signature_to_txin(self, i, signingPos, sig): - txin = self._inputs[i] - txin['signatures'][signingPos] = sig - txin['scriptSig'] = None # force re-serialization - txin['witness'] = None # force re-serialization - self.raw = None - - def deserialize(self, force_full_parse=False): - if self.raw is None: - return - #self.raw = self.serialize() - if self._inputs is not None: - return - d = deserialize(self.raw, force_full_parse) - self._inputs = d['inputs'] - self._outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']] - self.locktime = d['lockTime'] - self.version = d['version'] - self.is_partial_originally = d['partial'] - self._segwit_ser = d['segwit_ser'] - return d - - @classmethod - def from_io(klass, inputs, outputs, locktime=0): - self = klass(None) - self._inputs = inputs - self._outputs = outputs - self.locktime = locktime - return self - - @classmethod - def pay_script(self, output_type, addr): - if output_type == TYPE_SCRIPT: - return addr - elif output_type == TYPE_ADDRESS: - return bitcoin.address_to_script(addr) - elif output_type == TYPE_PUBKEY: - return bitcoin.public_key_to_p2pk_script(addr) - else: - raise TypeError('Unknown output type') - - @classmethod - def estimate_pubkey_size_from_x_pubkey(cls, x_pubkey): - try: - if x_pubkey[0:2] in ['02', '03']: # compressed pubkey - return 0x21 - elif x_pubkey[0:2] == '04': # uncompressed pubkey - return 0x41 - elif x_pubkey[0:2] == 'ff': # bip32 extended pubkey - return 0x21 - elif x_pubkey[0:2] == 'fe': # old electrum extended pubkey - return 0x41 - except Exception as e: - pass - return 0x21 # just guess it is compressed - - @classmethod - def estimate_pubkey_size_for_txin(cls, txin): - pubkeys = txin.get('pubkeys', []) - x_pubkeys = txin.get('x_pubkeys', []) - if pubkeys and len(pubkeys) > 0: - return cls.estimate_pubkey_size_from_x_pubkey(pubkeys[0]) - elif x_pubkeys and len(x_pubkeys) > 0: - return cls.estimate_pubkey_size_from_x_pubkey(x_pubkeys[0]) - else: - return 0x21 # just guess it is compressed - - @classmethod - def get_siglist(self, txin, estimate_size=False): - # if we have enough signatures, we use the actual pubkeys - # otherwise, use extended pubkeys (with bip32 derivation) - if txin['type'] == 'coinbase': - return [], [] - num_sig = txin.get('num_sig', 1) - if estimate_size: - pubkey_size = self.estimate_pubkey_size_for_txin(txin) - pk_list = ["00" * pubkey_size] * len(txin.get('x_pubkeys', [None])) - # we assume that signature will be 0x48 bytes long - sig_list = [ "00" * 0x48 ] * num_sig - else: - pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) - x_signatures = txin['signatures'] - signatures = list(filter(None, x_signatures)) - is_complete = len(signatures) == num_sig - if is_complete: - pk_list = pubkeys - sig_list = signatures - else: - pk_list = x_pubkeys - sig_list = [sig if sig else NO_SIGNATURE for sig in x_signatures] - return pk_list, sig_list - - @classmethod - def serialize_witness(self, txin, estimate_size=False): - _type = txin['type'] - if not self.is_segwit_input(txin) and not self.is_input_value_needed(txin): - return '00' - if _type == 'coinbase': - return txin['witness'] - - witness = txin.get('witness', None) - if witness is None or estimate_size: - if _type == 'address' and estimate_size: - _type = self.guess_txintype_from_address(txin['address']) - pubkeys, sig_list = self.get_siglist(txin, estimate_size) - if _type in ['p2wpkh', 'p2wpkh-p2sh']: - witness = construct_witness([sig_list[0], pubkeys[0]]) - elif _type in ['p2wsh', 'p2wsh-p2sh']: - witness_script = multisig_script(pubkeys, txin['num_sig']) - witness = construct_witness([0] + sig_list + [witness_script]) - else: - witness = txin.get('witness', '00') - - if self.is_txin_complete(txin) or estimate_size: - partial_format_witness_prefix = '' - else: - input_value = int_to_hex(txin['value'], 8) - witness_version = int_to_hex(txin.get('witness_version', 0), 2) - partial_format_witness_prefix = var_int(0xffffffff) + input_value + witness_version - return partial_format_witness_prefix + witness - - @classmethod - def is_segwit_input(cls, txin, guess_for_address=False): - _type = txin['type'] - if _type == 'address' and guess_for_address: - _type = cls.guess_txintype_from_address(txin['address']) - has_nonzero_witness = txin.get('witness', '00') not in ('00', None) - return cls.is_segwit_inputtype(_type) or has_nonzero_witness - - @classmethod - def is_segwit_inputtype(cls, txin_type): - return txin_type in ('p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh') - - @classmethod - def is_input_value_needed(cls, txin): - return cls.is_segwit_input(txin) or txin['type'] == 'address' - - @classmethod - def guess_txintype_from_address(cls, addr): - # It's not possible to tell the script type in general - # just from an address. - # - "1" addresses are of course p2pkh - # - "3" addresses are p2sh but we don't know the redeem script.. - # - "bc1" addresses (if they are 42-long) are p2wpkh - # - "bc1" addresses that are 62-long are p2wsh but we don't know the script.. - # If we don't know the script, we _guess_ it is pubkeyhash. - # As this method is used e.g. for tx size estimation, - # the estimation will not be precise. - witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr) - if witprog is not None: - return 'p2wpkh' - addrtype, hash_160 = b58_address_to_hash160(addr) - if addrtype == constants.net.ADDRTYPE_P2PKH: - return 'p2pkh' - elif addrtype == constants.net.ADDRTYPE_P2SH: - return 'p2wpkh-p2sh' - - @classmethod - def input_script(self, txin, estimate_size=False): - _type = txin['type'] - if _type == 'coinbase': - return txin['scriptSig'] - - # If there is already a saved scriptSig, just return that. - # This allows manual creation of txins of any custom type. - # However, if the txin is not complete, we might have some garbage - # saved from our partial txn ser format, so we re-serialize then. - script_sig = txin.get('scriptSig', None) - if script_sig is not None and self.is_txin_complete(txin): - return script_sig - - pubkeys, sig_list = self.get_siglist(txin, estimate_size) - script = ''.join(push_script(x) for x in sig_list) - if _type == 'address' and estimate_size: - _type = self.guess_txintype_from_address(txin['address']) - if _type == 'p2pk': - pass - elif _type == 'p2sh': - # put op_0 before script - script = '00' + script - redeem_script = multisig_script(pubkeys, txin['num_sig']) - script += push_script(redeem_script) - elif _type == 'p2pkh': - script += push_script(pubkeys[0]) - elif _type in ['p2wpkh', 'p2wsh']: - return '' - elif _type == 'p2wpkh-p2sh': - pubkey = safe_parse_pubkey(pubkeys[0]) - scriptSig = bitcoin.p2wpkh_nested_script(pubkey) - return push_script(scriptSig) - elif _type == 'p2wsh-p2sh': - if estimate_size: - witness_script = '' - else: - witness_script = self.get_preimage_script(txin) - scriptSig = bitcoin.p2wsh_nested_script(witness_script) - return push_script(scriptSig) - elif _type == 'address': - return 'ff00' + push_script(pubkeys[0]) # fd extended pubkey - elif _type == 'unknown': - return txin['scriptSig'] - return script - - @classmethod - def is_txin_complete(cls, txin): - if txin['type'] == 'coinbase': - return True - num_sig = txin.get('num_sig', 1) - if num_sig == 0: - return True - x_signatures = txin['signatures'] - signatures = list(filter(None, x_signatures)) - return len(signatures) == num_sig - - @classmethod - def get_preimage_script(self, txin): - preimage_script = txin.get('preimage_script', None) - if preimage_script is not None: - return preimage_script - - pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) - if txin['type'] == 'p2pkh': - return bitcoin.address_to_script(txin['address']) - elif txin['type'] in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: - return multisig_script(pubkeys, txin['num_sig']) - elif txin['type'] in ['p2wpkh', 'p2wpkh-p2sh']: - pubkey = pubkeys[0] - pkh = bh2u(bitcoin.hash_160(bfh(pubkey))) - return '76a9' + push_script(pkh) + '88ac' - elif txin['type'] == 'p2pk': - pubkey = pubkeys[0] - return bitcoin.public_key_to_p2pk_script(pubkey) - else: - raise TypeError('Unknown txin type', txin['type']) - - @classmethod - def serialize_outpoint(self, txin): - return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4) - - @classmethod - def get_outpoint_from_txin(cls, txin): - if txin['type'] == 'coinbase': - return None - prevout_hash = txin['prevout_hash'] - prevout_n = txin['prevout_n'] - return prevout_hash + ':%d' % prevout_n - - @classmethod - def serialize_input(self, txin, script): - # Prev hash and index - s = self.serialize_outpoint(txin) - # Script length, script, sequence - s += var_int(len(script)//2) - s += script - s += int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) - return s - - def set_rbf(self, rbf): - nSequence = 0xffffffff - (2 if rbf else 1) - for txin in self.inputs(): - txin['sequence'] = nSequence - - def BIP_LI01_sort(self): - # See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki - self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) - self._outputs.sort(key = lambda o: (o[2], self.pay_script(o[0], o[1]))) - - def serialize_output(self, output): - output_type, addr, amount = output - s = int_to_hex(amount, 8) - script = self.pay_script(output_type, addr) - s += var_int(len(script)//2) - s += script - return s - - def serialize_preimage(self, i): - nVersion = int_to_hex(self.version, 4) - nHashType = int_to_hex(1, 4) - nLocktime = int_to_hex(self.locktime, 4) - inputs = self.inputs() - outputs = self.outputs() - txin = inputs[i] - # TODO: py3 hex - if self.is_segwit_input(txin): - hashPrevouts = bh2u(Hash(bfh(''.join(self.serialize_outpoint(txin) for txin in inputs)))) - hashSequence = bh2u(Hash(bfh(''.join(int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) for txin in inputs)))) - hashOutputs = bh2u(Hash(bfh(''.join(self.serialize_output(o) for o in outputs)))) - outpoint = self.serialize_outpoint(txin) - preimage_script = self.get_preimage_script(txin) - scriptCode = var_int(len(preimage_script) // 2) + preimage_script - amount = int_to_hex(txin['value'], 8) - nSequence = int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) - preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType - else: - txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.get_preimage_script(txin) if i==k else '') for k, txin in enumerate(inputs)) - txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs) - preimage = nVersion + txins + txouts + nLocktime + nHashType - return preimage - - def is_segwit(self, guess_for_address=False): - if not self.is_partial_originally: - return self._segwit_ser - return any(self.is_segwit_input(x, guess_for_address=guess_for_address) for x in self.inputs()) - - def serialize(self, estimate_size=False, witness=True): - network_ser = self.serialize_to_network(estimate_size, witness) - if estimate_size: - return network_ser - if self.is_partial_originally and not self.is_complete(): - partial_format_version = '00' - return bh2u(PARTIAL_TXN_HEADER_MAGIC) + partial_format_version + network_ser - else: - return network_ser - - def serialize_to_network(self, estimate_size=False, witness=True): - nVersion = int_to_hex(self.version, 4) - nLocktime = int_to_hex(self.locktime, 4) - inputs = self.inputs() - outputs = self.outputs() - txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.input_script(txin, estimate_size)) for txin in inputs) - txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs) - use_segwit_ser_for_estimate_size = estimate_size and self.is_segwit(guess_for_address=True) - use_segwit_ser_for_actual_use = not estimate_size and \ - (self.is_segwit() or any(txin['type'] == 'address' for txin in inputs)) - use_segwit_ser = use_segwit_ser_for_estimate_size or use_segwit_ser_for_actual_use - if witness and use_segwit_ser: - marker = '00' - flag = '01' - witness = ''.join(self.serialize_witness(x, estimate_size) for x in inputs) - return nVersion + marker + flag + txins + txouts + witness + nLocktime - else: - return nVersion + txins + txouts + nLocktime - - def txid(self): - self.deserialize() - all_segwit = all(self.is_segwit_input(x) for x in self.inputs()) - if not all_segwit and not self.is_complete(): - return None - ser = self.serialize_to_network(witness=False) - return bh2u(Hash(bfh(ser))[::-1]) - - def wtxid(self): - self.deserialize() - if not self.is_complete(): - return None - ser = self.serialize_to_network(witness=True) - return bh2u(Hash(bfh(ser))[::-1]) - - def add_inputs(self, inputs): - self._inputs.extend(inputs) - self.raw = None - - def add_outputs(self, outputs): - self._outputs.extend(outputs) - self.raw = None - - def input_value(self): - return sum(x['value'] for x in self.inputs()) - - def output_value(self): - return sum(val for tp, addr, val in self.outputs()) - - def get_fee(self): - return self.input_value() - self.output_value() - - def is_final(self): - return not any([x.get('sequence', 0xffffffff - 1) < 0xffffffff - 1 for x in self.inputs()]) - - @profiler - def estimated_size(self): - """Return an estimated virtual tx size in vbytes. - BIP-0141 defines 'Virtual transaction size' to be weight/4 rounded up. - This definition is only for humans, and has little meaning otherwise. - If we wanted sub-byte precision, fee calculation should use transaction - weights, but for simplicity we approximate that with (virtual_size)x4 - """ - weight = self.estimated_weight() - return self.virtual_size_from_weight(weight) - - @classmethod - def estimated_input_weight(cls, txin, is_segwit_tx): - '''Return an estimate of serialized input weight in weight units.''' - script = cls.input_script(txin, True) - input_size = len(cls.serialize_input(txin, script)) // 2 - - if cls.is_segwit_input(txin, guess_for_address=True): - witness_size = len(cls.serialize_witness(txin, True)) // 2 - else: - witness_size = 1 if is_segwit_tx else 0 - - return 4 * input_size + witness_size - - @classmethod - def estimated_output_size(cls, address): - """Return an estimate of serialized output size in bytes.""" - script = bitcoin.address_to_script(address) - # 8 byte value + 1 byte script len + script - return 9 + len(script) // 2 - - @classmethod - def virtual_size_from_weight(cls, weight): - return weight // 4 + (weight % 4 > 0) - - def estimated_total_size(self): - """Return an estimated total transaction size in bytes.""" - return len(self.serialize(True)) // 2 if not self.is_complete() or self.raw is None else len(self.raw) // 2 # ASCII hex string - - def estimated_witness_size(self): - """Return an estimate of witness size in bytes.""" - estimate = not self.is_complete() - if not self.is_segwit(guess_for_address=estimate): - return 0 - inputs = self.inputs() - witness = ''.join(self.serialize_witness(x, estimate) for x in inputs) - witness_size = len(witness) // 2 + 2 # include marker and flag - return witness_size - - def estimated_base_size(self): - """Return an estimated base transaction size in bytes.""" - return self.estimated_total_size() - self.estimated_witness_size() - - def estimated_weight(self): - """Return an estimate of transaction weight.""" - total_tx_size = self.estimated_total_size() - base_tx_size = self.estimated_base_size() - return 3 * base_tx_size + total_tx_size - - def signature_count(self): - r = 0 - s = 0 - for txin in self.inputs(): - if txin['type'] == 'coinbase': - continue - signatures = list(filter(None, txin.get('signatures',[]))) - s += len(signatures) - r += txin.get('num_sig',-1) - return s, r - - def is_complete(self): - if not self.is_partial_originally: - return True - s, r = self.signature_count() - return r == s - - def sign(self, keypairs) -> None: - # keypairs: (x_)pubkey -> secret_bytes - for i, txin in enumerate(self.inputs()): - pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) - for j, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)): - if self.is_txin_complete(txin): - break - if pubkey in keypairs: - _pubkey = pubkey - elif x_pubkey in keypairs: - _pubkey = x_pubkey - else: - continue - print_error("adding signature for", _pubkey) - sec, compressed = keypairs.get(_pubkey) - sig = self.sign_txin(i, sec) - self.add_signature_to_txin(i, j, sig) - - print_error("is_complete", self.is_complete()) - self.raw = self.serialize() - - def sign_txin(self, txin_index, privkey_bytes) -> str: - pre_hash = Hash(bfh(self.serialize_preimage(txin_index))) - privkey = ecc.ECPrivkey(privkey_bytes) - sig = privkey.sign_transaction(pre_hash) - sig = bh2u(sig) + '01' - return sig - - def get_outputs(self): - """convert pubkeys to addresses""" - o = [] - for type, x, v in self.outputs(): - if type == TYPE_ADDRESS: - addr = x - elif type == TYPE_PUBKEY: - # TODO do we really want this conversion? it's not really that address after all - addr = bitcoin.public_key_to_p2pkh(bfh(x)) - else: - addr = 'SCRIPT ' + x - o.append((addr,v)) # consider using yield (addr, v) - return o - - def get_output_addresses(self): - return [addr for addr, val in self.get_outputs()] - - - def has_address(self, addr): - return (addr in self.get_output_addresses()) or (addr in (tx.get("address") for tx in self.inputs())) - - def as_dict(self): - if self.raw is None: - self.raw = self.serialize() - self.deserialize() - out = { - 'hex': self.raw, - 'complete': self.is_complete(), - 'final': self.is_final(), - } - return out - - -def tx_from_str(txt): - "json or raw hexadecimal" - import json - txt = txt.strip() - if not txt: - raise ValueError("empty string") - try: - bfh(txt) - is_hex = True - except: - is_hex = False - if is_hex: - return txt - tx_dict = json.loads(str(txt)) - assert "hex" in tx_dict.keys() - return tx_dict["hex"] diff --git a/lib/util.py b/lib/util.py @@ -1,903 +0,0 @@ -# Electrum - lightweight Bitcoin client -# Copyright (C) 2011 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 binascii -import os, sys, re, json -from collections import defaultdict -from datetime import datetime -import decimal -from decimal import Decimal -import traceback -import urllib -import threading -import hmac -import stat - -from .i18n import _ - - -import urllib.request, urllib.parse, urllib.error -import queue - -def inv_dict(d): - return {v: k for k, v in d.items()} - - -base_units = {'BTC':8, 'mBTC':5, 'bits':2, 'sat':0} -base_units_inverse = inv_dict(base_units) -base_units_list = ['BTC', 'mBTC', 'bits', 'sat'] # list(dict) does not guarantee order - - -def decimal_point_to_base_unit_name(dp: int) -> str: - # e.g. 8 -> "BTC" - try: - return base_units_inverse[dp] - except KeyError: - raise Exception('Unknown base unit') - - -def base_unit_name_to_decimal_point(unit_name: str) -> int: - # e.g. "BTC" -> 8 - try: - return base_units[unit_name] - except KeyError: - raise Exception('Unknown base unit') - - -def normalize_version(v): - return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] - -class NotEnoughFunds(Exception): pass - - -class NoDynamicFeeEstimates(Exception): - def __str__(self): - return _('Dynamic fee estimates not available') - - -class InvalidPassword(Exception): - def __str__(self): - return _("Incorrect password") - - -class FileImportFailed(Exception): - def __init__(self, message=''): - self.message = str(message) - - def __str__(self): - return _("Failed to import from file.") + "\n" + self.message - - -class FileExportFailed(Exception): - def __init__(self, message=''): - self.message = str(message) - - def __str__(self): - return _("Failed to export to file.") + "\n" + self.message - - -class TimeoutException(Exception): - def __init__(self, message=''): - self.message = str(message) - - def __str__(self): - if not self.message: - return _("Operation timed out.") - return self.message - - -class WalletFileException(Exception): pass - - -class BitcoinException(Exception): pass - - -# Throw this exception to unwind the stack like when an error occurs. -# However unlike other exceptions the user won't be informed. -class UserCancelled(Exception): - '''An exception that is suppressed from the user''' - pass - -class Satoshis(object): - def __new__(cls, value): - self = super(Satoshis, cls).__new__(cls) - self.value = value - return self - - def __repr__(self): - return 'Satoshis(%d)'%self.value - - def __str__(self): - return format_satoshis(self.value) + " BTC" - -class Fiat(object): - def __new__(cls, value, ccy): - self = super(Fiat, cls).__new__(cls) - self.ccy = ccy - self.value = value - return self - - def __repr__(self): - return 'Fiat(%s)'% self.__str__() - - def __str__(self): - if self.value.is_nan(): - return _('No Data') - else: - return "{:.2f}".format(self.value) + ' ' + self.ccy - -class MyEncoder(json.JSONEncoder): - def default(self, obj): - from .transaction import Transaction - if isinstance(obj, Transaction): - return obj.as_dict() - if isinstance(obj, Satoshis): - return str(obj) - if isinstance(obj, Fiat): - return str(obj) - if isinstance(obj, Decimal): - return str(obj) - if isinstance(obj, datetime): - return obj.isoformat(' ')[:-3] - if isinstance(obj, set): - return list(obj) - return super(MyEncoder, self).default(obj) - -class PrintError(object): - '''A handy base class''' - def diagnostic_name(self): - return self.__class__.__name__ - - def print_error(self, *msg): - # only prints with --verbose flag - print_error("[%s]" % self.diagnostic_name(), *msg) - - def print_stderr(self, *msg): - print_stderr("[%s]" % self.diagnostic_name(), *msg) - - def print_msg(self, *msg): - print_msg("[%s]" % self.diagnostic_name(), *msg) - -class ThreadJob(PrintError): - """A job that is run periodically from a thread's main loop. run() is - called from that thread's context. - """ - - def run(self): - """Called periodically from the thread""" - pass - -class DebugMem(ThreadJob): - '''A handy class for debugging GC memory leaks''' - def __init__(self, classes, interval=30): - self.next_time = 0 - self.classes = classes - self.interval = interval - - def mem_stats(self): - import gc - self.print_error("Start memscan") - gc.collect() - objmap = defaultdict(list) - for obj in gc.get_objects(): - for class_ in self.classes: - if isinstance(obj, class_): - objmap[class_].append(obj) - for class_, objs in objmap.items(): - self.print_error("%s: %d" % (class_.__name__, len(objs))) - self.print_error("Finish memscan") - - def run(self): - if time.time() > self.next_time: - self.mem_stats() - self.next_time = time.time() + self.interval - -class DaemonThread(threading.Thread, PrintError): - """ daemon thread that terminates cleanly """ - - def __init__(self): - threading.Thread.__init__(self) - self.parent_thread = threading.currentThread() - self.running = False - self.running_lock = threading.Lock() - self.job_lock = threading.Lock() - self.jobs = [] - - def add_jobs(self, jobs): - with self.job_lock: - self.jobs.extend(jobs) - - def run_jobs(self): - # Don't let a throwing job disrupt the thread, future runs of - # itself, or other jobs. This is useful protection against - # malformed or malicious server responses - with self.job_lock: - for job in self.jobs: - try: - job.run() - except Exception as e: - traceback.print_exc(file=sys.stderr) - - def remove_jobs(self, jobs): - with self.job_lock: - for job in jobs: - self.jobs.remove(job) - - def start(self): - with self.running_lock: - self.running = True - return threading.Thread.start(self) - - def is_running(self): - with self.running_lock: - return self.running and self.parent_thread.is_alive() - - def stop(self): - with self.running_lock: - self.running = False - - def on_stop(self): - if 'ANDROID_DATA' in os.environ: - import jnius - jnius.detach() - self.print_error("jnius detach") - self.print_error("stopped") - - -# TODO: disable -is_verbose = True -def set_verbosity(b): - global is_verbose - is_verbose = b - - -def print_error(*args): - if not is_verbose: return - print_stderr(*args) - -def print_stderr(*args): - args = [str(item) for item in args] - sys.stderr.write(" ".join(args) + "\n") - sys.stderr.flush() - -def print_msg(*args): - # Stringify args - args = [str(item) for item in args] - sys.stdout.write(" ".join(args) + "\n") - sys.stdout.flush() - -def json_encode(obj): - try: - s = json.dumps(obj, sort_keys = True, indent = 4, cls=MyEncoder) - except TypeError: - s = repr(obj) - return s - -def json_decode(x): - try: - return json.loads(x, parse_float=Decimal) - except: - return x - - -# taken from Django Source Code -def constant_time_compare(val1, val2): - """Return True if the two strings are equal, False otherwise.""" - return hmac.compare_digest(to_bytes(val1, 'utf8'), to_bytes(val2, 'utf8')) - - -# decorator that prints execution time -def profiler(func): - def do_profile(func, args, kw_args): - n = func.__name__ - t0 = time.time() - o = func(*args, **kw_args) - t = time.time() - t0 - print_error("[profiler]", n, "%.4f"%t) - return o - return lambda *args, **kw_args: do_profile(func, args, kw_args) - - -def android_ext_dir(): - import jnius - env = jnius.autoclass('android.os.Environment') - return env.getExternalStorageDirectory().getPath() - -def android_data_dir(): - import jnius - PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity') - return PythonActivity.mActivity.getFilesDir().getPath() + '/data' - -def android_headers_dir(): - d = android_ext_dir() + '/org.electrum.electrum' - if not os.path.exists(d): - try: - os.mkdir(d) - except FileExistsError: - pass # in case of race - return d - -def android_check_data_dir(): - """ if needed, move old directory to sandbox """ - ext_dir = android_ext_dir() - data_dir = android_data_dir() - old_electrum_dir = ext_dir + '/electrum' - if not os.path.exists(data_dir) and os.path.exists(old_electrum_dir): - import shutil - new_headers_path = android_headers_dir() + '/blockchain_headers' - old_headers_path = old_electrum_dir + '/blockchain_headers' - if not os.path.exists(new_headers_path) and os.path.exists(old_headers_path): - print_error("Moving headers file to", new_headers_path) - shutil.move(old_headers_path, new_headers_path) - print_error("Moving data to", data_dir) - shutil.move(old_electrum_dir, data_dir) - return data_dir - - -def get_headers_dir(config): - return android_headers_dir() if 'ANDROID_DATA' in os.environ else config.path - - -def assert_datadir_available(config_path): - path = config_path - if os.path.exists(path): - return - else: - raise FileNotFoundError( - 'Electrum datadir does not exist. Was it deleted while running?' + '\n' + - 'Should be at {}'.format(path)) - - -def assert_file_in_datadir_available(path, config_path): - if os.path.exists(path): - return - else: - assert_datadir_available(config_path) - raise FileNotFoundError( - 'Cannot find file but datadir is there.' + '\n' + - 'Should be at {}'.format(path)) - - -def assert_bytes(*args): - """ - porting helper, assert args type - """ - try: - for x in args: - assert isinstance(x, (bytes, bytearray)) - except: - print('assert bytes failed', list(map(type, args))) - raise - - -def assert_str(*args): - """ - porting helper, assert args type - """ - for x in args: - assert isinstance(x, str) - - - -def to_string(x, enc): - if isinstance(x, (bytes, bytearray)): - return x.decode(enc) - if isinstance(x, str): - return x - else: - raise TypeError("Not a string or bytes like object") - -def to_bytes(something, encoding='utf8'): - """ - cast string to bytes() like object, but for python2 support it's bytearray copy - """ - if isinstance(something, bytes): - return something - if isinstance(something, str): - return something.encode(encoding) - elif isinstance(something, bytearray): - return bytes(something) - else: - raise TypeError("Not a string or bytes like object") - - -bfh = bytes.fromhex -hfu = binascii.hexlify - - -def bh2u(x): - """ - str with hex representation of a bytes-like object - - >>> x = bytes((1, 2, 10)) - >>> bh2u(x) - '01020A' - - :param x: bytes - :rtype: str - """ - return hfu(x).decode('ascii') - - -def user_dir(): - if 'ANDROID_DATA' in os.environ: - return android_check_data_dir() - elif os.name == 'posix': - return os.path.join(os.environ["HOME"], ".electrum") - elif "APPDATA" in os.environ: - return os.path.join(os.environ["APPDATA"], "Electrum") - elif "LOCALAPPDATA" in os.environ: - return os.path.join(os.environ["LOCALAPPDATA"], "Electrum") - else: - #raise Exception("No home directory found in environment variables.") - return - -def is_valid_email(s): - regexp = r"[^@]+@[^@]+\.[^@]+" - return re.match(regexp, s) is not None - - -def format_satoshis_plain(x, decimal_point = 8): - """Display a satoshi amount scaled. Always uses a '.' as a decimal - point and has no thousands separator""" - scale_factor = pow(10, decimal_point) - return "{:.8f}".format(Decimal(x) / scale_factor).rstrip('0').rstrip('.') - - -def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=False, whitespaces=False): - from locale import localeconv - if x is None: - return 'unknown' - if precision is None: - precision = decimal_point - decimal_format = ".0" + str(precision) if precision > 0 else "" - if is_diff: - decimal_format = '+' + decimal_format - result = ("{:" + decimal_format + "f}").format(x / pow (10, decimal_point)).rstrip('0') - integer_part, fract_part = result.split(".") - dp = localeconv()['decimal_point'] - if len(fract_part) < num_zeros: - fract_part += "0" * (num_zeros - len(fract_part)) - result = integer_part + dp + fract_part - if whitespaces: - result += " " * (decimal_point - len(fract_part)) - result = " " * (15 - len(result)) + result - return result - - -FEERATE_PRECISION = 1 # num fractional decimal places for sat/byte fee rates -_feerate_quanta = Decimal(10) ** (-FEERATE_PRECISION) - - -def format_fee_satoshis(fee, num_zeros=0): - return format_satoshis(fee, num_zeros, 0, precision=FEERATE_PRECISION) - - -def quantize_feerate(fee): - """Strip sat/byte fee rate of excess precision.""" - if fee is None: - return None - return Decimal(fee).quantize(_feerate_quanta, rounding=decimal.ROUND_HALF_DOWN) - - -def timestamp_to_datetime(timestamp): - if timestamp is None: - return None - return datetime.fromtimestamp(timestamp) - -def format_time(timestamp): - date = timestamp_to_datetime(timestamp) - return date.isoformat(' ')[:-3] if date else _("Unknown") - - -# Takes a timestamp and returns a string with the approximation of the age -def age(from_date, since_date = None, target_tz=None, include_seconds=False): - if from_date is None: - return "Unknown" - - from_date = datetime.fromtimestamp(from_date) - if since_date is None: - since_date = datetime.now(target_tz) - - td = time_difference(from_date - since_date, include_seconds) - return td + " ago" if from_date < since_date else "in " + td - - -def time_difference(distance_in_time, include_seconds): - #distance_in_time = since_date - from_date - distance_in_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds))) - distance_in_minutes = int(round(distance_in_seconds/60)) - - if distance_in_minutes <= 1: - if include_seconds: - for remainder in [5, 10, 20]: - if distance_in_seconds < remainder: - return "less than %s seconds" % remainder - if distance_in_seconds < 40: - return "half a minute" - elif distance_in_seconds < 60: - return "less than a minute" - else: - return "1 minute" - else: - if distance_in_minutes == 0: - return "less than a minute" - else: - return "1 minute" - elif distance_in_minutes < 45: - return "%s minutes" % distance_in_minutes - elif distance_in_minutes < 90: - return "about 1 hour" - elif distance_in_minutes < 1440: - return "about %d hours" % (round(distance_in_minutes / 60.0)) - elif distance_in_minutes < 2880: - return "1 day" - elif distance_in_minutes < 43220: - return "%d days" % (round(distance_in_minutes / 1440)) - elif distance_in_minutes < 86400: - return "about 1 month" - elif distance_in_minutes < 525600: - return "%d months" % (round(distance_in_minutes / 43200)) - elif distance_in_minutes < 1051200: - return "about 1 year" - else: - return "over %d years" % (round(distance_in_minutes / 525600)) - -mainnet_block_explorers = { - 'Biteasy.com': ('https://www.biteasy.com/blockchain/', - {'tx': 'transactions/', 'addr': 'addresses/'}), - 'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/', - {'tx': 'Transaction/', 'addr': 'Address/'}), - 'Blockchain.info': ('https://blockchain.info/', - {'tx': 'tx/', 'addr': 'address/'}), - 'blockchainbdgpzk.onion': ('https://blockchainbdgpzk.onion/', - {'tx': 'tx/', 'addr': 'address/'}), - 'Blockr.io': ('https://btc.blockr.io/', - {'tx': 'tx/info/', 'addr': 'address/info/'}), - 'Blocktrail.com': ('https://www.blocktrail.com/BTC/', - {'tx': 'tx/', 'addr': 'address/'}), - 'BTC.com': ('https://chain.btc.com/', - {'tx': 'tx/', 'addr': 'address/'}), - 'Chain.so': ('https://www.chain.so/', - {'tx': 'tx/BTC/', 'addr': 'address/BTC/'}), - 'Insight.is': ('https://insight.bitpay.com/', - {'tx': 'tx/', 'addr': 'address/'}), - 'TradeBlock.com': ('https://tradeblock.com/blockchain/', - {'tx': 'tx/', 'addr': 'address/'}), - 'BlockCypher.com': ('https://live.blockcypher.com/btc/', - {'tx': 'tx/', 'addr': 'address/'}), - 'Blockchair.com': ('https://blockchair.com/bitcoin/', - {'tx': 'transaction/', 'addr': 'address/'}), - 'blockonomics.co': ('https://www.blockonomics.co/', - {'tx': 'api/tx?txid=', 'addr': '#/search?q='}), - 'OXT.me': ('https://oxt.me/', - {'tx': 'transaction/', 'addr': 'address/'}), - 'system default': ('blockchain:/', - {'tx': 'tx/', 'addr': 'address/'}), -} - -testnet_block_explorers = { - 'Blocktrail.com': ('https://www.blocktrail.com/tBTC/', - {'tx': 'tx/', 'addr': 'address/'}), - 'system default': ('blockchain://000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943/', - {'tx': 'tx/', 'addr': 'address/'}), -} - -def block_explorer_info(): - from . import constants - return testnet_block_explorers if constants.net.TESTNET else mainnet_block_explorers - -def block_explorer(config): - return config.get('block_explorer', 'Blocktrail.com') - -def block_explorer_tuple(config): - return block_explorer_info().get(block_explorer(config)) - -def block_explorer_URL(config, kind, item): - be_tuple = block_explorer_tuple(config) - if not be_tuple: - return - kind_str = be_tuple[1].get(kind) - if not kind_str: - return - url_parts = [be_tuple[0], kind_str, item] - return ''.join(url_parts) - -# URL decode -#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) -#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) - -def parse_URI(uri, on_pr=None): - from . import bitcoin - from .bitcoin import COIN - - if ':' not in uri: - if not bitcoin.is_address(uri): - raise Exception("Not a bitcoin address") - return {'address': uri} - - u = urllib.parse.urlparse(uri) - if u.scheme != 'bitcoin': - raise Exception("Not a bitcoin URI") - address = u.path - - # python for android fails to parse query - if address.find('?') > 0: - address, query = u.path.split('?') - pq = urllib.parse.parse_qs(query) - else: - pq = urllib.parse.parse_qs(u.query) - - for k, v in pq.items(): - if len(v)!=1: - raise Exception('Duplicate Key', k) - - out = {k: v[0] for k, v in pq.items()} - if address: - if not bitcoin.is_address(address): - raise Exception("Invalid bitcoin address:" + address) - out['address'] = address - if 'amount' in out: - am = out['amount'] - m = re.match('([0-9\.]+)X([0-9])', am) - if m: - k = int(m.group(2)) - 8 - amount = Decimal(m.group(1)) * pow( Decimal(10) , k) - else: - amount = Decimal(am) * COIN - out['amount'] = int(amount) - if 'message' in out: - out['message'] = out['message'] - out['memo'] = out['message'] - if 'time' in out: - out['time'] = int(out['time']) - if 'exp' in out: - out['exp'] = int(out['exp']) - if 'sig' in out: - out['sig'] = bh2u(bitcoin.base_decode(out['sig'], None, base=58)) - - r = out.get('r') - sig = out.get('sig') - name = out.get('name') - if on_pr and (r or (name and sig)): - def get_payment_request_thread(): - from . import paymentrequest as pr - if name and sig: - s = pr.serialize_request(out).SerializeToString() - request = pr.PaymentRequest(s) - else: - request = pr.get_payment_request(r) - if on_pr: - on_pr(request) - t = threading.Thread(target=get_payment_request_thread) - t.setDaemon(True) - t.start() - - return out - - -def create_URI(addr, amount, message): - from . import bitcoin - if not bitcoin.is_address(addr): - return "" - query = [] - if amount: - query.append('amount=%s'%format_satoshis_plain(amount)) - if message: - query.append('message=%s'%urllib.parse.quote(message)) - p = urllib.parse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='') - return urllib.parse.urlunparse(p) - - -# Python bug (http://bugs.python.org/issue1927) causes raw_input -# to be redirected improperly between stdin/stderr on Unix systems -#TODO: py3 -def raw_input(prompt=None): - if prompt: - sys.stdout.write(prompt) - return builtin_raw_input() - -import builtins -builtin_raw_input = builtins.input -builtins.input = raw_input - - -def parse_json(message): - # TODO: check \r\n pattern - n = message.find(b'\n') - if n==-1: - return None, message - try: - j = json.loads(message[0:n].decode('utf8')) - except: - j = None - return j, message[n+1:] - - -class timeout(Exception): - pass - -import socket -import json -import ssl -import time - - -class SocketPipe: - def __init__(self, socket): - self.socket = socket - self.message = b'' - self.set_timeout(0.1) - self.recv_time = time.time() - - def set_timeout(self, t): - self.socket.settimeout(t) - - def idle_time(self): - return time.time() - self.recv_time - - def get(self): - while True: - response, self.message = parse_json(self.message) - if response is not None: - return response - try: - data = self.socket.recv(1024) - except socket.timeout: - raise timeout - except ssl.SSLError: - raise timeout - except socket.error as err: - if err.errno == 60: - raise timeout - elif err.errno in [11, 35, 10035]: - print_error("socket errno %d (resource temporarily unavailable)"% err.errno) - time.sleep(0.2) - raise timeout - else: - print_error("pipe: socket error", err) - data = b'' - except: - traceback.print_exc(file=sys.stderr) - data = b'' - - if not data: # Connection closed remotely - return None - self.message += data - self.recv_time = time.time() - - def send(self, request): - out = json.dumps(request) + '\n' - out = out.encode('utf8') - self._send(out) - - def send_all(self, requests): - out = b''.join(map(lambda x: (json.dumps(x) + '\n').encode('utf8'), requests)) - self._send(out) - - def _send(self, out): - while out: - try: - sent = self.socket.send(out) - out = out[sent:] - except ssl.SSLError as e: - print_error("SSLError:", e) - time.sleep(0.1) - continue - - -class QueuePipe: - - def __init__(self, send_queue=None, get_queue=None): - self.send_queue = send_queue if send_queue else queue.Queue() - self.get_queue = get_queue if get_queue else queue.Queue() - self.set_timeout(0.1) - - def get(self): - try: - return self.get_queue.get(timeout=self.timeout) - except queue.Empty: - raise timeout - - def get_all(self): - responses = [] - while True: - try: - r = self.get_queue.get_nowait() - responses.append(r) - except queue.Empty: - break - return responses - - def set_timeout(self, t): - self.timeout = t - - def send(self, request): - self.send_queue.put(request) - - def send_all(self, requests): - for request in requests: - self.send(request) - - - - -def setup_thread_excepthook(): - """ - Workaround for `sys.excepthook` thread bug from: - http://bugs.python.org/issue1230540 - - Call once from the main thread before creating any threads. - """ - - init_original = threading.Thread.__init__ - - def init(self, *args, **kwargs): - - init_original(self, *args, **kwargs) - run_original = self.run - - def run_with_except_hook(*args2, **kwargs2): - try: - run_original(*args2, **kwargs2) - except Exception: - sys.excepthook(*sys.exc_info()) - - self.run = run_with_except_hook - - threading.Thread.__init__ = init - - -def versiontuple(v): - return tuple(map(int, (v.split(".")))) - - -def import_meta(path, validater, load_meta): - try: - with open(path, 'r', encoding='utf-8') as f: - d = validater(json.loads(f.read())) - load_meta(d) - #backwards compatibility for JSONDecodeError - except ValueError: - traceback.print_exc(file=sys.stderr) - raise FileImportFailed(_("Invalid JSON code.")) - except BaseException as e: - traceback.print_exc(file=sys.stdout) - raise FileImportFailed(e) - - -def export_meta(meta, fileName): - try: - with open(fileName, 'w+', encoding='utf-8') as f: - json.dump(meta, f, indent=4, sort_keys=True) - except (IOError, os.error) as e: - traceback.print_exc(file=sys.stderr) - raise FileExportFailed(e) - - -def make_dir(path, allow_symlink=True): - """Make directory if it does not yet exist.""" - if not os.path.exists(path): - if not allow_symlink and os.path.islink(path): - raise Exception('Dangling link: ' + path) - os.mkdir(path) - os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) diff --git a/lib/verifier.py b/lib/verifier.py @@ -1,158 +0,0 @@ -# Electrum - Lightweight Bitcoin Client -# Copyright (c) 2012 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 .util import ThreadJob, bh2u -from .bitcoin import Hash, hash_decode, hash_encode -from .transaction import Transaction - - -class InnerNodeOfSpvProofIsValidTx(Exception): pass - - -class SPV(ThreadJob): - """ Simple Payment Verification """ - - def __init__(self, network, wallet): - self.wallet = wallet - self.network = network - self.blockchain = network.blockchain() - self.merkle_roots = {} # txid -> merkle root (once it has been verified) - self.requested_merkle = set() # txid set of pending requests - - def run(self): - interface = self.network.interface - if not interface: - return - - blockchain = interface.blockchain - if not blockchain: - return - - local_height = self.network.get_local_height() - unverified = self.wallet.get_unverified_txs() - for tx_hash, tx_height in unverified.items(): - # do not request merkle branch before headers are available - if tx_height <= 0 or tx_height > local_height: - continue - - header = blockchain.read_header(tx_height) - if header is None: - index = tx_height // 2016 - if index < len(blockchain.checkpoints): - self.network.request_chunk(interface, index) - elif (tx_hash not in self.requested_merkle - and tx_hash not in self.merkle_roots): - self.network.get_merkle_for_transaction( - tx_hash, - tx_height, - self.verify_merkle) - self.print_error('requested merkle', tx_hash) - self.requested_merkle.add(tx_hash) - - if self.network.blockchain() != self.blockchain: - self.blockchain = self.network.blockchain() - self.undo_verifications() - - def verify_merkle(self, response): - if self.wallet.verifier is None: - return # we have been killed, this was just an orphan callback - if response.get('error'): - self.print_error('received an error:', response) - return - params = response['params'] - merkle = response['result'] - # Verify the hash of the server-provided merkle branch to a - # transaction matches the merkle root of its block - tx_hash = params[0] - tx_height = merkle.get('block_height') - pos = merkle.get('pos') - try: - merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos) - except InnerNodeOfSpvProofIsValidTx: - self.print_error("merkle verification failed for {} (inner node looks like tx)" - .format(tx_hash)) - return - header = self.network.blockchain().read_header(tx_height) - # FIXME: if verification fails below, - # we should make a fresh connection to a server to - # recover from this, as this TX will now never verify - if not header: - self.print_error( - "merkle verification failed for {} (missing header {})" - .format(tx_hash, tx_height)) - return - if header.get('merkle_root') != merkle_root: - self.print_error( - "merkle verification failed for {} (merkle root mismatch {} != {})" - .format(tx_hash, header.get('merkle_root'), merkle_root)) - return - # we passed all the tests - self.merkle_roots[tx_hash] = merkle_root - try: - # note: we could pop in the beginning, but then we would request - # this proof again in case of verification failure from the same server - self.requested_merkle.remove(tx_hash) - except KeyError: pass - self.print_error("verified %s" % tx_hash) - self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos)) - if self.is_up_to_date() and self.wallet.is_up_to_date(): - self.wallet.save_verified_tx(write=True) - - @classmethod - def hash_merkle_root(cls, merkle_s, target_hash, pos): - h = hash_decode(target_hash) - for i in range(len(merkle_s)): - item = merkle_s[i] - h = Hash(hash_decode(item) + h) if ((pos >> i) & 1) else Hash(h + hash_decode(item)) - cls._raise_if_valid_tx(bh2u(h)) - return hash_encode(h) - - @classmethod - def _raise_if_valid_tx(cls, raw_tx: str): - # If an inner node of the merkle proof is also a valid tx, chances are, this is an attack. - # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-June/016105.html - # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/attachments/20180609/9f4f5b1f/attachment-0001.pdf - # https://bitcoin.stackexchange.com/questions/76121/how-is-the-leaf-node-weakness-in-merkle-trees-exploitable/76122#76122 - tx = Transaction(raw_tx) - try: - tx.deserialize() - except: - pass - else: - raise InnerNodeOfSpvProofIsValidTx() - - def undo_verifications(self): - height = self.blockchain.get_checkpoint() - tx_hashes = self.wallet.undo_verifications(self.blockchain, height) - for tx_hash in tx_hashes: - self.print_error("redoing", tx_hash) - self.remove_spv_proof_for_tx(tx_hash) - - def remove_spv_proof_for_tx(self, tx_hash): - self.merkle_roots.pop(tx_hash, None) - try: - self.requested_merkle.remove(tx_hash) - except KeyError: - pass - - def is_up_to_date(self): - return not self.requested_merkle diff --git a/lib/version.py b/lib/version.py @@ -1,18 +0,0 @@ -ELECTRUM_VERSION = '3.2.2' # version of the client package -APK_VERSION = '3.2.2.0' # read by buildozer.spec - -PROTOCOL_VERSION = '1.2' # protocol version requested - -# The hash of the mnemonic seed must begin with this -SEED_PREFIX = '01' # Standard wallet -SEED_PREFIX_2FA = '101' # Two-factor authentication -SEED_PREFIX_SW = '100' # Segwit wallet - - -def seed_prefix(seed_type): - if seed_type == 'standard': - return SEED_PREFIX - elif seed_type == 'segwit': - return SEED_PREFIX_SW - elif seed_type == '2fa': - return SEED_PREFIX_2FA diff --git a/lib/wallet.py b/lib/wallet.py @@ -1,2377 +0,0 @@ -# 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. - -# Wallet classes: -# - Imported_Wallet: imported address, no keystore -# - Standard_Wallet: one keystore, P2PKH -# - Multisig_Wallet: several keystores, P2SH - - -import os -import threading -import random -import time -import json -import copy -import errno -import traceback -from functools import partial -from collections import defaultdict -from numbers import Number -from decimal import Decimal -import itertools - -import sys - -from .i18n import _ -from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, - format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, - TimeoutException, WalletFileException, BitcoinException, - InvalidPassword) - -from .bitcoin import * -from .version import * -from .keystore import load_keystore, Hardware_KeyStore -from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW - -from . import transaction -from .transaction import Transaction -from .plugins import run_hook -from . import bitcoin -from . import coinchooser -from .synchronizer import Synchronizer -from .verifier import SPV - -from . import paymentrequest -from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED -from .paymentrequest import InvoiceStore -from .contacts import Contacts - -TX_STATUS = [ - _('Unconfirmed'), - _('Unconfirmed parent'), - _('Not Verified'), - _('Local'), -] - -TX_HEIGHT_LOCAL = -2 -TX_HEIGHT_UNCONF_PARENT = -1 -TX_HEIGHT_UNCONFIRMED = 0 - - -def relayfee(network): - from .simple_config import FEERATE_DEFAULT_RELAY - MAX_RELAY_FEE = 50000 - f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY - return min(f, MAX_RELAY_FEE) - -def dust_threshold(network): - # Change <= dust threshold is added to the tx fee - return 182 * 3 * relayfee(network) / 1000 - - -def append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax): - if txin_type != 'p2pk': - address = bitcoin.pubkey_to_address(txin_type, pubkey) - scripthash = bitcoin.address_to_scripthash(address) - else: - script = bitcoin.public_key_to_p2pk_script(pubkey) - scripthash = bitcoin.script_to_scripthash(script) - address = '(pubkey)' - - u = network.listunspent_for_scripthash(scripthash) - for item in u: - if len(inputs) >= imax: - break - item['address'] = address - item['type'] = txin_type - item['prevout_hash'] = item['tx_hash'] - item['prevout_n'] = int(item['tx_pos']) - item['pubkeys'] = [pubkey] - item['x_pubkeys'] = [pubkey] - item['signatures'] = [None] - item['num_sig'] = 1 - inputs.append(item) - -def sweep_preparations(privkeys, network, imax=100): - - def find_utxos_for_privkey(txin_type, privkey, compressed): - pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) - append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax) - keypairs[pubkey] = privkey, compressed - inputs = [] - keypairs = {} - for sec in privkeys: - txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) - find_utxos_for_privkey(txin_type, privkey, compressed) - # do other lookups to increase support coverage - if is_minikey(sec): - # minikeys don't have a compressed byte - # we lookup both compressed and uncompressed pubkeys - find_utxos_for_privkey(txin_type, privkey, not compressed) - elif txin_type == 'p2pkh': - # WIF serialization does not distinguish p2pkh and p2pk - # we also search for pay-to-pubkey outputs - find_utxos_for_privkey('p2pk', privkey, compressed) - if not inputs: - raise Exception(_('No inputs found. (Note that inputs need to be confirmed)')) - # FIXME actually inputs need not be confirmed now, see https://github.com/kyuupichan/electrumx/issues/365 - return inputs, keypairs - - -def sweep(privkeys, network, config, recipient, fee=None, imax=100): - inputs, keypairs = sweep_preparations(privkeys, network, imax) - total = sum(i.get('value') for i in inputs) - if fee is None: - outputs = [(TYPE_ADDRESS, recipient, total)] - tx = Transaction.from_io(inputs, outputs) - fee = config.estimate_fee(tx.estimated_size()) - if total - fee < 0: - raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee)) - if total - fee < dust_threshold(network): - raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network))) - - outputs = [(TYPE_ADDRESS, recipient, total - fee)] - locktime = network.get_local_height() - - tx = Transaction.from_io(inputs, outputs, locktime=locktime) - tx.BIP_LI01_sort() - tx.set_rbf(True) - tx.sign(keypairs) - return tx - - -class AddTransactionException(Exception): - pass - - -class UnrelatedTransactionException(AddTransactionException): - def __str__(self): - return _("Transaction is unrelated to this wallet.") - - -class CannotBumpFee(Exception): pass - - -class Abstract_Wallet(PrintError): - """ - Wallet classes are created to handle various address generation methods. - Completion states (watching-only, single account, no seed, etc) are handled inside classes. - """ - - max_change_outputs = 3 - - def __init__(self, storage): - self.electrum_version = ELECTRUM_VERSION - self.storage = storage - self.network = None - # verifier (SPV) and synchronizer are started in start_threads - self.synchronizer = None - self.verifier = None - - self.gap_limit_for_change = 6 # constant - - # locks: if you need to take multiple ones, acquire them in the order they are defined here! - self.lock = threading.RLock() - self.transaction_lock = threading.RLock() - - # saved fields - self.use_change = storage.get('use_change', True) - self.multiple_change = storage.get('multiple_change', False) - self.labels = storage.get('labels', {}) - self.frozen_addresses = set(storage.get('frozen_addresses',[])) - self.history = storage.get('addr_history',{}) # address -> list(txid, height) - self.fiat_value = storage.get('fiat_value', {}) - self.receive_requests = storage.get('payment_requests', {}) - - # Verified transactions. txid -> (height, timestamp, block_pos). Access with self.lock. - self.verified_tx = storage.get('verified_tx3', {}) - # Transactions pending verification. txid -> tx_height. Access with self.lock. - self.unverified_tx = defaultdict(int) - - self.load_keystore() - self.load_addresses() - self.test_addresses_sanity() - self.load_transactions() - self.load_local_history() - self.check_history() - self.load_unverified_transactions() - self.remove_local_transactions_we_dont_have() - - # wallet.up_to_date is true when the wallet is synchronized - self.up_to_date = False - - # save wallet type the first time - if self.storage.get('wallet_type') is None: - self.storage.put('wallet_type', self.wallet_type) - - # invoices and contacts - self.invoices = InvoiceStore(self.storage) - self.contacts = Contacts(self.storage) - - self.coin_price_cache = {} - - - def diagnostic_name(self): - return self.basename() - - def __str__(self): - return self.basename() - - def get_master_public_key(self): - return None - - @profiler - def load_transactions(self): - # load txi, txo, tx_fees - self.txi = self.storage.get('txi', {}) - for txid, d in list(self.txi.items()): - for addr, lst in d.items(): - self.txi[txid][addr] = set([tuple(x) for x in lst]) - self.txo = self.storage.get('txo', {}) - self.tx_fees = self.storage.get('tx_fees', {}) - tx_list = self.storage.get('transactions', {}) - # load transactions - self.transactions = {} - for tx_hash, raw in tx_list.items(): - tx = Transaction(raw) - self.transactions[tx_hash] = tx - if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None: - self.print_error("removing unreferenced tx", tx_hash) - self.transactions.pop(tx_hash) - # load spent_outpoints - _spent_outpoints = self.storage.get('spent_outpoints', {}) - self.spent_outpoints = defaultdict(dict) - for prevout_hash, d in _spent_outpoints.items(): - for prevout_n_str, spending_txid in d.items(): - prevout_n = int(prevout_n_str) - self.spent_outpoints[prevout_hash][prevout_n] = spending_txid - - @profiler - def load_local_history(self): - self._history_local = {} # address -> set(txid) - for txid in itertools.chain(self.txi, self.txo): - self._add_tx_to_local_history(txid) - - def remove_local_transactions_we_dont_have(self): - txid_set = set(self.txi) | set(self.txo) - for txid in txid_set: - tx_height = self.get_tx_height(txid)[0] - if tx_height == TX_HEIGHT_LOCAL and txid not in self.transactions: - self.remove_transaction(txid) - - @profiler - def save_transactions(self, write=False): - with self.transaction_lock: - tx = {} - for k,v in self.transactions.items(): - tx[k] = str(v) - self.storage.put('transactions', tx) - self.storage.put('txi', self.txi) - self.storage.put('txo', self.txo) - self.storage.put('tx_fees', self.tx_fees) - self.storage.put('addr_history', self.history) - self.storage.put('spent_outpoints', self.spent_outpoints) - if write: - self.storage.write() - - def save_verified_tx(self, write=False): - with self.lock: - self.storage.put('verified_tx3', self.verified_tx) - if write: - self.storage.write() - - def clear_history(self): - with self.lock: - with self.transaction_lock: - self.txi = {} - self.txo = {} - self.tx_fees = {} - self.spent_outpoints = defaultdict(dict) - self.history = {} - self.verified_tx = {} - self.transactions = {} - self.save_transactions() - - @profiler - def check_history(self): - save = False - - hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.history.keys())) - hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.history.keys())) - - for addr in hist_addrs_not_mine: - self.history.pop(addr) - save = True - - for addr in hist_addrs_mine: - hist = self.history[addr] - - for tx_hash, tx_height in hist: - if self.txi.get(tx_hash) or self.txo.get(tx_hash): - continue - tx = self.transactions.get(tx_hash) - if tx is not None: - self.add_transaction(tx_hash, tx, allow_unrelated=True) - save = True - if save: - self.save_transactions() - - def basename(self): - return os.path.basename(self.storage.path) - - def save_addresses(self): - self.storage.put('addresses', {'receiving':self.receiving_addresses, 'change':self.change_addresses}) - - def load_addresses(self): - d = self.storage.get('addresses', {}) - if type(d) != dict: d={} - self.receiving_addresses = d.get('receiving', []) - self.change_addresses = d.get('change', []) - - def test_addresses_sanity(self): - addrs = self.get_receiving_addresses() - if len(addrs) > 0: - if not bitcoin.is_address(addrs[0]): - raise WalletFileException('The addresses in this wallet are not bitcoin addresses.') - - def synchronize(self): - pass - - def is_deterministic(self): - return self.keystore.is_deterministic() - - def set_up_to_date(self, up_to_date): - with self.lock: - self.up_to_date = up_to_date - if up_to_date: - self.save_transactions(write=True) - # if the verifier is also up to date, persist that too; - # otherwise it will persist its results when it finishes - if self.verifier and self.verifier.is_up_to_date(): - self.save_verified_tx(write=True) - - def is_up_to_date(self): - with self.lock: return self.up_to_date - - def set_label(self, name, text = None): - changed = False - old_text = self.labels.get(name) - if text: - text = text.replace("\n", " ") - if old_text != text: - self.labels[name] = text - changed = True - else: - if old_text: - self.labels.pop(name) - changed = True - if changed: - run_hook('set_label', self, name, text) - self.storage.put('labels', self.labels) - return changed - - def set_fiat_value(self, txid, ccy, text): - if txid not in self.transactions: - return - if not text: - d = self.fiat_value.get(ccy, {}) - if d and txid in d: - d.pop(txid) - else: - return - else: - try: - Decimal(text) - except: - return - if ccy not in self.fiat_value: - self.fiat_value[ccy] = {} - self.fiat_value[ccy][txid] = text - self.storage.put('fiat_value', self.fiat_value) - - def get_fiat_value(self, txid, ccy): - fiat_value = self.fiat_value.get(ccy, {}).get(txid) - try: - return Decimal(fiat_value) - except: - return - - def is_mine(self, address): - return address in self.get_addresses() - - def is_change(self, address): - if not self.is_mine(address): - return False - return self.get_address_index(address)[0] - - def get_address_index(self, address): - raise NotImplementedError() - - def get_redeem_script(self, address): - return None - - def export_private_key(self, address, password): - if self.is_watching_only(): - return [] - index = self.get_address_index(address) - pk, compressed = self.keystore.get_private_key(index, password) - txin_type = self.get_txin_type(address) - redeem_script = self.get_redeem_script(address) - serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type) - return serialized_privkey, redeem_script - - def get_public_keys(self, address): - return [self.get_public_key(address)] - - def add_unverified_tx(self, tx_hash, tx_height): - if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \ - and tx_hash in self.verified_tx: - with self.lock: - self.verified_tx.pop(tx_hash) - if self.verifier: - self.verifier.remove_spv_proof_for_tx(tx_hash) - - # tx will be verified only if height > 0 - if tx_hash not in self.verified_tx: - with self.lock: - self.unverified_tx[tx_hash] = tx_height - - def add_verified_tx(self, tx_hash, info): - # Remove from the unverified map and add to the verified map - with self.lock: - self.unverified_tx.pop(tx_hash, None) - self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos) - height, conf, timestamp = self.get_tx_height(tx_hash) - self.network.trigger_callback('verified', tx_hash, height, conf, timestamp) - - def get_unverified_txs(self): - '''Returns a map from tx hash to transaction height''' - with self.lock: - return dict(self.unverified_tx) # copy - - def undo_verifications(self, blockchain, height): - '''Used by the verifier when a reorg has happened''' - txs = set() - with self.lock: - for tx_hash, item in list(self.verified_tx.items()): - tx_height, timestamp, pos = item - if tx_height >= height: - header = blockchain.read_header(tx_height) - # fixme: use block hash, not timestamp - if not header or header.get('timestamp') != timestamp: - self.verified_tx.pop(tx_hash, None) - txs.add(tx_hash) - return txs - - def get_local_height(self): - """ return last known height if we are offline """ - return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) - - def get_tx_height(self, tx_hash): - """ Given a transaction, returns (height, conf, timestamp) """ - with self.lock: - if tx_hash in self.verified_tx: - height, timestamp, pos = self.verified_tx[tx_hash] - conf = max(self.get_local_height() - height + 1, 0) - return height, conf, timestamp - elif tx_hash in self.unverified_tx: - height = self.unverified_tx[tx_hash] - return height, 0, None - else: - # local transaction - return TX_HEIGHT_LOCAL, 0, None - - def get_txpos(self, tx_hash): - "return position, even if the tx is unverified" - with self.lock: - if tx_hash in self.verified_tx: - height, timestamp, pos = self.verified_tx[tx_hash] - return height, pos - elif tx_hash in self.unverified_tx: - height = self.unverified_tx[tx_hash] - return (height, 0) if height > 0 else ((1e9 - height), 0) - else: - return (1e9+1, 0) - - def is_found(self): - return self.history.values() != [[]] * len(self.history) - - def get_num_tx(self, address): - """ return number of transactions where address is involved """ - return len(self.history.get(address, [])) - - def get_tx_delta(self, tx_hash, address): - "effect of tx on address" - delta = 0 - # substract the value of coins sent from address - d = self.txi.get(tx_hash, {}).get(address, []) - for n, v in d: - delta -= v - # add the value of the coins received at address - d = self.txo.get(tx_hash, {}).get(address, []) - for n, v, cb in d: - delta += v - return delta - - def get_tx_value(self, txid): - " effect of tx on the entire domain" - delta = 0 - for addr, d in self.txi.get(txid, {}).items(): - for n, v in d: - delta -= v - for addr, d in self.txo.get(txid, {}).items(): - for n, v, cb in d: - delta += v - return delta - - def get_wallet_delta(self, tx): - """ effect of tx on wallet """ - is_relevant = False # "related to wallet?" - is_mine = False - is_pruned = False - is_partial = False - v_in = v_out = v_out_mine = 0 - for txin in tx.inputs(): - addr = self.get_txin_address(txin) - if self.is_mine(addr): - is_mine = True - is_relevant = True - d = self.txo.get(txin['prevout_hash'], {}).get(addr, []) - for n, v, cb in d: - if n == txin['prevout_n']: - value = v - break - else: - value = None - if value is None: - is_pruned = True - else: - v_in += value - else: - is_partial = True - if not is_mine: - is_partial = False - for addr, value in tx.get_outputs(): - v_out += value - if self.is_mine(addr): - v_out_mine += value - is_relevant = True - if is_pruned: - # some inputs are mine: - fee = None - if is_mine: - v = v_out_mine - v_out - else: - # no input is mine - v = v_out_mine - else: - v = v_out_mine - v_in - if is_partial: - # some inputs are mine, but not all - fee = None - else: - # all inputs are mine - fee = v_in - v_out - if not is_mine: - fee = None - return is_relevant, is_mine, v, fee - - def get_tx_info(self, tx): - is_relevant, is_mine, v, fee = self.get_wallet_delta(tx) - exp_n = None - can_broadcast = False - can_bump = False - label = '' - height = conf = timestamp = None - tx_hash = tx.txid() - if tx.is_complete(): - if tx_hash in self.transactions.keys(): - label = self.get_label(tx_hash) - height, conf, timestamp = self.get_tx_height(tx_hash) - if height > 0: - if conf: - status = _("{} confirmations").format(conf) - else: - status = _('Not verified') - elif height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED): - status = _('Unconfirmed') - if fee is None: - fee = self.tx_fees.get(tx_hash) - if fee and self.network and self.network.config.has_fee_mempool(): - size = tx.estimated_size() - fee_per_byte = fee / size - exp_n = self.network.config.fee_to_depth(fee_per_byte) - can_bump = is_mine and not tx.is_final() - else: - status = _('Local') - can_broadcast = self.network is not None - else: - status = _("Signed") - can_broadcast = self.network is not None - else: - s, r = tx.signature_count() - status = _("Unsigned") if s == 0 else _('Partially signed') + ' (%d/%d)'%(s,r) - - if is_relevant: - if is_mine: - if fee is not None: - amount = v + fee - else: - amount = v - else: - amount = v - else: - amount = None - - return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n - - def get_addr_io(self, address): - h = self.get_address_history(address) - received = {} - sent = {} - for tx_hash, height in h: - l = self.txo.get(tx_hash, {}).get(address, []) - for n, v, is_cb in l: - received[tx_hash + ':%d'%n] = (height, v, is_cb) - for tx_hash, height in h: - l = self.txi.get(tx_hash, {}).get(address, []) - for txi, v in l: - sent[txi] = height - return received, sent - - def get_addr_utxo(self, address): - coins, spent = self.get_addr_io(address) - for txi in spent: - coins.pop(txi) - out = {} - for txo, v in coins.items(): - tx_height, value, is_cb = v - prevout_hash, prevout_n = txo.split(':') - x = { - 'address':address, - 'value':value, - 'prevout_n':int(prevout_n), - 'prevout_hash':prevout_hash, - 'height':tx_height, - 'coinbase':is_cb - } - out[txo] = x - return out - - # return the total amount ever received by an address - def get_addr_received(self, address): - received, sent = self.get_addr_io(address) - return sum([v for height, v, is_cb in received.values()]) - - # return the balance of a bitcoin address: confirmed and matured, unconfirmed, unmatured - def get_addr_balance(self, address): - received, sent = self.get_addr_io(address) - c = u = x = 0 - local_height = self.get_local_height() - for txo, (tx_height, v, is_cb) in received.items(): - if is_cb and tx_height + COINBASE_MATURITY > local_height: - x += v - elif tx_height > 0: - c += v - else: - u += v - if txo in sent: - if sent[txo] > 0: - c -= v - else: - u -= v - return c, u, x - - def get_spendable_coins(self, domain, config): - confirmed_only = config.get('confirmed_only', False) - return self.get_utxos(domain, exclude_frozen=True, mature=True, confirmed_only=confirmed_only) - - def get_utxos(self, domain = None, exclude_frozen = False, mature = False, confirmed_only = False): - coins = [] - if domain is None: - domain = self.get_addresses() - domain = set(domain) - if exclude_frozen: - domain = set(domain) - self.frozen_addresses - for addr in domain: - utxos = self.get_addr_utxo(addr) - for x in utxos.values(): - if confirmed_only and x['height'] <= 0: - continue - if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height(): - continue - coins.append(x) - continue - return coins - - def dummy_address(self): - return self.get_receiving_addresses()[0] - - def get_addresses(self): - out = [] - out += self.get_receiving_addresses() - out += self.get_change_addresses() - return out - - def get_frozen_balance(self): - return self.get_balance(self.frozen_addresses) - - def get_balance(self, domain=None): - if domain is None: - domain = self.get_addresses() - domain = set(domain) - cc = uu = xx = 0 - for addr in domain: - c, u, x = self.get_addr_balance(addr) - cc += c - uu += u - xx += x - return cc, uu, xx - - def get_address_history(self, addr): - h = [] - # we need self.transaction_lock but get_tx_height will take self.lock - # so we need to take that too here, to enforce order of locks - with self.lock, self.transaction_lock: - related_txns = self._history_local.get(addr, set()) - for tx_hash in related_txns: - tx_height = self.get_tx_height(tx_hash)[0] - h.append((tx_hash, tx_height)) - return h - - def _add_tx_to_local_history(self, txid): - with self.transaction_lock: - for addr in itertools.chain(self.txi.get(txid, []), self.txo.get(txid, [])): - cur_hist = self._history_local.get(addr, set()) - cur_hist.add(txid) - self._history_local[addr] = cur_hist - - def _remove_tx_from_local_history(self, txid): - with self.transaction_lock: - for addr in itertools.chain(self.txi.get(txid, []), self.txo.get(txid, [])): - cur_hist = self._history_local.get(addr, set()) - try: - cur_hist.remove(txid) - except KeyError: - pass - else: - self._history_local[addr] = cur_hist - - def get_txin_address(self, txi): - addr = txi.get('address') - if addr and addr != "(pubkey)": - return addr - prevout_hash = txi.get('prevout_hash') - prevout_n = txi.get('prevout_n') - dd = self.txo.get(prevout_hash, {}) - for addr, l in dd.items(): - for n, v, is_cb in l: - if n == prevout_n: - return addr - return None - - def get_txout_address(self, txo): - _type, x, v = txo - if _type == TYPE_ADDRESS: - addr = x - elif _type == TYPE_PUBKEY: - addr = bitcoin.public_key_to_p2pkh(bfh(x)) - else: - addr = None - return addr - - def get_conflicting_transactions(self, tx): - """Returns a set of transaction hashes from the wallet history that are - directly conflicting with tx, i.e. they have common outpoints being - spent with tx. If the tx is already in wallet history, that will not be - reported as a conflict. - """ - conflicting_txns = set() - with self.transaction_lock: - for txin in tx.inputs(): - if txin['type'] == 'coinbase': - continue - prevout_hash = txin['prevout_hash'] - prevout_n = txin['prevout_n'] - spending_tx_hash = self.spent_outpoints[prevout_hash].get(prevout_n) - if spending_tx_hash is None: - continue - # this outpoint has already been spent, by spending_tx - assert spending_tx_hash in self.transactions - conflicting_txns |= {spending_tx_hash} - txid = tx.txid() - if txid in conflicting_txns: - # this tx is already in history, so it conflicts with itself - if len(conflicting_txns) > 1: - raise Exception('Found conflicting transactions already in wallet history.') - conflicting_txns -= {txid} - return conflicting_txns - - def add_transaction(self, tx_hash, tx, allow_unrelated=False): - assert tx_hash, tx_hash - assert tx, tx - assert tx.is_complete() - # we need self.transaction_lock but get_tx_height will take self.lock - # so we need to take that too here, to enforce order of locks - with self.lock, self.transaction_lock: - # NOTE: returning if tx in self.transactions might seem like a good idea - # BUT we track is_mine inputs in a txn, and during subsequent calls - # of add_transaction tx, we might learn of more-and-more inputs of - # being is_mine, as we roll the gap_limit forward - is_coinbase = tx.inputs()[0]['type'] == 'coinbase' - tx_height = self.get_tx_height(tx_hash)[0] - if not allow_unrelated: - # note that during sync, if the transactions are not properly sorted, - # it could happen that we think tx is unrelated but actually one of the inputs is is_mine. - # this is the main motivation for allow_unrelated - is_mine = any([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()]) - is_for_me = any([self.is_mine(self.get_txout_address(txo)) for txo in tx.outputs()]) - if not is_mine and not is_for_me: - raise UnrelatedTransactionException() - # Find all conflicting transactions. - # In case of a conflict, - # 1. confirmed > mempool > local - # 2. this new txn has priority over existing ones - # When this method exits, there must NOT be any conflict, so - # either keep this txn and remove all conflicting (along with dependencies) - # or drop this txn - conflicting_txns = self.get_conflicting_transactions(tx) - if conflicting_txns: - existing_mempool_txn = any( - self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) - for tx_hash2 in conflicting_txns) - existing_confirmed_txn = any( - self.get_tx_height(tx_hash2)[0] > 0 - for tx_hash2 in conflicting_txns) - if existing_confirmed_txn and tx_height <= 0: - # this is a non-confirmed tx that conflicts with confirmed txns; drop. - return False - if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL: - # this is a local tx that conflicts with non-local txns; drop. - return False - # keep this txn and remove all conflicting - to_remove = set() - to_remove |= conflicting_txns - for conflicting_tx_hash in conflicting_txns: - to_remove |= self.get_depending_transactions(conflicting_tx_hash) - for tx_hash2 in to_remove: - self.remove_transaction(tx_hash2) - # add inputs - def add_value_from_prev_output(): - dd = self.txo.get(prevout_hash, {}) - # note: this nested loop takes linear time in num is_mine outputs of prev_tx - for addr, outputs in dd.items(): - # note: instead of [(n, v, is_cb), ...]; we could store: {n -> (v, is_cb)} - for n, v, is_cb in outputs: - if n == prevout_n: - if addr and self.is_mine(addr): - if d.get(addr) is None: - d[addr] = set() - d[addr].add((ser, v)) - return - self.txi[tx_hash] = d = {} - for txi in tx.inputs(): - if txi['type'] == 'coinbase': - continue - prevout_hash = txi['prevout_hash'] - prevout_n = txi['prevout_n'] - ser = prevout_hash + ':%d' % prevout_n - self.spent_outpoints[prevout_hash][prevout_n] = tx_hash - add_value_from_prev_output() - # add outputs - self.txo[tx_hash] = d = {} - for n, txo in enumerate(tx.outputs()): - v = txo[2] - ser = tx_hash + ':%d'%n - addr = self.get_txout_address(txo) - if addr and self.is_mine(addr): - if d.get(addr) is None: - d[addr] = [] - d[addr].append((n, v, is_coinbase)) - # give v to txi that spends me - next_tx = self.spent_outpoints[tx_hash].get(n) - if next_tx is not None: - dd = self.txi.get(next_tx, {}) - if dd.get(addr) is None: - dd[addr] = set() - if (ser, v) not in dd[addr]: - dd[addr].add((ser, v)) - self._add_tx_to_local_history(next_tx) - # add to local history - self._add_tx_to_local_history(tx_hash) - # save - self.transactions[tx_hash] = tx - return True - - def remove_transaction(self, tx_hash): - def remove_from_spent_outpoints(): - # undo spends in spent_outpoints - if tx is not None: # if we have the tx, this branch is faster - for txin in tx.inputs(): - if txin['type'] == 'coinbase': - continue - prevout_hash = txin['prevout_hash'] - prevout_n = txin['prevout_n'] - self.spent_outpoints[prevout_hash].pop(prevout_n, None) - if not self.spent_outpoints[prevout_hash]: - self.spent_outpoints.pop(prevout_hash) - else: # expensive but always works - for prevout_hash, d in list(self.spent_outpoints.items()): - for prevout_n, spending_txid in d.items(): - if spending_txid == tx_hash: - self.spent_outpoints[prevout_hash].pop(prevout_n, None) - if not self.spent_outpoints[prevout_hash]: - self.spent_outpoints.pop(prevout_hash) - # Remove this tx itself; if nothing spends from it. - # It is not so clear what to do if other txns spend from it, but it will be - # removed when those other txns are removed. - if not self.spent_outpoints[tx_hash]: - self.spent_outpoints.pop(tx_hash) - - with self.transaction_lock: - self.print_error("removing tx from history", tx_hash) - tx = self.transactions.pop(tx_hash, None) - remove_from_spent_outpoints() - self._remove_tx_from_local_history(tx_hash) - self.txi.pop(tx_hash, None) - self.txo.pop(tx_hash, None) - - def receive_tx_callback(self, tx_hash, tx, tx_height): - self.add_unverified_tx(tx_hash, tx_height) - self.add_transaction(tx_hash, tx, allow_unrelated=True) - - def receive_history_callback(self, addr, hist, tx_fees): - with self.lock: - old_hist = self.get_address_history(addr) - for tx_hash, height in old_hist: - if (tx_hash, height) not in hist: - # make tx local - self.unverified_tx.pop(tx_hash, None) - self.verified_tx.pop(tx_hash, None) - if self.verifier: - self.verifier.remove_spv_proof_for_tx(tx_hash) - self.history[addr] = hist - - for tx_hash, tx_height in hist: - # add it in case it was previously unconfirmed - self.add_unverified_tx(tx_hash, tx_height) - # if addr is new, we have to recompute txi and txo - tx = self.transactions.get(tx_hash) - if tx is None: - continue - self.add_transaction(tx_hash, tx, allow_unrelated=True) - - # Store fees - self.tx_fees.update(tx_fees) - - def get_history(self, domain=None): - # get domain - if domain is None: - domain = self.get_addresses() - domain = set(domain) - # 1. Get the history of each address in the domain, maintain the - # delta of a tx as the sum of its deltas on domain addresses - tx_deltas = defaultdict(int) - for addr in domain: - h = self.get_address_history(addr) - for tx_hash, height in h: - delta = self.get_tx_delta(tx_hash, addr) - if delta is None or tx_deltas[tx_hash] is None: - tx_deltas[tx_hash] = None - else: - tx_deltas[tx_hash] += delta - - # 2. create sorted history - history = [] - for tx_hash in tx_deltas: - delta = tx_deltas[tx_hash] - height, conf, timestamp = self.get_tx_height(tx_hash) - history.append((tx_hash, height, conf, timestamp, delta)) - history.sort(key = lambda x: self.get_txpos(x[0])) - history.reverse() - - # 3. add balance - c, u, x = self.get_balance(domain) - balance = c + u + x - h2 = [] - for tx_hash, height, conf, timestamp, delta in history: - h2.append((tx_hash, height, conf, timestamp, delta, balance)) - if balance is None or delta is None: - balance = None - else: - balance -= delta - h2.reverse() - - # fixme: this may happen if history is incomplete - if balance not in [None, 0]: - self.print_error("Error: history not synchronized") - return [] - - return h2 - - def balance_at_timestamp(self, domain, target_timestamp): - h = self.get_history(domain) - for tx_hash, height, conf, timestamp, value, balance in h: - if timestamp > target_timestamp: - return balance - value - # return last balance - return balance - - @profiler - def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False): - from .util import timestamp_to_datetime, Satoshis, Fiat - out = [] - income = 0 - expenditures = 0 - capital_gains = Decimal(0) - fiat_income = Decimal(0) - fiat_expenditures = Decimal(0) - h = self.get_history(domain) - for tx_hash, height, conf, timestamp, value, balance in h: - if from_timestamp and (timestamp or time.time()) < from_timestamp: - continue - if to_timestamp and (timestamp or time.time()) >= to_timestamp: - continue - item = { - 'txid':tx_hash, - 'height':height, - 'confirmations':conf, - 'timestamp':timestamp, - 'value': Satoshis(value), - 'balance': Satoshis(balance) - } - item['date'] = timestamp_to_datetime(timestamp) - item['label'] = self.get_label(tx_hash) - if show_addresses: - tx = self.transactions.get(tx_hash) - item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs())) - item['outputs'] = list(map(lambda x:{'address':x[0], 'value':Satoshis(x[1])}, tx.get_outputs())) - # value may be None if wallet is not fully synchronized - if value is None: - continue - # fixme: use in and out values - if value < 0: - expenditures += -value - else: - income += value - # fiat computations - if fx and fx.is_enabled(): - date = timestamp_to_datetime(timestamp) - fiat_value = self.get_fiat_value(tx_hash, fx.ccy) - fiat_default = fiat_value is None - fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) - item['fiat_value'] = Fiat(fiat_value, fx.ccy) - item['fiat_default'] = fiat_default - if value < 0: - acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy) - liquidation_price = - fiat_value - item['acquisition_price'] = Fiat(acquisition_price, fx.ccy) - cg = liquidation_price - acquisition_price - item['capital_gain'] = Fiat(cg, fx.ccy) - capital_gains += cg - fiat_expenditures += -fiat_value - else: - fiat_income += fiat_value - out.append(item) - # add summary - if out: - b, v = out[0]['balance'].value, out[0]['value'].value - start_balance = None if b is None or v is None else b - v - end_balance = out[-1]['balance'].value - if from_timestamp is not None and to_timestamp is not None: - start_date = timestamp_to_datetime(from_timestamp) - end_date = timestamp_to_datetime(to_timestamp) - else: - start_date = None - end_date = None - summary = { - 'start_date': start_date, - 'end_date': end_date, - 'start_balance': Satoshis(start_balance), - 'end_balance': Satoshis(end_balance), - 'income': Satoshis(income), - 'expenditures': Satoshis(expenditures) - } - if fx and fx.is_enabled(): - unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy) - summary['capital_gains'] = Fiat(capital_gains, fx.ccy) - summary['fiat_income'] = Fiat(fiat_income, fx.ccy) - summary['fiat_expenditures'] = Fiat(fiat_expenditures, fx.ccy) - summary['unrealized_gains'] = Fiat(unrealized, fx.ccy) - summary['start_fiat_balance'] = Fiat(fx.historical_value(start_balance, start_date), fx.ccy) - summary['end_fiat_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy) - summary['start_fiat_value'] = Fiat(fx.historical_value(COIN, start_date), fx.ccy) - summary['end_fiat_value'] = Fiat(fx.historical_value(COIN, end_date), fx.ccy) - else: - summary = {} - return { - 'transactions': out, - 'summary': summary - } - - def get_label(self, tx_hash): - label = self.labels.get(tx_hash, '') - if label is '': - label = self.get_default_label(tx_hash) - return label - - def get_default_label(self, tx_hash): - if self.txi.get(tx_hash) == {}: - d = self.txo.get(tx_hash, {}) - labels = [] - for addr in d.keys(): - label = self.labels.get(addr) - if label: - labels.append(label) - return ', '.join(labels) - return '' - - def get_tx_status(self, tx_hash, height, conf, timestamp): - from .util import format_time - extra = [] - if conf == 0: - tx = self.transactions.get(tx_hash) - if not tx: - return 2, 'unknown' - is_final = tx and tx.is_final() - if not is_final: - extra.append('rbf') - fee = self.get_wallet_delta(tx)[3] - if fee is None: - fee = self.tx_fees.get(tx_hash) - if fee is not None: - size = tx.estimated_size() - fee_per_byte = fee / size - extra.append(format_fee_satoshis(fee_per_byte) + ' sat/b') - if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \ - and self.network and self.network.config.has_fee_mempool(): - exp_n = self.network.config.fee_to_depth(fee_per_byte) - if exp_n: - extra.append('%.2f MB'%(exp_n/1000000)) - if height == TX_HEIGHT_LOCAL: - status = 3 - elif height == TX_HEIGHT_UNCONF_PARENT: - status = 1 - elif height == TX_HEIGHT_UNCONFIRMED: - status = 0 - else: - status = 2 - else: - status = 3 + min(conf, 6) - time_str = format_time(timestamp) if timestamp else _("unknown") - status_str = TX_STATUS[status] if status < 4 else time_str - if extra: - status_str += ' [%s]'%(', '.join(extra)) - return status, status_str - - def relayfee(self): - return relayfee(self.network) - - def dust_threshold(self): - return dust_threshold(self.network) - - def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, - change_addr=None, is_sweep=False): - # check outputs - i_max = None - for i, o in enumerate(outputs): - _type, data, value = o - if _type == TYPE_ADDRESS: - if not is_address(data): - raise Exception("Invalid bitcoin address: {}".format(data)) - if value == '!': - if i_max is not None: - raise Exception("More than one output set to spend max") - i_max = i - - # Avoid index-out-of-range with inputs[0] below - if not inputs: - raise NotEnoughFunds() - - if fixed_fee is None and config.fee_per_kb() is None: - raise NoDynamicFeeEstimates() - - for item in inputs: - self.add_input_info(item) - - # change address - if change_addr: - change_addrs = [change_addr] - else: - addrs = self.get_change_addresses()[-self.gap_limit_for_change:] - if self.use_change and addrs: - # New change addresses are created only after a few - # confirmations. Select the unused addresses within the - # gap limit; if none take one at random - change_addrs = [addr for addr in addrs if - self.get_num_tx(addr) == 0] - if not change_addrs: - change_addrs = [random.choice(addrs)] - else: - # coin_chooser will set change address - change_addrs = [] - - # Fee estimator - if fixed_fee is None: - fee_estimator = config.estimate_fee - elif isinstance(fixed_fee, Number): - fee_estimator = lambda size: fixed_fee - elif callable(fixed_fee): - fee_estimator = fixed_fee - else: - raise Exception('Invalid argument fixed_fee: %s' % fixed_fee) - - if i_max is None: - # Let the coin chooser select the coins to spend - max_change = self.max_change_outputs if self.multiple_change else 1 - coin_chooser = coinchooser.get_coin_chooser(config) - tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change], - fee_estimator, self.dust_threshold()) - else: - # FIXME?? this might spend inputs with negative effective value... - sendable = sum(map(lambda x:x['value'], inputs)) - _type, data, value = outputs[i_max] - outputs[i_max] = (_type, data, 0) - tx = Transaction.from_io(inputs, outputs[:]) - fee = fee_estimator(tx.estimated_size()) - amount = sendable - tx.output_value() - fee - if amount < 0: - raise NotEnoughFunds() - outputs[i_max] = (_type, data, amount) - tx = Transaction.from_io(inputs, outputs[:]) - - # Sort the inputs and outputs deterministically - tx.BIP_LI01_sort() - # Timelock tx to current height. - tx.locktime = self.get_local_height() - run_hook('make_unsigned_transaction', self, tx) - return tx - - def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None): - coins = self.get_spendable_coins(domain, config) - tx = self.make_unsigned_transaction(coins, outputs, config, fee, change_addr) - self.sign_transaction(tx, password) - return tx - - def is_frozen(self, addr): - return addr in self.frozen_addresses - - def set_frozen_state(self, addrs, freeze): - '''Set frozen state of the addresses to FREEZE, True or False''' - if all(self.is_mine(addr) for addr in addrs): - if freeze: - self.frozen_addresses |= set(addrs) - else: - self.frozen_addresses -= set(addrs) - self.storage.put('frozen_addresses', list(self.frozen_addresses)) - return True - return False - - def load_unverified_transactions(self): - # review transactions that are in the history - for addr, hist in self.history.items(): - for tx_hash, tx_height in hist: - # add it in case it was previously unconfirmed - self.add_unverified_tx(tx_hash, tx_height) - - def start_threads(self, network): - self.network = network - if self.network is not None: - self.verifier = SPV(self.network, self) - self.synchronizer = Synchronizer(self, network) - network.add_jobs([self.verifier, self.synchronizer]) - else: - self.verifier = None - self.synchronizer = None - - def stop_threads(self): - if self.network: - self.network.remove_jobs([self.synchronizer, self.verifier]) - self.synchronizer.release() - self.synchronizer = None - self.verifier = None - # Now no references to the synchronizer or verifier - # remain so they will be GC-ed - self.storage.put('stored_height', self.get_local_height()) - self.save_transactions() - self.save_verified_tx() - self.storage.write() - - def wait_until_synchronized(self, callback=None): - def wait_for_wallet(): - self.set_up_to_date(False) - while not self.is_up_to_date(): - if callback: - msg = "%s\n%s %d"%( - _("Please wait..."), - _("Addresses generated:"), - len(self.addresses(True))) - callback(msg) - time.sleep(0.1) - def wait_for_network(): - while not self.network.is_connected(): - if callback: - msg = "%s \n" % (_("Connecting...")) - callback(msg) - time.sleep(0.1) - # wait until we are connected, because the user - # might have selected another server - if self.network: - wait_for_network() - wait_for_wallet() - else: - self.synchronize() - - def can_export(self): - return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key') - - def is_used(self, address): - h = self.history.get(address,[]) - if len(h) == 0: - return False - c, u, x = self.get_addr_balance(address) - return c + u + x == 0 - - def is_empty(self, address): - c, u, x = self.get_addr_balance(address) - return c+u+x == 0 - - def address_is_old(self, address, age_limit=2): - age = -1 - h = self.history.get(address, []) - for tx_hash, tx_height in h: - if tx_height <= 0: - tx_age = 0 - else: - tx_age = self.get_local_height() - tx_height + 1 - if tx_age > age: - age = tx_age - return age > age_limit - - def bump_fee(self, tx, delta): - if tx.is_final(): - raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final')) - tx = Transaction(tx.serialize()) - tx.deserialize(force_full_parse=True) # need to parse inputs - inputs = copy.deepcopy(tx.inputs()) - outputs = copy.deepcopy(tx.outputs()) - for txin in inputs: - txin['signatures'] = [None] * len(txin['signatures']) - self.add_input_info(txin) - # use own outputs - s = list(filter(lambda x: self.is_mine(x[1]), outputs)) - # ... unless there is none - if not s: - s = outputs - x_fee = run_hook('get_tx_extra_fee', self, tx) - if x_fee: - x_fee_address, x_fee_amount = x_fee - s = filter(lambda x: x[1]!=x_fee_address, s) - - # prioritize low value outputs, to get rid of dust - s = sorted(s, key=lambda x: x[2]) - for o in s: - i = outputs.index(o) - otype, address, value = o - if value - delta >= self.dust_threshold(): - outputs[i] = otype, address, value - delta - delta = 0 - break - else: - del outputs[i] - delta -= value - if delta > 0: - continue - if delta > 0: - raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) - locktime = self.get_local_height() - tx_new = Transaction.from_io(inputs, outputs, locktime=locktime) - tx_new.BIP_LI01_sort() - return tx_new - - def cpfp(self, tx, fee): - txid = tx.txid() - for i, o in enumerate(tx.outputs()): - otype, address, value = o - if otype == TYPE_ADDRESS and self.is_mine(address): - break - else: - return - coins = self.get_addr_utxo(address) - item = coins.get(txid+':%d'%i) - if not item: - return - self.add_input_info(item) - inputs = [item] - outputs = [(TYPE_ADDRESS, address, value - fee)] - locktime = self.get_local_height() - # note: no need to call tx.BIP_LI01_sort() here - single input/output - return Transaction.from_io(inputs, outputs, locktime=locktime) - - def add_input_sig_info(self, txin, address): - raise NotImplementedError() # implemented by subclasses - - def add_input_info(self, txin): - address = txin['address'] - if self.is_mine(address): - txin['type'] = self.get_txin_type(address) - # segwit needs value to sign - if txin.get('value') is None and Transaction.is_input_value_needed(txin): - received, spent = self.get_addr_io(address) - item = received.get(txin['prevout_hash']+':%d'%txin['prevout_n']) - tx_height, value, is_cb = item - txin['value'] = value - self.add_input_sig_info(txin, address) - - def add_input_info_to_all_inputs(self, tx): - if tx.is_complete(): - return - for txin in tx.inputs(): - self.add_input_info(txin) - - def can_sign(self, tx): - if tx.is_complete(): - return False - # add info to inputs if we can; otherwise we might return a false negative: - self.add_input_info_to_all_inputs(tx) # though note that this is a side-effect - for k in self.get_keystores(): - if k.can_sign(tx): - return True - return False - - def get_input_tx(self, tx_hash, ignore_timeout=False): - # First look up an input transaction in the wallet where it - # will likely be. If co-signing a transaction it may not have - # all the input txs, in which case we ask the network. - tx = self.transactions.get(tx_hash, None) - if not tx and self.network: - try: - tx = Transaction(self.network.get_transaction(tx_hash)) - except TimeoutException as e: - self.print_error('getting input txn from network timed out for {}'.format(tx_hash)) - if not ignore_timeout: - raise e - return tx - - def add_hw_info(self, tx): - # add previous tx for hw wallets - for txin in tx.inputs(): - tx_hash = txin['prevout_hash'] - # segwit inputs might not be needed for some hw wallets - ignore_timeout = Transaction.is_segwit_input(txin) - txin['prev_tx'] = self.get_input_tx(tx_hash, ignore_timeout) - # add output info for hw wallets - info = {} - xpubs = self.get_master_public_keys() - for txout in tx.outputs(): - _type, addr, amount = txout - if self.is_mine(addr): - index = self.get_address_index(addr) - pubkeys = self.get_public_keys(addr) - # sort xpubs using the order of pubkeys - sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) - info[addr] = index, sorted_xpubs, self.m if isinstance(self, Multisig_Wallet) else None - tx.output_info = info - - def sign_transaction(self, tx, password): - if self.is_watching_only(): - return - self.add_input_info_to_all_inputs(tx) - # hardware wallets require extra info - if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]): - self.add_hw_info(tx) - # sign. start with ready keystores. - for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True): - try: - if k.can_sign(tx): - k.sign_transaction(tx, password) - except UserCancelled: - continue - return tx - - def get_unused_addresses(self): - # fixme: use slots from expired requests - domain = self.get_receiving_addresses() - return [addr for addr in domain if not self.history.get(addr) - and addr not in self.receive_requests.keys()] - - def get_unused_address(self): - addrs = self.get_unused_addresses() - if addrs: - return addrs[0] - - def get_receiving_address(self): - # always return an address - domain = self.get_receiving_addresses() - if not domain: - return - choice = domain[0] - for addr in domain: - if not self.history.get(addr): - if addr not in self.receive_requests.keys(): - return addr - else: - choice = addr - return choice - - def get_payment_status(self, address, amount): - local_height = self.get_local_height() - received, sent = self.get_addr_io(address) - l = [] - for txo, x in received.items(): - h, v, is_cb = x - txid, n = txo.split(':') - info = self.verified_tx.get(txid) - if info: - tx_height, timestamp, pos = info - conf = local_height - tx_height - else: - conf = 0 - l.append((conf, v)) - vsum = 0 - for conf, v in reversed(sorted(l)): - vsum += v - if vsum >= amount: - return True, conf - return False, None - - def get_payment_request(self, addr, config): - r = self.receive_requests.get(addr) - if not r: - return - out = copy.copy(r) - out['URI'] = 'bitcoin:' + addr + '?amount=' + format_satoshis(out.get('amount')) - status, conf = self.get_request_status(addr) - out['status'] = status - if conf is not None: - out['confirmations'] = conf - # check if bip70 file exists - rdir = config.get('requests_dir') - if rdir: - key = out.get('id', addr) - path = os.path.join(rdir, 'req', key[0], key[1], key) - if os.path.exists(path): - baseurl = 'file://' + rdir - rewrite = config.get('url_rewrite') - if rewrite: - try: - baseurl = baseurl.replace(*rewrite) - except BaseException as e: - self.print_stderr('Invalid config setting for "url_rewrite". err:', e) - out['request_url'] = os.path.join(baseurl, 'req', key[0], key[1], key, key) - out['URI'] += '&r=' + out['request_url'] - out['index_url'] = os.path.join(baseurl, 'index.html') + '?id=' + key - websocket_server_announce = config.get('websocket_server_announce') - if websocket_server_announce: - out['websocket_server'] = websocket_server_announce - else: - out['websocket_server'] = config.get('websocket_server', 'localhost') - websocket_port_announce = config.get('websocket_port_announce') - if websocket_port_announce: - out['websocket_port'] = websocket_port_announce - else: - out['websocket_port'] = config.get('websocket_port', 9999) - return out - - def get_request_status(self, key): - r = self.receive_requests.get(key) - if r is None: - return PR_UNKNOWN - address = r['address'] - amount = r.get('amount') - timestamp = r.get('time', 0) - if timestamp and type(timestamp) != int: - timestamp = 0 - expiration = r.get('exp') - if expiration and type(expiration) != int: - expiration = 0 - conf = None - if amount: - if self.up_to_date: - paid, conf = self.get_payment_status(address, amount) - status = PR_PAID if paid else PR_UNPAID - if status == PR_UNPAID and expiration is not None and time.time() > timestamp + expiration: - status = PR_EXPIRED - else: - status = PR_UNKNOWN - else: - status = PR_UNKNOWN - return status, conf - - def make_payment_request(self, addr, amount, message, expiration): - timestamp = int(time.time()) - _id = bh2u(Hash(addr + "%d"%timestamp))[0:10] - r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id} - return r - - def sign_payment_request(self, key, alias, alias_addr, password): - req = self.receive_requests.get(key) - alias_privkey = self.export_private_key(alias_addr, password)[0] - pr = paymentrequest.make_unsigned_request(req) - paymentrequest.sign_request_with_alias(pr, alias, alias_privkey) - req['name'] = pr.pki_data - req['sig'] = bh2u(pr.signature) - self.receive_requests[key] = req - self.storage.put('payment_requests', self.receive_requests) - - def add_payment_request(self, req, config): - addr = req['address'] - if not bitcoin.is_address(addr): - raise Exception(_('Invalid Bitcoin address.')) - if not self.is_mine(addr): - raise Exception(_('Address not in wallet.')) - - amount = req.get('amount') - message = req.get('memo') - self.receive_requests[addr] = req - self.storage.put('payment_requests', self.receive_requests) - self.set_label(addr, message) # should be a default label - - rdir = config.get('requests_dir') - if rdir and amount is not None: - key = req.get('id', addr) - pr = paymentrequest.make_request(config, req) - path = os.path.join(rdir, 'req', key[0], key[1], key) - if not os.path.exists(path): - try: - os.makedirs(path) - except OSError as exc: - if exc.errno != errno.EEXIST: - raise - with open(os.path.join(path, key), 'wb') as f: - f.write(pr.SerializeToString()) - # reload - req = self.get_payment_request(addr, config) - with open(os.path.join(path, key + '.json'), 'w', encoding='utf-8') as f: - f.write(json.dumps(req)) - return req - - def remove_payment_request(self, addr, config): - if addr not in self.receive_requests: - return False - r = self.receive_requests.pop(addr) - rdir = config.get('requests_dir') - if rdir: - key = r.get('id', addr) - for s in ['.json', '']: - n = os.path.join(rdir, 'req', key[0], key[1], key, key + s) - if os.path.exists(n): - os.unlink(n) - self.storage.put('payment_requests', self.receive_requests) - return True - - def get_sorted_requests(self, config): - def f(addr): - try: - return self.get_address_index(addr) - except: - return - keys = map(lambda x: (f(x), x), self.receive_requests.keys()) - sorted_keys = sorted(filter(lambda x: x[0] is not None, keys)) - return [self.get_payment_request(x[1], config) for x in sorted_keys] - - def get_fingerprint(self): - raise NotImplementedError() - - def can_import_privkey(self): - return False - - def can_import_address(self): - return False - - def can_delete_address(self): - return False - - def add_address(self, address): - if address not in self.history: - self.history[address] = [] - if self.synchronizer: - self.synchronizer.add(address) - - def has_password(self): - return self.has_keystore_encryption() or self.has_storage_encryption() - - def can_have_keystore_encryption(self): - return self.keystore and self.keystore.may_have_password() - - def get_available_storage_encryption_version(self): - """Returns the type of storage encryption offered to the user. - - A wallet file (storage) is either encrypted with this version - or is stored in plaintext. - """ - if isinstance(self.keystore, Hardware_KeyStore): - return STO_EV_XPUB_PW - else: - return STO_EV_USER_PW - - def has_keystore_encryption(self): - """Returns whether encryption is enabled for the keystore. - - If True, e.g. signing a transaction will require a password. - """ - if self.can_have_keystore_encryption(): - return self.storage.get('use_encryption', False) - return False - - def has_storage_encryption(self): - """Returns whether encryption is enabled for the wallet file on disk.""" - return self.storage.is_encrypted() - - @classmethod - def may_have_password(cls): - return True - - def check_password(self, password): - if self.has_keystore_encryption(): - self.keystore.check_password(password) - self.storage.check_password(password) - - def update_password(self, old_pw, new_pw, encrypt_storage=False): - if old_pw is None and self.has_password(): - raise InvalidPassword() - self.check_password(old_pw) - - if encrypt_storage: - enc_version = self.get_available_storage_encryption_version() - else: - enc_version = STO_EV_PLAINTEXT - self.storage.set_password(new_pw, enc_version) - - # note: Encrypting storage with a hw device is currently only - # allowed for non-multisig wallets. Further, - # Hardware_KeyStore.may_have_password() == False. - # If these were not the case, - # extra care would need to be taken when encrypting keystores. - self._update_password_for_keystore(old_pw, new_pw) - encrypt_keystore = self.can_have_keystore_encryption() - self.storage.set_keystore_encryption(bool(new_pw) and encrypt_keystore) - - self.storage.write() - - def sign_message(self, address, message, password): - index = self.get_address_index(address) - return self.keystore.sign_message(index, message, password) - - def decrypt_message(self, pubkey, message, password): - addr = self.pubkeys_to_address(pubkey) - index = self.get_address_index(addr) - return self.keystore.decrypt_message(index, message, password) - - def get_depending_transactions(self, tx_hash): - """Returns all (grand-)children of tx_hash in this wallet.""" - children = set() - # TODO rewrite this to use self.spent_outpoints - for other_hash, tx in self.transactions.items(): - for input in (tx.inputs()): - if input["prevout_hash"] == tx_hash: - children.add(other_hash) - children |= self.get_depending_transactions(other_hash) - return children - - def txin_value(self, txin): - txid = txin['prevout_hash'] - prev_n = txin['prevout_n'] - for address, d in self.txo.get(txid, {}).items(): - for n, v, cb in d: - if n == prev_n: - return v - # may occur if wallet is not synchronized - return None - - def price_at_timestamp(self, txid, price_func): - """Returns fiat price of bitcoin at the time tx got confirmed.""" - height, conf, timestamp = self.get_tx_height(txid) - return price_func(timestamp if timestamp else time.time()) - - def unrealized_gains(self, domain, price_func, ccy): - coins = self.get_utxos(domain) - now = time.time() - p = price_func(now) - ap = sum(self.coin_price(coin['prevout_hash'], price_func, ccy, self.txin_value(coin)) for coin in coins) - lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN) - return lp - ap - - def average_price(self, txid, price_func, ccy): - """ Average acquisition price of the inputs of a transaction """ - input_value = 0 - total_price = 0 - for addr, d in self.txi.get(txid, {}).items(): - for ser, v in d: - input_value += v - total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v) - return total_price / (input_value/Decimal(COIN)) - - def coin_price(self, txid, price_func, ccy, txin_value): - """ - Acquisition price of a coin. - This assumes that either all inputs are mine, or no input is mine. - """ - if txin_value is None: - return Decimal('NaN') - cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value)) - result = self.coin_price_cache.get(cache_key, None) - if result is not None: - return result - if self.txi.get(txid, {}) != {}: - result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN) - self.coin_price_cache[cache_key] = result - return result - else: - fiat_value = self.get_fiat_value(txid, ccy) - if fiat_value is not None: - return fiat_value - else: - p = self.price_at_timestamp(txid, price_func) - return p * txin_value/Decimal(COIN) - - def is_billing_address(self, addr): - # overloaded for TrustedCoin wallets - return False - - -class Simple_Wallet(Abstract_Wallet): - # wallet with a single keystore - - def get_keystore(self): - return self.keystore - - def get_keystores(self): - return [self.keystore] - - def is_watching_only(self): - return self.keystore.is_watching_only() - - def _update_password_for_keystore(self, old_pw, new_pw): - if self.keystore and self.keystore.may_have_password(): - self.keystore.update_password(old_pw, new_pw) - self.save_keystore() - - def save_keystore(self): - self.storage.put('keystore', self.keystore.dump()) - - -class Imported_Wallet(Simple_Wallet): - # wallet made of imported addresses - - wallet_type = 'imported' - txin_type = 'address' - - def __init__(self, storage): - Abstract_Wallet.__init__(self, storage) - - def is_watching_only(self): - return self.keystore is None - - def get_keystores(self): - return [self.keystore] if self.keystore else [] - - def can_import_privkey(self): - return bool(self.keystore) - - def load_keystore(self): - self.keystore = load_keystore(self.storage, 'keystore') if self.storage.get('keystore') else None - - def save_keystore(self): - self.storage.put('keystore', self.keystore.dump()) - - def load_addresses(self): - self.addresses = self.storage.get('addresses', {}) - # fixme: a reference to addresses is needed - if self.keystore: - self.keystore.addresses = self.addresses - - def save_addresses(self): - self.storage.put('addresses', self.addresses) - - def can_import_address(self): - return self.is_watching_only() - - def can_delete_address(self): - return True - - def has_seed(self): - return False - - def is_deterministic(self): - return False - - def is_change(self, address): - return False - - def get_master_public_keys(self): - return [] - - def is_beyond_limit(self, address): - return False - - def is_mine(self, address): - return address in self.addresses - - def get_fingerprint(self): - return '' - - def get_addresses(self, include_change=False): - return sorted(self.addresses.keys()) - - def get_receiving_addresses(self): - return self.get_addresses() - - def get_change_addresses(self): - return [] - - def import_address(self, address): - if not bitcoin.is_address(address): - return '' - if address in self.addresses: - return '' - self.addresses[address] = {} - self.storage.put('addresses', self.addresses) - self.storage.write() - self.add_address(address) - return address - - def delete_address(self, address): - if address not in self.addresses: - return - - transactions_to_remove = set() # only referred to by this address - transactions_new = set() # txs that are not only referred to by address - with self.lock: - for addr, details in self.history.items(): - if addr == address: - for tx_hash, height in details: - transactions_to_remove.add(tx_hash) - else: - for tx_hash, height in details: - transactions_new.add(tx_hash) - transactions_to_remove -= transactions_new - self.history.pop(address, None) - - for tx_hash in transactions_to_remove: - self.remove_transaction(tx_hash) - self.tx_fees.pop(tx_hash, None) - self.verified_tx.pop(tx_hash, None) - self.unverified_tx.pop(tx_hash, None) - self.transactions.pop(tx_hash, None) - self.storage.put('verified_tx3', self.verified_tx) - self.save_transactions() - - self.set_label(address, None) - self.remove_payment_request(address, {}) - self.set_frozen_state([address], False) - - pubkey = self.get_public_key(address) - self.addresses.pop(address) - if pubkey: - # delete key iff no other address uses it (e.g. p2pkh and p2wpkh for same key) - for txin_type in bitcoin.WIF_SCRIPT_TYPES.keys(): - try: - addr2 = bitcoin.pubkey_to_address(txin_type, pubkey) - except NotImplementedError: - pass - else: - if addr2 in self.addresses: - break - else: - self.keystore.delete_imported_key(pubkey) - self.save_keystore() - self.storage.put('addresses', self.addresses) - - self.storage.write() - - def get_address_index(self, address): - return self.get_public_key(address) - - def get_public_key(self, address): - return self.addresses[address].get('pubkey') - - def import_private_key(self, sec, pw, redeem_script=None): - try: - txin_type, pubkey = self.keystore.import_privkey(sec, pw) - except Exception: - neutered_privkey = str(sec)[:3] + '..' + str(sec)[-2:] - raise BitcoinException('Invalid private key: {}'.format(neutered_privkey)) - if txin_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: - if redeem_script is not None: - raise BitcoinException('Cannot use redeem script with script type {}'.format(txin_type)) - addr = bitcoin.pubkey_to_address(txin_type, pubkey) - elif txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: - if redeem_script is None: - raise BitcoinException('Redeem script required for script type {}'.format(txin_type)) - addr = bitcoin.redeem_script_to_address(txin_type, redeem_script) - else: - raise NotImplementedError(txin_type) - self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':redeem_script} - self.save_keystore() - self.save_addresses() - self.storage.write() - self.add_address(addr) - return addr - - def get_redeem_script(self, address): - d = self.addresses[address] - redeem_script = d['redeem_script'] - return redeem_script - - def get_txin_type(self, address): - return self.addresses[address].get('type', 'address') - - def add_input_sig_info(self, txin, address): - if self.is_watching_only(): - x_pubkey = 'fd' + address_to_script(address) - txin['x_pubkeys'] = [x_pubkey] - txin['signatures'] = [None] - return - if txin['type'] in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: - pubkey = self.addresses[address]['pubkey'] - txin['num_sig'] = 1 - txin['x_pubkeys'] = [pubkey] - txin['signatures'] = [None] - else: - raise NotImplementedError('imported wallets for p2sh are not implemented') - - def pubkeys_to_address(self, pubkey): - for addr, v in self.addresses.items(): - if v.get('pubkey') == pubkey: - return addr - -class Deterministic_Wallet(Abstract_Wallet): - - def __init__(self, storage): - Abstract_Wallet.__init__(self, storage) - self.gap_limit = storage.get('gap_limit', 20) - - def has_seed(self): - return self.keystore.has_seed() - - def get_receiving_addresses(self): - return self.receiving_addresses - - def get_change_addresses(self): - return self.change_addresses - - def get_seed(self, password): - return self.keystore.get_seed(password) - - def add_seed(self, seed, pw): - self.keystore.add_seed(seed, pw) - - def change_gap_limit(self, value): - '''This method is not called in the code, it is kept for console use''' - if value >= self.gap_limit: - self.gap_limit = value - self.storage.put('gap_limit', self.gap_limit) - return True - elif value >= self.min_acceptable_gap(): - addresses = self.get_receiving_addresses() - k = self.num_unused_trailing_addresses(addresses) - n = len(addresses) - k + value - self.receiving_addresses = self.receiving_addresses[0:n] - self.gap_limit = value - self.storage.put('gap_limit', self.gap_limit) - self.save_addresses() - return True - else: - return False - - def num_unused_trailing_addresses(self, addresses): - k = 0 - for a in addresses[::-1]: - if self.history.get(a):break - k = k + 1 - return k - - def min_acceptable_gap(self): - # fixme: this assumes wallet is synchronized - n = 0 - nmax = 0 - addresses = self.get_receiving_addresses() - k = self.num_unused_trailing_addresses(addresses) - for a in addresses[0:-k]: - if self.history.get(a): - n = 0 - else: - n += 1 - if n > nmax: nmax = n - return nmax + 1 - - def load_addresses(self): - super().load_addresses() - self._addr_to_addr_index = {} # key: address, value: (is_change, index) - for i, addr in enumerate(self.receiving_addresses): - self._addr_to_addr_index[addr] = (False, i) - for i, addr in enumerate(self.change_addresses): - self._addr_to_addr_index[addr] = (True, i) - - def create_new_address(self, for_change=False): - assert type(for_change) is bool - with self.lock: - addr_list = self.change_addresses if for_change else self.receiving_addresses - n = len(addr_list) - x = self.derive_pubkeys(for_change, n) - address = self.pubkeys_to_address(x) - addr_list.append(address) - self._addr_to_addr_index[address] = (for_change, n) - self.save_addresses() - self.add_address(address) - return address - - def synchronize_sequence(self, for_change): - limit = self.gap_limit_for_change if for_change else self.gap_limit - while True: - addresses = self.get_change_addresses() if for_change else self.get_receiving_addresses() - if len(addresses) < limit: - self.create_new_address(for_change) - continue - if list(map(lambda a: self.address_is_old(a), addresses[-limit:] )) == limit*[False]: - break - else: - self.create_new_address(for_change) - - def synchronize(self): - with self.lock: - self.synchronize_sequence(False) - self.synchronize_sequence(True) - - def is_beyond_limit(self, address): - is_change, i = self.get_address_index(address) - addr_list = self.get_change_addresses() if is_change else self.get_receiving_addresses() - limit = self.gap_limit_for_change if is_change else self.gap_limit - if i < limit: - return False - prev_addresses = addr_list[max(0, i - limit):max(0, i)] - for addr in prev_addresses: - if self.history.get(addr): - return False - return True - - def is_mine(self, address): - return address in self._addr_to_addr_index - - def get_address_index(self, address): - return self._addr_to_addr_index[address] - - def get_master_public_keys(self): - return [self.get_master_public_key()] - - def get_fingerprint(self): - return self.get_master_public_key() - - def get_txin_type(self, address): - return self.txin_type - - -class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet): - - """ Deterministic Wallet with a single pubkey per address """ - - def __init__(self, storage): - Deterministic_Wallet.__init__(self, storage) - - def get_public_key(self, address): - sequence = self.get_address_index(address) - pubkey = self.get_pubkey(*sequence) - return pubkey - - def load_keystore(self): - self.keystore = load_keystore(self.storage, 'keystore') - try: - xtype = bitcoin.xpub_type(self.keystore.xpub) - except: - xtype = 'standard' - self.txin_type = 'p2pkh' if xtype == 'standard' else xtype - - def get_pubkey(self, c, i): - return self.derive_pubkeys(c, i) - - def add_input_sig_info(self, txin, address): - derivation = self.get_address_index(address) - x_pubkey = self.keystore.get_xpubkey(*derivation) - txin['x_pubkeys'] = [x_pubkey] - txin['signatures'] = [None] - txin['num_sig'] = 1 - - def get_master_public_key(self): - return self.keystore.get_master_public_key() - - def derive_pubkeys(self, c, i): - return self.keystore.derive_pubkey(c, i) - - - - - - -class Standard_Wallet(Simple_Deterministic_Wallet): - wallet_type = 'standard' - - def pubkeys_to_address(self, pubkey): - return bitcoin.pubkey_to_address(self.txin_type, pubkey) - - -class Multisig_Wallet(Deterministic_Wallet): - # generic m of n - gap_limit = 20 - - def __init__(self, storage): - self.wallet_type = storage.get('wallet_type') - self.m, self.n = multisig_type(self.wallet_type) - Deterministic_Wallet.__init__(self, storage) - - def get_pubkeys(self, c, i): - return self.derive_pubkeys(c, i) - - def get_public_keys(self, address): - sequence = self.get_address_index(address) - return self.get_pubkeys(*sequence) - - def pubkeys_to_address(self, pubkeys): - redeem_script = self.pubkeys_to_redeem_script(pubkeys) - return bitcoin.redeem_script_to_address(self.txin_type, redeem_script) - - def pubkeys_to_redeem_script(self, pubkeys): - return transaction.multisig_script(sorted(pubkeys), self.m) - - def get_redeem_script(self, address): - pubkeys = self.get_public_keys(address) - redeem_script = self.pubkeys_to_redeem_script(pubkeys) - return redeem_script - - def derive_pubkeys(self, c, i): - return [k.derive_pubkey(c, i) for k in self.get_keystores()] - - def load_keystore(self): - self.keystores = {} - for i in range(self.n): - name = 'x%d/'%(i+1) - self.keystores[name] = load_keystore(self.storage, name) - self.keystore = self.keystores['x1/'] - xtype = bitcoin.xpub_type(self.keystore.xpub) - self.txin_type = 'p2sh' if xtype == 'standard' else xtype - - def save_keystore(self): - for name, k in self.keystores.items(): - self.storage.put(name, k.dump()) - - def get_keystore(self): - return self.keystores.get('x1/') - - def get_keystores(self): - return [self.keystores[i] for i in sorted(self.keystores.keys())] - - def can_have_keystore_encryption(self): - return any([k.may_have_password() for k in self.get_keystores()]) - - def _update_password_for_keystore(self, old_pw, new_pw): - for name, keystore in self.keystores.items(): - if keystore.may_have_password(): - keystore.update_password(old_pw, new_pw) - self.storage.put(name, keystore.dump()) - - def check_password(self, password): - for name, keystore in self.keystores.items(): - if keystore.may_have_password(): - keystore.check_password(password) - self.storage.check_password(password) - - def get_available_storage_encryption_version(self): - # multisig wallets are not offered hw device encryption - return STO_EV_USER_PW - - def has_seed(self): - return self.keystore.has_seed() - - def is_watching_only(self): - return not any([not k.is_watching_only() for k in self.get_keystores()]) - - def get_master_public_key(self): - return self.keystore.get_master_public_key() - - def get_master_public_keys(self): - return [k.get_master_public_key() for k in self.get_keystores()] - - def get_fingerprint(self): - return ''.join(sorted(self.get_master_public_keys())) - - def add_input_sig_info(self, txin, address): - # x_pubkeys are not sorted here because it would be too slow - # they are sorted in transaction.get_sorted_pubkeys - # pubkeys is set to None to signal that x_pubkeys are unsorted - derivation = self.get_address_index(address) - x_pubkeys_expected = [k.get_xpubkey(*derivation) for k in self.get_keystores()] - x_pubkeys_actual = txin.get('x_pubkeys') - # if 'x_pubkeys' is already set correctly (ignoring order, as above), leave it. - # otherwise we might delete signatures - if x_pubkeys_actual and set(x_pubkeys_actual) == set(x_pubkeys_expected): - return - txin['x_pubkeys'] = x_pubkeys_expected - txin['pubkeys'] = None - # we need n place holders - txin['signatures'] = [None] * self.n - txin['num_sig'] = self.m - - -wallet_types = ['standard', 'multisig', 'imported'] - -def register_wallet_type(category): - wallet_types.append(category) - -wallet_constructors = { - 'standard': Standard_Wallet, - 'old': Standard_Wallet, - 'xpub': Standard_Wallet, - 'imported': Imported_Wallet -} - -def register_constructor(wallet_type, constructor): - wallet_constructors[wallet_type] = constructor - -# former WalletFactory -class Wallet(object): - """The main wallet "entry point". - This class is actually a factory that will return a wallet of the correct - type when passed a WalletStorage instance.""" - - def __new__(self, storage): - wallet_type = storage.get('wallet_type') - WalletClass = Wallet.wallet_class(wallet_type) - wallet = WalletClass(storage) - # Convert hardware wallets restored with older versions of - # Electrum to BIP44 wallets. A hardware wallet does not have - # a seed and plugins do not need to handle having one. - rwc = getattr(wallet, 'restore_wallet_class', None) - if rwc and storage.get('seed', ''): - storage.print_error("converting wallet type to " + rwc.wallet_type) - storage.put('wallet_type', rwc.wallet_type) - wallet = rwc(storage) - return wallet - - @staticmethod - def wallet_class(wallet_type): - if multisig_type(wallet_type): - return Multisig_Wallet - if wallet_type in wallet_constructors: - return wallet_constructors[wallet_type] - raise RuntimeError("Unknown wallet type: " + str(wallet_type)) diff --git a/lib/websockets.py b/lib/websockets.py @@ -1,140 +0,0 @@ -#!/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 queue -import threading, os, json -from collections import defaultdict -try: - from SimpleWebSocketServer import WebSocket, SimpleSSLWebSocketServer -except ImportError: - import sys - sys.exit("install SimpleWebSocketServer") - -from . import util -from . import bitcoin - -request_queue = queue.Queue() - -class ElectrumWebSocket(WebSocket): - - def handleMessage(self): - assert self.data[0:3] == 'id:' - util.print_error("message received", self.data) - request_id = self.data[3:] - request_queue.put((self, request_id)) - - def handleConnected(self): - util.print_error("connected", self.address) - - def handleClose(self): - util.print_error("closed", self.address) - - - -class WsClientThread(util.DaemonThread): - - def __init__(self, config, network): - util.DaemonThread.__init__(self) - self.network = network - self.config = config - self.response_queue = queue.Queue() - self.subscriptions = defaultdict(list) - - def make_request(self, request_id): - # read json file - rdir = self.config.get('requests_dir') - n = os.path.join(rdir, 'req', request_id[0], request_id[1], request_id, request_id + '.json') - with open(n, encoding='utf-8') as f: - s = f.read() - d = json.loads(s) - addr = d.get('address') - amount = d.get('amount') - return addr, amount - - def reading_thread(self): - while self.is_running(): - try: - ws, request_id = request_queue.get() - except queue.Empty: - continue - try: - addr, amount = self.make_request(request_id) - except: - continue - l = self.subscriptions.get(addr, []) - l.append((ws, amount)) - self.subscriptions[addr] = l - self.network.subscribe_to_addresses([addr], self.response_queue.put) - - def run(self): - threading.Thread(target=self.reading_thread).start() - while self.is_running(): - try: - r = self.response_queue.get(timeout=0.1) - except queue.Empty: - continue - util.print_error('response', r) - method = r.get('method') - result = r.get('result') - if result is None: - continue - if method == 'blockchain.scripthash.subscribe': - addr = r.get('params')[0] - scripthash = bitcoin.address_to_scripthash(addr) - self.network.get_balance_for_scripthash( - scripthash, self.response_queue.put) - elif method == 'blockchain.scripthash.get_balance': - scripthash = r.get('params')[0] - addr = self.network.h2addr.get(scripthash, None) - if addr is None: - util.print_error( - "can't find address for scripthash: %s" % scripthash) - l = self.subscriptions.get(addr, []) - for ws, amount in l: - if not ws.closed: - if sum(result.values()) >=amount: - ws.sendMessage('paid') - - - -class WebSocketServer(threading.Thread): - - def __init__(self, config, ns): - threading.Thread.__init__(self) - self.config = config - self.net_server = ns - self.daemon = True - - def run(self): - t = WsClientThread(self.config, self.net_server) - t.start() - - host = self.config.get('websocket_server') - port = self.config.get('websocket_port', 9999) - certfile = self.config.get('ssl_chain') - keyfile = self.config.get('ssl_privkey') - self.server = SimpleSSLWebSocketServer(host, port, ElectrumWebSocket, certfile, keyfile) - self.server.serveforever() - - diff --git a/lib/wordlist/chinese_simplified.txt b/lib/wordlist/chinese_simplified.txt @@ -1,2048 +0,0 @@ -的 -一 -是 -在 -不 -了 -有 -和 -人 -这 -中 -大 -为 -上 -个 -国 -我 -以 -要 -他 -时 -来 -用 -们 -生 -到 -作 -地 -于 -出 -就 -分 -对 -成 -会 -可 -主 -发 -年 -动 -同 -工 -也 -能 -下 -过 -子 -说 -产 -种 -面 -而 -方 -后 -多 -定 -行 -学 -法 -所 -民 -得 -经 -十 -三 -之 -进 -着 -等 -部 -度 -家 -电 -力 -里 -如 -水 -化 -高 -自 -二 -理 -起 -小 -物 -现 -实 -加 -量 -都 -两 -体 -制 -机 -当 -使 -点 -从 -业 -本 -去 -把 -性 -好 -应 -开 -它 -合 -还 -因 -由 -其 -些 -然 -前 -外 -天 -政 -四 -日 -那 -社 -义 -事 -平 -形 -相 -全 -表 -间 -样 -与 -关 -各 -重 -新 -线 -内 -数 -正 -心 -反 -你 -明 -看 -原 -又 -么 -利 -比 -或 -但 -质 -气 -第 -向 -道 -命 -此 -变 -条 -只 -没 -结 -解 -问 -意 -建 -月 -公 -无 -系 -军 -很 -情 -者 -最 -立 -代 -想 -已 -通 -并 -提 -直 -题 -党 -程 -展 -五 -果 -料 -象 -员 -革 -位 -入 -常 -文 -总 -次 -品 -式 -活 -设 -及 -管 -特 -件 -长 -求 -老 -头 -基 -资 -边 -流 -路 -级 -少 -图 -山 -统 -接 -知 -较 -将 -组 -见 -计 -别 -她 -手 -角 -期 -根 -论 -运 -农 -指 -几 -九 -区 -强 -放 -决 -西 -被 -干 -做 -必 -战 -先 -回 -则 -任 -取 -据 -处 -队 -南 -给 -色 -光 -门 -即 -保 -治 -北 -造 -百 -规 -热 -领 -七 -海 -口 -东 -导 -器 -压 -志 -世 -金 -增 -争 -济 -阶 -油 -思 -术 -极 -交 -受 -联 -什 -认 -六 -共 -权 -收 -证 -改 -清 -美 -再 -采 -转 -更 -单 -风 -切 -打 -白 -教 -速 -花 -带 -安 -场 -身 -车 -例 -真 -务 -具 -万 -每 -目 -至 -达 -走 -积 -示 -议 -声 -报 -斗 -完 -类 -八 -离 -华 -名 -确 -才 -科 -张 -信 -马 -节 -话 -米 -整 -空 -元 -况 -今 -集 -温 -传 -土 -许 -步 -群 -广 -石 -记 -需 -段 -研 -界 -拉 -林 -律 -叫 -且 -究 -观 -越 -织 -装 -影 -算 -低 -持 -音 -众 -书 -布 -复 -容 -儿 -须 -际 -商 -非 -验 -连 -断 -深 -难 -近 -矿 -千 -周 -委 -素 -技 -备 -半 -办 -青 -省 -列 -习 -响 -约 -支 -般 -史 -感 -劳 -便 -团 -往 -酸 -历 -市 -克 -何 -除 -消 -构 -府 -称 -太 -准 -精 -值 -号 -率 -族 -维 -划 -选 -标 -写 -存 -候 -毛 -亲 -快 -效 -斯 -院 -查 -江 -型 -眼 -王 -按 -格 -养 -易 -置 -派 -层 -片 -始 -却 -专 -状 -育 -厂 -京 -识 -适 -属 -圆 -包 -火 -住 -调 -满 -县 -局 -照 -参 -红 -细 -引 -听 -该 -铁 -价 -严 -首 -底 -液 -官 -德 -随 -病 -苏 -失 -尔 -死 -讲 -配 -女 -黄 -推 -显 -谈 -罪 -神 -艺 -呢 -席 -含 -企 -望 -密 -批 -营 -项 -防 -举 -球 -英 -氧 -势 -告 -李 -台 -落 -木 -帮 -轮 -破 -亚 -师 -围 -注 -远 -字 -材 -排 -供 -河 -态 -封 -另 -施 -减 -树 -溶 -怎 -止 -案 -言 -士 -均 -武 -固 -叶 -鱼 -波 -视 -仅 -费 -紧 -爱 -左 -章 -早 -朝 -害 -续 -轻 -服 -试 -食 -充 -兵 -源 -判 -护 -司 -足 -某 -练 -差 -致 -板 -田 -降 -黑 -犯 -负 -击 -范 -继 -兴 -似 -余 -坚 -曲 -输 -修 -故 -城 -夫 -够 -送 -笔 -船 -占 -右 -财 -吃 -富 -春 -职 -觉 -汉 -画 -功 -巴 -跟 -虽 -杂 -飞 -检 -吸 -助 -升 -阳 -互 -初 -创 -抗 -考 -投 -坏 -策 -古 -径 -换 -未 -跑 -留 -钢 -曾 -端 -责 -站 -简 -述 -钱 -副 -尽 -帝 -射 -草 -冲 -承 -独 -令 -限 -阿 -宣 -环 -双 -请 -超 -微 -让 -控 -州 -良 -轴 -找 -否 -纪 -益 -依 -优 -顶 -础 -载 -倒 -房 -突 -坐 -粉 -敌 -略 -客 -袁 -冷 -胜 -绝 -析 -块 -剂 -测 -丝 -协 -诉 -念 -陈 -仍 -罗 -盐 -友 -洋 -错 -苦 -夜 -刑 -移 -频 -逐 -靠 -混 -母 -短 -皮 -终 -聚 -汽 -村 -云 -哪 -既 -距 -卫 -停 -烈 -央 -察 -烧 -迅 -境 -若 -印 -洲 -刻 -括 -激 -孔 -搞 -甚 -室 -待 -核 -校 -散 -侵 -吧 -甲 -游 -久 -菜 -味 -旧 -模 -湖 -货 -损 -预 -阻 -毫 -普 -稳 -乙 -妈 -植 -息 -扩 -银 -语 -挥 -酒 -守 -拿 -序 -纸 -医 -缺 -雨 -吗 -针 -刘 -啊 -急 -唱 -误 -训 -愿 -审 -附 -获 -茶 -鲜 -粮 -斤 -孩 -脱 -硫 -肥 -善 -龙 -演 -父 -渐 -血 -欢 -械 -掌 -歌 -沙 -刚 -攻 -谓 -盾 -讨 -晚 -粒 -乱 -燃 -矛 -乎 -杀 -药 -宁 -鲁 -贵 -钟 -煤 -读 -班 -伯 -香 -介 -迫 -句 -丰 -培 -握 -兰 -担 -弦 -蛋 -沉 -假 -穿 -执 -答 -乐 -谁 -顺 -烟 -缩 -征 -脸 -喜 -松 -脚 -困 -异 -免 -背 -星 -福 -买 -染 -井 -概 -慢 -怕 -磁 -倍 -祖 -皇 -促 -静 -补 -评 -翻 -肉 -践 -尼 -衣 -宽 -扬 -棉 -希 -伤 -操 -垂 -秋 -宜 -氢 -套 -督 -振 -架 -亮 -末 -宪 -庆 -编 -牛 -触 -映 -雷 -销 -诗 -座 -居 -抓 -裂 -胞 -呼 -娘 -景 -威 -绿 -晶 -厚 -盟 -衡 -鸡 -孙 -延 -危 -胶 -屋 -乡 -临 -陆 -顾 -掉 -呀 -灯 -岁 -措 -束 -耐 -剧 -玉 -赵 -跳 -哥 -季 -课 -凯 -胡 -额 -款 -绍 -卷 -齐 -伟 -蒸 -殖 -永 -宗 -苗 -川 -炉 -岩 -弱 -零 -杨 -奏 -沿 -露 -杆 -探 -滑 -镇 -饭 -浓 -航 -怀 -赶 -库 -夺 -伊 -灵 -税 -途 -灭 -赛 -归 -召 -鼓 -播 -盘 -裁 -险 -康 -唯 -录 -菌 -纯 -借 -糖 -盖 -横 -符 -私 -努 -堂 -域 -枪 -润 -幅 -哈 -竟 -熟 -虫 -泽 -脑 -壤 -碳 -欧 -遍 -侧 -寨 -敢 -彻 -虑 -斜 -薄 -庭 -纳 -弹 -饲 -伸 -折 -麦 -湿 -暗 -荷 -瓦 -塞 -床 -筑 -恶 -户 -访 -塔 -奇 -透 -梁 -刀 -旋 -迹 -卡 -氯 -遇 -份 -毒 -泥 -退 -洗 -摆 -灰 -彩 -卖 -耗 -夏 -择 -忙 -铜 -献 -硬 -予 -繁 -圈 -雪 -函 -亦 -抽 -篇 -阵 -阴 -丁 -尺 -追 -堆 -雄 -迎 -泛 -爸 -楼 -避 -谋 -吨 -野 -猪 -旗 -累 -偏 -典 -馆 -索 -秦 -脂 -潮 -爷 -豆 -忽 -托 -惊 -塑 -遗 -愈 -朱 -替 -纤 -粗 -倾 -尚 -痛 -楚 -谢 -奋 -购 -磨 -君 -池 -旁 -碎 -骨 -监 -捕 -弟 -暴 -割 -贯 -殊 -释 -词 -亡 -壁 -顿 -宝 -午 -尘 -闻 -揭 -炮 -残 -冬 -桥 -妇 -警 -综 -招 -吴 -付 -浮 -遭 -徐 -您 -摇 -谷 -赞 -箱 -隔 -订 -男 -吹 -园 -纷 -唐 -败 -宋 -玻 -巨 -耕 -坦 -荣 -闭 -湾 -键 -凡 -驻 -锅 -救 -恩 -剥 -凝 -碱 -齿 -截 -炼 -麻 -纺 -禁 -废 -盛 -版 -缓 -净 -睛 -昌 -婚 -涉 -筒 -嘴 -插 -岸 -朗 -庄 -街 -藏 -姑 -贸 -腐 -奴 -啦 -惯 -乘 -伙 -恢 -匀 -纱 -扎 -辩 -耳 -彪 -臣 -亿 -璃 -抵 -脉 -秀 -萨 -俄 -网 -舞 -店 -喷 -纵 -寸 -汗 -挂 -洪 -贺 -闪 -柬 -爆 -烯 -津 -稻 -墙 -软 -勇 -像 -滚 -厘 -蒙 -芳 -肯 -坡 -柱 -荡 -腿 -仪 -旅 -尾 -轧 -冰 -贡 -登 -黎 -削 -钻 -勒 -逃 -障 -氨 -郭 -峰 -币 -港 -伏 -轨 -亩 -毕 -擦 -莫 -刺 -浪 -秘 -援 -株 -健 -售 -股 -岛 -甘 -泡 -睡 -童 -铸 -汤 -阀 -休 -汇 -舍 -牧 -绕 -炸 -哲 -磷 -绩 -朋 -淡 -尖 -启 -陷 -柴 -呈 -徒 -颜 -泪 -稍 -忘 -泵 -蓝 -拖 -洞 -授 -镜 -辛 -壮 -锋 -贫 -虚 -弯 -摩 -泰 -幼 -廷 -尊 -窗 -纲 -弄 -隶 -疑 -氏 -宫 -姐 -震 -瑞 -怪 -尤 -琴 -循 -描 -膜 -违 -夹 -腰 -缘 -珠 -穷 -森 -枝 -竹 -沟 -催 -绳 -忆 -邦 -剩 -幸 -浆 -栏 -拥 -牙 -贮 -礼 -滤 -钠 -纹 -罢 -拍 -咱 -喊 -袖 -埃 -勤 -罚 -焦 -潜 -伍 -墨 -欲 -缝 -姓 -刊 -饱 -仿 -奖 -铝 -鬼 -丽 -跨 -默 -挖 -链 -扫 -喝 -袋 -炭 -污 -幕 -诸 -弧 -励 -梅 -奶 -洁 -灾 -舟 -鉴 -苯 -讼 -抱 -毁 -懂 -寒 -智 -埔 -寄 -届 -跃 -渡 -挑 -丹 -艰 -贝 -碰 -拔 -爹 -戴 -码 -梦 -芽 -熔 -赤 -渔 -哭 -敬 -颗 -奔 -铅 -仲 -虎 -稀 -妹 -乏 -珍 -申 -桌 -遵 -允 -隆 -螺 -仓 -魏 -锐 -晓 -氮 -兼 -隐 -碍 -赫 -拨 -忠 -肃 -缸 -牵 -抢 -博 -巧 -壳 -兄 -杜 -讯 -诚 -碧 -祥 -柯 -页 -巡 -矩 -悲 -灌 -龄 -伦 -票 -寻 -桂 -铺 -圣 -恐 -恰 -郑 -趣 -抬 -荒 -腾 -贴 -柔 -滴 -猛 -阔 -辆 -妻 -填 -撤 -储 -签 -闹 -扰 -紫 -砂 -递 -戏 -吊 -陶 -伐 -喂 -疗 -瓶 -婆 -抚 -臂 -摸 -忍 -虾 -蜡 -邻 -胸 -巩 -挤 -偶 -弃 -槽 -劲 -乳 -邓 -吉 -仁 -烂 -砖 -租 -乌 -舰 -伴 -瓜 -浅 -丙 -暂 -燥 -橡 -柳 -迷 -暖 -牌 -秧 -胆 -详 -簧 -踏 -瓷 -谱 -呆 -宾 -糊 -洛 -辉 -愤 -竞 -隙 -怒 -粘 -乃 -绪 -肩 -籍 -敏 -涂 -熙 -皆 -侦 -悬 -掘 -享 -纠 -醒 -狂 -锁 -淀 -恨 -牲 -霸 -爬 -赏 -逆 -玩 -陵 -祝 -秒 -浙 -貌 -役 -彼 -悉 -鸭 -趋 -凤 -晨 -畜 -辈 -秩 -卵 -署 -梯 -炎 -滩 -棋 -驱 -筛 -峡 -冒 -啥 -寿 -译 -浸 -泉 -帽 -迟 -硅 -疆 -贷 -漏 -稿 -冠 -嫩 -胁 -芯 -牢 -叛 -蚀 -奥 -鸣 -岭 -羊 -凭 -串 -塘 -绘 -酵 -融 -盆 -锡 -庙 -筹 -冻 -辅 -摄 -袭 -筋 -拒 -僚 -旱 -钾 -鸟 -漆 -沈 -眉 -疏 -添 -棒 -穗 -硝 -韩 -逼 -扭 -侨 -凉 -挺 -碗 -栽 -炒 -杯 -患 -馏 -劝 -豪 -辽 -勃 -鸿 -旦 -吏 -拜 -狗 -埋 -辊 -掩 -饮 -搬 -骂 -辞 -勾 -扣 -估 -蒋 -绒 -雾 -丈 -朵 -姆 -拟 -宇 -辑 -陕 -雕 -偿 -蓄 -崇 -剪 -倡 -厅 -咬 -驶 -薯 -刷 -斥 -番 -赋 -奉 -佛 -浇 -漫 -曼 -扇 -钙 -桃 -扶 -仔 -返 -俗 -亏 -腔 -鞋 -棱 -覆 -框 -悄 -叔 -撞 -骗 -勘 -旺 -沸 -孤 -吐 -孟 -渠 -屈 -疾 -妙 -惜 -仰 -狠 -胀 -谐 -抛 -霉 -桑 -岗 -嘛 -衰 -盗 -渗 -脏 -赖 -涌 -甜 -曹 -阅 -肌 -哩 -厉 -烃 -纬 -毅 -昨 -伪 -症 -煮 -叹 -钉 -搭 -茎 -笼 -酷 -偷 -弓 -锥 -恒 -杰 -坑 -鼻 -翼 -纶 -叙 -狱 -逮 -罐 -络 -棚 -抑 -膨 -蔬 -寺 -骤 -穆 -冶 -枯 -册 -尸 -凸 -绅 -坯 -牺 -焰 -轰 -欣 -晋 -瘦 -御 -锭 -锦 -丧 -旬 -锻 -垄 -搜 -扑 -邀 -亭 -酯 -迈 -舒 -脆 -酶 -闲 -忧 -酚 -顽 -羽 -涨 -卸 -仗 -陪 -辟 -惩 -杭 -姚 -肚 -捉 -飘 -漂 -昆 -欺 -吾 -郎 -烷 -汁 -呵 -饰 -萧 -雅 -邮 -迁 -燕 -撒 -姻 -赴 -宴 -烦 -债 -帐 -斑 -铃 -旨 -醇 -董 -饼 -雏 -姿 -拌 -傅 -腹 -妥 -揉 -贤 -拆 -歪 -葡 -胺 -丢 -浩 -徽 -昂 -垫 -挡 -览 -贪 -慰 -缴 -汪 -慌 -冯 -诺 -姜 -谊 -凶 -劣 -诬 -耀 -昏 -躺 -盈 -骑 -乔 -溪 -丛 -卢 -抹 -闷 -咨 -刮 -驾 -缆 -悟 -摘 -铒 -掷 -颇 -幻 -柄 -惠 -惨 -佳 -仇 -腊 -窝 -涤 -剑 -瞧 -堡 -泼 -葱 -罩 -霍 -捞 -胎 -苍 -滨 -俩 -捅 -湘 -砍 -霞 -邵 -萄 -疯 -淮 -遂 -熊 -粪 -烘 -宿 -档 -戈 -驳 -嫂 -裕 -徙 -箭 -捐 -肠 -撑 -晒 -辨 -殿 -莲 -摊 -搅 -酱 -屏 -疫 -哀 -蔡 -堵 -沫 -皱 -畅 -叠 -阁 -莱 -敲 -辖 -钩 -痕 -坝 -巷 -饿 -祸 -丘 -玄 -溜 -曰 -逻 -彭 -尝 -卿 -妨 -艇 -吞 -韦 -怨 -矮 -歇 diff --git a/lib/wordlist/english.txt b/lib/wordlist/english.txt @@ -1,2048 +0,0 @@ -abandon -ability -able -about -above -absent -absorb -abstract -absurd -abuse -access -accident -account -accuse -achieve -acid -acoustic -acquire -across -act -action -actor -actress -actual -adapt -add -addict -address -adjust -admit -adult -advance -advice -aerobic -affair -afford -afraid -again -age -agent -agree -ahead -aim -air -airport -aisle -alarm -album -alcohol -alert -alien -all -alley -allow -almost -alone -alpha -already -also -alter -always -amateur -amazing -among -amount -amused -analyst -anchor -ancient -anger -angle -angry -animal -ankle -announce -annual -another -answer -antenna -antique -anxiety -any -apart -apology -appear -apple -approve -april -arch -arctic -area -arena -argue -arm -armed -armor -army -around -arrange -arrest -arrive -arrow -art -artefact -artist -artwork -ask -aspect -assault -asset -assist -assume -asthma -athlete -atom -attack -attend -attitude -attract -auction -audit -august -aunt -author -auto -autumn -average -avocado -avoid -awake -aware -away -awesome -awful -awkward -axis -baby -bachelor -bacon -badge -bag -balance -balcony -ball -bamboo -banana -banner -bar -barely -bargain -barrel -base -basic -basket -battle -beach -bean -beauty -because -become -beef -before -begin -behave -behind -believe -below -belt -bench -benefit -best -betray -better -between -beyond -bicycle -bid -bike -bind -biology -bird -birth -bitter -black -blade -blame -blanket -blast -bleak -bless -blind -blood -blossom -blouse -blue -blur -blush -board -boat -body -boil -bomb -bone -bonus -book -boost -border -boring -borrow -boss -bottom -bounce -box -boy -bracket -brain -brand -brass -brave -bread -breeze -brick -bridge -brief -bright -bring -brisk -broccoli -broken -bronze -broom -brother -brown -brush -bubble -buddy -budget -buffalo -build -bulb -bulk -bullet -bundle -bunker -burden -burger -burst -bus -business -busy -butter -buyer -buzz -cabbage -cabin -cable -cactus -cage -cake -call -calm -camera -camp -can -canal -cancel -candy -cannon -canoe -canvas -canyon -capable -capital -captain -car -carbon -card -cargo -carpet -carry -cart -case -cash -casino -castle -casual -cat -catalog -catch -category -cattle -caught -cause -caution -cave -ceiling -celery -cement -census -century -cereal -certain -chair -chalk -champion -change -chaos -chapter -charge -chase -chat -cheap -check -cheese -chef -cherry -chest -chicken -chief -child -chimney -choice -choose -chronic -chuckle -chunk -churn -cigar -cinnamon -circle -citizen -city -civil -claim -clap -clarify -claw -clay -clean -clerk -clever -click -client -cliff -climb -clinic -clip -clock -clog -close -cloth -cloud -clown -club -clump -cluster -clutch -coach -coast -coconut -code -coffee -coil -coin -collect -color -column -combine -come -comfort -comic -common -company -concert -conduct -confirm -congress -connect -consider -control -convince -cook -cool -copper -copy -coral -core -corn -correct -cost -cotton -couch -country -couple -course -cousin -cover -coyote -crack -cradle -craft -cram -crane -crash -crater -crawl -crazy -cream -credit -creek -crew -cricket -crime -crisp -critic -crop -cross -crouch -crowd -crucial -cruel -cruise -crumble -crunch -crush -cry -crystal -cube -culture -cup -cupboard -curious -current -curtain -curve -cushion -custom -cute -cycle -dad -damage -damp -dance -danger -daring -dash -daughter -dawn -day -deal -debate -debris -decade -december -decide -decline -decorate -decrease -deer -defense -define -defy -degree -delay -deliver -demand -demise -denial -dentist -deny -depart -depend -deposit -depth -deputy -derive -describe -desert -design -desk -despair -destroy -detail -detect -develop -device -devote -diagram -dial -diamond -diary -dice -diesel -diet -differ -digital -dignity -dilemma -dinner -dinosaur -direct -dirt -disagree -discover -disease -dish -dismiss -disorder -display -distance -divert -divide -divorce -dizzy -doctor -document -dog -doll -dolphin -domain -donate -donkey -donor -door -dose -double -dove -draft -dragon -drama -drastic -draw -dream -dress -drift -drill -drink -drip -drive -drop -drum -dry -duck -dumb -dune -during -dust -dutch -duty -dwarf -dynamic -eager -eagle -early -earn -earth -easily -east -easy -echo -ecology -economy -edge -edit -educate -effort -egg -eight -either -elbow -elder -electric -elegant -element -elephant -elevator -elite -else -embark -embody -embrace -emerge -emotion -employ -empower -empty -enable -enact -end -endless -endorse -enemy -energy -enforce -engage -engine -enhance -enjoy -enlist -enough -enrich -enroll -ensure -enter -entire -entry -envelope -episode -equal -equip -era -erase -erode -erosion -error -erupt -escape -essay -essence -estate -eternal -ethics -evidence -evil -evoke -evolve -exact -example -excess -exchange -excite -exclude -excuse -execute -exercise -exhaust -exhibit -exile -exist -exit -exotic -expand -expect -expire -explain -expose -express -extend -extra -eye -eyebrow -fabric -face -faculty -fade -faint -faith -fall -false -fame -family -famous -fan -fancy -fantasy -farm -fashion -fat -fatal -father -fatigue -fault -favorite -feature -february -federal -fee -feed -feel -female -fence -festival -fetch -fever -few -fiber -fiction -field -figure -file -film -filter -final -find -fine -finger -finish -fire -firm -first -fiscal -fish -fit -fitness -fix -flag -flame -flash -flat -flavor -flee -flight -flip -float -flock -floor -flower -fluid -flush -fly -foam -focus -fog -foil -fold -follow -food -foot -force -forest -forget -fork -fortune -forum -forward -fossil -foster -found -fox -fragile -frame -frequent -fresh -friend -fringe -frog -front -frost -frown -frozen -fruit -fuel -fun -funny -furnace -fury -future -gadget -gain -galaxy -gallery -game -gap -garage -garbage -garden -garlic -garment -gas -gasp -gate -gather -gauge -gaze -general -genius -genre -gentle -genuine -gesture -ghost -giant -gift -giggle -ginger -giraffe -girl -give -glad -glance -glare -glass -glide -glimpse -globe -gloom -glory -glove -glow -glue -goat -goddess -gold -good -goose -gorilla -gospel -gossip -govern -gown -grab -grace -grain -grant -grape -grass -gravity -great -green -grid -grief -grit -grocery -group -grow -grunt -guard -guess -guide -guilt -guitar -gun -gym -habit -hair -half -hammer -hamster -hand -happy -harbor -hard -harsh -harvest -hat -have -hawk -hazard -head -health -heart -heavy -hedgehog -height -hello -helmet -help -hen -hero -hidden -high -hill -hint -hip -hire -history -hobby -hockey -hold -hole -holiday -hollow -home -honey -hood -hope -horn -horror -horse -hospital -host -hotel -hour -hover -hub -huge -human -humble -humor -hundred -hungry -hunt -hurdle -hurry -hurt -husband -hybrid -ice -icon -idea -identify -idle -ignore -ill -illegal -illness -image -imitate -immense -immune -impact -impose -improve -impulse -inch -include -income -increase -index -indicate -indoor -industry -infant -inflict -inform -inhale -inherit -initial -inject -injury -inmate -inner -innocent -input -inquiry -insane -insect -inside -inspire -install -intact -interest -into -invest -invite -involve -iron -island -isolate -issue -item -ivory -jacket -jaguar -jar -jazz -jealous -jeans -jelly -jewel -job -join -joke -journey -joy -judge -juice -jump -jungle -junior -junk -just -kangaroo -keen -keep -ketchup -key -kick -kid -kidney -kind -kingdom -kiss -kit -kitchen -kite -kitten -kiwi -knee -knife -knock -know -lab -label -labor -ladder -lady -lake -lamp -language -laptop -large -later -latin -laugh -laundry -lava -law -lawn -lawsuit -layer -lazy -leader -leaf -learn -leave -lecture -left -leg -legal -legend -leisure -lemon -lend -length -lens -leopard -lesson -letter -level -liar -liberty -library -license -life -lift -light -like -limb -limit -link -lion -liquid -list -little -live -lizard -load -loan -lobster -local -lock -logic -lonely -long -loop -lottery -loud -lounge -love -loyal -lucky -luggage -lumber -lunar -lunch -luxury -lyrics -machine -mad -magic -magnet -maid -mail -main -major -make -mammal -man -manage -mandate -mango -mansion -manual -maple -marble -march -margin -marine -market -marriage -mask -mass -master -match -material -math -matrix -matter -maximum -maze -meadow -mean -measure -meat -mechanic -medal -media -melody -melt -member -memory -mention -menu -mercy -merge -merit -merry -mesh -message -metal -method -middle -midnight -milk -million -mimic -mind -minimum -minor -minute -miracle -mirror -misery -miss -mistake -mix -mixed -mixture -mobile -model -modify -mom -moment -monitor -monkey -monster -month -moon -moral -more -morning -mosquito -mother -motion -motor -mountain -mouse -move -movie -much -muffin -mule -multiply -muscle -museum -mushroom -music -must -mutual -myself -mystery -myth -naive -name -napkin -narrow -nasty -nation -nature -near -neck -need -negative -neglect -neither -nephew -nerve -nest -net -network -neutral -never -news -next -nice -night -noble -noise -nominee -noodle -normal -north -nose -notable -note -nothing -notice -novel -now -nuclear -number -nurse -nut -oak -obey -object -oblige -obscure -observe -obtain -obvious -occur -ocean -october -odor -off -offer -office -often -oil -okay -old -olive -olympic -omit -once -one -onion -online -only -open -opera -opinion -oppose -option -orange -orbit -orchard -order -ordinary -organ -orient -original -orphan -ostrich -other -outdoor -outer -output -outside -oval -oven -over -own -owner -oxygen -oyster -ozone -pact -paddle -page -pair -palace -palm -panda -panel -panic -panther -paper -parade -parent -park -parrot -party -pass -patch -path -patient -patrol -pattern -pause -pave -payment -peace -peanut -pear -peasant -pelican -pen -penalty -pencil -people -pepper -perfect -permit -person -pet -phone -photo -phrase -physical -piano -picnic -picture -piece -pig -pigeon -pill -pilot -pink -pioneer -pipe -pistol -pitch -pizza -place -planet -plastic -plate -play -please -pledge -pluck -plug -plunge -poem -poet -point -polar -pole -police -pond -pony -pool -popular -portion -position -possible -post -potato -pottery -poverty -powder -power -practice -praise -predict -prefer -prepare -present -pretty -prevent -price -pride -primary -print -priority -prison -private -prize -problem -process -produce -profit -program -project -promote -proof -property -prosper -protect -proud -provide -public -pudding -pull -pulp -pulse -pumpkin -punch -pupil -puppy -purchase -purity -purpose -purse -push -put -puzzle -pyramid -quality -quantum -quarter -question -quick -quit -quiz -quote -rabbit -raccoon -race -rack -radar -radio -rail -rain -raise -rally -ramp -ranch -random -range -rapid -rare -rate -rather -raven -raw -razor -ready -real -reason -rebel -rebuild -recall -receive -recipe -record -recycle -reduce -reflect -reform -refuse -region -regret -regular -reject -relax -release -relief -rely -remain -remember -remind -remove -render -renew -rent -reopen -repair -repeat -replace -report -require -rescue -resemble -resist -resource -response -result -retire -retreat -return -reunion -reveal -review -reward -rhythm -rib -ribbon -rice -rich -ride -ridge -rifle -right -rigid -ring -riot -ripple -risk -ritual -rival -river -road -roast -robot -robust -rocket -romance -roof -rookie -room -rose -rotate -rough -round -route -royal -rubber -rude -rug -rule -run -runway -rural -sad -saddle -sadness -safe -sail -salad -salmon -salon -salt -salute -same -sample -sand -satisfy -satoshi -sauce -sausage -save -say -scale -scan -scare -scatter -scene -scheme -school -science -scissors -scorpion -scout -scrap -screen -script -scrub -sea -search -season -seat -second -secret -section -security -seed -seek -segment -select -sell -seminar -senior -sense -sentence -series -service -session -settle -setup -seven -shadow -shaft -shallow -share -shed -shell -sheriff -shield -shift -shine -ship -shiver -shock -shoe -shoot -shop -short -shoulder -shove -shrimp -shrug -shuffle -shy -sibling -sick -side -siege -sight -sign -silent -silk -silly -silver -similar -simple -since -sing -siren -sister -situate -six -size -skate -sketch -ski -skill -skin -skirt -skull -slab -slam -sleep -slender -slice -slide -slight -slim -slogan -slot -slow -slush -small -smart -smile -smoke -smooth -snack -snake -snap -sniff -snow -soap -soccer -social -sock -soda -soft -solar -soldier -solid -solution -solve -someone -song -soon -sorry -sort -soul -sound -soup -source -south -space -spare -spatial -spawn -speak -special -speed -spell -spend -sphere -spice -spider -spike -spin -spirit -split -spoil -sponsor -spoon -sport -spot -spray -spread -spring -spy -square -squeeze -squirrel -stable -stadium -staff -stage -stairs -stamp -stand -start -state -stay -steak -steel -stem -step -stereo -stick -still -sting -stock -stomach -stone -stool -story -stove -strategy -street -strike -strong -struggle -student -stuff -stumble -style -subject -submit -subway -success -such -sudden -suffer -sugar -suggest -suit -summer -sun -sunny -sunset -super -supply -supreme -sure -surface -surge -surprise -surround -survey -suspect -sustain -swallow -swamp -swap -swarm -swear -sweet -swift -swim -swing -switch -sword -symbol -symptom -syrup -system -table -tackle -tag -tail -talent -talk -tank -tape -target -task -taste -tattoo -taxi -teach -team -tell -ten -tenant -tennis -tent -term -test -text -thank -that -theme -then -theory -there -they -thing -this -thought -three -thrive -throw -thumb -thunder -ticket -tide -tiger -tilt -timber -time -tiny -tip -tired -tissue -title -toast -tobacco -today -toddler -toe -together -toilet -token -tomato -tomorrow -tone -tongue -tonight -tool -tooth -top -topic -topple -torch -tornado -tortoise -toss -total -tourist -toward -tower -town -toy -track -trade -traffic -tragic -train -transfer -trap -trash -travel -tray -treat -tree -trend -trial -tribe -trick -trigger -trim -trip -trophy -trouble -truck -true -truly -trumpet -trust -truth -try -tube -tuition -tumble -tuna -tunnel -turkey -turn -turtle -twelve -twenty -twice -twin -twist -two -type -typical -ugly -umbrella -unable -unaware -uncle -uncover -under -undo -unfair -unfold -unhappy -uniform -unique -unit -universe -unknown -unlock -until -unusual -unveil -update -upgrade -uphold -upon -upper -upset -urban -urge -usage -use -used -useful -useless -usual -utility -vacant -vacuum -vague -valid -valley -valve -van -vanish -vapor -various -vast -vault -vehicle -velvet -vendor -venture -venue -verb -verify -version -very -vessel -veteran -viable -vibrant -vicious -victory -video -view -village -vintage -violin -virtual -virus -visa -visit -visual -vital -vivid -vocal -voice -void -volcano -volume -vote -voyage -wage -wagon -wait -walk -wall -walnut -want -warfare -warm -warrior -wash -wasp -waste -water -wave -way -wealth -weapon -wear -weasel -weather -web -wedding -weekend -weird -welcome -west -wet -whale -what -wheat -wheel -when -where -whip -whisper -wide -width -wife -wild -will -win -window -wine -wing -wink -winner -winter -wire -wisdom -wise -wish -witness -wolf -woman -wonder -wood -wool -word -work -world -worry -worth -wrap -wreck -wrestle -wrist -write -wrong -yard -year -yellow -you -young -youth -zebra -zero -zone -zoo diff --git a/lib/wordlist/japanese.txt b/lib/wordlist/japanese.txt @@ -1,2048 +0,0 @@ -あいこくしん -あいさつ -あいだ -あおぞら -あかちゃん -あきる -あけがた -あける -あこがれる -あさい -あさひ -あしあと -あじわう -あずかる -あずき -あそぶ -あたえる -あたためる -あたりまえ -あたる -あつい -あつかう -あっしゅく -あつまり -あつめる -あてな -あてはまる -あひる -あぶら -あぶる -あふれる -あまい -あまど -あまやかす -あまり -あみもの -あめりか -あやまる -あゆむ -あらいぐま -あらし -あらすじ -あらためる -あらゆる -あらわす -ありがとう -あわせる -あわてる -あんい -あんがい -あんこ -あんぜん -あんてい -あんない -あんまり -いいだす -いおん -いがい -いがく -いきおい -いきなり -いきもの -いきる -いくじ -いくぶん -いけばな -いけん -いこう -いこく -いこつ -いさましい -いさん -いしき -いじゅう -いじょう -いじわる -いずみ -いずれ -いせい -いせえび -いせかい -いせき -いぜん -いそうろう -いそがしい -いだい -いだく -いたずら -いたみ -いたりあ -いちおう -いちじ -いちど -いちば -いちぶ -いちりゅう -いつか -いっしゅん -いっせい -いっそう -いったん -いっち -いってい -いっぽう -いてざ -いてん -いどう -いとこ -いない -いなか -いねむり -いのち -いのる -いはつ -いばる -いはん -いびき -いひん -いふく -いへん -いほう -いみん -いもうと -いもたれ -いもり -いやがる -いやす -いよかん -いよく -いらい -いらすと -いりぐち -いりょう -いれい -いれもの -いれる -いろえんぴつ -いわい -いわう -いわかん -いわば -いわゆる -いんげんまめ -いんさつ -いんしょう -いんよう -うえき -うえる -うおざ -うがい -うかぶ -うかべる -うきわ -うくらいな -うくれれ -うけたまわる -うけつけ -うけとる -うけもつ -うける -うごかす -うごく -うこん -うさぎ -うしなう -うしろがみ -うすい -うすぎ -うすぐらい -うすめる -うせつ -うちあわせ -うちがわ -うちき -うちゅう -うっかり -うつくしい -うったえる -うつる -うどん -うなぎ -うなじ -うなずく -うなる -うねる -うのう -うぶげ -うぶごえ -うまれる -うめる -うもう -うやまう -うよく -うらがえす -うらぐち -うらない -うりあげ -うりきれ -うるさい -うれしい -うれゆき -うれる -うろこ -うわき -うわさ -うんこう -うんちん -うんてん -うんどう -えいえん -えいが -えいきょう -えいご -えいせい -えいぶん -えいよう -えいわ -えおり -えがお -えがく -えきたい -えくせる -えしゃく -えすて -えつらん -えのぐ -えほうまき -えほん -えまき -えもじ -えもの -えらい -えらぶ -えりあ -えんえん -えんかい -えんぎ -えんげき -えんしゅう -えんぜつ -えんそく -えんちょう -えんとつ -おいかける -おいこす -おいしい -おいつく -おうえん -おうさま -おうじ -おうせつ -おうたい -おうふく -おうべい -おうよう -おえる -おおい -おおう -おおどおり -おおや -おおよそ -おかえり -おかず -おがむ -おかわり -おぎなう -おきる -おくさま -おくじょう -おくりがな -おくる -おくれる -おこす -おこなう -おこる -おさえる -おさない -おさめる -おしいれ -おしえる -おじぎ -おじさん -おしゃれ -おそらく -おそわる -おたがい -おたく -おだやか -おちつく -おっと -おつり -おでかけ -おとしもの -おとなしい -おどり -おどろかす -おばさん -おまいり -おめでとう -おもいで -おもう -おもたい -おもちゃ -おやつ -おやゆび -およぼす -おらんだ -おろす -おんがく -おんけい -おんしゃ -おんせん -おんだん -おんちゅう -おんどけい -かあつ -かいが -がいき -がいけん -がいこう -かいさつ -かいしゃ -かいすいよく -かいぜん -かいぞうど -かいつう -かいてん -かいとう -かいふく -がいへき -かいほう -かいよう -がいらい -かいわ -かえる -かおり -かかえる -かがく -かがし -かがみ -かくご -かくとく -かざる -がぞう -かたい -かたち -がちょう -がっきゅう -がっこう -がっさん -がっしょう -かなざわし -かのう -がはく -かぶか -かほう -かほご -かまう -かまぼこ -かめれおん -かゆい -かようび -からい -かるい -かろう -かわく -かわら -がんか -かんけい -かんこう -かんしゃ -かんそう -かんたん -かんち -がんばる -きあい -きあつ -きいろ -ぎいん -きうい -きうん -きえる -きおう -きおく -きおち -きおん -きかい -きかく -きかんしゃ -ききて -きくばり -きくらげ -きけんせい -きこう -きこえる -きこく -きさい -きさく -きさま -きさらぎ -ぎじかがく -ぎしき -ぎじたいけん -ぎじにってい -ぎじゅつしゃ -きすう -きせい -きせき -きせつ -きそう -きぞく -きぞん -きたえる -きちょう -きつえん -ぎっちり -きつつき -きつね -きてい -きどう -きどく -きない -きなが -きなこ -きぬごし -きねん -きのう -きのした -きはく -きびしい -きひん -きふく -きぶん -きぼう -きほん -きまる -きみつ -きむずかしい -きめる -きもだめし -きもち -きもの -きゃく -きやく -ぎゅうにく -きよう -きょうりゅう -きらい -きらく -きりん -きれい -きれつ -きろく -ぎろん -きわめる -ぎんいろ -きんかくじ -きんじょ -きんようび -ぐあい -くいず -くうかん -くうき -くうぐん -くうこう -ぐうせい -くうそう -ぐうたら -くうふく -くうぼ -くかん -くきょう -くげん -ぐこう -くさい -くさき -くさばな -くさる -くしゃみ -くしょう -くすのき -くすりゆび -くせげ -くせん -ぐたいてき -くださる -くたびれる -くちこみ -くちさき -くつした -ぐっすり -くつろぐ -くとうてん -くどく -くなん -くねくね -くのう -くふう -くみあわせ -くみたてる -くめる -くやくしょ -くらす -くらべる -くるま -くれる -くろう -くわしい -ぐんかん -ぐんしょく -ぐんたい -ぐんて -けあな -けいかく -けいけん -けいこ -けいさつ -げいじゅつ -けいたい -げいのうじん -けいれき -けいろ -けおとす -けおりもの -げきか -げきげん -げきだん -げきちん -げきとつ -げきは -げきやく -げこう -げこくじょう -げざい -けさき -げざん -けしき -けしごむ -けしょう -げすと -けたば -けちゃっぷ -けちらす -けつあつ -けつい -けつえき -けっこん -けつじょ -けっせき -けってい -けつまつ -げつようび -げつれい -けつろん -げどく -けとばす -けとる -けなげ -けなす -けなみ -けぬき -げねつ -けねん -けはい -げひん -けぶかい -げぼく -けまり -けみかる -けむし -けむり -けもの -けらい -けろけろ -けわしい -けんい -けんえつ -けんお -けんか -げんき -けんげん -けんこう -けんさく -けんしゅう -けんすう -げんそう -けんちく -けんてい -けんとう -けんない -けんにん -げんぶつ -けんま -けんみん -けんめい -けんらん -けんり -こあくま -こいぬ -こいびと -ごうい -こうえん -こうおん -こうかん -ごうきゅう -ごうけい -こうこう -こうさい -こうじ -こうすい -ごうせい -こうそく -こうたい -こうちゃ -こうつう -こうてい -こうどう -こうない -こうはい -ごうほう -ごうまん -こうもく -こうりつ -こえる -こおり -ごかい -ごがつ -ごかん -こくご -こくさい -こくとう -こくない -こくはく -こぐま -こけい -こける -ここのか -こころ -こさめ -こしつ -こすう -こせい -こせき -こぜん -こそだて -こたい -こたえる -こたつ -こちょう -こっか -こつこつ -こつばん -こつぶ -こてい -こてん -ことがら -ことし -ことば -ことり -こなごな -こねこね -このまま -このみ -このよ -ごはん -こひつじ -こふう -こふん -こぼれる -ごまあぶら -こまかい -ごますり -こまつな -こまる -こむぎこ -こもじ -こもち -こもの -こもん -こやく -こやま -こゆう -こゆび -こよい -こよう -こりる -これくしょん -ころっけ -こわもて -こわれる -こんいん -こんかい -こんき -こんしゅう -こんすい -こんだて -こんとん -こんなん -こんびに -こんぽん -こんまけ -こんや -こんれい -こんわく -ざいえき -さいかい -さいきん -ざいげん -ざいこ -さいしょ -さいせい -ざいたく -ざいちゅう -さいてき -ざいりょう -さうな -さかいし -さがす -さかな -さかみち -さがる -さぎょう -さくし -さくひん -さくら -さこく -さこつ -さずかる -ざせき -さたん -さつえい -ざつおん -ざっか -ざつがく -さっきょく -ざっし -さつじん -ざっそう -さつたば -さつまいも -さてい -さといも -さとう -さとおや -さとし -さとる -さのう -さばく -さびしい -さべつ -さほう -さほど -さます -さみしい -さみだれ -さむけ -さめる -さやえんどう -さゆう -さよう -さよく -さらだ -ざるそば -さわやか -さわる -さんいん -さんか -さんきゃく -さんこう -さんさい -ざんしょ -さんすう -さんせい -さんそ -さんち -さんま -さんみ -さんらん -しあい -しあげ -しあさって -しあわせ -しいく -しいん -しうち -しえい -しおけ -しかい -しかく -じかん -しごと -しすう -じだい -したうけ -したぎ -したて -したみ -しちょう -しちりん -しっかり -しつじ -しつもん -してい -してき -してつ -じてん -じどう -しなぎれ -しなもの -しなん -しねま -しねん -しのぐ -しのぶ -しはい -しばかり -しはつ -しはらい -しはん -しひょう -しふく -じぶん -しへい -しほう -しほん -しまう -しまる -しみん -しむける -じむしょ -しめい -しめる -しもん -しゃいん -しゃうん -しゃおん -じゃがいも -しやくしょ -しゃくほう -しゃけん -しゃこ -しゃざい -しゃしん -しゃせん -しゃそう -しゃたい -しゃちょう -しゃっきん -じゃま -しゃりん -しゃれい -じゆう -じゅうしょ -しゅくはく -じゅしん -しゅっせき -しゅみ -しゅらば -じゅんばん -しょうかい -しょくたく -しょっけん -しょどう -しょもつ -しらせる -しらべる -しんか -しんこう -じんじゃ -しんせいじ -しんちく -しんりん -すあげ -すあし -すあな -ずあん -すいえい -すいか -すいとう -ずいぶん -すいようび -すうがく -すうじつ -すうせん -すおどり -すきま -すくう -すくない -すける -すごい -すこし -ずさん -すずしい -すすむ -すすめる -すっかり -ずっしり -ずっと -すてき -すてる -すねる -すのこ -すはだ -すばらしい -ずひょう -ずぶぬれ -すぶり -すふれ -すべて -すべる -ずほう -すぼん -すまい -すめし -すもう -すやき -すらすら -するめ -すれちがう -すろっと -すわる -すんぜん -すんぽう -せあぶら -せいかつ -せいげん -せいじ -せいよう -せおう -せかいかん -せきにん -せきむ -せきゆ -せきらんうん -せけん -せこう -せすじ -せたい -せたけ -せっかく -せっきゃく -ぜっく -せっけん -せっこつ -せっさたくま -せつぞく -せつだん -せつでん -せっぱん -せつび -せつぶん -せつめい -せつりつ -せなか -せのび -せはば -せびろ -せぼね -せまい -せまる -せめる -せもたれ -せりふ -ぜんあく -せんい -せんえい -せんか -せんきょ -せんく -せんげん -ぜんご -せんさい -せんしゅ -せんすい -せんせい -せんぞ -せんたく -せんちょう -せんてい -せんとう -せんぬき -せんねん -せんぱい -ぜんぶ -ぜんぽう -せんむ -せんめんじょ -せんもん -せんやく -せんゆう -せんよう -ぜんら -ぜんりゃく -せんれい -せんろ -そあく -そいとげる -そいね -そうがんきょう -そうき -そうご -そうしん -そうだん -そうなん -そうび -そうめん -そうり -そえもの -そえん -そがい -そげき -そこう -そこそこ -そざい -そしな -そせい -そせん -そそぐ -そだてる -そつう -そつえん -そっかん -そつぎょう -そっけつ -そっこう -そっせん -そっと -そとがわ -そとづら -そなえる -そなた -そふぼ -そぼく -そぼろ -そまつ -そまる -そむく -そむりえ -そめる -そもそも -そよかぜ -そらまめ -そろう -そんかい -そんけい -そんざい -そんしつ -そんぞく -そんちょう -ぞんび -ぞんぶん -そんみん -たあい -たいいん -たいうん -たいえき -たいおう -だいがく -たいき -たいぐう -たいけん -たいこ -たいざい -だいじょうぶ -だいすき -たいせつ -たいそう -だいたい -たいちょう -たいてい -だいどころ -たいない -たいねつ -たいのう -たいはん -だいひょう -たいふう -たいへん -たいほ -たいまつばな -たいみんぐ -たいむ -たいめん -たいやき -たいよう -たいら -たいりょく -たいる -たいわん -たうえ -たえる -たおす -たおる -たおれる -たかい -たかね -たきび -たくさん -たこく -たこやき -たさい -たしざん -だじゃれ -たすける -たずさわる -たそがれ -たたかう -たたく -ただしい -たたみ -たちばな -だっかい -だっきゃく -だっこ -だっしゅつ -だったい -たてる -たとえる -たなばた -たにん -たぬき -たのしみ -たはつ -たぶん -たべる -たぼう -たまご -たまる -だむる -ためいき -ためす -ためる -たもつ -たやすい -たよる -たらす -たりきほんがん -たりょう -たりる -たると -たれる -たれんと -たろっと -たわむれる -だんあつ -たんい -たんおん -たんか -たんき -たんけん -たんご -たんさん -たんじょうび -だんせい -たんそく -たんたい -だんち -たんてい -たんとう -だんな -たんにん -だんねつ -たんのう -たんぴん -だんぼう -たんまつ -たんめい -だんれつ -だんろ -だんわ -ちあい -ちあん -ちいき -ちいさい -ちえん -ちかい -ちから -ちきゅう -ちきん -ちけいず -ちけん -ちこく -ちさい -ちしき -ちしりょう -ちせい -ちそう -ちたい -ちたん -ちちおや -ちつじょ -ちてき -ちてん -ちぬき -ちぬり -ちのう -ちひょう -ちへいせん -ちほう -ちまた -ちみつ -ちみどろ -ちめいど -ちゃんこなべ -ちゅうい -ちゆりょく -ちょうし -ちょさくけん -ちらし -ちらみ -ちりがみ -ちりょう -ちるど -ちわわ -ちんたい -ちんもく -ついか -ついたち -つうか -つうじょう -つうはん -つうわ -つかう -つかれる -つくね -つくる -つけね -つける -つごう -つたえる -つづく -つつじ -つつむ -つとめる -つながる -つなみ -つねづね -つのる -つぶす -つまらない -つまる -つみき -つめたい -つもり -つもる -つよい -つるぼ -つるみく -つわもの -つわり -てあし -てあて -てあみ -ていおん -ていか -ていき -ていけい -ていこく -ていさつ -ていし -ていせい -ていたい -ていど -ていねい -ていひょう -ていへん -ていぼう -てうち -ておくれ -てきとう -てくび -でこぼこ -てさぎょう -てさげ -てすり -てそう -てちがい -てちょう -てつがく -てつづき -でっぱ -てつぼう -てつや -でぬかえ -てぬき -てぬぐい -てのひら -てはい -てぶくろ -てふだ -てほどき -てほん -てまえ -てまきずし -てみじか -てみやげ -てらす -てれび -てわけ -てわたし -でんあつ -てんいん -てんかい -てんき -てんぐ -てんけん -てんごく -てんさい -てんし -てんすう -でんち -てんてき -てんとう -てんない -てんぷら -てんぼうだい -てんめつ -てんらんかい -でんりょく -でんわ -どあい -といれ -どうかん -とうきゅう -どうぐ -とうし -とうむぎ -とおい -とおか -とおく -とおす -とおる -とかい -とかす -ときおり -ときどき -とくい -とくしゅう -とくてん -とくに -とくべつ -とけい -とける -とこや -とさか -としょかん -とそう -とたん -とちゅう -とっきゅう -とっくん -とつぜん -とつにゅう -とどける -ととのえる -とない -となえる -となり -とのさま -とばす -どぶがわ -とほう -とまる -とめる -ともだち -ともる -どようび -とらえる -とんかつ -どんぶり -ないかく -ないこう -ないしょ -ないす -ないせん -ないそう -なおす -ながい -なくす -なげる -なこうど -なさけ -なたでここ -なっとう -なつやすみ -ななおし -なにごと -なにもの -なにわ -なのか -なふだ -なまいき -なまえ -なまみ -なみだ -なめらか -なめる -なやむ -ならう -ならび -ならぶ -なれる -なわとび -なわばり -にあう -にいがた -にうけ -におい -にかい -にがて -にきび -にくしみ -にくまん -にげる -にさんかたんそ -にしき -にせもの -にちじょう -にちようび -にっか -にっき -にっけい -にっこう -にっさん -にっしょく -にっすう -にっせき -にってい -になう -にほん -にまめ -にもつ -にやり -にゅういん -にりんしゃ -にわとり -にんい -にんか -にんき -にんげん -にんしき -にんずう -にんそう -にんたい -にんち -にんてい -にんにく -にんぷ -にんまり -にんむ -にんめい -にんよう -ぬいくぎ -ぬかす -ぬぐいとる -ぬぐう -ぬくもり -ぬすむ -ぬまえび -ぬめり -ぬらす -ぬんちゃく -ねあげ -ねいき -ねいる -ねいろ -ねぐせ -ねくたい -ねくら -ねこぜ -ねこむ -ねさげ -ねすごす -ねそべる -ねだん -ねつい -ねっしん -ねつぞう -ねったいぎょ -ねぶそく -ねふだ -ねぼう -ねほりはほり -ねまき -ねまわし -ねみみ -ねむい -ねむたい -ねもと -ねらう -ねわざ -ねんいり -ねんおし -ねんかん -ねんきん -ねんぐ -ねんざ -ねんし -ねんちゃく -ねんど -ねんぴ -ねんぶつ -ねんまつ -ねんりょう -ねんれい -のいず -のおづま -のがす -のきなみ -のこぎり -のこす -のこる -のせる -のぞく -のぞむ -のたまう -のちほど -のっく -のばす -のはら -のべる -のぼる -のみもの -のやま -のらいぬ -のらねこ -のりもの -のりゆき -のれん -のんき -ばあい -はあく -ばあさん -ばいか -ばいく -はいけん -はいご -はいしん -はいすい -はいせん -はいそう -はいち -ばいばい -はいれつ -はえる -はおる -はかい -ばかり -はかる -はくしゅ -はけん -はこぶ -はさみ -はさん -はしご -ばしょ -はしる -はせる -ぱそこん -はそん -はたん -はちみつ -はつおん -はっかく -はづき -はっきり -はっくつ -はっけん -はっこう -はっさん -はっしん -はったつ -はっちゅう -はってん -はっぴょう -はっぽう -はなす -はなび -はにかむ -はぶらし -はみがき -はむかう -はめつ -はやい -はやし -はらう -はろうぃん -はわい -はんい -はんえい -はんおん -はんかく -はんきょう -ばんぐみ -はんこ -はんしゃ -はんすう -はんだん -ぱんち -ぱんつ -はんてい -はんとし -はんのう -はんぱ -はんぶん -はんぺん -はんぼうき -はんめい -はんらん -はんろん -ひいき -ひうん -ひえる -ひかく -ひかり -ひかる -ひかん -ひくい -ひけつ -ひこうき -ひこく -ひさい -ひさしぶり -ひさん -びじゅつかん -ひしょ -ひそか -ひそむ -ひたむき -ひだり -ひたる -ひつぎ -ひっこし -ひっし -ひつじゅひん -ひっす -ひつぜん -ぴったり -ぴっちり -ひつよう -ひてい -ひとごみ -ひなまつり -ひなん -ひねる -ひはん -ひびく -ひひょう -ひほう -ひまわり -ひまん -ひみつ -ひめい -ひめじし -ひやけ -ひやす -ひよう -びょうき -ひらがな -ひらく -ひりつ -ひりょう -ひるま -ひるやすみ -ひれい -ひろい -ひろう -ひろき -ひろゆき -ひんかく -ひんけつ -ひんこん -ひんしゅ -ひんそう -ぴんち -ひんぱん -びんぼう -ふあん -ふいうち -ふうけい -ふうせん -ぷうたろう -ふうとう -ふうふ -ふえる -ふおん -ふかい -ふきん -ふくざつ -ふくぶくろ -ふこう -ふさい -ふしぎ -ふじみ -ふすま -ふせい -ふせぐ -ふそく -ぶたにく -ふたん -ふちょう -ふつう -ふつか -ふっかつ -ふっき -ふっこく -ぶどう -ふとる -ふとん -ふのう -ふはい -ふひょう -ふへん -ふまん -ふみん -ふめつ -ふめん -ふよう -ふりこ -ふりる -ふるい -ふんいき -ぶんがく -ぶんぐ -ふんしつ -ぶんせき -ふんそう -ぶんぽう -へいあん -へいおん -へいがい -へいき -へいげん -へいこう -へいさ -へいしゃ -へいせつ -へいそ -へいたく -へいてん -へいねつ -へいわ -へきが -へこむ -べにいろ -べにしょうが -へらす -へんかん -べんきょう -べんごし -へんさい -へんたい -べんり -ほあん -ほいく -ぼうぎょ -ほうこく -ほうそう -ほうほう -ほうもん -ほうりつ -ほえる -ほおん -ほかん -ほきょう -ぼきん -ほくろ -ほけつ -ほけん -ほこう -ほこる -ほしい -ほしつ -ほしゅ -ほしょう -ほせい -ほそい -ほそく -ほたて -ほたる -ぽちぶくろ -ほっきょく -ほっさ -ほったん -ほとんど -ほめる -ほんい -ほんき -ほんけ -ほんしつ -ほんやく -まいにち -まかい -まかせる -まがる -まける -まこと -まさつ -まじめ -ますく -まぜる -まつり -まとめ -まなぶ -まぬけ -まねく -まほう -まもる -まゆげ -まよう -まろやか -まわす -まわり -まわる -まんが -まんきつ -まんぞく -まんなか -みいら -みうち -みえる -みがく -みかた -みかん -みけん -みこん -みじかい -みすい -みすえる -みせる -みっか -みつかる -みつける -みてい -みとめる -みなと -みなみかさい -みねらる -みのう -みのがす -みほん -みもと -みやげ -みらい -みりょく -みわく -みんか -みんぞく -むいか -むえき -むえん -むかい -むかう -むかえ -むかし -むぎちゃ -むける -むげん -むさぼる -むしあつい -むしば -むじゅん -むしろ -むすう -むすこ -むすぶ -むすめ -むせる -むせん -むちゅう -むなしい -むのう -むやみ -むよう -むらさき -むりょう -むろん -めいあん -めいうん -めいえん -めいかく -めいきょく -めいさい -めいし -めいそう -めいぶつ -めいれい -めいわく -めぐまれる -めざす -めした -めずらしい -めだつ -めまい -めやす -めんきょ -めんせき -めんどう -もうしあげる -もうどうけん -もえる -もくし -もくてき -もくようび -もちろん -もどる -もらう -もんく -もんだい -やおや -やける -やさい -やさしい -やすい -やすたろう -やすみ -やせる -やそう -やたい -やちん -やっと -やっぱり -やぶる -やめる -ややこしい -やよい -やわらかい -ゆうき -ゆうびんきょく -ゆうべ -ゆうめい -ゆけつ -ゆしゅつ -ゆせん -ゆそう -ゆたか -ゆちゃく -ゆでる -ゆにゅう -ゆびわ -ゆらい -ゆれる -ようい -ようか -ようきゅう -ようじ -ようす -ようちえん -よかぜ -よかん -よきん -よくせい -よくぼう -よけい -よごれる -よさん -よしゅう -よそう -よそく -よっか -よてい -よどがわく -よねつ -よやく -よゆう -よろこぶ -よろしい -らいう -らくがき -らくご -らくさつ -らくだ -らしんばん -らせん -らぞく -らたい -らっか -られつ -りえき -りかい -りきさく -りきせつ -りくぐん -りくつ -りけん -りこう -りせい -りそう -りそく -りてん -りねん -りゆう -りゅうがく -りよう -りょうり -りょかん -りょくちゃ -りょこう -りりく -りれき -りろん -りんご -るいけい -るいさい -るいじ -るいせき -るすばん -るりがわら -れいかん -れいぎ -れいせい -れいぞうこ -れいとう -れいぼう -れきし -れきだい -れんあい -れんけい -れんこん -れんさい -れんしゅう -れんぞく -れんらく -ろうか -ろうご -ろうじん -ろうそく -ろくが -ろこつ -ろじうら -ろしゅつ -ろせん -ろてん -ろめん -ろれつ -ろんぎ -ろんぱ -ろんぶん -ろんり -わかす -わかめ -わかやま -わかれる -わしつ -わじまし -わすれもの -わらう -われる diff --git a/lib/wordlist/portuguese.txt b/lib/wordlist/portuguese.txt @@ -1,1654 +0,0 @@ -# Copyright (c) 2014, The Monero Project -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, are -# permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this list of -# conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, this list -# of conditions and the following disclaimer in the documentation and/or other -# materials provided with the distribution. -# -# 3. Neither the name of the copyright holder nor the names of its contributors may be -# used to endorse or promote products derived from this software without specific -# prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL -# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -abaular -abdominal -abeto -abissinio -abjeto -ablucao -abnegar -abotoar -abrutalhar -absurdo -abutre -acautelar -accessorios -acetona -achocolatado -acirrar -acne -acovardar -acrostico -actinomicete -acustico -adaptavel -adeus -adivinho -adjunto -admoestar -adnominal -adotivo -adquirir -adriatico -adsorcao -adutora -advogar -aerossol -afazeres -afetuoso -afixo -afluir -afortunar -afrouxar -aftosa -afunilar -agentes -agito -aglutinar -aiatola -aimore -aino -aipo -airoso -ajeitar -ajoelhar -ajudante -ajuste -alazao -albumina -alcunha -alegria -alexandre -alforriar -alguns -alhures -alivio -almoxarife -alotropico -alpiste -alquimista -alsaciano -altura -aluviao -alvura -amazonico -ambulatorio -ametodico -amizades -amniotico -amovivel -amurada -anatomico -ancorar -anexo -anfora -aniversario -anjo -anotar -ansioso -anturio -anuviar -anverso -anzol -aonde -apaziguar -apito -aplicavel -apoteotico -aprimorar -aprumo -apto -apuros -aquoso -arauto -arbusto -arduo -aresta -arfar -arguto -aritmetico -arlequim -armisticio -aromatizar -arpoar -arquivo -arrumar -arsenio -arturiano -aruaque -arvores -asbesto -ascorbico -aspirina -asqueroso -assustar -astuto -atazanar -ativo -atletismo -atmosferico -atormentar -atroz -aturdir -audivel -auferir -augusto -aula -aumento -aurora -autuar -avatar -avexar -avizinhar -avolumar -avulso -axiomatico -azerbaijano -azimute -azoto -azulejo -bacteriologista -badulaque -baforada -baixote -bajular -balzaquiana -bambuzal -banzo -baoba -baqueta -barulho -bastonete -batuta -bauxita -bavaro -bazuca -bcrepuscular -beato -beduino -begonia -behaviorista -beisebol -belzebu -bemol -benzido -beocio -bequer -berro -besuntar -betume -bexiga -bezerro -biatlon -biboca -bicuspide -bidirecional -bienio -bifurcar -bigorna -bijuteria -bimotor -binormal -bioxido -bipolarizacao -biquini -birutice -bisturi -bituca -biunivoco -bivalve -bizarro -blasfemo -blenorreia -blindar -bloqueio -blusao -boazuda -bofete -bojudo -bolso -bombordo -bonzo -botina -boquiaberto -bostoniano -botulismo -bourbon -bovino -boximane -bravura -brevidade -britar -broxar -bruno -bruxuleio -bubonico -bucolico -buda -budista -bueiro -buffer -bugre -bujao -bumerangue -burundines -busto -butique -buzios -caatinga -cabuqui -cacunda -cafuzo -cajueiro -camurca -canudo -caquizeiro -carvoeiro -casulo -catuaba -cauterizar -cebolinha -cedula -ceifeiro -celulose -cerzir -cesto -cetro -ceus -cevar -chavena -cheroqui -chita -chovido -chuvoso -ciatico -cibernetico -cicuta -cidreira -cientistas -cifrar -cigarro -cilio -cimo -cinzento -cioso -cipriota -cirurgico -cisto -citrico -ciumento -civismo -clavicula -clero -clitoris -cluster -coaxial -cobrir -cocota -codorniz -coexistir -cogumelo -coito -colusao -compaixao -comutativo -contentamento -convulsivo -coordenativa -coquetel -correto -corvo -costureiro -cotovia -covil -cozinheiro -cretino -cristo -crivo -crotalo -cruzes -cubo -cucuia -cueiro -cuidar -cujo -cultural -cunilingua -cupula -curvo -custoso -cutucar -czarismo -dablio -dacota -dados -daguerreotipo -daiquiri -daltonismo -damista -dantesco -daquilo -darwinista -dasein -dativo -deao -debutantes -decurso -deduzir -defunto -degustar -dejeto -deltoide -demover -denunciar -deputado -deque -dervixe -desvirtuar -deturpar -deuteronomio -devoto -dextrose -dezoito -diatribe -dicotomico -didatico -dietista -difuso -digressao -diluvio -diminuto -dinheiro -dinossauro -dioxido -diplomatico -dique -dirimivel -disturbio -diurno -divulgar -dizivel -doar -dobro -docura -dodoi -doer -dogue -doloso -domo -donzela -doping -dorsal -dossie -dote -doutro -doze -dravidico -dreno -driver -dropes -druso -dubnio -ducto -dueto -dulija -dundum -duodeno -duquesa -durou -duvidoso -duzia -ebano -ebrio -eburneo -echarpe -eclusa -ecossistema -ectoplasma -ecumenismo -eczema -eden -editorial -edredom -edulcorar -efetuar -efigie -efluvio -egiptologo -egresso -egua -einsteiniano -eira -eivar -eixos -ejetar -elastomero -eldorado -elixir -elmo -eloquente -elucidativo -emaranhar -embutir -emerito -emfa -emitir -emotivo -empuxo -emulsao -enamorar -encurvar -enduro -enevoar -enfurnar -enguico -enho -enigmista -enlutar -enormidade -enpreendimento -enquanto -enriquecer -enrugar -entusiastico -enunciar -envolvimento -enxuto -enzimatico -eolico -epiteto -epoxi -epura -equivoco -erario -erbio -ereto -erguido -erisipela -ermo -erotizar -erros -erupcao -ervilha -esburacar -escutar -esfuziante -esguio -esloveno -esmurrar -esoterismo -esperanca -espirito -espurio -essencialmente -esturricar -esvoacar -etario -eterno -etiquetar -etnologo -etos -etrusco -euclidiano -euforico -eugenico -eunuco -europio -eustaquio -eutanasia -evasivo -eventualidade -evitavel -evoluir -exaustor -excursionista -exercito -exfoliado -exito -exotico -expurgo -exsudar -extrusora -exumar -fabuloso -facultativo -fado -fagulha -faixas -fajuto -faltoso -famoso -fanzine -fapesp -faquir -fartura -fastio -faturista -fausto -favorito -faxineira -fazer -fealdade -febril -fecundo -fedorento -feerico -feixe -felicidade -felipe -feltro -femur -fenotipo -fervura -festivo -feto -feudo -fevereiro -fezinha -fiasco -fibra -ficticio -fiduciario -fiesp -fifa -figurino -fijiano -filtro -finura -fiorde -fiquei -firula -fissurar -fitoteca -fivela -fixo -flavio -flexor -flibusteiro -flotilha -fluxograma -fobos -foco -fofura -foguista -foie -foliculo -fominha -fonte -forum -fosso -fotossintese -foxtrote -fraudulento -frevo -frivolo -frouxo -frutose -fuba -fucsia -fugitivo -fuinha -fujao -fulustreco -fumo -funileiro -furunculo -fustigar -futurologo -fuxico -fuzue -gabriel -gado -gaelico -gafieira -gaguejo -gaivota -gajo -galvanoplastico -gamo -ganso -garrucha -gastronomo -gatuno -gaussiano -gaviao -gaxeta -gazeteiro -gear -geiser -geminiano -generoso -genuino -geossinclinal -gerundio -gestual -getulista -gibi -gigolo -gilete -ginseng -giroscopio -glaucio -glacial -gleba -glifo -glote -glutonia -gnostico -goela -gogo -goitaca -golpista -gomo -gonzo -gorro -gostou -goticula -gourmet -governo -gozo -graxo -grevista -grito -grotesco -gruta -guaxinim -gude -gueto -guizo -guloso -gume -guru -gustativo -gustavo -gutural -habitue -haitiano -halterofilista -hamburguer -hanseniase -happening -harpista -hastear -haveres -hebreu -hectometro -hedonista -hegira -helena -helminto -hemorroidas -henrique -heptassilabo -hertziano -hesitar -heterossexual -heuristico -hexagono -hiato -hibrido -hidrostatico -hieroglifo -hifenizar -higienizar -hilario -himen -hino -hippie -hirsuto -historiografia -hitlerista -hodometro -hoje -holograma -homus -honroso -hoquei -horto -hostilizar -hotentote -huguenote -humilde -huno -hurra -hutu -iaia -ialorixa -iambico -iansa -iaque -iara -iatista -iberico -ibis -icar -iceberg -icosagono -idade -ideologo -idiotice -idoso -iemenita -iene -igarape -iglu -ignorar -igreja -iguaria -iidiche -ilativo -iletrado -ilharga -ilimitado -ilogismo -ilustrissimo -imaturo -imbuzeiro -imerso -imitavel -imovel -imputar -imutavel -inaveriguavel -incutir -induzir -inextricavel -infusao -ingua -inhame -iniquo -injusto -inning -inoxidavel -inquisitorial -insustentavel -intumescimento -inutilizavel -invulneravel -inzoneiro -iodo -iogurte -ioio -ionosfera -ioruba -iota -ipsilon -irascivel -iris -irlandes -irmaos -iroques -irrupcao -isca -isento -islandes -isotopo -isqueiro -israelita -isso -isto -iterbio -itinerario -itrio -iuane -iugoslavo -jabuticabeira -jacutinga -jade -jagunco -jainista -jaleco -jambo -jantarada -japones -jaqueta -jarro -jasmim -jato -jaula -javel -jazz -jegue -jeitoso -jejum -jenipapo -jeova -jequitiba -jersei -jesus -jetom -jiboia -jihad -jilo -jingle -jipe -jocoso -joelho -joguete -joio -jojoba -jorro -jota -joule -joviano -jubiloso -judoca -jugular -juizo -jujuba -juliano -jumento -junto -jururu -justo -juta -juventude -labutar -laguna -laico -lajota -lanterninha -lapso -laquear -lastro -lauto -lavrar -laxativo -lazer -leasing -lebre -lecionar -ledo -leguminoso -leitura -lele -lemure -lento -leonardo -leopardo -lepton -leque -leste -letreiro -leucocito -levitico -lexicologo -lhama -lhufas -liame -licoroso -lidocaina -liliputiano -limusine -linotipo -lipoproteina -liquidos -lirismo -lisura -liturgico -livros -lixo -lobulo -locutor -lodo -logro -lojista -lombriga -lontra -loop -loquaz -lorota -losango -lotus -louvor -luar -lubrificavel -lucros -lugubre -luis -luminoso -luneta -lustroso -luto -luvas -luxuriante -luzeiro -maduro -maestro -mafioso -magro -maiuscula -majoritario -malvisto -mamute -manutencao -mapoteca -maquinista -marzipa -masturbar -matuto -mausoleu -mavioso -maxixe -mazurca -meandro -mecha -medusa -mefistofelico -megera -meirinho -melro -memorizar -menu -mequetrefe -mertiolate -mestria -metroviario -mexilhao -mezanino -miau -microssegundo -midia -migratorio -mimosa -minuto -miosotis -mirtilo -misturar -mitzvah -miudos -mixuruca -mnemonico -moagem -mobilizar -modulo -moer -mofo -mogno -moita -molusco -monumento -moqueca -morubixaba -mostruario -motriz -mouse -movivel -mozarela -muarra -muculmano -mudo -mugir -muitos -mumunha -munir -muon -muquira -murros -musselina -nacoes -nado -naftalina -nago -naipe -naja -nalgum -namoro -nanquim -napolitano -naquilo -nascimento -nautilo -navios -nazista -nebuloso -nectarina -nefrologo -negus -nelore -nenufar -nepotismo -nervura -neste -netuno -neutron -nevoeiro -newtoniano -nexo -nhenhenhem -nhoque -nigeriano -niilista -ninho -niobio -niponico -niquelar -nirvana -nisto -nitroglicerina -nivoso -nobreza -nocivo -noel -nogueira -noivo -nojo -nominativo -nonuplo -noruegues -nostalgico -noturno -nouveau -nuanca -nublar -nucleotideo -nudista -nulo -numismatico -nunquinha -nupcias -nutritivo -nuvens -oasis -obcecar -obeso -obituario -objetos -oblongo -obnoxio -obrigatorio -obstruir -obtuso -obus -obvio -ocaso -occipital -oceanografo -ocioso -oclusivo -ocorrer -ocre -octogono -odalisca -odisseia -odorifico -oersted -oeste -ofertar -ofidio -oftalmologo -ogiva -ogum -oigale -oitavo -oitocentos -ojeriza -olaria -oleoso -olfato -olhos -oliveira -olmo -olor -olvidavel -ombudsman -omeleteira -omitir -omoplata -onanismo -ondular -oneroso -onomatopeico -ontologico -onus -onze -opalescente -opcional -operistico -opio -oposto -oprobrio -optometrista -opusculo -oratorio -orbital -orcar -orfao -orixa -orla -ornitologo -orquidea -ortorrombico -orvalho -osculo -osmotico -ossudo -ostrogodo -otario -otite -ouro -ousar -outubro -ouvir -ovario -overnight -oviparo -ovni -ovoviviparo -ovulo -oxala -oxente -oxiuro -oxossi -ozonizar -paciente -pactuar -padronizar -paete -pagodeiro -paixao -pajem -paludismo -pampas -panturrilha -papudo -paquistanes -pastoso -patua -paulo -pauzinhos -pavoroso -paxa -pazes -peao -pecuniario -pedunculo -pegaso -peixinho -pejorativo -pelvis -penuria -pequno -petunia -pezada -piauiense -pictorico -pierro -pigmeu -pijama -pilulas -pimpolho -pintura -piorar -pipocar -piqueteiro -pirulito -pistoleiro -pituitaria -pivotar -pixote -pizzaria -plistoceno -plotar -pluviometrico -pneumonico -poco -podridao -poetisa -pogrom -pois -polvorosa -pomposo -ponderado -pontudo -populoso -poquer -porvir -posudo -potro -pouso -povoar -prazo -prezar -privilegios -proximo -prussiano -pseudopode -psoriase -pterossauros -ptialina -ptolemaico -pudor -pueril -pufe -pugilista -puir -pujante -pulverizar -pumba -punk -purulento -pustula -putsch -puxe -quatrocentos -quetzal -quixotesco -quotizavel -rabujice -racista -radonio -rafia -ragu -rajado -ralo -rampeiro -ranzinza -raptor -raquitismo -raro -rasurar -ratoeira -ravioli -razoavel -reavivar -rebuscar -recusavel -reduzivel -reexposicao -refutavel -regurgitar -reivindicavel -rejuvenescimento -relva -remuneravel -renunciar -reorientar -repuxo -requisito -resumo -returno -reutilizar -revolvido -rezonear -riacho -ribossomo -ricota -ridiculo -rifle -rigoroso -rijo -rimel -rins -rios -riqueza -riquixa -rissole -ritualistico -rivalizar -rixa -robusto -rococo -rodoviario -roer -rogo -rojao -rolo -rompimento -ronronar -roqueiro -rorqual -rosto -rotundo -rouxinol -roxo -royal -ruas -rucula -rudimentos -ruela -rufo -rugoso -ruivo -rule -rumoroso -runico -ruptura -rural -rustico -rutilar -saariano -sabujo -sacudir -sadomasoquista -safra -sagui -sais -samurai -santuario -sapo -saquear -sartriano -saturno -saude -sauva -saveiro -saxofonista -sazonal -scherzo -script -seara -seborreia -secura -seduzir -sefardim -seguro -seja -selvas -sempre -senzala -sepultura -sequoia -sestercio -setuplo -seus -seviciar -sezonismo -shalom -siames -sibilante -sicrano -sidra -sifilitico -signos -silvo -simultaneo -sinusite -sionista -sirio -sisudo -situar -sivan -slide -slogan -soar -sobrio -socratico -sodomizar -soerguer -software -sogro -soja -solver -somente -sonso -sopro -soquete -sorveteiro -sossego -soturno -sousafone -sovinice -sozinho -suavizar -subverter -sucursal -sudoriparo -sufragio -sugestoes -suite -sujo -sultao -sumula -suntuoso -suor -supurar -suruba -susto -suturar -suvenir -tabuleta -taco -tadjique -tafeta -tagarelice -taitiano -talvez -tampouco -tanzaniano -taoista -tapume -taquion -tarugo -tascar -tatuar -tautologico -tavola -taxionomista -tchecoslovaco -teatrologo -tectonismo -tedioso -teflon -tegumento -teixo -telurio -temporas -tenue -teosofico -tepido -tequila -terrorista -testosterona -tetrico -teutonico -teve -texugo -tiara -tibia -tiete -tifoide -tigresa -tijolo -tilintar -timpano -tintureiro -tiquete -tiroteio -tisico -titulos -tive -toar -toboga -tofu -togoles -toicinho -tolueno -tomografo -tontura -toponimo -toquio -torvelinho -tostar -toto -touro -toxina -trazer -trezentos -trivialidade -trovoar -truta -tuaregue -tubular -tucano -tudo -tufo -tuiste -tulipa -tumultuoso -tunisino -tupiniquim -turvo -tutu -ucraniano -udenista -ufanista -ufologo -ugaritico -uiste -uivo -ulceroso -ulema -ultravioleta -umbilical -umero -umido -umlaut -unanimidade -unesco -ungulado -unheiro -univoco -untuoso -urano -urbano -urdir -uretra -urgente -urinol -urna -urologo -urro -ursulina -urtiga -urupe -usavel -usbeque -usei -usineiro -usurpar -utero -utilizar -utopico -uvular -uxoricidio -vacuo -vadio -vaguear -vaivem -valvula -vampiro -vantajoso -vaporoso -vaquinha -varziano -vasto -vaticinio -vaudeville -vazio -veado -vedico -veemente -vegetativo -veio -veja -veludo -venusiano -verdade -verve -vestuario -vetusto -vexatorio -vezes -viavel -vibratorio -victor -vicunha -vidros -vietnamita -vigoroso -vilipendiar -vime -vintem -violoncelo -viquingue -virus -visualizar -vituperio -viuvo -vivo -vizir -voar -vociferar -vodu -vogar -voile -volver -vomito -vontade -vortice -vosso -voto -vovozinha -voyeuse -vozes -vulva -vupt -western -xadrez -xale -xampu -xango -xarope -xaual -xavante -xaxim -xenonio -xepa -xerox -xicara -xifopago -xiita -xilogravura -xinxim -xistoso -xixi -xodo -xogum -xucro -zabumba -zagueiro -zambiano -zanzar -zarpar -zebu -zefiro -zeloso -zenite -zumbi diff --git a/lib/wordlist/spanish.txt b/lib/wordlist/spanish.txt @@ -1,2048 +0,0 @@ -ábaco -abdomen -abeja -abierto -abogado -abono -aborto -abrazo -abrir -abuelo -abuso -acabar -academia -acceso -acción -aceite -acelga -acento -aceptar -ácido -aclarar -acné -acoger -acoso -activo -acto -actriz -actuar -acudir -acuerdo -acusar -adicto -admitir -adoptar -adorno -aduana -adulto -aéreo -afectar -afición -afinar -afirmar -ágil -agitar -agonía -agosto -agotar -agregar -agrio -agua -agudo -águila -aguja -ahogo -ahorro -aire -aislar -ajedrez -ajeno -ajuste -alacrán -alambre -alarma -alba -álbum -alcalde -aldea -alegre -alejar -alerta -aleta -alfiler -alga -algodón -aliado -aliento -alivio -alma -almeja -almíbar -altar -alteza -altivo -alto -altura -alumno -alzar -amable -amante -amapola -amargo -amasar -ámbar -ámbito -ameno -amigo -amistad -amor -amparo -amplio -ancho -anciano -ancla -andar -andén -anemia -ángulo -anillo -ánimo -anís -anotar -antena -antiguo -antojo -anual -anular -anuncio -añadir -añejo -año -apagar -aparato -apetito -apio -aplicar -apodo -aporte -apoyo -aprender -aprobar -apuesta -apuro -arado -araña -arar -árbitro -árbol -arbusto -archivo -arco -arder -ardilla -arduo -área -árido -aries -armonía -arnés -aroma -arpa -arpón -arreglo -arroz -arruga -arte -artista -asa -asado -asalto -ascenso -asegurar -aseo -asesor -asiento -asilo -asistir -asno -asombro -áspero -astilla -astro -astuto -asumir -asunto -atajo -ataque -atar -atento -ateo -ático -atleta -átomo -atraer -atroz -atún -audaz -audio -auge -aula -aumento -ausente -autor -aval -avance -avaro -ave -avellana -avena -avestruz -avión -aviso -ayer -ayuda -ayuno -azafrán -azar -azote -azúcar -azufre -azul -baba -babor -bache -bahía -baile -bajar -balanza -balcón -balde -bambú -banco -banda -baño -barba -barco -barniz -barro -báscula -bastón -basura -batalla -batería -batir -batuta -baúl -bazar -bebé -bebida -bello -besar -beso -bestia -bicho -bien -bingo -blanco -bloque -blusa -boa -bobina -bobo -boca -bocina -boda -bodega -boina -bola -bolero -bolsa -bomba -bondad -bonito -bono -bonsái -borde -borrar -bosque -bote -botín -bóveda -bozal -bravo -brazo -brecha -breve -brillo -brinco -brisa -broca -broma -bronce -brote -bruja -brusco -bruto -buceo -bucle -bueno -buey -bufanda -bufón -búho -buitre -bulto -burbuja -burla -burro -buscar -butaca -buzón -caballo -cabeza -cabina -cabra -cacao -cadáver -cadena -caer -café -caída -caimán -caja -cajón -cal -calamar -calcio -caldo -calidad -calle -calma -calor -calvo -cama -cambio -camello -camino -campo -cáncer -candil -canela -canguro -canica -canto -caña -cañón -caoba -caos -capaz -capitán -capote -captar -capucha -cara -carbón -cárcel -careta -carga -cariño -carne -carpeta -carro -carta -casa -casco -casero -caspa -castor -catorce -catre -caudal -causa -cazo -cebolla -ceder -cedro -celda -célebre -celoso -célula -cemento -ceniza -centro -cerca -cerdo -cereza -cero -cerrar -certeza -césped -cetro -chacal -chaleco -champú -chancla -chapa -charla -chico -chiste -chivo -choque -choza -chuleta -chupar -ciclón -ciego -cielo -cien -cierto -cifra -cigarro -cima -cinco -cine -cinta -ciprés -circo -ciruela -cisne -cita -ciudad -clamor -clan -claro -clase -clave -cliente -clima -clínica -cobre -cocción -cochino -cocina -coco -código -codo -cofre -coger -cohete -cojín -cojo -cola -colcha -colegio -colgar -colina -collar -colmo -columna -combate -comer -comida -cómodo -compra -conde -conejo -conga -conocer -consejo -contar -copa -copia -corazón -corbata -corcho -cordón -corona -correr -coser -cosmos -costa -cráneo -cráter -crear -crecer -creído -crema -cría -crimen -cripta -crisis -cromo -crónica -croqueta -crudo -cruz -cuadro -cuarto -cuatro -cubo -cubrir -cuchara -cuello -cuento -cuerda -cuesta -cueva -cuidar -culebra -culpa -culto -cumbre -cumplir -cuna -cuneta -cuota -cupón -cúpula -curar -curioso -curso -curva -cutis -dama -danza -dar -dardo -dátil -deber -débil -década -decir -dedo -defensa -definir -dejar -delfín -delgado -delito -demora -denso -dental -deporte -derecho -derrota -desayuno -deseo -desfile -desnudo -destino -desvío -detalle -detener -deuda -día -diablo -diadema -diamante -diana -diario -dibujo -dictar -diente -dieta -diez -difícil -digno -dilema -diluir -dinero -directo -dirigir -disco -diseño -disfraz -diva -divino -doble -doce -dolor -domingo -don -donar -dorado -dormir -dorso -dos -dosis -dragón -droga -ducha -duda -duelo -dueño -dulce -dúo -duque -durar -dureza -duro -ébano -ebrio -echar -eco -ecuador -edad -edición -edificio -editor -educar -efecto -eficaz -eje -ejemplo -elefante -elegir -elemento -elevar -elipse -élite -elixir -elogio -eludir -embudo -emitir -emoción -empate -empeño -empleo -empresa -enano -encargo -enchufe -encía -enemigo -enero -enfado -enfermo -engaño -enigma -enlace -enorme -enredo -ensayo -enseñar -entero -entrar -envase -envío -época -equipo -erizo -escala -escena -escolar -escribir -escudo -esencia -esfera -esfuerzo -espada -espejo -espía -esposa -espuma -esquí -estar -este -estilo -estufa -etapa -eterno -ética -etnia -evadir -evaluar -evento -evitar -exacto -examen -exceso -excusa -exento -exigir -exilio -existir -éxito -experto -explicar -exponer -extremo -fábrica -fábula -fachada -fácil -factor -faena -faja -falda -fallo -falso -faltar -fama -familia -famoso -faraón -farmacia -farol -farsa -fase -fatiga -fauna -favor -fax -febrero -fecha -feliz -feo -feria -feroz -fértil -fervor -festín -fiable -fianza -fiar -fibra -ficción -ficha -fideo -fiebre -fiel -fiera -fiesta -figura -fijar -fijo -fila -filete -filial -filtro -fin -finca -fingir -finito -firma -flaco -flauta -flecha -flor -flota -fluir -flujo -flúor -fobia -foca -fogata -fogón -folio -folleto -fondo -forma -forro -fortuna -forzar -fosa -foto -fracaso -frágil -franja -frase -fraude -freír -freno -fresa -frío -frito -fruta -fuego -fuente -fuerza -fuga -fumar -función -funda -furgón -furia -fusil -fútbol -futuro -gacela -gafas -gaita -gajo -gala -galería -gallo -gamba -ganar -gancho -ganga -ganso -garaje -garza -gasolina -gastar -gato -gavilán -gemelo -gemir -gen -género -genio -gente -geranio -gerente -germen -gesto -gigante -gimnasio -girar -giro -glaciar -globo -gloria -gol -golfo -goloso -golpe -goma -gordo -gorila -gorra -gota -goteo -gozar -grada -gráfico -grano -grasa -gratis -grave -grieta -grillo -gripe -gris -grito -grosor -grúa -grueso -grumo -grupo -guante -guapo -guardia -guerra -guía -guiño -guion -guiso -guitarra -gusano -gustar -haber -hábil -hablar -hacer -hacha -hada -hallar -hamaca -harina -haz -hazaña -hebilla -hebra -hecho -helado -helio -hembra -herir -hermano -héroe -hervir -hielo -hierro -hígado -higiene -hijo -himno -historia -hocico -hogar -hoguera -hoja -hombre -hongo -honor -honra -hora -hormiga -horno -hostil -hoyo -hueco -huelga -huerta -hueso -huevo -huida -huir -humano -húmedo -humilde -humo -hundir -huracán -hurto -icono -ideal -idioma -ídolo -iglesia -iglú -igual -ilegal -ilusión -imagen -imán -imitar -impar -imperio -imponer -impulso -incapaz -índice -inerte -infiel -informe -ingenio -inicio -inmenso -inmune -innato -insecto -instante -interés -íntimo -intuir -inútil -invierno -ira -iris -ironía -isla -islote -jabalí -jabón -jamón -jarabe -jardín -jarra -jaula -jazmín -jefe -jeringa -jinete -jornada -joroba -joven -joya -juerga -jueves -juez -jugador -jugo -juguete -juicio -junco -jungla -junio -juntar -júpiter -jurar -justo -juvenil -juzgar -kilo -koala -labio -lacio -lacra -lado -ladrón -lagarto -lágrima -laguna -laico -lamer -lámina -lámpara -lana -lancha -langosta -lanza -lápiz -largo -larva -lástima -lata -látex -latir -laurel -lavar -lazo -leal -lección -leche -lector -leer -legión -legumbre -lejano -lengua -lento -leña -león -leopardo -lesión -letal -letra -leve -leyenda -libertad -libro -licor -líder -lidiar -lienzo -liga -ligero -lima -límite -limón -limpio -lince -lindo -línea -lingote -lino -linterna -líquido -liso -lista -litera -litio -litro -llaga -llama -llanto -llave -llegar -llenar -llevar -llorar -llover -lluvia -lobo -loción -loco -locura -lógica -logro -lombriz -lomo -lonja -lote -lucha -lucir -lugar -lujo -luna -lunes -lupa -lustro -luto -luz -maceta -macho -madera -madre -maduro -maestro -mafia -magia -mago -maíz -maldad -maleta -malla -malo -mamá -mambo -mamut -manco -mando -manejar -manga -maniquí -manjar -mano -manso -manta -mañana -mapa -máquina -mar -marco -marea -marfil -margen -marido -mármol -marrón -martes -marzo -masa -máscara -masivo -matar -materia -matiz -matriz -máximo -mayor -mazorca -mecha -medalla -medio -médula -mejilla -mejor -melena -melón -memoria -menor -mensaje -mente -menú -mercado -merengue -mérito -mes -mesón -meta -meter -método -metro -mezcla -miedo -miel -miembro -miga -mil -milagro -militar -millón -mimo -mina -minero -mínimo -minuto -miope -mirar -misa -miseria -misil -mismo -mitad -mito -mochila -moción -moda -modelo -moho -mojar -molde -moler -molino -momento -momia -monarca -moneda -monja -monto -moño -morada -morder -moreno -morir -morro -morsa -mortal -mosca -mostrar -motivo -mover -móvil -mozo -mucho -mudar -mueble -muela -muerte -muestra -mugre -mujer -mula -muleta -multa -mundo -muñeca -mural -muro -músculo -museo -musgo -música -muslo -nácar -nación -nadar -naipe -naranja -nariz -narrar -nasal -natal -nativo -natural -náusea -naval -nave -navidad -necio -néctar -negar -negocio -negro -neón -nervio -neto -neutro -nevar -nevera -nicho -nido -niebla -nieto -niñez -niño -nítido -nivel -nobleza -noche -nómina -noria -norma -norte -nota -noticia -novato -novela -novio -nube -nuca -núcleo -nudillo -nudo -nuera -nueve -nuez -nulo -número -nutria -oasis -obeso -obispo -objeto -obra -obrero -observar -obtener -obvio -oca -ocaso -océano -ochenta -ocho -ocio -ocre -octavo -octubre -oculto -ocupar -ocurrir -odiar -odio -odisea -oeste -ofensa -oferta -oficio -ofrecer -ogro -oído -oír -ojo -ola -oleada -olfato -olivo -olla -olmo -olor -olvido -ombligo -onda -onza -opaco -opción -ópera -opinar -oponer -optar -óptica -opuesto -oración -orador -oral -órbita -orca -orden -oreja -órgano -orgía -orgullo -oriente -origen -orilla -oro -orquesta -oruga -osadía -oscuro -osezno -oso -ostra -otoño -otro -oveja -óvulo -óxido -oxígeno -oyente -ozono -pacto -padre -paella -página -pago -país -pájaro -palabra -palco -paleta -pálido -palma -paloma -palpar -pan -panal -pánico -pantera -pañuelo -papá -papel -papilla -paquete -parar -parcela -pared -parir -paro -párpado -parque -párrafo -parte -pasar -paseo -pasión -paso -pasta -pata -patio -patria -pausa -pauta -pavo -payaso -peatón -pecado -pecera -pecho -pedal -pedir -pegar -peine -pelar -peldaño -pelea -peligro -pellejo -pelo -peluca -pena -pensar -peñón -peón -peor -pepino -pequeño -pera -percha -perder -pereza -perfil -perico -perla -permiso -perro -persona -pesa -pesca -pésimo -pestaña -pétalo -petróleo -pez -pezuña -picar -pichón -pie -piedra -pierna -pieza -pijama -pilar -piloto -pimienta -pino -pintor -pinza -piña -piojo -pipa -pirata -pisar -piscina -piso -pista -pitón -pizca -placa -plan -plata -playa -plaza -pleito -pleno -plomo -pluma -plural -pobre -poco -poder -podio -poema -poesía -poeta -polen -policía -pollo -polvo -pomada -pomelo -pomo -pompa -poner -porción -portal -posada -poseer -posible -poste -potencia -potro -pozo -prado -precoz -pregunta -premio -prensa -preso -previo -primo -príncipe -prisión -privar -proa -probar -proceso -producto -proeza -profesor -programa -prole -promesa -pronto -propio -próximo -prueba -público -puchero -pudor -pueblo -puerta -puesto -pulga -pulir -pulmón -pulpo -pulso -puma -punto -puñal -puño -pupa -pupila -puré -quedar -queja -quemar -querer -queso -quieto -química -quince -quitar -rábano -rabia -rabo -ración -radical -raíz -rama -rampa -rancho -rango -rapaz -rápido -rapto -rasgo -raspa -rato -rayo -raza -razón -reacción -realidad -rebaño -rebote -recaer -receta -rechazo -recoger -recreo -recto -recurso -red -redondo -reducir -reflejo -reforma -refrán -refugio -regalo -regir -regla -regreso -rehén -reino -reír -reja -relato -relevo -relieve -relleno -reloj -remar -remedio -remo -rencor -rendir -renta -reparto -repetir -reposo -reptil -res -rescate -resina -respeto -resto -resumen -retiro -retorno -retrato -reunir -revés -revista -rey -rezar -rico -riego -rienda -riesgo -rifa -rígido -rigor -rincón -riñón -río -riqueza -risa -ritmo -rito -rizo -roble -roce -rociar -rodar -rodeo -rodilla -roer -rojizo -rojo -romero -romper -ron -ronco -ronda -ropa -ropero -rosa -rosca -rostro -rotar -rubí -rubor -rudo -rueda -rugir -ruido -ruina -ruleta -rulo -rumbo -rumor -ruptura -ruta -rutina -sábado -saber -sabio -sable -sacar -sagaz -sagrado -sala -saldo -salero -salir -salmón -salón -salsa -salto -salud -salvar -samba -sanción -sandía -sanear -sangre -sanidad -sano -santo -sapo -saque -sardina -sartén -sastre -satán -sauna -saxofón -sección -seco -secreto -secta -sed -seguir -seis -sello -selva -semana -semilla -senda -sensor -señal -señor -separar -sepia -sequía -ser -serie -sermón -servir -sesenta -sesión -seta -setenta -severo -sexo -sexto -sidra -siesta -siete -siglo -signo -sílaba -silbar -silencio -silla -símbolo -simio -sirena -sistema -sitio -situar -sobre -socio -sodio -sol -solapa -soldado -soledad -sólido -soltar -solución -sombra -sondeo -sonido -sonoro -sonrisa -sopa -soplar -soporte -sordo -sorpresa -sorteo -sostén -sótano -suave -subir -suceso -sudor -suegra -suelo -sueño -suerte -sufrir -sujeto -sultán -sumar -superar -suplir -suponer -supremo -sur -surco -sureño -surgir -susto -sutil -tabaco -tabique -tabla -tabú -taco -tacto -tajo -talar -talco -talento -talla -talón -tamaño -tambor -tango -tanque -tapa -tapete -tapia -tapón -taquilla -tarde -tarea -tarifa -tarjeta -tarot -tarro -tarta -tatuaje -tauro -taza -tazón -teatro -techo -tecla -técnica -tejado -tejer -tejido -tela -teléfono -tema -temor -templo -tenaz -tender -tener -tenis -tenso -teoría -terapia -terco -término -ternura -terror -tesis -tesoro -testigo -tetera -texto -tez -tibio -tiburón -tiempo -tienda -tierra -tieso -tigre -tijera -tilde -timbre -tímido -timo -tinta -tío -típico -tipo -tira -tirón -titán -títere -título -tiza -toalla -tobillo -tocar -tocino -todo -toga -toldo -tomar -tono -tonto -topar -tope -toque -tórax -torero -tormenta -torneo -toro -torpedo -torre -torso -tortuga -tos -tosco -toser -tóxico -trabajo -tractor -traer -tráfico -trago -traje -tramo -trance -trato -trauma -trazar -trébol -tregua -treinta -tren -trepar -tres -tribu -trigo -tripa -triste -triunfo -trofeo -trompa -tronco -tropa -trote -trozo -truco -trueno -trufa -tubería -tubo -tuerto -tumba -tumor -túnel -túnica -turbina -turismo -turno -tutor -ubicar -úlcera -umbral -unidad -unir -universo -uno -untar -uña -urbano -urbe -urgente -urna -usar -usuario -útil -utopía -uva -vaca -vacío -vacuna -vagar -vago -vaina -vajilla -vale -válido -valle -valor -válvula -vampiro -vara -variar -varón -vaso -vecino -vector -vehículo -veinte -vejez -vela -velero -veloz -vena -vencer -venda -veneno -vengar -venir -venta -venus -ver -verano -verbo -verde -vereda -verja -verso -verter -vía -viaje -vibrar -vicio -víctima -vida -vídeo -vidrio -viejo -viernes -vigor -vil -villa -vinagre -vino -viñedo -violín -viral -virgo -virtud -visor -víspera -vista -vitamina -viudo -vivaz -vivero -vivir -vivo -volcán -volumen -volver -voraz -votar -voto -voz -vuelo -vulgar -yacer -yate -yegua -yema -yerno -yeso -yodo -yoga -yogur -zafiro -zanja -zapato -zarza -zona -zorro -zumo -zurdo diff --git a/lib/x509.py b/lib/x509.py @@ -1,341 +0,0 @@ -#!/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. -from . import util -from .util import profiler, bh2u -import ecdsa -import hashlib - -# algo OIDs -ALGO_RSA_SHA1 = '1.2.840.113549.1.1.5' -ALGO_RSA_SHA256 = '1.2.840.113549.1.1.11' -ALGO_RSA_SHA384 = '1.2.840.113549.1.1.12' -ALGO_RSA_SHA512 = '1.2.840.113549.1.1.13' -ALGO_ECDSA_SHA256 = '1.2.840.10045.4.3.2' - -# prefixes, see http://stackoverflow.com/questions/3713774/c-sharp-how-to-calculate-asn-1-der-encoding-of-a-particular-hash-algorithm -PREFIX_RSA_SHA256 = bytearray( - [0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20]) -PREFIX_RSA_SHA384 = bytearray( - [0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30]) -PREFIX_RSA_SHA512 = bytearray( - [0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40]) - -# types used in ASN1 structured data -ASN1_TYPES = { - 'BOOLEAN' : 0x01, - 'INTEGER' : 0x02, - 'BIT STRING' : 0x03, - 'OCTET STRING' : 0x04, - 'NULL' : 0x05, - 'OBJECT IDENTIFIER': 0x06, - 'SEQUENCE' : 0x70, - 'SET' : 0x71, - 'PrintableString' : 0x13, - 'IA5String' : 0x16, - 'UTCTime' : 0x17, - 'GeneralizedTime' : 0x18, - 'ENUMERATED' : 0x0A, - 'UTF8String' : 0x0C, -} - - -class CertificateError(Exception): - pass - - -# helper functions -def bitstr_to_bytestr(s): - if s[0] != 0x00: - raise TypeError('no padding') - return s[1:] - - -def bytestr_to_int(s): - i = 0 - for char in s: - i <<= 8 - i |= char - return i - - -def decode_OID(s): - r = [] - r.append(s[0] // 40) - r.append(s[0] % 40) - k = 0 - for i in s[1:]: - if i < 128: - r.append(i + 128 * k) - k = 0 - else: - k = (i - 128) + 128 * k - return '.'.join(map(str, r)) - - -def encode_OID(oid): - x = [int(i) for i in oid.split('.')] - s = chr(x[0] * 40 + x[1]) - for i in x[2:]: - ss = chr(i % 128) - while i > 128: - i //= 128 - ss = chr(128 + i % 128) + ss - s += ss - return s - - -class ASN1_Node(bytes): - def get_node(self, ix): - # return index of first byte, first content byte and last byte. - first = self[ix + 1] - if (first & 0x80) == 0: - length = first - ixf = ix + 2 - ixl = ixf + length - 1 - else: - lengthbytes = first & 0x7F - length = bytestr_to_int(self[ix + 2:ix + 2 + lengthbytes]) - ixf = ix + 2 + lengthbytes - ixl = ixf + length - 1 - return ix, ixf, ixl - - def root(self): - return self.get_node(0) - - def next_node(self, node): - ixs, ixf, ixl = node - return self.get_node(ixl + 1) - - def first_child(self, node): - ixs, ixf, ixl = node - if self[ixs] & 0x20 != 0x20: - raise TypeError('Can only open constructed types.', hex(self[ixs])) - return self.get_node(ixf) - - def is_child_of(node1, node2): - ixs, ixf, ixl = node1 - jxs, jxf, jxl = node2 - return ((ixf <= jxs) and (jxl <= ixl)) or ((jxf <= ixs) and (ixl <= jxl)) - - def get_all(self, node): - # return type + length + value - ixs, ixf, ixl = node - return self[ixs:ixl + 1] - - def get_value_of_type(self, node, asn1_type): - # verify type byte and return content - ixs, ixf, ixl = node - if ASN1_TYPES[asn1_type] != self[ixs]: - raise TypeError('Wrong type:', hex(self[ixs]), hex(ASN1_TYPES[asn1_type])) - return self[ixf:ixl + 1] - - def get_value(self, node): - ixs, ixf, ixl = node - return self[ixf:ixl + 1] - - def get_children(self, node): - nodes = [] - ii = self.first_child(node) - nodes.append(ii) - while ii[2] < node[2]: - ii = self.next_node(ii) - nodes.append(ii) - return nodes - - def get_sequence(self): - return list(map(lambda j: self.get_value(j), self.get_children(self.root()))) - - def get_dict(self, node): - p = {} - for ii in self.get_children(node): - for iii in self.get_children(ii): - iiii = self.first_child(iii) - oid = decode_OID(self.get_value_of_type(iiii, 'OBJECT IDENTIFIER')) - iiii = self.next_node(iiii) - value = self.get_value(iiii) - p[oid] = value - return p - - -class X509(object): - def __init__(self, b): - - self.bytes = bytearray(b) - - der = ASN1_Node(b) - root = der.root() - cert = der.first_child(root) - # data for signature - self.data = der.get_all(cert) - - # optional version field - if der.get_value(cert)[0] == 0xa0: - version = der.first_child(cert) - serial_number = der.next_node(version) - else: - serial_number = der.first_child(cert) - self.serial_number = bytestr_to_int(der.get_value_of_type(serial_number, 'INTEGER')) - - # signature algorithm - sig_algo = der.next_node(serial_number) - ii = der.first_child(sig_algo) - self.sig_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER')) - - # issuer - issuer = der.next_node(sig_algo) - self.issuer = der.get_dict(issuer) - - # validity - validity = der.next_node(issuer) - ii = der.first_child(validity) - try: - self.notBefore = der.get_value_of_type(ii, 'UTCTime') - except TypeError: - self.notBefore = der.get_value_of_type(ii, 'GeneralizedTime')[2:] # strip year - ii = der.next_node(ii) - try: - self.notAfter = der.get_value_of_type(ii, 'UTCTime') - except TypeError: - self.notAfter = der.get_value_of_type(ii, 'GeneralizedTime')[2:] # strip year - - # subject - subject = der.next_node(validity) - self.subject = der.get_dict(subject) - subject_pki = der.next_node(subject) - public_key_algo = der.first_child(subject_pki) - ii = der.first_child(public_key_algo) - self.public_key_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER')) - - if self.public_key_algo != '1.2.840.10045.2.1': # for non EC public key - # pubkey modulus and exponent - subject_public_key = der.next_node(public_key_algo) - spk = der.get_value_of_type(subject_public_key, 'BIT STRING') - spk = ASN1_Node(bitstr_to_bytestr(spk)) - r = spk.root() - modulus = spk.first_child(r) - exponent = spk.next_node(modulus) - rsa_n = spk.get_value_of_type(modulus, 'INTEGER') - rsa_e = spk.get_value_of_type(exponent, 'INTEGER') - self.modulus = ecdsa.util.string_to_number(rsa_n) - self.exponent = ecdsa.util.string_to_number(rsa_e) - else: - subject_public_key = der.next_node(public_key_algo) - spk = der.get_value_of_type(subject_public_key, 'BIT STRING') - self.ec_public_key = spk - - # extensions - self.CA = False - self.AKI = None - self.SKI = None - i = subject_pki - while i[2] < cert[2]: - i = der.next_node(i) - d = der.get_dict(i) - for oid, value in d.items(): - value = ASN1_Node(value) - if oid == '2.5.29.19': - # Basic Constraints - self.CA = bool(value) - elif oid == '2.5.29.14': - # Subject Key Identifier - r = value.root() - value = value.get_value_of_type(r, 'OCTET STRING') - self.SKI = bh2u(value) - elif oid == '2.5.29.35': - # Authority Key Identifier - self.AKI = bh2u(value.get_sequence()[0]) - else: - pass - - # cert signature - cert_sig_algo = der.next_node(cert) - ii = der.first_child(cert_sig_algo) - self.cert_sig_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER')) - cert_sig = der.next_node(cert_sig_algo) - self.signature = der.get_value(cert_sig)[1:] - - def get_keyID(self): - # http://security.stackexchange.com/questions/72077/validating-an-ssl-certificate-chain-according-to-rfc-5280-am-i-understanding-th - return self.SKI if self.SKI else repr(self.subject) - - def get_issuer_keyID(self): - return self.AKI if self.AKI else repr(self.issuer) - - def get_common_name(self): - return self.subject.get('2.5.4.3', b'unknown').decode() - - def get_signature(self): - return self.cert_sig_algo, self.signature, self.data - - def check_ca(self): - return self.CA - - def check_date(self): - import time - now = time.time() - TIMESTAMP_FMT = '%y%m%d%H%M%SZ' - not_before = time.mktime(time.strptime(self.notBefore.decode('ascii'), TIMESTAMP_FMT)) - not_after = time.mktime(time.strptime(self.notAfter.decode('ascii'), TIMESTAMP_FMT)) - if not_before > now: - raise CertificateError('Certificate has not entered its valid date range. (%s)' % self.get_common_name()) - if not_after <= now: - raise CertificateError('Certificate has expired. (%s)' % self.get_common_name()) - - def getFingerprint(self): - return hashlib.sha1(self.bytes).digest() - - -@profiler -def load_certificates(ca_path): - from . import pem - ca_list = {} - ca_keyID = {} - # ca_path = '/tmp/tmp.txt' - with open(ca_path, 'r', encoding='utf-8') as f: - s = f.read() - bList = pem.dePemList(s, "CERTIFICATE") - for b in bList: - try: - x = X509(b) - x.check_date() - except BaseException as e: - # with open('/tmp/tmp.txt', 'w') as f: - # f.write(pem.pem(b, 'CERTIFICATE').decode('ascii')) - util.print_error("cert error:", e) - continue - - fp = x.getFingerprint() - ca_list[fp] = x - ca_keyID[x.get_keyID()] = fp - - return ca_list, ca_keyID - - -if __name__ == "__main__": - import requests - - util.set_verbosity(True) - ca_path = requests.certs.where() - ca_list, ca_keyID = load_certificates(ca_path) diff --git a/plugins/README b/plugins/README @@ -1,31 +0,0 @@ -Plugin rules: - - * The plugin system of Electrum is designed to allow the development - of new features without increasing the core code of Electrum. - - * Electrum is written in pure python. if you want to add a feature - that requires non-python libraries, then it must be submitted as a - plugin. If the feature you want to add requires communication with - a remote server (not an Electrum server), then it should be a - plugin as well. If the feature you want to add introduces new - dependencies in the code, then it should probably be a plugin. - - * We expect plugin developers to maintain their plugin code. However, - once a plugin is merged in Electrum, we will have to maintain it - too, because changes in the Electrum code often require updates in - the plugin code. Therefore, plugins have to be easy to maintain. If - we believe that a plugin will create too much maintenance work in - the future, it will be rejected. - - * Plugins should be compatible with Electrum's conventions. If your - plugin does not fit with Electrum's architecture, or if we believe - that it will create too much maintenance work, it will not be - accepted. In particular, do not duplicate existing Electrum code in - your plugin. - - * We may decide to remove a plugin after it has been merged in - Electrum. For this reason, a plugin must be easily removable, - without putting at risk the user's bitcoins. If we feel that a - plugin cannot be removed without threatening users who rely on it, - we will not merge it. - diff --git a/plugins/__init__.py b/plugins/__init__.py @@ -1,26 +0,0 @@ -#!/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. - - diff --git a/plugins/audio_modem/__init__.py b/plugins/audio_modem/__init__.py @@ -1,7 +0,0 @@ -from electrum.i18n import _ - -fullname = _('Audio MODEM') -description = _('Provides support for air-gapped transaction signing.') -requires = [('amodem', 'http://github.com/romanz/amodem/')] -available_for = ['qt'] - diff --git a/plugins/audio_modem/qt.py b/plugins/audio_modem/qt.py @@ -1,128 +0,0 @@ -from functools import partial -import zlib -import json -from io import BytesIO -import sys -import platform - -from electrum.plugins import BasePlugin, hook -from electrum_gui.qt.util import WaitingDialog, EnterButton, WindowModalDialog -from electrum.util import print_msg, print_error -from electrum.i18n import _ - -from PyQt5.QtGui import * -from PyQt5.QtCore import * -from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QPushButton) - -try: - import amodem.audio - import amodem.main - import amodem.config - print_error('Audio MODEM is available.') - amodem.log.addHandler(amodem.logging.StreamHandler(sys.stderr)) - amodem.log.setLevel(amodem.logging.INFO) -except ImportError: - amodem = None - print_error('Audio MODEM is not found.') - - -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - if self.is_available(): - self.modem_config = amodem.config.slowest() - self.library_name = { - 'Linux': 'libportaudio.so' - }[platform.system()] - - def is_available(self): - return amodem is not None - - def requires_settings(self): - return True - - def settings_widget(self, window): - return EnterButton(_('Settings'), partial(self.settings_dialog, window)) - - def settings_dialog(self, window): - d = WindowModalDialog(window, _("Audio Modem Settings")) - - layout = QGridLayout(d) - layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0) - - bitrates = list(sorted(amodem.config.bitrates.keys())) - - def _index_changed(index): - bitrate = bitrates[index] - self.modem_config = amodem.config.bitrates[bitrate] - - combo = QComboBox() - combo.addItems([str(x) for x in bitrates]) - combo.currentIndexChanged.connect(_index_changed) - layout.addWidget(combo, 0, 1) - - ok_button = QPushButton(_("OK")) - ok_button.clicked.connect(d.accept) - layout.addWidget(ok_button, 1, 1) - - return bool(d.exec_()) - - @hook - def transaction_dialog(self, dialog): - b = QPushButton() - b.setIcon(QIcon(":icons/speaker.png")) - - def handler(): - blob = json.dumps(dialog.tx.as_dict()) - self._send(parent=dialog, blob=blob) - b.clicked.connect(handler) - dialog.sharing_buttons.insert(-1, b) - - @hook - def scan_text_edit(self, parent): - parent.addButton(':icons/microphone.png', partial(self._recv, parent), - _("Read from microphone")) - - @hook - def show_text_edit(self, parent): - def handler(): - blob = str(parent.toPlainText()) - self._send(parent=parent, blob=blob) - parent.addButton(':icons/speaker.png', handler, _("Send to speaker")) - - def _audio_interface(self): - interface = amodem.audio.Interface(config=self.modem_config) - return interface.load(self.library_name) - - def _send(self, parent, blob): - def sender_thread(): - with self._audio_interface() as interface: - src = BytesIO(blob) - dst = interface.player() - amodem.main.send(config=self.modem_config, src=src, dst=dst) - - print_msg('Sending:', repr(blob)) - blob = zlib.compress(blob.encode('ascii')) - - kbps = self.modem_config.modem_bps / 1e3 - msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps) - WaitingDialog(parent, msg, sender_thread) - - def _recv(self, parent): - def receiver_thread(): - with self._audio_interface() as interface: - src = interface.recorder() - dst = BytesIO() - amodem.main.recv(config=self.modem_config, src=src, dst=dst) - return dst.getvalue() - - def on_finished(blob): - if blob: - blob = zlib.decompress(blob).decode('ascii') - print_msg('Received:', repr(blob)) - parent.setText(blob) - - kbps = self.modem_config.modem_bps / 1e3 - msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps) - WaitingDialog(parent, msg, receiver_thread, on_finished) diff --git a/plugins/cosigner_pool/__init__.py b/plugins/cosigner_pool/__init__.py @@ -1,9 +0,0 @@ -from electrum.i18n import _ -fullname = _('Cosigner Pool') -description = ' '.join([ - _("This plugin facilitates the use of multi-signatures wallets."), - _("It sends and receives partially signed transactions from/to your cosigner wallet."), - _("Transactions are encrypted and stored on a remote server.") -]) -#requires_wallet_type = ['2of2', '2of3'] -available_for = ['qt'] diff --git a/plugins/cosigner_pool/qt.py b/plugins/cosigner_pool/qt.py @@ -1,228 +0,0 @@ -#!/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 time -from xmlrpc.client import ServerProxy - -from PyQt5.QtGui import * -from PyQt5.QtCore import * -from PyQt5.QtWidgets import QPushButton - -from electrum import bitcoin, util, keystore, ecc -from electrum import transaction -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ -from electrum.wallet import Multisig_Wallet -from electrum.util import bh2u, bfh - -from electrum_gui.qt.transaction_dialog import show_transaction - -import sys -import traceback - - -server = ServerProxy('https://cosigner.electrum.org/', allow_none=True) - - -class Listener(util.DaemonThread): - - def __init__(self, parent): - util.DaemonThread.__init__(self) - self.daemon = True - self.parent = parent - self.received = set() - self.keyhashes = [] - - def set_keyhashes(self, keyhashes): - self.keyhashes = keyhashes - - def clear(self, keyhash): - server.delete(keyhash) - self.received.remove(keyhash) - - def run(self): - while self.running: - if not self.keyhashes: - time.sleep(2) - continue - for keyhash in self.keyhashes: - if keyhash in self.received: - continue - try: - message = server.get(keyhash) - except Exception as e: - self.print_error("cannot contact cosigner pool") - time.sleep(30) - continue - if message: - self.received.add(keyhash) - self.print_error("received message for", keyhash) - self.parent.obj.cosigner_receive_signal.emit( - keyhash, message) - # poll every 30 seconds - time.sleep(30) - - -class QReceiveSignalObject(QObject): - cosigner_receive_signal = pyqtSignal(object, object) - - -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.listener = None - self.obj = QReceiveSignalObject() - self.obj.cosigner_receive_signal.connect(self.on_receive) - self.keys = [] - self.cosigner_list = [] - - @hook - def init_qt(self, gui): - for window in gui.windows: - self.on_new_window(window) - - @hook - def on_new_window(self, window): - self.update(window) - - @hook - def on_close_window(self, window): - self.update(window) - - def is_available(self): - return True - - def update(self, window): - wallet = window.wallet - if type(wallet) != Multisig_Wallet: - return - if self.listener is None: - self.print_error("starting listener") - self.listener = Listener(self) - self.listener.start() - elif self.listener: - self.print_error("shutting down listener") - self.listener.stop() - self.listener = None - self.keys = [] - self.cosigner_list = [] - for key, keystore in wallet.keystores.items(): - xpub = keystore.get_master_public_key() - K = bitcoin.deserialize_xpub(xpub)[-1] - _hash = bh2u(bitcoin.Hash(K)) - if not keystore.is_watching_only(): - self.keys.append((key, _hash, window)) - else: - self.cosigner_list.append((window, xpub, K, _hash)) - if self.listener: - self.listener.set_keyhashes([t[1] for t in self.keys]) - - @hook - def transaction_dialog(self, d): - d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) - b.clicked.connect(lambda: self.do_send(d.tx)) - d.buttons.insert(0, b) - self.transaction_dialog_update(d) - - @hook - def transaction_dialog_update(self, d): - if d.tx.is_complete() or d.wallet.can_sign(d.tx): - d.cosigner_send_button.hide() - return - for window, xpub, K, _hash in self.cosigner_list: - if window.wallet == d.wallet and self.cosigner_can_sign(d.tx, xpub): - d.cosigner_send_button.show() - break - else: - d.cosigner_send_button.hide() - - def cosigner_can_sign(self, tx, cosigner_xpub): - from electrum.keystore import is_xpubkey, parse_xpubkey - xpub_set = set([]) - for txin in tx.inputs(): - for x_pubkey in txin['x_pubkeys']: - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - xpub_set.add(xpub) - return cosigner_xpub in xpub_set - - def do_send(self, tx): - for window, xpub, K, _hash in self.cosigner_list: - if not self.cosigner_can_sign(tx, xpub): - continue - raw_tx_bytes = bfh(str(tx)) - public_key = ecc.ECPubkey(K) - message = public_key.encrypt_message(raw_tx_bytes).decode('ascii') - try: - server.put(_hash, message) - except Exception as e: - traceback.print_exc(file=sys.stdout) - window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + str(e)) - return - window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' + - _("Open your cosigner wallet to retrieve it.")) - - def on_receive(self, keyhash, message): - self.print_error("signal arrived for", keyhash) - for key, _hash, window in self.keys: - if _hash == keyhash: - break - else: - self.print_error("keyhash not found") - return - - wallet = window.wallet - if isinstance(wallet.keystore, keystore.Hardware_KeyStore): - window.show_warning(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' + - _('However, hardware wallets do not support message decryption, ' - 'which makes them not compatible with the current design of cosigner pool.')) - return - elif wallet.has_keystore_encryption(): - password = window.password_dialog(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' + - _('Please enter your password to decrypt it.')) - if not password: - return - else: - password = None - if not window.question(_("An encrypted transaction was retrieved from cosigning pool.") + '\n' + - _("Do you want to open it now?")): - return - - xprv = wallet.keystore.get_master_private_key(password) - if not xprv: - return - try: - k = bitcoin.deserialize_xprv(xprv)[-1] - EC = ecc.ECPrivkey(k) - message = bh2u(EC.decrypt_message(message)) - except Exception as e: - traceback.print_exc(file=sys.stdout) - window.show_error(_('Error decrypting message') + ':\n' + str(e)) - return - - self.listener.clear(keyhash) - tx = transaction.Transaction(message) - show_transaction(tx, window, prompt_if_unsaved=True) diff --git a/plugins/digitalbitbox/__init__.py b/plugins/digitalbitbox/__init__.py @@ -1,6 +0,0 @@ -from electrum.i18n import _ - -fullname = 'Digital Bitbox' -description = _('Provides support for Digital Bitbox hardware wallet') -registers_keystore = ('hardware', 'digitalbitbox', _("Digital Bitbox wallet")) -available_for = ['qt', 'cmdline'] diff --git a/plugins/digitalbitbox/cmdline.py b/plugins/digitalbitbox/cmdline.py @@ -1,14 +0,0 @@ -from electrum.plugins import hook -from .digitalbitbox import DigitalBitboxPlugin -from ..hw_wallet import CmdLineHandler - -class Plugin(DigitalBitboxPlugin): - handler = CmdLineHandler() - @hook - def init_keystore(self, keystore): - if not isinstance(keystore, self.keystore_class): - return - keystore.handler = self.handler - - def create_handler(self, window): - return self.handler diff --git a/plugins/digitalbitbox/digitalbitbox.py b/plugins/digitalbitbox/digitalbitbox.py @@ -1,768 +0,0 @@ -# ---------------------------------------------------------------------------------- -# Electrum plugin for the Digital Bitbox hardware wallet by Shift Devices AG -# digitalbitbox.com -# - -try: - import electrum - from electrum.crypto import Hash, EncodeAES, DecodeAES - from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, is_address, - serialize_xpub, deserialize_xpub) - from electrum import ecc - from electrum.ecc import msg_magic - from electrum.wallet import Standard_Wallet - from electrum import constants - from electrum.transaction import Transaction - from electrum.i18n import _ - from electrum.keystore import Hardware_KeyStore - from ..hw_wallet import HW_PluginBase - from electrum.util import print_error, to_string, UserCancelled - from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET - - import time - import hid - import json - import math - import binascii - import struct - import hashlib - import requests - import base64 - import os - import sys - DIGIBOX = True -except ImportError as e: - DIGIBOX = False - - - -# ---------------------------------------------------------------------------------- -# USB HID interface -# - -def to_hexstr(s): - return binascii.hexlify(s).decode('ascii') - -class DigitalBitbox_Client(): - - def __init__(self, plugin, hidDevice): - self.plugin = plugin - self.dbb_hid = hidDevice - self.opened = True - self.password = None - self.isInitialized = False - self.setupRunning = False - self.usbReportSize = 64 # firmware > v2.0.0 - - - def close(self): - if self.opened: - try: - self.dbb_hid.close() - except: - pass - self.opened = False - - - def timeout(self, cutoff): - pass - - - def label(self): - return " " - - - def is_pairable(self): - return True - - - def is_initialized(self): - return self.dbb_has_password() - - - def is_paired(self): - return self.password is not None - - def has_usable_connection_with_device(self): - try: - self.dbb_has_password() - except BaseException: - return False - return True - - def _get_xpub(self, bip32_path): - if self.check_device_dialog(): - return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8')) - - - def get_xpub(self, bip32_path, xtype): - assert xtype in self.plugin.SUPPORTED_XTYPES - reply = self._get_xpub(bip32_path) - if reply: - xpub = reply['xpub'] - # Change type of xpub to the requested type. The firmware - # only ever returns the mainnet standard type, but it is agnostic - # to the type when signing. - if xtype != 'standard' or constants.net.TESTNET: - _, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub, net=constants.BitcoinMainnet) - xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) - return xpub - else: - raise Exception('no reply') - - - def dbb_has_password(self): - reply = self.hid_send_plain(b'{"ping":""}') - if 'ping' not in reply: - raise Exception(_('Device communication error. Please unplug and replug your Digital Bitbox.')) - if reply['ping'] == 'password': - return True - return False - - - def stretch_key(self, key): - import pbkdf2, hmac - return to_hexstr(pbkdf2.PBKDF2(key, b'Digital Bitbox', iterations = 20480, macmodule = hmac, digestmodule = hashlib.sha512).read(64)) - - - def backup_password_dialog(self): - msg = _("Enter the password used when the backup was created:") - while True: - password = self.handler.get_passphrase(msg, False) - if password is None: - return None - if len(password) < 4: - msg = _("Password must have at least 4 characters.") \ - + "\n\n" + _("Enter password:") - elif len(password) > 64: - msg = _("Password must have less than 64 characters.") \ - + "\n\n" + _("Enter password:") - else: - return password.encode('utf8') - - - def password_dialog(self, msg): - while True: - password = self.handler.get_passphrase(msg, False) - if password is None: - return False - if len(password) < 4: - msg = _("Password must have at least 4 characters.") + \ - "\n\n" + _("Enter password:") - elif len(password) > 64: - msg = _("Password must have less than 64 characters.") + \ - "\n\n" + _("Enter password:") - else: - self.password = password.encode('utf8') - return True - - - def check_device_dialog(self): - # Set password if fresh device - if self.password is None and not self.dbb_has_password(): - if not self.setupRunning: - return False # A fresh device cannot connect to an existing wallet - msg = _("An uninitialized Digital Bitbox is detected.") + " " + \ - _("Enter a new password below.") + "\n\n" + \ - _("REMEMBER THE PASSWORD!") + "\n\n" + \ - _("You cannot access your coins or a backup without the password.") + "\n" + \ - _("A backup is saved automatically when generating a new wallet.") - if self.password_dialog(msg): - reply = self.hid_send_plain(b'{"password":"' + self.password + b'"}') - else: - return False - - # Get password from user if not yet set - msg = _("Enter your Digital Bitbox password:") - while self.password is None: - if not self.password_dialog(msg): - raise UserCancelled() - reply = self.hid_send_encrypt(b'{"led":"blink"}') - if 'error' in reply: - self.password = None - if reply['error']['code'] == 109: - msg = _("Incorrect password entered.") + "\n\n" + \ - reply['error']['message'] + "\n\n" + \ - _("Enter your Digital Bitbox password:") - else: - # Should never occur - msg = _("Unexpected error occurred.") + "\n\n" + \ - reply['error']['message'] + "\n\n" + \ - _("Enter your Digital Bitbox password:") - - # Initialize device if not yet initialized - if not self.setupRunning: - self.isInitialized = True # Wallet exists. Electrum code later checks if the device matches the wallet - elif not self.isInitialized: - reply = self.hid_send_encrypt(b'{"device":"info"}') - if reply['device']['id'] != "": - self.recover_or_erase_dialog() # Already seeded - else: - self.seed_device_dialog() # Seed if not initialized - self.mobile_pairing_dialog() - return self.isInitialized - - - def recover_or_erase_dialog(self): - msg = _("The Digital Bitbox is already seeded. Choose an option:") + "\n" - choices = [ - (_("Create a wallet using the current seed")), - (_("Load a wallet from the micro SD card (the current seed is overwritten)")), - (_("Erase the Digital Bitbox")) - ] - try: - reply = self.handler.win.query_choice(msg, choices) - except Exception: - return # Back button pushed - if reply == 2: - self.dbb_erase() - elif reply == 1: - if not self.dbb_load_backup(): - return - else: - if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']: - raise Exception(_("Full 2FA enabled. This is not supported yet.")) - # Use existing seed - self.isInitialized = True - - - def seed_device_dialog(self): - msg = _("Choose how to initialize your Digital Bitbox:") + "\n" - choices = [ - (_("Generate a new random wallet")), - (_("Load a wallet from the micro SD card")) - ] - try: - reply = self.handler.win.query_choice(msg, choices) - except Exception: - return # Back button pushed - if reply == 0: - self.dbb_generate_wallet() - else: - if not self.dbb_load_backup(show_msg=False): - return - self.isInitialized = True - - def mobile_pairing_dialog(self): - dbb_user_dir = None - if sys.platform == 'darwin': - dbb_user_dir = os.path.join(os.environ.get("HOME", ""), "Library", "Application Support", "DBB") - elif sys.platform == 'win32': - dbb_user_dir = os.path.join(os.environ["APPDATA"], "DBB") - else: - dbb_user_dir = os.path.join(os.environ["HOME"], ".dbb") - - if not dbb_user_dir: - return - - try: - # Python 3.5+ - jsonDecodeError = json.JSONDecodeError - except AttributeError: - jsonDecodeError = ValueError - try: - with open(os.path.join(dbb_user_dir, "config.dat")) as f: - dbb_config = json.load(f) - except (FileNotFoundError, jsonDecodeError): - return - - if 'encryptionprivkey' not in dbb_config or 'comserverchannelid' not in dbb_config: - return - - choices = [ - _('Do not pair'), - _('Import pairing from the Digital Bitbox desktop app'), - ] - try: - reply = self.handler.win.query_choice(_('Mobile pairing options'), choices) - except Exception: - return # Back button pushed - - if reply == 0: - if self.plugin.is_mobile_paired(): - del self.plugin.digitalbitbox_config['encryptionprivkey'] - del self.plugin.digitalbitbox_config['comserverchannelid'] - elif reply == 1: - # import pairing from dbb app - self.plugin.digitalbitbox_config['encryptionprivkey'] = dbb_config['encryptionprivkey'] - self.plugin.digitalbitbox_config['comserverchannelid'] = dbb_config['comserverchannelid'] - self.plugin.config.set_key('digitalbitbox', self.plugin.digitalbitbox_config) - - def dbb_generate_wallet(self): - key = self.stretch_key(self.password) - filename = ("Electrum-" + time.strftime("%Y-%m-%d-%H-%M-%S") + ".pdf") - msg = ('{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, 'Digital Bitbox Electrum Plugin')).encode('utf8') - reply = self.hid_send_encrypt(msg) - if 'error' in reply: - raise Exception(reply['error']['message']) - - - def dbb_erase(self): - self.handler.show_message(_("Are you sure you want to erase the Digital Bitbox?") + "\n\n" + - _("To continue, touch the Digital Bitbox's light for 3 seconds.") + "\n\n" + - _("To cancel, briefly touch the light or wait for the timeout.")) - hid_reply = self.hid_send_encrypt(b'{"reset":"__ERASE__"}') - self.handler.finished() - if 'error' in hid_reply: - raise Exception(hid_reply['error']['message']) - else: - self.password = None - raise Exception('Device erased') - - - def dbb_load_backup(self, show_msg=True): - backups = self.hid_send_encrypt(b'{"backup":"list"}') - if 'error' in backups: - raise Exception(backups['error']['message']) - try: - f = self.handler.win.query_choice(_("Choose a backup file:"), backups['backup']) - except Exception: - return False # Back button pushed - key = self.backup_password_dialog() - if key is None: - raise Exception('Canceled by user') - key = self.stretch_key(key) - if show_msg: - self.handler.show_message(_("Loading backup...") + "\n\n" + - _("To continue, touch the Digital Bitbox's light for 3 seconds.") + "\n\n" + - _("To cancel, briefly touch the light or wait for the timeout.")) - msg = ('{"seed":{"source": "backup", "key": "%s", "filename": "%s"}}' % (key, backups['backup'][f])).encode('utf8') - hid_reply = self.hid_send_encrypt(msg) - self.handler.finished() - if 'error' in hid_reply: - raise Exception(hid_reply['error']['message']) - return True - - - def hid_send_frame(self, data): - HWW_CID = 0xFF000000 - HWW_CMD = 0x80 + 0x40 + 0x01 - data_len = len(data) - seq = 0; - idx = 0; - write = [] - while idx < data_len: - if idx == 0: - # INIT frame - write = data[idx : idx + min(data_len, self.usbReportSize - 7)] - self.dbb_hid.write(b'\0' + struct.pack(">IBH", HWW_CID, HWW_CMD, data_len & 0xFFFF) + write + b'\xEE' * (self.usbReportSize - 7 - len(write))) - else: - # CONT frame - write = data[idx : idx + min(data_len, self.usbReportSize - 5)] - self.dbb_hid.write(b'\0' + struct.pack(">IB", HWW_CID, seq) + write + b'\xEE' * (self.usbReportSize - 5 - len(write))) - seq += 1 - idx += len(write) - - - def hid_read_frame(self): - # INIT response - read = bytearray(self.dbb_hid.read(self.usbReportSize)) - cid = ((read[0] * 256 + read[1]) * 256 + read[2]) * 256 + read[3] - cmd = read[4] - data_len = read[5] * 256 + read[6] - data = read[7:] - idx = len(read) - 7; - while idx < data_len: - # CONT response - read = bytearray(self.dbb_hid.read(self.usbReportSize)) - data += read[5:] - idx += len(read) - 5 - return data - - - def hid_send_plain(self, msg): - reply = "" - try: - serial_number = self.dbb_hid.get_serial_number_string() - if "v2.0." in serial_number or "v1." in serial_number: - hidBufSize = 4096 - self.dbb_hid.write('\0' + msg + '\0' * (hidBufSize - len(msg))) - r = bytearray() - while len(r) < hidBufSize: - r += bytearray(self.dbb_hid.read(hidBufSize)) - else: - self.hid_send_frame(msg) - r = self.hid_read_frame() - r = r.rstrip(b' \t\r\n\0') - r = r.replace(b"\0", b'') - r = to_string(r, 'utf8') - reply = json.loads(r) - except Exception as e: - print_error('Exception caught ' + str(e)) - return reply - - - def hid_send_encrypt(self, msg): - reply = "" - try: - secret = Hash(self.password) - msg = EncodeAES(secret, msg) - reply = self.hid_send_plain(msg) - if 'ciphertext' in reply: - reply = DecodeAES(secret, ''.join(reply["ciphertext"])) - reply = to_string(reply, 'utf8') - reply = json.loads(reply) - if 'error' in reply: - self.password = None - except Exception as e: - print_error('Exception caught ' + str(e)) - return reply - - - -# ---------------------------------------------------------------------------------- -# -# - -class DigitalBitbox_KeyStore(Hardware_KeyStore): - hw_type = 'digitalbitbox' - device = 'DigitalBitbox' - - - def __init__(self, d): - Hardware_KeyStore.__init__(self, d) - self.force_watching_only = False - self.maxInputs = 14 # maximum inputs per single sign command - - - def get_derivation(self): - return str(self.derivation) - - - def is_p2pkh(self): - return self.derivation.startswith("m/44'/") - - - def give_error(self, message, clear_client = False): - if clear_client: - self.client = None - raise Exception(message) - - - def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) - - - def sign_message(self, sequence, message, password): - sig = None - try: - message = message.encode('utf8') - inputPath = self.get_derivation() + "/%d/%d" % sequence - msg_hash = Hash(msg_magic(message)) - inputHash = to_hexstr(msg_hash) - hasharray = [] - hasharray.append({'hash': inputHash, 'keypath': inputPath}) - hasharray = json.dumps(hasharray) - - msg = ('{"sign":{"meta":"sign message", "data":%s}}' % hasharray).encode('utf8') - - dbb_client = self.plugin.get_client(self) - - if not dbb_client.is_paired(): - raise Exception(_("Could not sign message.")) - - reply = dbb_client.hid_send_encrypt(msg) - self.handler.show_message(_("Signing message ...") + "\n\n" + - _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + "\n\n" + - _("To cancel, briefly touch the blinking light or wait for the timeout.")) - reply = dbb_client.hid_send_encrypt(msg) # Send twice, first returns an echo for smart verification (not implemented) - self.handler.finished() - - if 'error' in reply: - raise Exception(reply['error']['message']) - - if 'sign' not in reply: - raise Exception(_("Could not sign message.")) - - if 'recid' in reply['sign'][0]: - # firmware > v2.1.1 - sig_string = binascii.unhexlify(reply['sign'][0]['sig']) - recid = int(reply['sign'][0]['recid'], 16) - sig = ecc.construct_sig65(sig_string, recid, True) - pubkey, compressed = ecc.ECPubkey.from_signature65(sig, msg_hash) - addr = public_key_to_p2pkh(pubkey.get_public_key_bytes(compressed=compressed)) - if ecc.verify_message_with_address(addr, sig, message) is False: - raise Exception(_("Could not sign message")) - elif 'pubkey' in reply['sign'][0]: - # firmware <= v2.1.1 - for recid in range(4): - sig_string = binascii.unhexlify(reply['sign'][0]['sig']) - sig = ecc.construct_sig65(sig_string, recid, True) - try: - addr = public_key_to_p2pkh(binascii.unhexlify(reply['sign'][0]['pubkey'])) - if ecc.verify_message_with_address(addr, sig, message): - break - except Exception: - continue - else: - raise Exception(_("Could not sign message")) - - - except BaseException as e: - self.give_error(e) - return sig - - - def sign_transaction(self, tx, password): - if tx.is_complete(): - return - - try: - p2pkhTransaction = True - derivations = self.get_tx_derivations(tx) - inputhasharray = [] - hasharray = [] - pubkeyarray = [] - - # Build hasharray from inputs - for i, txin in enumerate(tx.inputs()): - if txin['type'] == 'coinbase': - self.give_error("Coinbase not supported") # should never happen - - if txin['type'] != 'p2pkh': - p2pkhTransaction = False - - for x_pubkey in txin['x_pubkeys']: - if x_pubkey in derivations: - index = derivations.get(x_pubkey) - inputPath = "%s/%d/%d" % (self.get_derivation(), index[0], index[1]) - inputHash = Hash(binascii.unhexlify(tx.serialize_preimage(i))) - hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath} - hasharray.append(hasharray_i) - inputhasharray.append(inputHash) - break - else: - self.give_error("No matching x_key for sign_transaction") # should never happen - - # Build pubkeyarray from outputs - for _type, address, amount in tx.outputs(): - assert _type == TYPE_ADDRESS - info = tx.output_info.get(address) - if info is not None: - index, xpubs, m = info - changePath = self.get_derivation() + "/%d/%d" % index - changePubkey = self.derive_pubkey(index[0], index[1]) - pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath} - pubkeyarray.append(pubkeyarray_i) - - # Special serialization of the unsigned transaction for - # the mobile verification app. - # At the moment, verification only works for p2pkh transactions. - if p2pkhTransaction: - class CustomTXSerialization(Transaction): - @classmethod - def input_script(self, txin, estimate_size=False): - if txin['type'] == 'p2pkh': - return Transaction.get_preimage_script(txin) - if txin['type'] == 'p2sh': - # Multisig verification has partial support, but is disabled. This is the - # expected serialization though, so we leave it here until we activate it. - return '00' + push_script(Transaction.get_preimage_script(txin)) - raise Exception("unsupported type %s" % txin['type']) - tx_dbb_serialized = CustomTXSerialization(tx.serialize()).serialize_to_network() - else: - # We only need this for the signing echo / verification. - tx_dbb_serialized = None - - # Build sign command - dbb_signatures = [] - steps = math.ceil(1.0 * len(hasharray) / self.maxInputs) - for step in range(int(steps)): - hashes = hasharray[step * self.maxInputs : (step + 1) * self.maxInputs] - - msg = { - "sign": { - "data": hashes, - "checkpub": pubkeyarray, - }, - } - if tx_dbb_serialized is not None: - msg["sign"]["meta"] = to_hexstr(Hash(tx_dbb_serialized)) - msg = json.dumps(msg).encode('ascii') - dbb_client = self.plugin.get_client(self) - - if not dbb_client.is_paired(): - raise Exception("Could not sign transaction.") - - reply = dbb_client.hid_send_encrypt(msg) - if 'error' in reply: - raise Exception(reply['error']['message']) - - if 'echo' not in reply: - raise Exception("Could not sign transaction.") - - if self.plugin.is_mobile_paired() and tx_dbb_serialized is not None: - reply['tx'] = tx_dbb_serialized - self.plugin.comserver_post_notification(reply) - - if steps > 1: - self.handler.show_message(_("Signing large transaction. Please be patient ...") + "\n\n" + - _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + " " + - _("(Touch {} of {})").format((step + 1), steps) + "\n\n" + - _("To cancel, briefly touch the blinking light or wait for the timeout.") + "\n\n") - else: - self.handler.show_message(_("Signing transaction...") + "\n\n" + - _("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + "\n\n" + - _("To cancel, briefly touch the blinking light or wait for the timeout.")) - - # Send twice, first returns an echo for smart verification - reply = dbb_client.hid_send_encrypt(msg) - self.handler.finished() - - if 'error' in reply: - if reply["error"].get('code') in (600, 601): - # aborted via LED short touch or timeout - raise UserCancelled() - raise Exception(reply['error']['message']) - - if 'sign' not in reply: - raise Exception("Could not sign transaction.") - - dbb_signatures.extend(reply['sign']) - - # Fill signatures - if len(dbb_signatures) != len(tx.inputs()): - raise Exception("Incorrect number of transactions signed.") # Should never occur - for i, txin in enumerate(tx.inputs()): - num = txin['num_sig'] - for pubkey in txin['pubkeys']: - signatures = list(filter(None, txin['signatures'])) - if len(signatures) == num: - break # txin is complete - ii = txin['pubkeys'].index(pubkey) - signed = dbb_signatures[i] - if 'recid' in signed: - # firmware > v2.1.1 - recid = int(signed['recid'], 16) - s = binascii.unhexlify(signed['sig']) - h = inputhasharray[i] - pk = ecc.ECPubkey.from_sig_string(s, recid, h) - pk = pk.get_public_key_hex(compressed=True) - elif 'pubkey' in signed: - # firmware <= v2.1.1 - pk = signed['pubkey'] - if pk != pubkey: - continue - sig_r = int(signed['sig'][:64], 16) - sig_s = int(signed['sig'][64:], 16) - sig = ecc.der_sig_from_r_and_s(sig_r, sig_s) - sig = to_hexstr(sig) + '01' - tx.add_signature_to_txin(i, ii, sig) - except UserCancelled: - raise - except BaseException as e: - self.give_error(e, True) - else: - print_error("Transaction is_complete", tx.is_complete()) - tx.raw = tx.serialize() - - -class DigitalBitboxPlugin(HW_PluginBase): - - libraries_available = DIGIBOX - keystore_class = DigitalBitbox_KeyStore - client = None - DEVICE_IDS = [ - (0x03eb, 0x2402) # Digital Bitbox - ] - SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') - - def __init__(self, parent, config, name): - HW_PluginBase.__init__(self, parent, config, name) - if self.libraries_available: - self.device_manager().register_devices(self.DEVICE_IDS) - - self.digitalbitbox_config = self.config.get('digitalbitbox', {}) - - - def get_dbb_device(self, device): - dev = hid.device() - dev.open_path(device.path) - return dev - - - def create_client(self, device, handler): - if device.interface_number == 0 or device.usage_page == 0xffff: - if handler: - self.handler = handler - client = self.get_dbb_device(device) - if client is not None: - client = DigitalBitbox_Client(self, client) - return client - else: - return None - - - def setup_device(self, device_info, wizard, purpose): - devmgr = self.device_manager() - device_id = device_info.device.id_ - client = devmgr.client_by_id(device_id) - if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) - client.handler = self.create_handler(wizard) - if purpose == HWD_SETUP_NEW_WALLET: - client.setupRunning = True - client.get_xpub("m/44'/0'", 'standard') - - - def is_mobile_paired(self): - return 'encryptionprivkey' in self.digitalbitbox_config - - - def comserver_post_notification(self, payload): - assert self.is_mobile_paired(), "unexpected mobile pairing error" - url = 'https://digitalbitbox.com/smartverification/index.php' - key_s = base64.b64decode(self.digitalbitbox_config['encryptionprivkey']) - args = 'c=data&s=0&dt=0&uuid=%s&pl=%s' % ( - self.digitalbitbox_config['comserverchannelid'], - EncodeAES(key_s, json.dumps(payload).encode('ascii')).decode('ascii'), - ) - try: - requests.post(url, args) - except Exception as e: - self.handler.show_error(str(e)) - - - def get_xpub(self, device_id, derivation, xtype, wizard): - if xtype not in self.SUPPORTED_XTYPES: - raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - client.handler = self.create_handler(wizard) - client.check_device_dialog() - xpub = client.get_xpub(derivation, xtype) - return xpub - - - def get_client(self, keystore, force_pair=True): - devmgr = self.device_manager() - handler = keystore.handler - with devmgr.hid_lock: - client = devmgr.client_for_keystore(self, handler, keystore, force_pair) - if client is not None: - client.check_device_dialog() - return client - - def show_address(self, wallet, address, keystore=None): - if keystore is None: - keystore = wallet.get_keystore() - if not self.show_address_helper(wallet, address, keystore): - return - if type(wallet) is not Standard_Wallet: - keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) - return - if not self.is_mobile_paired(): - keystore.handler.show_error(_('This function is only available after pairing your {} with a mobile device.').format(self.device)) - return - if not keystore.is_p2pkh(): - keystore.handler.show_error(_('This function is only available for p2pkh keystores when using {}.').format(self.device)) - return - change, index = wallet.get_address_index(address) - keypath = '%s/%d/%d' % (keystore.derivation, change, index) - xpub = self.get_client(keystore)._get_xpub(keypath) - verify_request_payload = { - "type": 'p2pkh', - "echo": xpub['echo'], - } - self.comserver_post_notification(verify_request_payload) diff --git a/plugins/digitalbitbox/qt.py b/plugins/digitalbitbox/qt.py @@ -1,43 +0,0 @@ -from functools import partial - -from ..hw_wallet.qt import QtHandlerBase, QtPluginBase -from .digitalbitbox import DigitalBitboxPlugin - -from electrum.i18n import _ -from electrum.plugins import hook -from electrum.wallet import Standard_Wallet - - -class Plugin(DigitalBitboxPlugin, QtPluginBase): - icon_unpaired = ":icons/digitalbitbox_unpaired.png" - icon_paired = ":icons/digitalbitbox.png" - - def create_handler(self, window): - return DigitalBitbox_Handler(window) - - @hook - def receive_menu(self, menu, addrs, wallet): - if type(wallet) is not Standard_Wallet: - return - - keystore = wallet.get_keystore() - if type(keystore) is not self.keystore_class: - return - - if not self.is_mobile_paired(): - return - - if not keystore.is_p2pkh(): - return - - if len(addrs) == 1: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) - - menu.addAction(_("Show on {}").format(self.device), show_address) - - -class DigitalBitbox_Handler(QtHandlerBase): - - def __init__(self, win): - super(DigitalBitbox_Handler, self).__init__(win, 'Digital Bitbox') diff --git a/plugins/email_requests/__init__.py b/plugins/email_requests/__init__.py @@ -1,5 +0,0 @@ -from electrum.i18n import _ - -fullname = _('Email') -description = _("Send and receive payment request with an email account") -available_for = ['qt'] diff --git a/plugins/email_requests/qt.py b/plugins/email_requests/qt.py @@ -1,271 +0,0 @@ -#!/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 random -import time -import threading -import base64 -from functools import partial -import traceback -import sys - -import smtplib -import imaplib -import email -from email.mime.multipart import MIMEMultipart -from email.mime.base import MIMEBase -from email.encoders import encode_base64 - -from PyQt5.QtGui import * -from PyQt5.QtCore import * -from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit, - QInputDialog) - -from electrum.plugins import BasePlugin, hook -from electrum.paymentrequest import PaymentRequest -from electrum.i18n import _ -from electrum.util import PrintError -from electrum_gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, - WindowModalDialog, get_parent_main_window) - - -class Processor(threading.Thread, PrintError): - polling_interval = 5*60 - - def __init__(self, imap_server, username, password, callback): - threading.Thread.__init__(self) - self.daemon = True - self.username = username - self.password = password - self.imap_server = imap_server - self.on_receive = callback - self.M = None - self.reset_connect_wait() - - def reset_connect_wait(self): - self.connect_wait = 100 # ms, between failed connection attempts - - def poll(self): - try: - self.M.select() - except: - return - typ, data = self.M.search(None, 'ALL') - for num in str(data[0], 'utf8').split(): - typ, msg_data = self.M.fetch(num, '(RFC822)') - msg = email.message_from_bytes(msg_data[0][1]) - p = msg.get_payload() - if not msg.is_multipart(): - p = [p] - continue - for item in p: - if item.get_content_type() == "application/bitcoin-paymentrequest": - pr_str = item.get_payload() - pr_str = base64.b64decode(pr_str) - self.on_receive(pr_str) - - def run(self): - while True: - try: - self.M = imaplib.IMAP4_SSL(self.imap_server) - self.M.login(self.username, self.password) - except BaseException as e: - self.print_error('connecting failed: {}'.format(e)) - self.connect_wait *= 2 - else: - self.reset_connect_wait() - # Reconnect when host changes - while self.M and self.M.host == self.imap_server: - try: - self.poll() - except BaseException as e: - self.print_error('polling failed: {}'.format(e)) - break - time.sleep(self.polling_interval) - time.sleep(random.randint(0, self.connect_wait)) - - def send(self, recipient, message, payment_request): - msg = MIMEMultipart() - msg['Subject'] = message - msg['To'] = recipient - msg['From'] = self.username - part = MIMEBase('application', "bitcoin-paymentrequest") - part.set_payload(payment_request) - encode_base64(part) - part.add_header('Content-Disposition', 'attachment; filename="payreq.btc"') - msg.attach(part) - try: - s = smtplib.SMTP_SSL(self.imap_server, timeout=2) - s.login(self.username, self.password) - s.sendmail(self.username, [recipient], msg.as_string()) - s.quit() - except BaseException as e: - self.print_error(e) - - -class QEmailSignalObject(QObject): - email_new_invoice_signal = pyqtSignal() - - -class Plugin(BasePlugin): - - def fullname(self): - return 'Email' - - def description(self): - return _("Send and receive payment requests via email") - - def is_available(self): - return True - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.imap_server = self.config.get('email_server', '') - self.username = self.config.get('email_username', '') - self.password = self.config.get('email_password', '') - if self.imap_server and self.username and self.password: - self.processor = Processor(self.imap_server, self.username, self.password, self.on_receive) - self.processor.start() - self.obj = QEmailSignalObject() - self.obj.email_new_invoice_signal.connect(self.new_invoice) - self.wallets = set() - - def on_receive(self, pr_str): - self.print_error('received payment request') - self.pr = PaymentRequest(pr_str) - self.obj.email_new_invoice_signal.emit() - - @hook - def load_wallet(self, wallet, main_window): - self.wallets |= {wallet} - - @hook - def close_wallet(self, wallet): - self.wallets -= {wallet} - - def new_invoice(self): - for wallet in self.wallets: - wallet.invoices.add(self.pr) - #main_window.invoice_list.update() - - @hook - def receive_list_menu(self, menu, addr): - window = get_parent_main_window(menu) - menu.addAction(_("Send via e-mail"), lambda: self.send(window, addr)) - - def send(self, window, addr): - from electrum import paymentrequest - r = window.wallet.receive_requests.get(addr) - message = r.get('memo', '') - if r.get('signature'): - pr = paymentrequest.serialize_request(r) - else: - pr = paymentrequest.make_request(self.config, r) - if not pr: - return - recipient, ok = QInputDialog.getText(window, 'Send request', 'Email invoice to:') - if not ok: - return - recipient = str(recipient) - payload = pr.SerializeToString() - self.print_error('sending mail to', recipient) - try: - # FIXME this runs in the GUI thread and blocks it... - self.processor.send(recipient, message, payload) - except BaseException as e: - traceback.print_exc(file=sys.stderr) - window.show_message(str(e)) - else: - window.show_message(_('Request sent.')) - - def requires_settings(self): - return True - - def settings_widget(self, window): - return EnterButton(_('Settings'), partial(self.settings_dialog, window)) - - def settings_dialog(self, window): - d = WindowModalDialog(window, _("Email settings")) - d.setMinimumSize(500, 200) - - vbox = QVBoxLayout(d) - vbox.addWidget(QLabel(_('Server hosting your email account'))) - grid = QGridLayout() - vbox.addLayout(grid) - grid.addWidget(QLabel('Server (IMAP)'), 0, 0) - server_e = QLineEdit() - server_e.setText(self.imap_server) - grid.addWidget(server_e, 0, 1) - - grid.addWidget(QLabel('Username'), 1, 0) - username_e = QLineEdit() - username_e.setText(self.username) - grid.addWidget(username_e, 1, 1) - - grid.addWidget(QLabel('Password'), 2, 0) - password_e = QLineEdit() - password_e.setText(self.password) - grid.addWidget(password_e, 2, 1) - - vbox.addStretch() - vbox.addLayout(Buttons(CloseButton(d), OkButton(d))) - - if not d.exec_(): - return - - server = str(server_e.text()) - self.config.set_key('email_server', server) - self.imap_server = server - - username = str(username_e.text()) - self.config.set_key('email_username', username) - self.username = username - - password = str(password_e.text()) - self.config.set_key('email_password', password) - self.password = password - - check_connection = CheckConnectionThread(server, username, password) - check_connection.connection_error_signal.connect(lambda e: window.show_message( - _("Unable to connect to mail server:\n {}").format(e) + "\n" + - _("Please check your connection and credentials.") - )) - check_connection.start() - - -class CheckConnectionThread(QThread): - connection_error_signal = pyqtSignal(str) - - def __init__(self, server, username, password): - super().__init__() - self.server = server - self.username = username - self.password = password - - def run(self): - try: - conn = imaplib.IMAP4_SSL(self.server) - conn.login(self.username, self.password) - except BaseException as e: - self.connection_error_signal.emit(str(e)) diff --git a/plugins/greenaddress_instant/__init__.py b/plugins/greenaddress_instant/__init__.py @@ -1,5 +0,0 @@ -from electrum.i18n import _ - -fullname = 'GreenAddress instant' -description = _("Allows validating if your transactions have instant confirmations by GreenAddress") -available_for = ['qt'] diff --git a/plugins/greenaddress_instant/qt.py b/plugins/greenaddress_instant/qt.py @@ -1,107 +0,0 @@ -#!/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 base64 -import urllib.parse -import sys -import requests - -from PyQt5.QtWidgets import QApplication, QPushButton - -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ - - - -class Plugin(BasePlugin): - - button_label = _("Verify GA instant") - - @hook - def transaction_dialog(self, d): - d.verify_button = QPushButton(self.button_label) - d.verify_button.clicked.connect(lambda: self.do_verify(d)) - d.buttons.insert(0, d.verify_button) - self.transaction_dialog_update(d) - - def get_my_addr(self, d): - """Returns the address for given tx which can be used to request - instant confirmation verification from GreenAddress""" - for addr, _ in d.tx.get_outputs(): - if d.wallet.is_mine(addr): - return addr - return None - - @hook - def transaction_dialog_update(self, d): - if d.tx.is_complete() and self.get_my_addr(d): - d.verify_button.show() - else: - d.verify_button.hide() - - def do_verify(self, d): - tx = d.tx - wallet = d.wallet - window = d.main_window - - if wallet.is_watching_only(): - d.show_critical(_('This feature is not available for watch-only wallets.')) - return - - # 1. get the password and sign the verification request - password = None - if wallet.has_keystore_encryption(): - msg = _('GreenAddress requires your signature \n' - 'to verify that transaction is instant.\n' - 'Please enter your password to sign a\n' - 'verification request.') - password = window.password_dialog(msg, parent=d) - if not password: - return - try: - d.verify_button.setText(_('Verifying...')) - QApplication.processEvents() # update the button label - - addr = self.get_my_addr(d) - message = "Please verify if %s is GreenAddress instant confirmed" % tx.txid() - sig = wallet.sign_message(addr, message, password) - sig = base64.b64encode(sig).decode('ascii') - - # 2. send the request - response = requests.request("GET", ("https://greenaddress.it/verify/?signature=%s&txhash=%s" % (urllib.parse.quote(sig), tx.txid())), - headers = {'User-Agent': 'Electrum'}) - response = response.json() - - # 3. display the result - if response.get('verified'): - d.show_message(_('{} is covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification successful!')) - else: - d.show_critical(_('{} is not covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification failed!')) - except BaseException as e: - import traceback - traceback.print_exc(file=sys.stdout) - d.show_error(str(e)) - finally: - d.verify_button.setText(self.button_label) diff --git a/plugins/hw_wallet/__init__.py b/plugins/hw_wallet/__init__.py @@ -1,2 +0,0 @@ -from .plugin import HW_PluginBase -from .cmdline import CmdLineHandler diff --git a/plugins/hw_wallet/cmdline.py b/plugins/hw_wallet/cmdline.py @@ -1,46 +0,0 @@ -from electrum.util import print_msg, print_error, raw_input - - -class CmdLineHandler: - - def get_passphrase(self, msg, confirm): - import getpass - print_msg(msg) - return getpass.getpass('') - - def get_pin(self, msg): - t = { 'a':'7', 'b':'8', 'c':'9', 'd':'4', 'e':'5', 'f':'6', 'g':'1', 'h':'2', 'i':'3'} - print_msg(msg) - print_msg("a b c\nd e f\ng h i\n-----") - o = raw_input() - try: - return ''.join(map(lambda x: t[x], o)) - except KeyError as e: - raise Exception("Character {} not in matrix!".format(e)) from e - - def prompt_auth(self, msg): - import getpass - print_msg(msg) - response = getpass.getpass('') - if len(response) == 0: - return None - return response - - def yes_no_question(self, msg): - print_msg(msg) - return raw_input() in 'yY' - - def stop(self): - pass - - def show_message(self, msg, on_cancel=None): - print_msg(msg) - - def show_error(self, msg, blocking=False): - print_msg(msg) - - def update_status(self, b): - print_error('hw device status', b) - - def finished(self): - pass diff --git a/plugins/hw_wallet/plugin.py b/plugins/hw_wallet/plugin.py @@ -1,89 +0,0 @@ -#!/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 electrum.plugins import BasePlugin, hook -from electrum.i18n import _ -from electrum.bitcoin import is_address - - -class HW_PluginBase(BasePlugin): - # Derived classes provide: - # - # class-static variables: client_class, firmware_URL, handler_class, - # libraries_available, libraries_URL, minimum_firmware, - # wallet_class, ckd_public, types, HidTransport - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.device = self.keystore_class.device - self.keystore_class.plugin = self - - def is_enabled(self): - return True - - def device_manager(self): - return self.parent.device_manager - - @hook - def close_wallet(self, wallet): - for keystore in wallet.get_keystores(): - if isinstance(keystore, self.keystore_class): - self.device_manager().unpair_xpub(keystore.xpub) - - def setup_device(self, device_info, wizard, purpose): - """Called when creating a new wallet or when using the device to decrypt - an existing wallet. Select the device to use. If the device is - uninitialized, go through the initialization process. - """ - raise NotImplementedError() - - def show_address(self, wallet, address, keystore=None): - pass # implemented in child classes - - def show_address_helper(self, wallet, address, keystore=None): - if keystore is None: - keystore = wallet.get_keystore() - if not is_address(address): - keystore.handler.show_error(_('Invalid Bitcoin Address')) - return False - if not wallet.is_mine(address): - keystore.handler.show_error(_('Address not in wallet.')) - return False - if type(keystore) != self.keystore_class: - return False - return True - - -def is_any_tx_output_on_change_branch(tx): - if not hasattr(tx, 'output_info'): - return False - for _type, address, amount in tx.outputs(): - info = tx.output_info.get(address) - if info is not None: - index, xpubs, m = info - if index[0] == 1: - return True - return False diff --git a/plugins/hw_wallet/qt.py b/plugins/hw_wallet/qt.py @@ -1,235 +0,0 @@ -#!/usr/bin/env python3 -# -*- 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. - -import threading - -from PyQt5.Qt import QVBoxLayout, QLabel -from electrum_gui.qt.password_dialog import PasswordDialog, PW_PASSPHRASE -from electrum_gui.qt.util import * - -from electrum.i18n import _ -from electrum.util import PrintError - -# The trickiest thing about this handler was getting windows properly -# parented on macOS. -class QtHandlerBase(QObject, PrintError): - '''An interface between the GUI (here, QT) and the device handling - logic for handling I/O.''' - - passphrase_signal = pyqtSignal(object, object) - message_signal = pyqtSignal(object, object) - error_signal = pyqtSignal(object, object) - word_signal = pyqtSignal(object) - clear_signal = pyqtSignal() - query_signal = pyqtSignal(object, object) - yes_no_signal = pyqtSignal(object) - status_signal = pyqtSignal(object) - - def __init__(self, win, device): - super(QtHandlerBase, self).__init__() - self.clear_signal.connect(self.clear_dialog) - self.error_signal.connect(self.error_dialog) - self.message_signal.connect(self.message_dialog) - self.passphrase_signal.connect(self.passphrase_dialog) - self.word_signal.connect(self.word_dialog) - self.query_signal.connect(self.win_query_choice) - self.yes_no_signal.connect(self.win_yes_no_question) - self.status_signal.connect(self._update_status) - self.win = win - self.device = device - self.dialog = None - self.done = threading.Event() - - def top_level_window(self): - return self.win.top_level_window() - - def update_status(self, paired): - self.status_signal.emit(paired) - - def _update_status(self, paired): - if hasattr(self, 'button'): - button = self.button - icon = button.icon_paired if paired else button.icon_unpaired - button.setIcon(QIcon(icon)) - - def query_choice(self, msg, labels): - self.done.clear() - self.query_signal.emit(msg, labels) - self.done.wait() - return self.choice - - def yes_no_question(self, msg): - self.done.clear() - self.yes_no_signal.emit(msg) - self.done.wait() - return self.ok - - def show_message(self, msg, on_cancel=None): - self.message_signal.emit(msg, on_cancel) - - def show_error(self, msg, blocking=False): - self.done.clear() - self.error_signal.emit(msg, blocking) - if blocking: - self.done.wait() - - def finished(self): - self.clear_signal.emit() - - def get_word(self, msg): - self.done.clear() - self.word_signal.emit(msg) - self.done.wait() - return self.word - - def get_passphrase(self, msg, confirm): - self.done.clear() - self.passphrase_signal.emit(msg, confirm) - self.done.wait() - return self.passphrase - - def passphrase_dialog(self, msg, confirm): - # If confirm is true, require the user to enter the passphrase twice - parent = self.top_level_window() - if confirm: - d = PasswordDialog(parent, None, msg, PW_PASSPHRASE) - confirmed, p, passphrase = d.run() - else: - d = WindowModalDialog(parent, _("Enter Passphrase")) - pw = QLineEdit() - pw.setEchoMode(2) - pw.setMinimumWidth(200) - vbox = QVBoxLayout() - vbox.addWidget(WWLabel(msg)) - vbox.addWidget(pw) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - d.setLayout(vbox) - passphrase = pw.text() if d.exec_() else None - self.passphrase = passphrase - self.done.set() - - def word_dialog(self, msg): - dialog = WindowModalDialog(self.top_level_window(), "") - hbox = QHBoxLayout(dialog) - hbox.addWidget(QLabel(msg)) - text = QLineEdit() - text.setMaximumWidth(100) - text.returnPressed.connect(dialog.accept) - hbox.addWidget(text) - hbox.addStretch(1) - dialog.exec_() # Firmware cannot handle cancellation - self.word = text.text() - self.done.set() - - def message_dialog(self, msg, on_cancel): - # Called more than once during signing, to confirm output and fee - self.clear_dialog() - title = _('Please check your {} device').format(self.device) - self.dialog = dialog = WindowModalDialog(self.top_level_window(), title) - l = QLabel(msg) - vbox = QVBoxLayout(dialog) - vbox.addWidget(l) - if on_cancel: - dialog.rejected.connect(on_cancel) - vbox.addLayout(Buttons(CancelButton(dialog))) - dialog.show() - - def error_dialog(self, msg, blocking): - self.win.show_error(msg, parent=self.top_level_window()) - if blocking: - self.done.set() - - def clear_dialog(self): - if self.dialog: - self.dialog.accept() - self.dialog = None - - def win_query_choice(self, msg, labels): - self.choice = self.win.query_choice(msg, labels) - self.done.set() - - def win_yes_no_question(self, msg): - self.ok = self.win.question(msg) - self.done.set() - - - -from electrum.plugins import hook -from electrum.util import UserCancelled -from electrum_gui.qt.main_window import StatusBarButton - -class QtPluginBase(object): - - @hook - def load_wallet(self, wallet, window): - for keystore in wallet.get_keystores(): - if not isinstance(keystore, self.keystore_class): - continue - if not self.libraries_available: - if hasattr(self, 'libraries_available_message'): - message = self.libraries_available_message + '\n' - else: - message = _("Cannot find python library for") + " '%s'.\n" % self.name - message += _("Make sure you install it with python3") - window.show_error(message) - return - tooltip = self.device + '\n' + (keystore.label or 'unnamed') - cb = partial(self.show_settings_dialog, window, keystore) - button = StatusBarButton(QIcon(self.icon_unpaired), tooltip, cb) - button.icon_paired = self.icon_paired - button.icon_unpaired = self.icon_unpaired - window.statusBar().addPermanentWidget(button) - handler = self.create_handler(window) - handler.button = button - keystore.handler = handler - keystore.thread = TaskThread(window, window.on_error) - self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window) - # Trigger a pairing - keystore.thread.add(partial(self.get_client, keystore)) - - def choose_device(self, window, keystore): - '''This dialog box should be usable even if the user has - forgotten their PIN or it is in bootloader mode.''' - device_id = self.device_manager().xpub_id(keystore.xpub) - if not device_id: - try: - info = self.device_manager().select_device(self, keystore.handler, keystore) - except UserCancelled: - return - device_id = info.device.id_ - return device_id - - def show_settings_dialog(self, window, keystore): - device_id = self.choose_device(window, keystore) - - def add_show_address_on_hw_device_button_for_receive_addr(self, wallet, keystore, main_window): - plugin = keystore.plugin - receive_address_e = main_window.receive_address_e - - def show_address(): - addr = receive_address_e.text() - keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore)) - receive_address_e.addButton(":icons/eye1.png", show_address, _("Show on {}").format(plugin.device)) diff --git a/plugins/keepkey/__init__.py b/plugins/keepkey/__init__.py @@ -1,7 +0,0 @@ -from electrum.i18n import _ - -fullname = 'KeepKey' -description = _('Provides support for KeepKey hardware wallet') -requires = [('keepkeylib','github.com/keepkey/python-keepkey')] -registers_keystore = ('hardware', 'keepkey', _("KeepKey wallet")) -available_for = ['qt', 'cmdline'] diff --git a/plugins/keepkey/client.py b/plugins/keepkey/client.py @@ -1,14 +0,0 @@ -from keepkeylib.client import proto, BaseClient, ProtocolMixin -from .clientbase import KeepKeyClientBase - -class KeepKeyClient(KeepKeyClientBase, ProtocolMixin, BaseClient): - def __init__(self, transport, handler, plugin): - BaseClient.__init__(self, transport) - ProtocolMixin.__init__(self, transport) - KeepKeyClientBase.__init__(self, handler, plugin, proto) - - def recovery_device(self, *args): - ProtocolMixin.recovery_device(self, False, *args) - - -KeepKeyClientBase.wrap_methods(KeepKeyClient) diff --git a/plugins/keepkey/clientbase.py b/plugins/keepkey/clientbase.py @@ -1,250 +0,0 @@ -import time -from struct import pack - -from electrum.i18n import _ -from electrum.util import PrintError, UserCancelled -from electrum.keystore import bip39_normalize_passphrase -from electrum.bitcoin import serialize_xpub - - -class GuiMixin(object): - # Requires: self.proto, self.device - - messages = { - 3: _("Confirm the transaction output on your {} device"), - 4: _("Confirm internal entropy on your {} device to begin"), - 5: _("Write down the seed word shown on your {}"), - 6: _("Confirm on your {} that you want to wipe it clean"), - 7: _("Confirm on your {} device the message to sign"), - 8: _("Confirm the total amount spent and the transaction fee on your " - "{} device"), - 10: _("Confirm wallet address on your {} device"), - 'default': _("Check your {} device to continue"), - } - - def callback_Failure(self, msg): - # BaseClient's unfortunate call() implementation forces us to - # raise exceptions on failure in order to unwind the stack. - # However, making the user acknowledge they cancelled - # gets old very quickly, so we suppress those. The NotInitialized - # one is misnamed and indicates a passphrase request was cancelled. - if msg.code in (self.types.Failure_PinCancelled, - self.types.Failure_ActionCancelled, - self.types.Failure_NotInitialized): - raise UserCancelled() - raise RuntimeError(msg.message) - - def callback_ButtonRequest(self, msg): - message = self.msg - if not message: - message = self.messages.get(msg.code, self.messages['default']) - self.handler.show_message(message.format(self.device), self.cancel) - return self.proto.ButtonAck() - - def callback_PinMatrixRequest(self, msg): - if msg.type == 2: - msg = _("Enter a new PIN for your {}:") - elif msg.type == 3: - msg = (_("Re-enter the new PIN for your {}.\n\n" - "NOTE: the positions of the numbers have changed!")) - else: - msg = _("Enter your current {} PIN:") - pin = self.handler.get_pin(msg.format(self.device)) - if len(pin) > 9: - self.handler.show_error(_('The PIN cannot be longer than 9 characters.')) - pin = '' # to cancel below - if not pin: - return self.proto.Cancel() - return self.proto.PinMatrixAck(pin=pin) - - def callback_PassphraseRequest(self, req): - if self.creating_wallet: - msg = _("Enter a passphrase to generate this wallet. Each time " - "you use this wallet your {} will prompt you for the " - "passphrase. If you forget the passphrase you cannot " - "access the bitcoins in the wallet.").format(self.device) - else: - msg = _("Enter the passphrase to unlock this wallet:") - passphrase = self.handler.get_passphrase(msg, self.creating_wallet) - if passphrase is None: - return self.proto.Cancel() - passphrase = bip39_normalize_passphrase(passphrase) - - ack = self.proto.PassphraseAck(passphrase=passphrase) - length = len(ack.passphrase) - if length > 50: - self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length)) - return self.proto.Cancel() - return ack - - def callback_WordRequest(self, msg): - self.step += 1 - msg = _("Step {}/24. Enter seed word as explained on " - "your {}:").format(self.step, self.device) - word = self.handler.get_word(msg) - # Unfortunately the device can't handle self.proto.Cancel() - return self.proto.WordAck(word=word) - - def callback_CharacterRequest(self, msg): - char_info = self.handler.get_char(msg) - if not char_info: - return self.proto.Cancel() - return self.proto.CharacterAck(**char_info) - - -class KeepKeyClientBase(GuiMixin, PrintError): - - def __init__(self, handler, plugin, proto): - assert hasattr(self, 'tx_api') # ProtocolMixin already constructed? - self.proto = proto - self.device = plugin.device - self.handler = handler - self.tx_api = plugin - self.types = plugin.types - self.msg = None - self.creating_wallet = False - self.used() - - def __str__(self): - return "%s/%s" % (self.label(), self.features.device_id) - - def label(self): - '''The name given by the user to the device.''' - return self.features.label - - def is_initialized(self): - '''True if initialized, False if wiped.''' - return self.features.initialized - - def is_pairable(self): - return not self.features.bootloader_mode - - def has_usable_connection_with_device(self): - try: - res = self.ping("electrum pinging device") - assert res == "electrum pinging device" - except BaseException: - return False - return True - - def used(self): - self.last_operation = time.time() - - def prevent_timeouts(self): - self.last_operation = float('inf') - - def timeout(self, cutoff): - '''Time out the client if the last operation was before cutoff.''' - if self.last_operation < cutoff: - self.print_error("timed out") - self.clear_session() - - @staticmethod - def expand_path(n): - '''Convert bip32 path to list of uint32 integers with prime flags - 0/-1/1' -> [0, 0x80000001, 0x80000001]''' - # This code is similar to code in trezorlib where it unfortunately - # is not declared as a staticmethod. Our n has an extra element. - PRIME_DERIVATION_FLAG = 0x80000000 - path = [] - for x in n.split('/')[1:]: - prime = 0 - if x.endswith("'"): - x = x.replace('\'', '') - prime = PRIME_DERIVATION_FLAG - if x.startswith('-'): - prime = PRIME_DERIVATION_FLAG - path.append(abs(int(x)) | prime) - return path - - def cancel(self): - '''Provided here as in keepkeylib but not trezorlib.''' - self.transport.write(self.proto.Cancel()) - - def i4b(self, x): - return pack('>I', x) - - def get_xpub(self, bip32_path, xtype): - address_n = self.expand_path(bip32_path) - creating = False - node = self.get_public_node(address_n, creating).node - return serialize_xpub(xtype, node.chain_code, node.public_key, node.depth, self.i4b(node.fingerprint), self.i4b(node.child_num)) - - def toggle_passphrase(self): - if self.features.passphrase_protection: - self.msg = _("Confirm on your {} device to disable passphrases") - else: - self.msg = _("Confirm on your {} device to enable passphrases") - enabled = not self.features.passphrase_protection - self.apply_settings(use_passphrase=enabled) - - def change_label(self, label): - self.msg = _("Confirm the new label on your {} device") - self.apply_settings(label=label) - - def change_homescreen(self, homescreen): - self.msg = _("Confirm on your {} device to change your home screen") - self.apply_settings(homescreen=homescreen) - - def set_pin(self, remove): - if remove: - self.msg = _("Confirm on your {} device to disable PIN protection") - elif self.features.pin_protection: - self.msg = _("Confirm on your {} device to change your PIN") - else: - self.msg = _("Confirm on your {} device to set a PIN") - self.change_pin(remove) - - def clear_session(self): - '''Clear the session to force pin (and passphrase if enabled) - re-entry. Does not leak exceptions.''' - self.print_error("clear session:", self) - self.prevent_timeouts() - try: - super(KeepKeyClientBase, self).clear_session() - except BaseException as e: - # If the device was removed it has the same effect... - self.print_error("clear_session: ignoring error", str(e)) - - def get_public_node(self, address_n, creating): - self.creating_wallet = creating - return super(KeepKeyClientBase, self).get_public_node(address_n) - - def close(self): - '''Called when Our wallet was closed or the device removed.''' - self.print_error("closing client") - self.clear_session() - # Release the device - self.transport.close() - - def firmware_version(self): - f = self.features - return (f.major_version, f.minor_version, f.patch_version) - - def atleast_version(self, major, minor=0, patch=0): - return self.firmware_version() >= (major, minor, patch) - - @staticmethod - def wrapper(func): - '''Wrap methods to clear any message box they opened.''' - - def wrapped(self, *args, **kwargs): - try: - self.prevent_timeouts() - return func(self, *args, **kwargs) - finally: - self.used() - self.handler.finished() - self.creating_wallet = False - self.msg = None - - return wrapped - - @staticmethod - def wrap_methods(cls): - for method in ['apply_settings', 'change_pin', - 'get_address', 'get_public_node', - 'load_device_by_mnemonic', 'load_device_by_xprv', - 'recovery_device', 'reset_device', 'sign_message', - 'sign_tx', 'wipe_device']: - setattr(cls, method, cls.wrapper(getattr(cls, method))) diff --git a/plugins/keepkey/cmdline.py b/plugins/keepkey/cmdline.py @@ -1,14 +0,0 @@ -from electrum.plugins import hook -from .keepkey import KeepKeyPlugin -from ..hw_wallet import CmdLineHandler - -class Plugin(KeepKeyPlugin): - handler = CmdLineHandler() - @hook - def init_keystore(self, keystore): - if not isinstance(keystore, self.keystore_class): - return - keystore.handler = self.handler - - def create_handler(self, window): - return self.handler diff --git a/plugins/keepkey/keepkey.py b/plugins/keepkey/keepkey.py @@ -1,438 +0,0 @@ -from binascii import hexlify, unhexlify -import traceback -import sys - -from electrum.util import bfh, bh2u, UserCancelled -from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, - TYPE_ADDRESS, TYPE_SCRIPT, - is_segwit_address) -from electrum import constants -from electrum.i18n import _ -from electrum.plugins import BasePlugin -from electrum.transaction import deserialize, Transaction -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey -from electrum.wallet import Standard_Wallet -from electrum.base_wizard import ScriptTypeNotSupported - -from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch - - -# TREZOR initialization methods -TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) - - -class KeepKey_KeyStore(Hardware_KeyStore): - hw_type = 'keepkey' - device = 'KeepKey' - - def get_derivation(self): - return self.derivation - - def is_segwit(self): - return self.derivation.startswith("m/49'/") - - def get_client(self, force_pair=True): - return self.plugin.get_client(self, force_pair) - - def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) - - def sign_message(self, sequence, message, password): - client = self.get_client() - address_path = self.get_derivation() + "/%d/%d"%sequence - address_n = client.expand_path(address_path) - msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) - return msg_sig.signature - - def sign_transaction(self, tx, password): - if tx.is_complete(): - return - # previous transactions used as inputs - prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} - for txin in tx.inputs(): - pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) - tx_hash = txin['prevout_hash'] - if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) - prev_tx[tx_hash] = txin['prev_tx'] - for x_pubkey in x_pubkeys: - if not is_xpubkey(x_pubkey): - continue - xpub, s = parse_xpubkey(x_pubkey) - if xpub == self.get_master_public_key(): - xpub_path[xpub] = self.get_derivation() - - self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) - - -class KeepKeyPlugin(HW_PluginBase): - # Derived classes provide: - # - # class-static variables: client_class, firmware_URL, handler_class, - # libraries_available, libraries_URL, minimum_firmware, - # wallet_class, ckd_public, types, HidTransport - - firmware_URL = 'https://www.keepkey.com' - libraries_URL = 'https://github.com/keepkey/python-keepkey' - minimum_firmware = (1, 0, 0) - keystore_class = KeepKey_KeyStore - SUPPORTED_XTYPES = ('standard', ) - - MAX_LABEL_LEN = 32 - - def __init__(self, parent, config, name): - HW_PluginBase.__init__(self, parent, config, name) - - try: - from . import client - import keepkeylib - import keepkeylib.ckd_public - import keepkeylib.transport_hid - self.client_class = client.KeepKeyClient - self.ckd_public = keepkeylib.ckd_public - self.types = keepkeylib.client.types - self.DEVICE_IDS = keepkeylib.transport_hid.DEVICE_IDS - self.device_manager().register_devices(self.DEVICE_IDS) - self.libraries_available = True - except ImportError: - self.libraries_available = False - - def hid_transport(self, pair): - from keepkeylib.transport_hid import HidTransport - return HidTransport(pair) - - def _try_hid(self, device): - self.print_error("Trying to connect over USB...") - if device.interface_number == 1: - pair = [None, device.path] - else: - pair = [device.path, None] - - try: - return self.hid_transport(pair) - except BaseException as e: - # see fdb810ba622dc7dbe1259cbafb5b28e19d2ab114 - # raise - self.print_error("cannot connect at", device.path, str(e)) - return None - - def create_client(self, device, handler): - transport = self._try_hid(device) - if not transport: - self.print_error("cannot connect to device") - return - - self.print_error("connected to device at", device.path) - - client = self.client_class(transport, handler, self) - - # Try a ping for device sanity - try: - client.ping('t') - except BaseException as e: - self.print_error("ping failed", str(e)) - return None - - if not client.atleast_version(*self.minimum_firmware): - msg = (_('Outdated {} firmware for device labelled {}. Please ' - 'download the updated firmware from {}') - .format(self.device, client.label(), self.firmware_URL)) - self.print_error(msg) - handler.show_error(msg) - return None - - return client - - def get_client(self, keystore, force_pair=True): - devmgr = self.device_manager() - handler = keystore.handler - with devmgr.hid_lock: - client = devmgr.client_for_keystore(self, handler, keystore, force_pair) - # returns the client for a given keystore. can use xpub - if client: - client.used() - return client - - def get_coin_name(self): - return "Testnet" if constants.net.TESTNET else "Bitcoin" - - def initialize_device(self, device_id, wizard, handler): - # Initialization method - msg = _("Choose how you want to initialize your {}.\n\n" - "The first two methods are secure as no secret information " - "is entered into your computer.\n\n" - "For the last two methods you input secrets on your keyboard " - "and upload them to your {}, and so you should " - "only do those on a computer you know to be trustworthy " - "and free of malware." - ).format(self.device, self.device) - choices = [ - # Must be short as QT doesn't word-wrap radio button text - (TIM_NEW, _("Let the device generate a completely new seed randomly")), - (TIM_RECOVER, _("Recover from a seed you have previously written down")), - (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), - (TIM_PRIVKEY, _("Upload a master private key")) - ] - def f(method): - import threading - settings = self.request_trezor_init_settings(wizard, method, self.device) - t = threading.Thread(target=self._initialize_device_safe, args=(settings, method, device_id, wizard, handler)) - t.setDaemon(True) - t.start() - exit_code = wizard.loop.exec_() - if exit_code != 0: - # this method (initialize_device) was called with the expectation - # of leaving the device in an initialized state when finishing. - # signal that this is not the case: - raise UserCancelled() - wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f) - - def _initialize_device_safe(self, settings, method, device_id, wizard, handler): - exit_code = 0 - try: - self._initialize_device(settings, method, device_id, wizard, handler) - except UserCancelled: - exit_code = 1 - except BaseException as e: - traceback.print_exc(file=sys.stderr) - handler.show_error(str(e)) - exit_code = 1 - finally: - wizard.loop.exit(exit_code) - - def _initialize_device(self, settings, method, device_id, wizard, handler): - item, label, pin_protection, passphrase_protection = settings - - language = 'english' - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - - if method == TIM_NEW: - strength = 64 * (item + 2) # 128, 192 or 256 - client.reset_device(True, strength, passphrase_protection, - pin_protection, label, language) - elif method == TIM_RECOVER: - word_count = 6 * (item + 2) # 12, 18 or 24 - client.step = 0 - client.recovery_device(word_count, passphrase_protection, - pin_protection, label, language) - elif method == TIM_MNEMONIC: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_mnemonic(str(item), pin, - passphrase_protection, - label, language) - else: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_xprv(item, pin, passphrase_protection, - label, language) - - def setup_device(self, device_info, wizard, purpose): - devmgr = self.device_manager() - device_id = device_info.device.id_ - client = devmgr.client_by_id(device_id) - if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) - # fixme: we should use: client.handler = wizard - client.handler = self.create_handler(wizard) - if not device_info.initialized: - self.initialize_device(device_id, wizard, client.handler) - client.get_xpub('m', 'standard') - client.used() - - def get_xpub(self, device_id, derivation, xtype, wizard): - if xtype not in self.SUPPORTED_XTYPES: - raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - client.handler = wizard - xpub = client.get_xpub(derivation, xtype) - client.used() - return xpub - - def sign_transaction(self, keystore, tx, prev_tx, xpub_path): - self.prev_tx = prev_tx - self.xpub_path = xpub_path - client = self.get_client(keystore) - inputs = self.tx_inputs(tx, True, keystore.is_segwit()) - outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.is_segwit()) - signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0] - signatures = [(bh2u(x) + '01') for x in signatures] - tx.update_signatures(signatures) - - def show_address(self, wallet, address, keystore=None): - if keystore is None: - keystore = wallet.get_keystore() - if not self.show_address_helper(wallet, address, keystore): - return - if type(wallet) is not Standard_Wallet: - keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) - return - client = self.get_client(wallet.keystore) - if not client.atleast_version(1, 3): - wallet.keystore.handler.show_error(_("Your device firmware is too old")) - return - change, index = wallet.get_address_index(address) - derivation = wallet.keystore.derivation - address_path = "%s/%d/%d"%(derivation, change, index) - address_n = client.expand_path(address_path) - segwit = wallet.keystore.is_segwit() - script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDADDRESS - client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) - - def tx_inputs(self, tx, for_sig=False, segwit=False): - inputs = [] - for txin in tx.inputs(): - txinputtype = self.types.TxInputType() - if txin['type'] == 'coinbase': - prev_hash = "\0"*32 - prev_index = 0xffffffff # signed int -1 - else: - if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = parse_xpubkey(x_pubkey) - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - txinputtype.script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDADDRESS - else: - def f(x_pubkey): - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(0, bfh(x_pubkey)) - s = [] - node = self.ckd_public.deserialize(xpub) - return self.types.HDNodePathType(node=node, address_n=s) - pubkeys = map(f, x_pubkeys) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures')), - m=txin.get('num_sig'), - ) - script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDMULTISIG - txinputtype = self.types.TxInputType( - script_type=script_type, - multisig=multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - if 'value' in txin: - txinputtype.amount = txin['value'] - txinputtype.prev_hash = prev_hash - txinputtype.prev_index = prev_index - - if txin.get('scriptSig') is not None: - script_sig = bfh(txin['scriptSig']) - txinputtype.script_sig = script_sig - - txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) - - inputs.append(txinputtype) - - return inputs - - def tx_outputs(self, derivation, tx, segwit=False): - - def create_output_by_derivation(info): - index, xpubs, m = info - if len(xpubs) == 1: - script_type = self.types.PAYTOP2SHWITNESS if segwit else self.types.PAYTOADDRESS - address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) - txoutputtype = self.types.TxOutputType( - amount=amount, - script_type=script_type, - address_n=address_n, - ) - else: - script_type = self.types.PAYTOP2SHWITNESS if segwit else self.types.PAYTOMULTISIG - address_n = self.client_class.expand_path("/%d/%d" % index) - nodes = map(self.ckd_public.deserialize, xpubs) - pubkeys = [self.types.HDNodePathType(node=node, address_n=address_n) for node in nodes] - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * len(pubkeys), - m=m) - txoutputtype = self.types.TxOutputType( - multisig=multisig, - amount=amount, - address_n=self.client_class.expand_path(derivation + "/%d/%d" % index), - script_type=script_type) - return txoutputtype - - def create_output_by_address(): - txoutputtype = self.types.TxOutputType() - txoutputtype.amount = amount - if _type == TYPE_SCRIPT: - txoutputtype.script_type = self.types.PAYTOOPRETURN - txoutputtype.op_return_data = address[2:] - elif _type == TYPE_ADDRESS: - if is_segwit_address(address): - txoutputtype.script_type = self.types.PAYTOWITNESS - else: - addrtype, hash_160 = b58_address_to_hash160(address) - if addrtype == constants.net.ADDRTYPE_P2PKH: - txoutputtype.script_type = self.types.PAYTOADDRESS - elif addrtype == constants.net.ADDRTYPE_P2SH: - txoutputtype.script_type = self.types.PAYTOSCRIPTHASH - else: - raise Exception('addrtype: ' + str(addrtype)) - txoutputtype.address = address - return txoutputtype - - outputs = [] - has_change = False - any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - - for _type, address, amount in tx.outputs(): - use_create_by_derivation = False - - info = tx.output_info.get(address) - if info is not None and not has_change: - index, xpubs, m = info - on_change_branch = index[0] == 1 - # prioritise hiding outputs on the 'change' branch from user - # because no more than one change address allowed - if on_change_branch == any_output_on_change_branch: - use_create_by_derivation = True - has_change = True - - if use_create_by_derivation: - txoutputtype = create_output_by_derivation(info) - else: - txoutputtype = create_output_by_address() - outputs.append(txoutputtype) - - return outputs - - def electrum_tx_to_txtype(self, tx): - t = self.types.TransactionType() - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] - inputs = self.tx_inputs(tx) - t.inputs.extend(inputs) - for vout in d['outputs']: - o = t.bin_outputs.add() - o.amount = vout['value'] - o.script_pubkey = bfh(vout['scriptPubKey']) - return t - - # This function is called from the TREZOR libraries (via tx_api) - def get_tx(self, tx_hash): - tx = self.prev_tx[tx_hash] - return self.electrum_tx_to_txtype(tx) diff --git a/plugins/keepkey/qt.py b/plugins/keepkey/qt.py @@ -1,586 +0,0 @@ -from functools import partial -import threading - -from PyQt5.Qt import Qt -from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton -from PyQt5.Qt import QVBoxLayout, QLabel - -from electrum_gui.qt.util import * -from electrum.i18n import _ -from electrum.plugins import hook, DeviceMgr -from electrum.util import PrintError, UserCancelled, bh2u -from electrum.wallet import Wallet, Standard_Wallet - -from ..hw_wallet.qt import QtHandlerBase, QtPluginBase -from .keepkey import KeepKeyPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC - - -PASSPHRASE_HELP_SHORT =_( - "Passphrases allow you to access new wallets, each " - "hidden behind a particular case-sensitive passphrase.") -PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _( - "You need to create a separate Electrum wallet for each passphrase " - "you use as they each generate different addresses. Changing " - "your passphrase does not lose other wallets, each is still " - "accessible behind its own passphrase.") -RECOMMEND_PIN = _( - "You should enable PIN protection. Your PIN is the only protection " - "for your bitcoins if your device is lost or stolen.") -PASSPHRASE_NOT_PIN = _( - "If you forget a passphrase you will be unable to access any " - "bitcoins in the wallet behind it. A passphrase is not a PIN. " - "Only change this if you are sure you understand it.") -CHARACTER_RECOVERY = ( - "Use the recovery cipher shown on your device to input your seed words. " - "The cipher changes with every keypress.\n" - "After at most 4 letters the device will auto-complete a word.\n" - "Press SPACE or the Accept Word button to accept the device's auto-" - "completed word and advance to the next one.\n" - "Press BACKSPACE to go back a character or word.\n" - "Press ENTER or the Seed Entered button once the last word in your " - "seed is auto-completed.") - -class CharacterButton(QPushButton): - def __init__(self, text=None): - QPushButton.__init__(self, text) - - def keyPressEvent(self, event): - event.setAccepted(False) # Pass through Enter and Space keys - - -class CharacterDialog(WindowModalDialog): - - def __init__(self, parent): - super(CharacterDialog, self).__init__(parent) - self.setWindowTitle(_("KeepKey Seed Recovery")) - self.character_pos = 0 - self.word_pos = 0 - self.loop = QEventLoop() - self.word_help = QLabel() - self.char_buttons = [] - - vbox = QVBoxLayout(self) - vbox.addWidget(WWLabel(CHARACTER_RECOVERY)) - hbox = QHBoxLayout() - hbox.addWidget(self.word_help) - for i in range(4): - char_button = CharacterButton('*') - char_button.setMaximumWidth(36) - self.char_buttons.append(char_button) - hbox.addWidget(char_button) - self.accept_button = CharacterButton(_("Accept Word")) - self.accept_button.clicked.connect(partial(self.process_key, 32)) - self.rejected.connect(partial(self.loop.exit, 1)) - hbox.addWidget(self.accept_button) - hbox.addStretch(1) - vbox.addLayout(hbox) - - self.finished_button = QPushButton(_("Seed Entered")) - self.cancel_button = QPushButton(_("Cancel")) - self.finished_button.clicked.connect(partial(self.process_key, - Qt.Key_Return)) - self.cancel_button.clicked.connect(self.rejected) - buttons = Buttons(self.finished_button, self.cancel_button) - vbox.addSpacing(40) - vbox.addLayout(buttons) - self.refresh() - self.show() - - def refresh(self): - self.word_help.setText("Enter seed word %2d:" % (self.word_pos + 1)) - self.accept_button.setEnabled(self.character_pos >= 3) - self.finished_button.setEnabled((self.word_pos in (11, 17, 23) - and self.character_pos >= 3)) - for n, button in enumerate(self.char_buttons): - button.setEnabled(n == self.character_pos) - if n == self.character_pos: - button.setFocus() - - def is_valid_alpha_space(self, key): - # Auto-completion requires at least 3 characters - if key == ord(' ') and self.character_pos >= 3: - return True - # Firmware aborts protocol if the 5th character is non-space - if self.character_pos >= 4: - return False - return (key >= ord('a') and key <= ord('z') - or (key >= ord('A') and key <= ord('Z'))) - - def process_key(self, key): - self.data = None - if key == Qt.Key_Return and self.finished_button.isEnabled(): - self.data = {'done': True} - elif key == Qt.Key_Backspace and (self.word_pos or self.character_pos): - self.data = {'delete': True} - elif self.is_valid_alpha_space(key): - self.data = {'character': chr(key).lower()} - if self.data: - self.loop.exit(0) - - def keyPressEvent(self, event): - self.process_key(event.key()) - if not self.data: - QDialog.keyPressEvent(self, event) - - def get_char(self, word_pos, character_pos): - self.word_pos = word_pos - self.character_pos = character_pos - self.refresh() - if self.loop.exec_(): - self.data = None # User cancelled - - -class QtHandler(QtHandlerBase): - - char_signal = pyqtSignal(object) - pin_signal = pyqtSignal(object) - close_char_dialog_signal = pyqtSignal() - - def __init__(self, win, pin_matrix_widget_class, device): - super(QtHandler, self).__init__(win, device) - self.char_signal.connect(self.update_character_dialog) - self.pin_signal.connect(self.pin_dialog) - self.close_char_dialog_signal.connect(self._close_char_dialog) - self.pin_matrix_widget_class = pin_matrix_widget_class - self.character_dialog = None - - def get_char(self, msg): - self.done.clear() - self.char_signal.emit(msg) - self.done.wait() - data = self.character_dialog.data - if not data or 'done' in data: - self.close_char_dialog_signal.emit() - return data - - def _close_char_dialog(self): - if self.character_dialog: - self.character_dialog.accept() - self.character_dialog = None - - def get_pin(self, msg): - self.done.clear() - self.pin_signal.emit(msg) - self.done.wait() - return self.response - - def pin_dialog(self, msg): - # Needed e.g. when resetting a device - self.clear_dialog() - dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN")) - matrix = self.pin_matrix_widget_class() - vbox = QVBoxLayout() - vbox.addWidget(QLabel(msg)) - vbox.addWidget(matrix) - vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) - dialog.setLayout(vbox) - dialog.exec_() - self.response = str(matrix.get_value()) - self.done.set() - - def update_character_dialog(self, msg): - if not self.character_dialog: - self.character_dialog = CharacterDialog(self.top_level_window()) - self.character_dialog.get_char(msg.word_pos, msg.character_pos) - self.done.set() - - - -class QtPlugin(QtPluginBase): - # Derived classes must provide the following class-static variables: - # icon_file - # pin_matrix_widget_class - - def create_handler(self, window): - return QtHandler(window, self.pin_matrix_widget_class(), self.device) - - @hook - def receive_menu(self, menu, addrs, wallet): - if type(wallet) is not Standard_Wallet: - return - keystore = wallet.get_keystore() - if type(keystore) == self.keystore_class and len(addrs) == 1: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0])) - menu.addAction(_("Show on {}").format(self.device), show_address) - - def show_settings_dialog(self, window, keystore): - device_id = self.choose_device(window, keystore) - if device_id: - SettingsDialog(window, self, keystore, device_id).exec_() - - def request_trezor_init_settings(self, wizard, method, device): - vbox = QVBoxLayout() - next_enabled = True - label = QLabel(_("Enter a label to name your device:")) - name = QLineEdit() - hl = QHBoxLayout() - hl.addWidget(label) - hl.addWidget(name) - hl.addStretch(1) - vbox.addLayout(hl) - - def clean_text(widget): - text = widget.toPlainText().strip() - return ' '.join(text.split()) - - if method in [TIM_NEW, TIM_RECOVER]: - gb = QGroupBox() - hbox1 = QHBoxLayout() - gb.setLayout(hbox1) - # KeepKey recovery doesn't need a word count - if method == TIM_NEW: - vbox.addWidget(gb) - gb.setTitle(_("Select your seed length:")) - bg = QButtonGroup() - for i, count in enumerate([12, 18, 24]): - rb = QRadioButton(gb) - rb.setText(_("{} words").format(count)) - bg.addButton(rb) - bg.setId(rb, i) - hbox1.addWidget(rb) - rb.setChecked(True) - cb_pin = QCheckBox(_('Enable PIN protection')) - cb_pin.setChecked(True) - else: - text = QTextEdit() - text.setMaximumHeight(60) - if method == TIM_MNEMONIC: - msg = _("Enter your BIP39 mnemonic:") - else: - msg = _("Enter the master private key beginning with xprv:") - def set_enabled(): - from electrum.keystore import is_xprv - wizard.next_button.setEnabled(is_xprv(clean_text(text))) - text.textChanged.connect(set_enabled) - next_enabled = False - - vbox.addWidget(QLabel(msg)) - vbox.addWidget(text) - pin = QLineEdit() - pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) - pin.setMaximumWidth(100) - hbox_pin = QHBoxLayout() - hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) - hbox_pin.addWidget(pin) - hbox_pin.addStretch(1) - - if method in [TIM_NEW, TIM_RECOVER]: - vbox.addWidget(WWLabel(RECOMMEND_PIN)) - vbox.addWidget(cb_pin) - else: - vbox.addLayout(hbox_pin) - - passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - cb_phrase = QCheckBox(_('Enable passphrases')) - cb_phrase.setChecked(False) - vbox.addWidget(passphrase_msg) - vbox.addWidget(passphrase_warning) - vbox.addWidget(cb_phrase) - - wizard.exec_layout(vbox, next_enabled=next_enabled) - - if method in [TIM_NEW, TIM_RECOVER]: - item = bg.checkedId() - pin = cb_pin.isChecked() - else: - item = ' '.join(str(clean_text(text)).split()) - pin = str(pin.text()) - - return (item, name.text(), pin, cb_phrase.isChecked()) - - -class Plugin(KeepKeyPlugin, QtPlugin): - icon_paired = ":icons/keepkey.png" - icon_unpaired = ":icons/keepkey_unpaired.png" - - @classmethod - def pin_matrix_widget_class(self): - from keepkeylib.qt.pinmatrix import PinMatrixWidget - return PinMatrixWidget - - -class SettingsDialog(WindowModalDialog): - '''This dialog doesn't require a device be paired with a wallet. - We want users to be able to wipe a device even if they've forgotten - their PIN.''' - - def __init__(self, window, plugin, keystore, device_id): - title = _("{} Settings").format(plugin.device) - super(SettingsDialog, self).__init__(window, title) - self.setMaximumWidth(540) - - devmgr = plugin.device_manager() - config = devmgr.config - handler = keystore.handler - thread = keystore.thread - hs_rows, hs_cols = (64, 128) - - def invoke_client(method, *args, **kw_args): - unpair_after = kw_args.pop('unpair_after', False) - - def task(): - client = devmgr.client_by_id(device_id) - if not client: - raise RuntimeError("Device not connected") - if method: - getattr(client, method)(*args, **kw_args) - if unpair_after: - devmgr.unpair_id(device_id) - return client.features - - thread.add(task, on_success=update) - - def update(features): - self.features = features - set_label_enabled() - bl_hash = bh2u(features.bootloader_hash) - bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) - noyes = [_("No"), _("Yes")] - endis = [_("Enable Passphrases"), _("Disable Passphrases")] - disen = [_("Disabled"), _("Enabled")] - setchange = [_("Set a PIN"), _("Change PIN")] - - version = "%d.%d.%d" % (features.major_version, - features.minor_version, - features.patch_version) - coins = ", ".join(coin.coin_name for coin in features.coins) - - device_label.setText(features.label) - pin_set_label.setText(noyes[features.pin_protection]) - passphrases_label.setText(disen[features.passphrase_protection]) - bl_hash_label.setText(bl_hash) - label_edit.setText(features.label) - device_id_label.setText(features.device_id) - initialized_label.setText(noyes[features.initialized]) - version_label.setText(version) - coins_label.setText(coins) - clear_pin_button.setVisible(features.pin_protection) - clear_pin_warning.setVisible(features.pin_protection) - pin_button.setText(setchange[features.pin_protection]) - pin_msg.setVisible(not features.pin_protection) - passphrase_button.setText(endis[features.passphrase_protection]) - language_label.setText(features.language) - - def set_label_enabled(): - label_apply.setEnabled(label_edit.text() != self.features.label) - - def rename(): - invoke_client('change_label', label_edit.text()) - - def toggle_passphrase(): - title = _("Confirm Toggle Passphrase Protection") - currently_enabled = self.features.passphrase_protection - if currently_enabled: - msg = _("After disabling passphrases, you can only pair this " - "Electrum wallet if it had an empty passphrase. " - "If its passphrase was not empty, you will need to " - "create a new wallet with the install wizard. You " - "can use this wallet again at any time by re-enabling " - "passphrases and entering its passphrase.") - else: - msg = _("Your current Electrum wallet can only be used with " - "an empty passphrase. You must create a separate " - "wallet with the install wizard for other passphrases " - "as each one generates a new set of addresses.") - msg += "\n\n" + _("Are you sure you want to proceed?") - if not self.question(msg, title=title): - return - invoke_client('toggle_passphrase', unpair_after=currently_enabled) - - def change_homescreen(): - from PIL import Image # FIXME - dialog = QFileDialog(self, _("Choose Homescreen")) - filename, __ = dialog.getOpenFileName() - if filename: - im = Image.open(str(filename)) - if im.size != (hs_cols, hs_rows): - raise Exception('Image must be 64 x 128 pixels') - im = im.convert('1') - pix = im.load() - img = '' - for j in range(hs_rows): - for i in range(hs_cols): - img += '1' if pix[i, j] else '0' - img = ''.join(chr(int(img[i:i + 8], 2)) - for i in range(0, len(img), 8)) - invoke_client('change_homescreen', img) - - def clear_homescreen(): - invoke_client('change_homescreen', '\x00') - - def set_pin(): - invoke_client('set_pin', remove=False) - - def clear_pin(): - invoke_client('set_pin', remove=True) - - def wipe_device(): - wallet = window.wallet - if wallet and sum(wallet.get_balance()): - title = _("Confirm Device Wipe") - msg = _("Are you SURE you want to wipe the device?\n" - "Your wallet still has bitcoins in it!") - if not self.question(msg, title=title, - icon=QMessageBox.Critical): - return - invoke_client('wipe_device', unpair_after=True) - - def slider_moved(): - mins = timeout_slider.sliderPosition() - timeout_minutes.setText(_("%2d minutes") % mins) - - def slider_released(): - config.set_session_timeout(timeout_slider.sliderPosition() * 60) - - # Information tab - info_tab = QWidget() - info_layout = QVBoxLayout(info_tab) - info_glayout = QGridLayout() - info_glayout.setColumnStretch(2, 1) - device_label = QLabel() - pin_set_label = QLabel() - passphrases_label = QLabel() - version_label = QLabel() - device_id_label = QLabel() - bl_hash_label = QLabel() - bl_hash_label.setWordWrap(True) - coins_label = QLabel() - coins_label.setWordWrap(True) - language_label = QLabel() - initialized_label = QLabel() - rows = [ - (_("Device Label"), device_label), - (_("PIN set"), pin_set_label), - (_("Passphrases"), passphrases_label), - (_("Firmware Version"), version_label), - (_("Device ID"), device_id_label), - (_("Bootloader Hash"), bl_hash_label), - (_("Supported Coins"), coins_label), - (_("Language"), language_label), - (_("Initialized"), initialized_label), - ] - for row_num, (label, widget) in enumerate(rows): - info_glayout.addWidget(QLabel(label), row_num, 0) - info_glayout.addWidget(widget, row_num, 1) - info_layout.addLayout(info_glayout) - - # Settings tab - settings_tab = QWidget() - settings_layout = QVBoxLayout(settings_tab) - settings_glayout = QGridLayout() - - # Settings tab - Label - label_msg = QLabel(_("Name this {}. If you have multiple devices " - "their labels help distinguish them.") - .format(plugin.device)) - label_msg.setWordWrap(True) - label_label = QLabel(_("Device Label")) - label_edit = QLineEdit() - label_edit.setMinimumWidth(150) - label_edit.setMaxLength(plugin.MAX_LABEL_LEN) - label_apply = QPushButton(_("Apply")) - label_apply.clicked.connect(rename) - label_edit.textChanged.connect(set_label_enabled) - settings_glayout.addWidget(label_label, 0, 0) - settings_glayout.addWidget(label_edit, 0, 1, 1, 2) - settings_glayout.addWidget(label_apply, 0, 3) - settings_glayout.addWidget(label_msg, 1, 1, 1, -1) - - # Settings tab - PIN - pin_label = QLabel(_("PIN Protection")) - pin_button = QPushButton() - pin_button.clicked.connect(set_pin) - settings_glayout.addWidget(pin_label, 2, 0) - settings_glayout.addWidget(pin_button, 2, 1) - pin_msg = QLabel(_("PIN protection is strongly recommended. " - "A PIN is your only protection against someone " - "stealing your bitcoins if they obtain physical " - "access to your {}.").format(plugin.device)) - pin_msg.setWordWrap(True) - pin_msg.setStyleSheet("color: red") - settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) - - # Settings tab - Session Timeout - timeout_label = QLabel(_("Session Timeout")) - timeout_minutes = QLabel() - timeout_slider = QSlider(Qt.Horizontal) - timeout_slider.setRange(1, 60) - timeout_slider.setSingleStep(1) - timeout_slider.setTickInterval(5) - timeout_slider.setTickPosition(QSlider.TicksBelow) - timeout_slider.setTracking(True) - timeout_msg = QLabel( - _("Clear the session after the specified period " - "of inactivity. Once a session has timed out, " - "your PIN and passphrase (if enabled) must be " - "re-entered to use the device.")) - timeout_msg.setWordWrap(True) - timeout_slider.setSliderPosition(config.get_session_timeout() // 60) - slider_moved() - timeout_slider.valueChanged.connect(slider_moved) - timeout_slider.sliderReleased.connect(slider_released) - settings_glayout.addWidget(timeout_label, 6, 0) - settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3) - settings_glayout.addWidget(timeout_minutes, 6, 4) - settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1) - settings_layout.addLayout(settings_glayout) - settings_layout.addStretch(1) - - # Advanced tab - advanced_tab = QWidget() - advanced_layout = QVBoxLayout(advanced_tab) - advanced_glayout = QGridLayout() - - # Advanced tab - clear PIN - clear_pin_button = QPushButton(_("Disable PIN")) - clear_pin_button.clicked.connect(clear_pin) - clear_pin_warning = QLabel( - _("If you disable your PIN, anyone with physical access to your " - "{} device can spend your bitcoins.").format(plugin.device)) - clear_pin_warning.setWordWrap(True) - clear_pin_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(clear_pin_button, 0, 2) - advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5) - - # Advanced tab - toggle passphrase protection - passphrase_button = QPushButton() - passphrase_button.clicked.connect(toggle_passphrase) - passphrase_msg = WWLabel(PASSPHRASE_HELP) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(passphrase_button, 3, 2) - advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5) - advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5) - - # Advanced tab - wipe device - wipe_device_button = QPushButton(_("Wipe Device")) - wipe_device_button.clicked.connect(wipe_device) - wipe_device_msg = QLabel( - _("Wipe the device, removing all data from it. The firmware " - "is left unchanged.")) - wipe_device_msg.setWordWrap(True) - wipe_device_warning = QLabel( - _("Only wipe a device if you have the recovery seed written down " - "and the device wallet(s) are empty, otherwise the bitcoins " - "will be lost forever.")) - wipe_device_warning.setWordWrap(True) - wipe_device_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(wipe_device_button, 6, 2) - advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5) - advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5) - advanced_layout.addLayout(advanced_glayout) - advanced_layout.addStretch(1) - - tabs = QTabWidget(self) - tabs.addTab(info_tab, _("Information")) - tabs.addTab(settings_tab, _("Settings")) - tabs.addTab(advanced_tab, _("Advanced")) - dialog_vbox = QVBoxLayout(self) - dialog_vbox.addWidget(tabs) - dialog_vbox.addLayout(Buttons(CloseButton(self))) - - # Update information - invoke_client(None) diff --git a/plugins/labels/__init__.py b/plugins/labels/__init__.py @@ -1,9 +0,0 @@ -from electrum.i18n import _ - -fullname = _('LabelSync') -description = ' '.join([ - _("Save your wallet labels on a remote server, and synchronize them across multiple devices where you use Electrum."), - _("Labels, transactions IDs and addresses are encrypted before they are sent to the remote server.") -]) -available_for = ['qt', 'kivy', 'cmdline'] - diff --git a/plugins/labels/cmdline.py b/plugins/labels/cmdline.py @@ -1,11 +0,0 @@ -from .labels import LabelsPlugin -from electrum.plugins import hook - -class Plugin(LabelsPlugin): - - @hook - def load_wallet(self, wallet, window): - self.start_wallet(wallet) - - def on_pulled(self, wallet): - self.print_error('labels pulled from server') diff --git a/plugins/labels/kivy.py b/plugins/labels/kivy.py @@ -1,14 +0,0 @@ -from .labels import LabelsPlugin -from electrum.plugins import hook - -class Plugin(LabelsPlugin): - - @hook - def load_wallet(self, wallet, window): - self.window = window - self.start_wallet(wallet) - - def on_pulled(self, wallet): - self.print_error('on pulled') - self.window._trigger_update_history() - diff --git a/plugins/labels/labels.py b/plugins/labels/labels.py @@ -1,168 +0,0 @@ -import hashlib -import requests -import threading -import json -import sys -import traceback - -import base64 - -import electrum -from electrum.plugins import BasePlugin, hook -from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv -from electrum.i18n import _ - - -class LabelsPlugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.target_host = 'labels.electrum.org' - self.wallets = {} - - def encode(self, wallet, msg): - password, iv, wallet_id = self.wallets[wallet] - encrypted = aes_encrypt_with_iv(password, iv, - msg.encode('utf8')) - return base64.b64encode(encrypted).decode() - - def decode(self, wallet, message): - password, iv, wallet_id = self.wallets[wallet] - decoded = base64.b64decode(message) - decrypted = aes_decrypt_with_iv(password, iv, decoded) - return decrypted.decode('utf8') - - def get_nonce(self, wallet): - # nonce is the nonce to be used with the next change - nonce = wallet.storage.get('wallet_nonce') - if nonce is None: - nonce = 1 - self.set_nonce(wallet, nonce) - return nonce - - def set_nonce(self, wallet, nonce): - self.print_error("set", wallet.basename(), "nonce to", nonce) - wallet.storage.put("wallet_nonce", nonce) - - @hook - def set_label(self, wallet, item, label): - if wallet not in self.wallets: - return - if not item: - return - nonce = self.get_nonce(wallet) - wallet_id = self.wallets[wallet][2] - bundle = {"walletId": wallet_id, - "walletNonce": nonce, - "externalId": self.encode(wallet, item), - "encryptedLabel": self.encode(wallet, label)} - t = threading.Thread(target=self.do_request_safe, - args=["POST", "/label", False, bundle]) - t.setDaemon(True) - t.start() - # Caller will write the wallet - self.set_nonce(wallet, nonce + 1) - - def do_request(self, method, url = "/labels", is_batch=False, data=None): - url = 'https://' + self.target_host + url - kwargs = {'headers': {}} - if method == 'GET' and data: - kwargs['params'] = data - elif method == 'POST' and data: - kwargs['data'] = json.dumps(data) - kwargs['headers']['Content-Type'] = 'application/json' - response = requests.request(method, url, **kwargs) - if response.status_code != 200: - raise Exception(response.status_code, response.text) - response = response.json() - if "error" in response: - raise Exception(response["error"]) - return response - - def do_request_safe(self, *args, **kwargs): - try: - self.do_request(*args, **kwargs) - except BaseException as e: - #traceback.print_exc(file=sys.stderr) - self.print_error('error doing request') - - def push_thread(self, wallet): - wallet_data = self.wallets.get(wallet, None) - if not wallet_data: - raise Exception('Wallet {} not loaded'.format(wallet)) - wallet_id = wallet_data[2] - bundle = {"labels": [], - "walletId": wallet_id, - "walletNonce": self.get_nonce(wallet)} - for key, value in wallet.labels.items(): - try: - encoded_key = self.encode(wallet, key) - encoded_value = self.encode(wallet, value) - except: - self.print_error('cannot encode', repr(key), repr(value)) - continue - bundle["labels"].append({'encryptedLabel': encoded_value, - 'externalId': encoded_key}) - self.do_request("POST", "/labels", True, bundle) - - def pull_thread(self, wallet, force): - wallet_data = self.wallets.get(wallet, None) - if not wallet_data: - raise Exception('Wallet {} not loaded'.format(wallet)) - wallet_id = wallet_data[2] - nonce = 1 if force else self.get_nonce(wallet) - 1 - self.print_error("asking for labels since nonce", nonce) - response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) )) - if response["labels"] is None: - self.print_error('no new labels') - return - result = {} - for label in response["labels"]: - try: - key = self.decode(wallet, label["externalId"]) - value = self.decode(wallet, label["encryptedLabel"]) - except: - continue - try: - json.dumps(key) - json.dumps(value) - except: - self.print_error('error: no json', key) - continue - result[key] = value - - for key, value in result.items(): - if force or not wallet.labels.get(key): - wallet.labels[key] = value - - self.print_error("received %d labels" % len(response)) - # do not write to disk because we're in a daemon thread - wallet.storage.put('labels', wallet.labels) - self.set_nonce(wallet, response["nonce"] + 1) - self.on_pulled(wallet) - - def pull_thread_safe(self, wallet, force): - try: - self.pull_thread(wallet, force) - except BaseException as e: - # traceback.print_exc(file=sys.stderr) - self.print_error('could not retrieve labels') - - def start_wallet(self, wallet): - nonce = self.get_nonce(wallet) - self.print_error("wallet", wallet.basename(), "nonce is", nonce) - mpk = wallet.get_fingerprint() - if not mpk: - return - mpk = mpk.encode('ascii') - password = hashlib.sha1(mpk).hexdigest()[:32].encode('ascii') - iv = hashlib.sha256(password).digest()[:16] - wallet_id = hashlib.sha256(mpk).hexdigest() - self.wallets[wallet] = (password, iv, wallet_id) - # If there is an auth token we can try to actually start syncing - t = threading.Thread(target=self.pull_thread_safe, args=(wallet, False)) - t.setDaemon(True) - t.start() - - def stop_wallet(self, wallet): - self.wallets.pop(wallet, None) diff --git a/plugins/labels/qt.py b/plugins/labels/qt.py @@ -1,78 +0,0 @@ -from functools import partial -import traceback -import sys - -from PyQt5.QtGui import * -from PyQt5.QtCore import * -from PyQt5.QtWidgets import (QHBoxLayout, QLabel, QVBoxLayout) - -from electrum.plugins import hook -from electrum.i18n import _ -from electrum_gui.qt import EnterButton -from electrum_gui.qt.util import ThreadedButton, Buttons -from electrum_gui.qt.util import WindowModalDialog, OkButton - -from .labels import LabelsPlugin - - -class QLabelsSignalObject(QObject): - labels_changed_signal = pyqtSignal(object) - - -class Plugin(LabelsPlugin): - - def __init__(self, *args): - LabelsPlugin.__init__(self, *args) - self.obj = QLabelsSignalObject() - - def requires_settings(self): - return True - - def settings_widget(self, window): - return EnterButton(_('Settings'), - partial(self.settings_dialog, window)) - - def settings_dialog(self, window): - wallet = window.parent().wallet - d = WindowModalDialog(window, _("Label Settings")) - hbox = QHBoxLayout() - hbox.addWidget(QLabel("Label sync options:")) - upload = ThreadedButton("Force upload", - partial(self.push_thread, wallet), - partial(self.done_processing_success, d), - partial(self.done_processing_error, d)) - download = ThreadedButton("Force download", - partial(self.pull_thread, wallet, True), - partial(self.done_processing_success, d), - partial(self.done_processing_error, d)) - vbox = QVBoxLayout() - vbox.addWidget(upload) - vbox.addWidget(download) - hbox.addLayout(vbox) - vbox = QVBoxLayout(d) - vbox.addLayout(hbox) - vbox.addSpacing(20) - vbox.addLayout(Buttons(OkButton(d))) - return bool(d.exec_()) - - def on_pulled(self, wallet): - self.obj.labels_changed_signal.emit(wallet) - - def done_processing_success(self, dialog, result): - dialog.show_message(_("Your labels have been synchronised.")) - - def done_processing_error(self, dialog, result): - traceback.print_exception(*result, file=sys.stderr) - dialog.show_error(_("Error synchronising labels") + ':\n' + str(result[:2])) - - @hook - def load_wallet(self, wallet, window): - # FIXME if the user just enabled the plugin, this hook won't be called - # as the wallet is already loaded, and hence the plugin will be in - # a non-functional state for that window - self.obj.labels_changed_signal.connect(window.update_tabs) - self.start_wallet(wallet) - - @hook - def on_close_window(self, window): - self.stop_wallet(window.wallet) diff --git a/plugins/ledger/__init__.py b/plugins/ledger/__init__.py @@ -1,7 +0,0 @@ -from electrum.i18n import _ - -fullname = 'Ledger Wallet' -description = 'Provides support for Ledger hardware wallet' -requires = [('btchip', 'github.com/ledgerhq/btchip-python')] -registers_keystore = ('hardware', 'ledger', _("Ledger wallet")) -available_for = ['qt', 'cmdline'] diff --git a/plugins/ledger/auth2fa.py b/plugins/ledger/auth2fa.py @@ -1,358 +0,0 @@ -import os -import hashlib -import logging -import json -import copy -from binascii import hexlify, unhexlify - -import websocket - -from PyQt5.Qt import QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel -import PyQt5.QtCore as QtCore -from PyQt5.QtWidgets import * - -from btchip.btchip import * - -from electrum.i18n import _ -from electrum_gui.qt.util import * -from electrum.util import print_msg -from electrum import constants, bitcoin -from electrum_gui.qt.qrcodewidget import QRCodeWidget - - -DEBUG = False - -helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code.<br><br>" \ - "For best security you should unplug your device, open a text editor on another computer, " \ - "put your cursor into it, and plug your device into that computer. " \ - "It will output a summary of the transaction being signed and a one-time PIN.<br><br>" \ - "Verify the transaction summary and type the PIN code here.<br><br>" \ - "Before pressing enter, plug the device back into this computer.<br>" ), - _("Verify the address below.<br>Type the character from your security card corresponding to the <u><b>BOLD</b></u> character."), - _("Waiting for authentication on your mobile phone"), - _("Transaction accepted by mobile phone. Waiting for confirmation."), - _("Click Pair button to begin pairing a mobile phone."), - _("Scan this QR code with your Ledger Wallet phone app to pair it with this Ledger device.<br>" - "To complete pairing you will need your security card to answer a challenge." ) - ] - -class LedgerAuthDialog(QDialog): - def __init__(self, handler, data): - '''Ask user for 2nd factor authentication. Support text, security card and paired mobile methods. - Use last method from settings, but support new pairing and downgrade. - ''' - QDialog.__init__(self, handler.top_level_window()) - self.handler = handler - self.txdata = data - self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else '' - self.setMinimumWidth(650) - self.setWindowTitle(_("Ledger Wallet Authentication")) - self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg) - self.dongle = self.handler.win.wallet.get_keystore().get_client().dongle - self.ws = None - self.pin = '' - - self.devmode = self.getDevice2FAMode() - if self.devmode == 0x11 or self.txdata['confirmationType'] == 1: - self.cfg['mode'] = 0 - - vbox = QVBoxLayout() - self.setLayout(vbox) - - def on_change_mode(idx): - if idx < 2 and self.ws: - self.ws.stop() - self.ws = None - self.cfg['mode'] = 0 if self.devmode == 0x11 else idx if idx > 0 else 1 - if self.cfg['mode'] > 1 and self.cfg['pair'] and not self.ws: - self.req_validation() - if self.cfg['mode'] > 0: - self.handler.win.wallet.get_keystore().cfg = self.cfg - self.handler.win.wallet.save_keystore() - self.update_dlg() - def add_pairing(): - self.do_pairing() - def return_pin(): - self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text() - if self.cfg['mode'] == 1: - self.pin = ''.join(chr(int(str(i),16)) for i in self.pin) - self.accept() - - self.modebox = QWidget() - modelayout = QHBoxLayout() - self.modebox.setLayout(modelayout) - modelayout.addWidget(QLabel(_("Method:"))) - self.modes = QComboBox() - modelayout.addWidget(self.modes, 2) - self.addPair = QPushButton(_("Pair")) - self.addPair.setMaximumWidth(60) - modelayout.addWidget(self.addPair) - modelayout.addStretch(1) - self.modebox.setMaximumHeight(50) - vbox.addWidget(self.modebox) - - self.populate_modes() - self.modes.currentIndexChanged.connect(on_change_mode) - self.addPair.clicked.connect(add_pairing) - - self.helpmsg = QTextEdit() - self.helpmsg.setStyleSheet("QTextEdit { background-color: lightgray; }") - self.helpmsg.setReadOnly(True) - vbox.addWidget(self.helpmsg) - - self.pinbox = QWidget() - pinlayout = QHBoxLayout() - self.pinbox.setLayout(pinlayout) - self.pintxt = QLineEdit() - self.pintxt.setEchoMode(2) - self.pintxt.setMaxLength(4) - self.pintxt.returnPressed.connect(return_pin) - pinlayout.addWidget(QLabel(_("Enter PIN:"))) - pinlayout.addWidget(self.pintxt) - pinlayout.addWidget(QLabel(_("NOT DEVICE PIN - see above"))) - pinlayout.addStretch(1) - self.pinbox.setVisible(self.cfg['mode'] == 0) - vbox.addWidget(self.pinbox) - - self.cardbox = QWidget() - card = QVBoxLayout() - self.cardbox.setLayout(card) - self.addrtext = QTextEdit() - self.addrtext.setStyleSheet("QTextEdit { color:blue; background-color:lightgray; padding:15px 10px; border:none; font-size:20pt; font-family:monospace; }") - self.addrtext.setReadOnly(True) - self.addrtext.setMaximumHeight(130) - card.addWidget(self.addrtext) - - def pin_changed(s): - if len(s) < len(self.idxs): - i = self.idxs[len(s)] - addr = self.txdata['address'] - if not constants.net.TESTNET: - text = addr[:i] + '<u><b>' + addr[i:i+1] + '</u></b>' + addr[i+1:] - else: - # pin needs to be created from mainnet address - addr_mainnet = bitcoin.script_to_address(bitcoin.address_to_script(addr), net=constants.BitcoinMainnet) - addr_mainnet = addr_mainnet[:i] + '<u><b>' + addr_mainnet[i:i+1] + '</u></b>' + addr_mainnet[i+1:] - text = str(addr) + '\n' + str(addr_mainnet) - self.addrtext.setHtml(str(text)) - else: - self.addrtext.setHtml(_("Press Enter")) - - pin_changed('') - cardpin = QHBoxLayout() - cardpin.addWidget(QLabel(_("Enter PIN:"))) - self.cardtxt = QLineEdit() - self.cardtxt.setEchoMode(2) - self.cardtxt.setMaxLength(len(self.idxs)) - self.cardtxt.textChanged.connect(pin_changed) - self.cardtxt.returnPressed.connect(return_pin) - cardpin.addWidget(self.cardtxt) - cardpin.addWidget(QLabel(_("NOT DEVICE PIN - see above"))) - cardpin.addStretch(1) - card.addLayout(cardpin) - self.cardbox.setVisible(self.cfg['mode'] == 1) - vbox.addWidget(self.cardbox) - - self.pairbox = QWidget() - pairlayout = QVBoxLayout() - self.pairbox.setLayout(pairlayout) - pairhelp = QTextEdit(helpTxt[5]) - pairhelp.setStyleSheet("QTextEdit { background-color: lightgray; }") - pairhelp.setReadOnly(True) - pairlayout.addWidget(pairhelp, 1) - self.pairqr = QRCodeWidget() - pairlayout.addWidget(self.pairqr, 4) - self.pairbox.setVisible(False) - vbox.addWidget(self.pairbox) - self.update_dlg() - - if self.cfg['mode'] > 1 and not self.ws: - self.req_validation() - - def populate_modes(self): - self.modes.blockSignals(True) - self.modes.clear() - self.modes.addItem(_("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _("Summary Text PIN is Disabled")) - if self.txdata['confirmationType'] > 1: - self.modes.addItem(_("Security Card Challenge")) - if not self.cfg['pair']: - self.modes.addItem(_("Mobile - Not paired")) - else: - self.modes.addItem(_("Mobile - {}").format(self.cfg['pair'][1])) - self.modes.blockSignals(False) - - def update_dlg(self): - self.modes.setCurrentIndex(self.cfg['mode']) - self.modebox.setVisible(True) - self.addPair.setText(_("Pair") if not self.cfg['pair'] else _("Re-Pair")) - self.addPair.setVisible(self.txdata['confirmationType'] > 2) - self.helpmsg.setText(helpTxt[self.cfg['mode'] if self.cfg['mode'] < 2 else 2 if self.cfg['pair'] else 4]) - self.helpmsg.setMinimumHeight(180 if self.txdata['confirmationType'] == 1 else 100) - self.pairbox.setVisible(False) - self.helpmsg.setVisible(True) - self.pinbox.setVisible(self.cfg['mode'] == 0) - self.cardbox.setVisible(self.cfg['mode'] == 1) - self.pintxt.setFocus(True) if self.cfg['mode'] == 0 else self.cardtxt.setFocus(True) - self.setMaximumHeight(400) - - def do_pairing(self): - rng = os.urandom(16) - pairID = (hexlify(rng) + hexlify(hashlib.sha256(rng).digest()[0:1])).decode('utf-8') - self.pairqr.setData(pairID) - self.modebox.setVisible(False) - self.helpmsg.setVisible(False) - self.pinbox.setVisible(False) - self.cardbox.setVisible(False) - self.pairbox.setVisible(True) - self.pairqr.setMinimumSize(300,300) - if self.ws: - self.ws.stop() - self.ws = LedgerWebSocket(self, pairID) - self.ws.pairing_done.connect(self.pairing_done) - self.ws.start() - - def pairing_done(self, data): - if data is not None: - self.cfg['pair'] = [ data['pairid'], data['name'], data['platform'] ] - self.cfg['mode'] = 2 - self.handler.win.wallet.get_keystore().cfg = self.cfg - self.handler.win.wallet.save_keystore() - self.pin = 'paired' - self.accept() - - def req_validation(self): - if self.cfg['pair'] and 'secureScreenData' in self.txdata: - if self.ws: - self.ws.stop() - self.ws = LedgerWebSocket(self, self.cfg['pair'][0], self.txdata) - self.ws.req_updated.connect(self.req_updated) - self.ws.start() - - def req_updated(self, pin): - if pin == 'accepted': - self.helpmsg.setText(helpTxt[3]) - else: - self.pin = str(pin) - self.accept() - - def getDevice2FAMode(self): - apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode - try: - mode = self.dongle.exchange( bytearray(apdu) ) - return mode - except BTChipException as e: - debug_msg('Device getMode Failed') - return 0x11 - - def closeEvent(self, evnt): - debug_msg("CLOSE - Stop WS") - if self.ws: - self.ws.stop() - if self.pairbox.isVisible(): - evnt.ignore() - self.update_dlg() - -class LedgerWebSocket(QThread): - pairing_done = pyqtSignal(object) - req_updated = pyqtSignal(str) - - def __init__(self, dlg, pairID, txdata=None): - QThread.__init__(self) - self.stopping = False - self.pairID = pairID - self.txreq = '{"type":"request","second_factor_data":"' + hexlify(txdata['secureScreenData']).decode('utf-8') + '"}' if txdata else None - self.dlg = dlg - self.dongle = self.dlg.dongle - self.data = None - - #websocket.enableTrace(True) - logging.basicConfig(level=logging.INFO) - self.ws = websocket.WebSocketApp('wss://ws.ledgerwallet.com/2fa/channels', - on_message = self.on_message, on_error = self.on_error, - on_close = self.on_close, on_open = self.on_open) - - def run(self): - while not self.stopping: - self.ws.run_forever() - def stop(self): - debug_msg("WS: Stopping") - self.stopping = True - self.ws.close() - - def on_message(self, ws, msg): - data = json.loads(msg) - if data['type'] == 'identify': - debug_msg('Identify') - apdu = [0xe0, 0x12, 0x01, 0x00, 0x41] # init pairing - apdu.extend(unhexlify(data['public_key'])) - try: - challenge = self.dongle.exchange( bytearray(apdu) ) - ws.send( '{"type":"challenge","data":"%s" }' % hexlify(challenge).decode('utf-8') ) - self.data = data - except BTChipException as e: - debug_msg('Identify Failed') - - if data['type'] == 'challenge': - debug_msg('Challenge') - apdu = [0xe0, 0x12, 0x02, 0x00, 0x10] # confirm pairing - apdu.extend(unhexlify(data['data'])) - try: - self.dongle.exchange( bytearray(apdu) ) - debug_msg('Pairing Successful') - ws.send( '{"type":"pairing","is_successful":"true"}' ) - self.data['pairid'] = self.pairID - self.pairing_done.emit(self.data) - except BTChipException as e: - debug_msg('Pairing Failed') - ws.send( '{"type":"pairing","is_successful":"false"}' ) - self.pairing_done.emit(None) - ws.send( '{"type":"disconnect"}' ) - self.stopping = True - ws.close() - - if data['type'] == 'accept': - debug_msg('Accepted') - self.req_updated.emit('accepted') - if data['type'] == 'response': - debug_msg('Responded', data) - self.req_updated.emit(str(data['pin']) if data['is_accepted'] else '') - self.txreq = None - self.stopping = True - ws.close() - - if data['type'] == 'repeat': - debug_msg('Repeat') - if self.txreq: - ws.send( self.txreq ) - debug_msg("Req Sent", self.txreq) - if data['type'] == 'connect': - debug_msg('Connected') - if self.txreq: - ws.send( self.txreq ) - debug_msg("Req Sent", self.txreq) - if data['type'] == 'disconnect': - debug_msg('Disconnected') - ws.close() - - def on_error(self, ws, error): - message = getattr(error, 'strerror', '') - if not message: - message = getattr(error, 'message', '') - debug_msg("WS: %s" % message) - - def on_close(self, ws): - debug_msg("WS: ### socket closed ###") - - def on_open(self, ws): - debug_msg("WS: ### socket open ###") - debug_msg("Joining with pairing ID", self.pairID) - ws.send( '{"type":"join","room":"%s"}' % self.pairID ) - ws.send( '{"type":"repeat"}' ) - if self.txreq: - ws.send( self.txreq ) - debug_msg("Req Sent", self.txreq) - - -def debug_msg(*args): - if DEBUG: - print_msg(*args) diff --git a/plugins/ledger/cmdline.py b/plugins/ledger/cmdline.py @@ -1,14 +0,0 @@ -from electrum.plugins import hook -from .ledger import LedgerPlugin -from ..hw_wallet import CmdLineHandler - -class Plugin(LedgerPlugin): - handler = CmdLineHandler() - @hook - def init_keystore(self, keystore): - if not isinstance(keystore, self.keystore_class): - return - keystore.handler = self.handler - - def create_handler(self, window): - return self.handler diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py @@ -1,637 +0,0 @@ -from struct import pack, unpack -import hashlib -import sys -import traceback - -from electrum import bitcoin -from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int -from electrum.i18n import _ -from electrum.plugins import BasePlugin -from electrum.keystore import Hardware_KeyStore -from electrum.transaction import Transaction -from electrum.wallet import Standard_Wallet -from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch -from electrum.util import print_error, is_verbose, bfh, bh2u, versiontuple -from electrum.base_wizard import ScriptTypeNotSupported - -try: - import hid - from btchip.btchipComm import HIDDongleHIDAPI, DongleWait - from btchip.btchip import btchip - from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script - from btchip.bitcoinTransaction import bitcoinTransaction - from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware - from btchip.btchipException import BTChipException - BTCHIP = True - BTCHIP_DEBUG = is_verbose -except ImportError: - BTCHIP = False - -MSG_NEEDS_FW_UPDATE_GENERIC = _('Firmware version too old. Please update at') + \ - ' https://www.ledgerwallet.com' -MSG_NEEDS_FW_UPDATE_SEGWIT = _('Firmware version (or "Bitcoin" app) too old for Segwit support. Please update at') + \ - ' https://www.ledgerwallet.com' -MULTI_OUTPUT_SUPPORT = '1.1.4' -SEGWIT_SUPPORT = '1.1.10' -SEGWIT_SUPPORT_SPECIAL = '1.0.4' - - -def test_pin_unlocked(func): - """Function decorator to test the Ledger for being unlocked, and if not, - raise a human-readable exception. - """ - def catch_exception(self, *args, **kwargs): - try: - return func(self, *args, **kwargs) - except BTChipException as e: - if e.sw == 0x6982: - raise Exception(_('Your Ledger is locked. Please unlock it.')) - else: - raise - return catch_exception - - -class Ledger_Client(): - def __init__(self, hidDevice): - self.dongleObject = btchip(hidDevice) - self.preflightDone = False - - def is_pairable(self): - return True - - def close(self): - self.dongleObject.dongle.close() - - def timeout(self, cutoff): - pass - - def is_initialized(self): - return True - - def label(self): - return "" - - def i4b(self, x): - return pack('>I', x) - - def has_usable_connection_with_device(self): - try: - self.dongleObject.getFirmwareVersion() - except BaseException: - return False - return True - - @test_pin_unlocked - def get_xpub(self, bip32_path, xtype): - self.checkDevice() - # bip32_path is of the form 44'/0'/1' - # S-L-O-W - we don't handle the fingerprint directly, so compute - # it manually from the previous node - # This only happens once so it's bearable - #self.get_client() # prompt for the PIN before displaying the dialog if necessary - #self.handler.show_message("Computing master public key") - if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit(): - raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT) - if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): - raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT) - splitPath = bip32_path.split('/') - if splitPath[0] == 'm': - splitPath = splitPath[1:] - bip32_path = bip32_path[2:] - fingerprint = 0 - if len(splitPath) > 1: - prevPath = "/".join(splitPath[0:len(splitPath) - 1]) - nodeData = self.dongleObject.getWalletPublicKey(prevPath) - publicKey = compress_public_key(nodeData['publicKey']) - h = hashlib.new('ripemd160') - h.update(hashlib.sha256(publicKey).digest()) - fingerprint = unpack(">I", h.digest()[0:4])[0] - nodeData = self.dongleObject.getWalletPublicKey(bip32_path) - publicKey = compress_public_key(nodeData['publicKey']) - depth = len(splitPath) - lastChild = splitPath[len(splitPath) - 1].split('\'') - childnum = int(lastChild[0]) if len(lastChild) == 1 else 0x80000000 | int(lastChild[0]) - xpub = bitcoin.serialize_xpub(xtype, nodeData['chainCode'], publicKey, depth, self.i4b(fingerprint), self.i4b(childnum)) - return xpub - - def has_detached_pin_support(self, client): - try: - client.getVerifyPinRemainingAttempts() - return True - except BTChipException as e: - if e.sw == 0x6d00: - return False - raise e - - def is_pin_validated(self, client): - try: - # Invalid SET OPERATION MODE to verify the PIN status - client.dongle.exchange(bytearray([0xe0, 0x26, 0x00, 0x00, 0x01, 0xAB])) - except BTChipException as e: - if (e.sw == 0x6982): - return False - if (e.sw == 0x6A80): - return True - raise e - - def supports_multi_output(self): - return self.multiOutputSupported - - def supports_segwit(self): - return self.segwitSupported - - def supports_native_segwit(self): - return self.nativeSegwitSupported - - def perform_hw1_preflight(self): - try: - firmwareInfo = self.dongleObject.getFirmwareVersion() - firmware = firmwareInfo['version'] - self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT) - self.nativeSegwitSupported = versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT) - self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL)) - - if not checkFirmware(firmwareInfo): - self.dongleObject.dongle.close() - raise Exception(MSG_NEEDS_FW_UPDATE_GENERIC) - try: - self.dongleObject.getOperationMode() - except BTChipException as e: - if (e.sw == 0x6985): - self.dongleObject.dongle.close() - self.handler.get_setup( ) - # Acquire the new client on the next run - else: - raise e - if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject) and (self.handler is not None): - remaining_attempts = self.dongleObject.getVerifyPinRemainingAttempts() - if remaining_attempts != 1: - msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) - else: - msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." - confirmed, p, pin = self.password_dialog(msg) - if not confirmed: - raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') - pin = pin.encode() - self.dongleObject.verifyPin(pin) - except BTChipException as e: - if (e.sw == 0x6faa): - raise Exception("Dongle is temporarily locked - please unplug it and replug it again") - if ((e.sw & 0xFFF0) == 0x63c0): - raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") - if e.sw == 0x6f00 and e.message == 'Invalid channel': - # based on docs 0x6f00 might be a more general error, hence we also compare message to be sure - raise Exception("Invalid channel.\n" - "Please make sure that 'Browser support' is disabled on your device.") - raise e - - def checkDevice(self): - if not self.preflightDone: - try: - self.perform_hw1_preflight() - except BTChipException as e: - if (e.sw == 0x6d00 or e.sw == 0x6700): - raise Exception(_("Device not in Bitcoin mode")) from e - raise e - self.preflightDone = True - - def password_dialog(self, msg=None): - response = self.handler.get_word(msg) - if response is None: - return False, None, None - return True, response, response - - -class Ledger_KeyStore(Hardware_KeyStore): - hw_type = 'ledger' - device = 'Ledger' - - def __init__(self, d): - Hardware_KeyStore.__init__(self, d) - # Errors and other user interaction is done through the wallet's - # handler. The handler is per-window and preserved across - # device reconnects - self.force_watching_only = False - self.signing = False - self.cfg = d.get('cfg', {'mode':0,'pair':''}) - - def dump(self): - obj = Hardware_KeyStore.dump(self) - obj['cfg'] = self.cfg - return obj - - def get_derivation(self): - return self.derivation - - def get_client(self): - return self.plugin.get_client(self).dongleObject - - def get_client_electrum(self): - return self.plugin.get_client(self) - - def give_error(self, message, clear_client = False): - print_error(message) - if not self.signing: - self.handler.show_error(message) - else: - self.signing = False - if clear_client: - self.client = None - raise Exception(message) - - def set_and_unset_signing(func): - """Function decorator to set and unset self.signing.""" - def wrapper(self, *args, **kwargs): - try: - self.signing = True - return func(self, *args, **kwargs) - finally: - self.signing = False - return wrapper - - def address_id_stripped(self, address): - # Strip the leading "m/" - change, index = self.get_address_index(address) - derivation = self.derivation - address_path = "%s/%d/%d"%(derivation, change, index) - return address_path[2:] - - def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) - - @test_pin_unlocked - @set_and_unset_signing - def sign_message(self, sequence, message, password): - message = message.encode('utf8') - message_hash = hashlib.sha256(message).hexdigest().upper() - # prompt for the PIN before displaying the dialog if necessary - client = self.get_client() - address_path = self.get_derivation()[2:] + "/%d/%d"%sequence - self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash) - try: - info = self.get_client().signMessagePrepare(address_path, message) - pin = "" - if info['confirmationNeeded']: - pin = self.handler.get_auth( info ) # does the authenticate dialog and returns pin - if not pin: - raise UserWarning(_('Cancelled by user')) - pin = str(pin).encode() - signature = self.get_client().signMessageSign(pin) - except BTChipException as e: - if e.sw == 0x6a80: - self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") - elif e.sw == 0x6985: # cancelled by user - return b'' - elif e.sw == 0x6982: - raise # pin lock. decorator will catch it - else: - self.give_error(e, True) - except UserWarning: - self.handler.show_error(_('Cancelled by user')) - return b'' - except Exception as e: - self.give_error(e, True) - finally: - self.handler.finished() - # Parse the ASN.1 signature - rLength = signature[3] - r = signature[4 : 4 + rLength] - sLength = signature[4 + rLength + 1] - s = signature[4 + rLength + 2:] - if rLength == 33: - r = r[1:] - if sLength == 33: - s = s[1:] - # And convert it - return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s - - @test_pin_unlocked - @set_and_unset_signing - def sign_transaction(self, tx, password): - if tx.is_complete(): - return - client = self.get_client() - inputs = [] - inputsPaths = [] - pubKeys = [] - chipInputs = [] - redeemScripts = [] - signatures = [] - preparedTrustedInputs = [] - changePath = "" - output = None - p2shTransaction = False - segwitTransaction = False - pin = "" - self.get_client() # prompt for the PIN before displaying the dialog if necessary - - # Fetch inputs of the transaction to sign - derivations = self.get_tx_derivations(tx) - for txin in tx.inputs(): - if txin['type'] == 'coinbase': - self.give_error("Coinbase not supported") # should never happen - - if txin['type'] in ['p2sh']: - p2shTransaction = True - - if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']: - if not self.get_client_electrum().supports_segwit(): - self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) - segwitTransaction = True - - if txin['type'] in ['p2wpkh', 'p2wsh']: - if not self.get_client_electrum().supports_native_segwit(): - self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) - segwitTransaction = True - - pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) - for i, x_pubkey in enumerate(x_pubkeys): - if x_pubkey in derivations: - signingPos = i - s = derivations.get(x_pubkey) - hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1]) - break - else: - self.give_error("No matching x_key for sign_transaction") # should never happen - - redeemScript = Transaction.get_preimage_script(txin) - txin_prev_tx = txin.get('prev_tx') - if txin_prev_tx is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) - txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None - inputs.append([txin_prev_tx_raw, - txin['prevout_n'], - redeemScript, - txin['prevout_hash'], - signingPos, - txin.get('sequence', 0xffffffff - 1), - txin.get('value')]) - inputsPaths.append(hwAddress) - pubKeys.append(pubkeys) - - # Sanity check - if p2shTransaction: - for txin in tx.inputs(): - if txin['type'] != 'p2sh': - self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen - - txOutput = var_int(len(tx.outputs())) - for txout in tx.outputs(): - output_type, addr, amount = txout - txOutput += int_to_hex(amount, 8) - script = tx.pay_script(output_type, addr) - txOutput += var_int(len(script)//2) - txOutput += script - txOutput = bfh(txOutput) - - # Recognize outputs - # - only one output and one change is authorized (for hw.1 and nano) - # - at most one output can bypass confirmation (~change) (for all) - if not p2shTransaction: - if not self.get_client_electrum().supports_multi_output(): - if len(tx.outputs()) > 2: - self.give_error("Transaction with more than 2 outputs not supported") - has_change = False - any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for _type, address, amount in tx.outputs(): - assert _type == TYPE_ADDRESS - info = tx.output_info.get(address) - if (info is not None) and len(tx.outputs()) > 1 \ - and not has_change: - index, xpubs, m = info - on_change_branch = index[0] == 1 - # prioritise hiding outputs on the 'change' branch from user - # because no more than one change address allowed - if on_change_branch == any_output_on_change_branch: - changePath = self.get_derivation()[2:] + "/%d/%d"%index - has_change = True - else: - output = address - else: - output = address - - self.handler.show_message(_("Confirm Transaction on your Ledger device...")) - try: - # Get trusted inputs from the original transactions - for utxo in inputs: - sequence = int_to_hex(utxo[5], 4) - if segwitTransaction: - tmp = bfh(utxo[3])[::-1] - tmp += bfh(int_to_hex(utxo[1], 4)) - tmp += bfh(int_to_hex(utxo[6], 8)) # txin['value'] - chipInputs.append({'value' : tmp, 'witness' : True, 'sequence' : sequence}) - redeemScripts.append(bfh(utxo[2])) - elif not p2shTransaction: - txtmp = bitcoinTransaction(bfh(utxo[0])) - trustedInput = self.get_client().getTrustedInput(txtmp, utxo[1]) - trustedInput['sequence'] = sequence - chipInputs.append(trustedInput) - redeemScripts.append(txtmp.outputs[utxo[1]].script) - else: - tmp = bfh(utxo[3])[::-1] - tmp += bfh(int_to_hex(utxo[1], 4)) - chipInputs.append({'value' : tmp, 'sequence' : sequence}) - redeemScripts.append(bfh(utxo[2])) - - # Sign all inputs - firstTransaction = True - inputIndex = 0 - rawTx = tx.serialize_to_network() - self.get_client().enableAlternate2fa(False) - if segwitTransaction: - self.get_client().startUntrustedTransaction(True, inputIndex, - chipInputs, redeemScripts[inputIndex]) - if changePath: - # we don't set meaningful outputAddress, amount and fees - # as we only care about the alternateEncoding==True branch - outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) - else: - outputData = self.get_client().finalizeInputFull(txOutput) - outputData['outputData'] = txOutput - transactionOutput = outputData['outputData'] - if outputData['confirmationNeeded']: - outputData['address'] = output - self.handler.finished() - pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin - if not pin: - raise UserWarning() - if pin != 'paired': - self.handler.show_message(_("Confirmed. Signing Transaction...")) - while inputIndex < len(inputs): - singleInput = [ chipInputs[inputIndex] ] - self.get_client().startUntrustedTransaction(False, 0, - singleInput, redeemScripts[inputIndex]) - inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) - inputSignature[0] = 0x30 # force for 1.4.9+ - signatures.append(inputSignature) - inputIndex = inputIndex + 1 - else: - while inputIndex < len(inputs): - self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, - chipInputs, redeemScripts[inputIndex]) - if changePath: - # we don't set meaningful outputAddress, amount and fees - # as we only care about the alternateEncoding==True branch - outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) - else: - outputData = self.get_client().finalizeInputFull(txOutput) - outputData['outputData'] = txOutput - if firstTransaction: - transactionOutput = outputData['outputData'] - if outputData['confirmationNeeded']: - outputData['address'] = output - self.handler.finished() - pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin - if not pin: - raise UserWarning() - if pin != 'paired': - self.handler.show_message(_("Confirmed. Signing Transaction...")) - else: - # Sign input with the provided PIN - inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) - inputSignature[0] = 0x30 # force for 1.4.9+ - signatures.append(inputSignature) - inputIndex = inputIndex + 1 - if pin != 'paired': - firstTransaction = False - except UserWarning: - self.handler.show_error(_('Cancelled by user')) - return - except BTChipException as e: - if e.sw == 0x6985: # cancelled by user - return - elif e.sw == 0x6982: - raise # pin lock. decorator will catch it - else: - traceback.print_exc(file=sys.stderr) - self.give_error(e, True) - except BaseException as e: - traceback.print_exc(file=sys.stdout) - self.give_error(e, True) - finally: - self.handler.finished() - - for i, txin in enumerate(tx.inputs()): - signingPos = inputs[i][4] - tx.add_signature_to_txin(i, signingPos, bh2u(signatures[i])) - tx.raw = tx.serialize() - - @test_pin_unlocked - @set_and_unset_signing - def show_address(self, sequence, txin_type): - client = self.get_client() - address_path = self.get_derivation()[2:] + "/%d/%d"%sequence - self.handler.show_message(_("Showing address ...")) - segwit = Transaction.is_segwit_inputtype(txin_type) - segwitNative = txin_type == 'p2wpkh' - try: - client.getWalletPublicKey(address_path, showOnScreen=True, segwit=segwit, segwitNative=segwitNative) - except BTChipException as e: - if e.sw == 0x6985: # cancelled by user - pass - elif e.sw == 0x6982: - raise # pin lock. decorator will catch it - elif e.sw == 0x6b00: # hw.1 raises this - self.handler.show_error('{}\n{}\n{}'.format( - _('Error showing address') + ':', - e, - _('Your device might not have support for this functionality.'))) - else: - traceback.print_exc(file=sys.stderr) - self.handler.show_error(e) - except BaseException as e: - traceback.print_exc(file=sys.stderr) - self.handler.show_error(e) - finally: - self.handler.finished() - -class LedgerPlugin(HW_PluginBase): - libraries_available = BTCHIP - keystore_class = Ledger_KeyStore - client = None - DEVICE_IDS = [ - (0x2581, 0x1807), # HW.1 legacy btchip - (0x2581, 0x2b7c), # HW.1 transitional production - (0x2581, 0x3b7c), # HW.1 ledger production - (0x2581, 0x4b7c), # HW.1 ledger test - (0x2c97, 0x0000), # Blue - (0x2c97, 0x0001) # Nano-S - ] - SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') - - def __init__(self, parent, config, name): - self.segwit = config.get("segwit") - HW_PluginBase.__init__(self, parent, config, name) - if self.libraries_available: - self.device_manager().register_devices(self.DEVICE_IDS) - - def get_btchip_device(self, device): - ledger = False - if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c: - ledger = True - if device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c: - ledger = True - if device.product_key[0] == 0x2c97: - if device.interface_number == 0 or device.usage_page == 0xffa0: - ledger = True - else: - return None # non-compatible interface of a Nano S or Blue - dev = hid.device() - dev.open_path(device.path) - dev.set_nonblocking(True) - return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) - - def create_client(self, device, handler): - if handler: - self.handler = handler - - client = self.get_btchip_device(device) - if client is not None: - client = Ledger_Client(client) - return client - - def setup_device(self, device_info, wizard, purpose): - devmgr = self.device_manager() - device_id = device_info.device.id_ - client = devmgr.client_by_id(device_id) - if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) - client.handler = self.create_handler(wizard) - client.get_xpub("m/44'/0'", 'standard') # TODO replace by direct derivation once Nano S > 1.1 - - def get_xpub(self, device_id, derivation, xtype, wizard): - if xtype not in self.SUPPORTED_XTYPES: - raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - client.handler = self.create_handler(wizard) - client.checkDevice() - xpub = client.get_xpub(derivation, xtype) - return xpub - - def get_client(self, keystore, force_pair=True): - # All client interaction should not be in the main GUI thread - devmgr = self.device_manager() - handler = keystore.handler - with devmgr.hid_lock: - client = devmgr.client_for_keystore(self, handler, keystore, force_pair) - # returns the client for a given keystore. can use xpub - #if client: - # client.used() - if client is not None: - client.checkDevice() - return client - - def show_address(self, wallet, address, keystore=None): - if keystore is None: - keystore = wallet.get_keystore() - if not self.show_address_helper(wallet, address, keystore): - return - if type(wallet) is not Standard_Wallet: - keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) - return - sequence = wallet.get_address_index(address) - txin_type = wallet.get_txin_type(address) - keystore.show_address(sequence, txin_type) diff --git a/plugins/ledger/qt.py b/plugins/ledger/qt.py @@ -1,81 +0,0 @@ -#from btchip.btchipPersoWizard import StartBTChipPersoDialog - -from electrum.i18n import _ -from electrum.plugins import hook -from electrum.wallet import Standard_Wallet -from electrum_gui.qt.util import * - -from .ledger import LedgerPlugin -from ..hw_wallet.qt import QtHandlerBase, QtPluginBase - - -class Plugin(LedgerPlugin, QtPluginBase): - icon_unpaired = ":icons/ledger_unpaired.png" - icon_paired = ":icons/ledger.png" - - def create_handler(self, window): - return Ledger_Handler(window) - - @hook - def receive_menu(self, menu, addrs, wallet): - if type(wallet) is not Standard_Wallet: - return - keystore = wallet.get_keystore() - if type(keystore) == self.keystore_class and len(addrs) == 1: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0])) - menu.addAction(_("Show on Ledger"), show_address) - -class Ledger_Handler(QtHandlerBase): - setup_signal = pyqtSignal() - auth_signal = pyqtSignal(object) - - def __init__(self, win): - super(Ledger_Handler, self).__init__(win, 'Ledger') - self.setup_signal.connect(self.setup_dialog) - self.auth_signal.connect(self.auth_dialog) - - def word_dialog(self, msg): - response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, QLineEdit.Password) - if not response[1]: - self.word = None - else: - self.word = str(response[0]) - self.done.set() - - def message_dialog(self, msg): - self.clear_dialog() - self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Ledger Status")) - l = QLabel(msg) - vbox = QVBoxLayout(dialog) - vbox.addWidget(l) - dialog.show() - - def auth_dialog(self, data): - try: - from .auth2fa import LedgerAuthDialog - except ImportError as e: - self.message_dialog(str(e)) - return - dialog = LedgerAuthDialog(self, data) - dialog.exec_() - self.word = dialog.pin - self.done.set() - - def get_auth(self, data): - self.done.clear() - self.auth_signal.emit(data) - self.done.wait() - return self.word - - def get_setup(self): - self.done.clear() - self.setup_signal.emit() - self.done.wait() - return - - def setup_dialog(self): - self.show_error(_('Initialization of Ledger HW devices is currently disabled.')) - return - dialog = StartBTChipPersoDialog() - dialog.exec_() diff --git a/plugins/revealer/DejaVuSansMono-Bold.ttf b/plugins/revealer/DejaVuSansMono-Bold.ttf Binary files differ. diff --git a/plugins/revealer/LICENSE_DEJAVU.txt b/plugins/revealer/LICENSE_DEJAVU.txt @@ -1,99 +0,0 @@ -Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. -Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) - -Bitstream Vera Fonts Copyright ------------------------------- - -Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is -a trademark of Bitstream, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of the fonts accompanying this license ("Fonts") and associated -documentation files (the "Font Software"), to reproduce and distribute the -Font Software, including without limitation the rights to use, copy, merge, -publish, distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to the -following conditions: - -The above copyright and trademark notices and this permission notice shall -be included in all copies of one or more of the Font Software typefaces. - -The Font Software may be modified, altered, or added to, and in particular -the designs of glyphs or characters in the Fonts may be modified and -additional glyphs or characters may be added to the Fonts, only if the fonts -are renamed to names not containing either the words "Bitstream" or the word -"Vera". - -This License becomes null and void to the extent applicable to Fonts or Font -Software that has been modified and is distributed under the "Bitstream -Vera" names. - -The Font Software may be sold as part of a larger software package but no -copy of one or more of the Font Software typefaces may be sold by itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, -TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME -FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING -ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF -THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE -FONT SOFTWARE. - -Except as contained in this notice, the names of Gnome, the Gnome -Foundation, and Bitstream Inc., shall not be used in advertising or -otherwise to promote the sale, use or other dealings in this Font Software -without prior written authorization from the Gnome Foundation or Bitstream -Inc., respectively. For further information, contact: fonts at gnome dot -org. - -Arev Fonts Copyright ------------------------------- - -Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of the fonts accompanying this license ("Fonts") and -associated documentation files (the "Font Software"), to reproduce -and distribute the modifications to the Bitstream Vera Font Software, -including without limitation the rights to use, copy, merge, publish, -distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to -the following conditions: - -The above copyright and trademark notices and this permission notice -shall be included in all copies of one or more of the Font Software -typefaces. - -The Font Software may be modified, altered, or added to, and in -particular the designs of glyphs or characters in the Fonts may be -modified and additional glyphs or characters may be added to the -Fonts, only if the fonts are renamed to names not containing either -the words "Tavmjong Bah" or the word "Arev". - -This License becomes null and void to the extent applicable to Fonts -or Font Software that has been modified and is distributed under the -"Tavmjong Bah Arev" names. - -The Font Software may be sold as part of a larger software package but -no copy of one or more of the Font Software typefaces may be sold by -itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL -TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. - -Except as contained in this notice, the name of Tavmjong Bah shall not -be used in advertising or otherwise to promote the sale, use or other -dealings in this Font Software without prior written authorization -from Tavmjong Bah. For further information, contact: tavmjong @ free -. fr. - -$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $ diff --git a/plugins/revealer/SIL Open Font License.txt b/plugins/revealer/SIL Open Font License.txt @@ -1,43 +0,0 @@ -Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. - -The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the copyright statement(s). - -"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. - -"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. - -5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.- \ No newline at end of file diff --git a/plugins/revealer/SourceSansPro-Bold.otf b/plugins/revealer/SourceSansPro-Bold.otf Binary files differ. diff --git a/plugins/revealer/__init__.py b/plugins/revealer/__init__.py @@ -1,17 +0,0 @@ -from electrum.i18n import _ - -fullname = _('Revealer') -description = ''.join(["<br/>", - "<b>"+_("Do you have something to hide ?")+"</b>", '<br/>', '<br/>', - _("Revealer is a seed phrase back-up solution. It allows you to create a cold, analog, multi-factor backup of your wallet seeds, or of any arbitrary secret."), '<br/>', '<br/>', - _("Using a Revealer is better than writing your seed phrases on paper: a revealer is invulnerable to physical access and allows creation of trustless redundancy."), '<br/>', '<br/>', - _("This plug-in allows you to generate a pdf file of your secret phrase encrypted visually for your physical Revealer. You can print it trustlessly - it can only be decrypted optically with your Revealer."), '<br/>', '<br/>', - _("The plug-in also allows you to generate a digital Revealer file and print it yourself on a transparent overhead foil."), '<br/>', '<br/>', - _("Once activated you can access the plug-in through the icon at the seed dialog."), '<br/>', '<br/>', - _("For more information, visit"), - " <a href=\"https://revealer.cc\">https://revealer.cc</a>", '<br/>', '<br/>', - -]) -available_for = ['qt'] - - diff --git a/plugins/revealer/qt.py b/plugins/revealer/qt.py @@ -1,723 +0,0 @@ -''' - -Revealer -So you have something to hide? - -plug-in for the electrum wallet. - -Features: - - Deep Cold multi-factor backup solution - - Safety - One time pad security - - Redundancy - Trustless printing & distribution - - Encrypt your seedphrase or any secret you want for your revealer - - Based on crypto by legendary cryptographers Naor and Shamir - -Tiago Romagnani Silveira, 2017 - -''' - -import os -import random -import qrcode -import traceback -from hashlib import sha256 -from decimal import Decimal - -from PyQt5.QtPrintSupport import QPrinter - -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ -from electrum_gui.qt.util import * -from electrum_gui.qt.qrtextedit import ScanQRTextEdit -from electrum.util import to_bytes, make_dir - - -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.base_dir = config.electrum_path()+'/revealer/' - - if self.config.get('calibration_h') == None: - self.config.set_key('calibration_h', 0) - if self.config.get('calibration_v') == None: - self.config.set_key('calibration_v', 0) - - self.calibration_h = self.config.get('calibration_h') - self.calibration_v = self.config.get('calibration_v') - - self.version = '0' - self.size = (159, 97) - self.f_size = QSize(1014*2, 642*2) - self.abstand_h = 21 - self.abstand_v = 34 - self.calibration_noise = int('10' * 128) - self.rawnoise = False - make_dir(self.base_dir) - - @hook - def set_seed(self, seed, parent): - self.cseed = seed.upper() - parent.addButton(':icons/revealer.png', partial(self.setup_dialog, parent), "Revealer"+_(" secret backup utility")) - - def requires_settings(self): - return True - - def settings_widget(self, window): - return EnterButton(_('Printer Calibration'), partial(self.calibration_dialog, window)) - - def setup_dialog(self, window): - self.update_wallet_name(window.parent().parent().wallet) - self.user_input = False - self.noise_seed = False - self.d = WindowModalDialog(window, "Revealer") - self.d.setMinimumWidth(420) - vbox = QVBoxLayout(self.d) - vbox.addSpacing(21) - logo = QLabel() - vbox.addWidget(logo) - logo.setPixmap(QPixmap(':icons/revealer.png')) - logo.setAlignment(Qt.AlignCenter) - vbox.addSpacing(42) - - self.load_noise = ScanQRTextEdit() - self.load_noise.setTabChangesFocus(True) - self.load_noise.textChanged.connect(self.on_edit) - self.load_noise.setMaximumHeight(33) - - vbox.addWidget(WWLabel("<b>"+_("Enter your physical revealer code:")+"<b>")) - vbox.addWidget(self.load_noise) - vbox.addSpacing(11) - - self.next_button = QPushButton(_("Next"), self.d) - self.next_button.setDefault(True) - self.next_button.setEnabled(False) - vbox.addLayout(Buttons(self.next_button)) - self.next_button.clicked.connect(self.d.close) - self.next_button.clicked.connect(partial(self.cypherseed_dialog, window)) - vbox.addSpacing(21) - - vbox.addWidget(WWLabel(_("or, alternatively: "))) - bcreate = QPushButton(_("Create a digital Revealer")) - - def mk_digital(): - try: - self.make_digital(self.d) - except Exception: - traceback.print_exc(file=sys.stdout) - else: - self.cypherseed_dialog(window) - - bcreate.clicked.connect(mk_digital) - - vbox.addWidget(bcreate) - vbox.addSpacing(11) - vbox.addWidget(QLabel(''.join([ "<b>"+_("WARNING")+ "</b>:" + _("Printing a revealer and encrypted seed"), '<br/>', - _("on the same printer is not trustless towards the printer."), '<br/>', - ]))) - vbox.addSpacing(11) - vbox.addLayout(Buttons(CloseButton(self.d))) - - return bool(self.d.exec_()) - - def get_noise(self): - text = self.load_noise.text() - return ''.join(text.split()).lower() - - def on_edit(self): - s = self.get_noise() - b = self.is_noise(s) - if b: - self.noise_seed = s[:-3] - self.user_input = True - self.next_button.setEnabled(b) - - def code_hashid(self, txt): - x = to_bytes(txt, 'utf8') - hash = sha256(x).hexdigest() - return hash[-3:].upper() - - def is_noise(self, txt): - if (len(txt) >= 34): - try: - int(txt, 16) - except: - self.user_input = False - return False - else: - id = self.code_hashid(txt[:-3]) - if (txt[-3:].upper() == id.upper()): - self.code_id = id - self.user_input = True - return True - else: - return False - else: - self.user_input = False - return False - - def make_digital(self, dialog): - self.make_rawnoise(True) - self.bdone(dialog) - self.d.close() - - def bcrypt(self, dialog): - self.rawnoise = False - dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at:").format(self.was, self.version, self.code_id), - "<br/>","<b>", self.base_dir+ self.filename+self.version+"_"+self.code_id,"</b>"])) - dialog.close() - - def bdone(self, dialog): - dialog.show_message(''.join([_("Digital Revealer ({}_{}) saved as PNG and PDF at:").format(self.version, self.code_id), - "<br/>","<b>", self.base_dir + 'revealer_' +self.version + '_'+ self.code_id, '</b>'])) - - - def customtxt_limits(self): - txt = self.text.text() - self.max_chars.setVisible(False) - self.char_count.setText("("+str(len(txt))+"/216)") - if len(txt)>0: - self.ctext.setEnabled(True) - if len(txt) > 216: - self.text.setPlainText(self.text.toPlainText()[:216]) - self.max_chars.setVisible(True) - - def t(self): - self.txt = self.text.text() - self.seed_img(is_seed=False) - - def cypherseed_dialog(self, window): - - d = WindowModalDialog(window, "Revealer") - d.setMinimumWidth(420) - - self.c_dialog = d - - self.vbox = QVBoxLayout(d) - self.vbox.addSpacing(21) - - logo = QLabel() - self.vbox.addWidget(logo) - logo.setPixmap(QPixmap(':icons/revealer.png')) - logo.setAlignment(Qt.AlignCenter) - self.vbox.addSpacing(42) - - grid = QGridLayout() - self.vbox.addLayout(grid) - - cprint = QPushButton(_("Generate encrypted seed PDF")) - cprint.clicked.connect(partial(self.seed_img, True)) - self.vbox.addWidget(cprint) - self.vbox.addSpacing(14) - - self.vbox.addWidget(WWLabel(_("and/or type any secret below:"))) - self.text = ScanQRTextEdit() - self.text.setTabChangesFocus(True) - self.text.setMaximumHeight(70) - self.text.textChanged.connect(self.customtxt_limits) - self.vbox.addWidget(self.text) - - self.char_count = WWLabel("") - self.char_count.setAlignment(Qt.AlignRight) - self.vbox.addWidget(self.char_count) - - self.max_chars = WWLabel("<font color='red'>" + _("This version supports a maximum of 216 characters.")+"</font>") - self.vbox.addWidget(self.max_chars) - self.max_chars.setVisible(False) - - self.ctext = QPushButton(_("Generate custom secret encrypted PDF")) - self.ctext.clicked.connect(self.t) - - self.vbox.addWidget(self.ctext) - self.ctext.setEnabled(False) - - self.vbox.addSpacing(21) - self.vbox.addLayout(Buttons(CloseButton(d))) - return bool(d.exec_()) - - - def update_wallet_name (self, name): - self.wallet_name = str(name) - self.base_name = self.base_dir + self.wallet_name - - def seed_img(self, is_seed = True): - - if not self.cseed and self.txt == False: - return - - if is_seed: - txt = self.cseed - else: - txt = self.txt.upper() - - img = QImage(self.size[0],self.size[1], QImage.Format_Mono) - bitmap = QBitmap.fromImage(img, Qt.MonoOnly) - bitmap.fill(Qt.white) - painter = QPainter() - painter.begin(bitmap) - QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(__file__), 'SourceSansPro-Bold.otf') ) - if len(txt) < 102 : - fontsize = 12 - linespace = 15 - max_letters = 17 - max_lines = 6 - max_words = 3 - if len(txt) > 102: - fontsize = 9 - linespace = 10 - max_letters = 24 - max_lines = 9 - max_words = int(max_letters/4) - - font = QFont('Source Sans Pro', fontsize, QFont.Bold) - font.setLetterSpacing(QFont.PercentageSpacing, 100) - painter.setFont(font) - seed_array = txt.split(' ') - - for n in range(max_lines): - nwords = max_words - temp_seed = seed_array[:nwords] - while len(' '.join(map(str, temp_seed))) > max_letters: - nwords = nwords - 1 - temp_seed = seed_array[:nwords] - painter.drawText(QRect(0, linespace*n , self.size[0], self.size[1]), Qt.AlignHCenter, ' '.join(map(str, temp_seed))) - del seed_array[:nwords] - - painter.end() - img = bitmap.toImage() - if (self.rawnoise == False): - self.make_rawnoise() - - self.make_cypherseed(img, self.rawnoise, False, is_seed) - return img - - def make_rawnoise(self, create_revealer=False): - w = self.size[0] - h = self.size[1] - rawnoise = QImage(w, h, QImage.Format_Mono) - - if(self.noise_seed == False): - self.noise_seed = random.SystemRandom().getrandbits(128) - self.hex_noise = format(self.noise_seed, '02x') - self.hex_noise = self.version + str(self.hex_noise) - - if (self.user_input == True): - self.noise_seed = int(self.noise_seed, 16) - self.hex_noise = self.version + str(format(self.noise_seed, '02x')) - - - self.code_id = self.code_hashid(self.hex_noise) - self.hex_noise = ' '.join(self.hex_noise[i:i+4] for i in range(0,len(self.hex_noise),4)) - random.seed(self.noise_seed) - - for x in range(w): - for y in range(h): - rawnoise.setPixel(x,y,random.randint(0, 1)) - - self.rawnoise = rawnoise - if create_revealer==True: - self.make_revealer() - self.noise_seed = False - - def make_calnoise(self): - random.seed(self.calibration_noise) - w = self.size[0] - h = self.size[1] - rawnoise = QImage(w, h, QImage.Format_Mono) - for x in range(w): - for y in range(h): - rawnoise.setPixel(x,y,random.randint(0, 1)) - self.calnoise = self.pixelcode_2x2(rawnoise) - - def make_revealer(self): - revealer = self.pixelcode_2x2(self.rawnoise) - revealer.invertPixels() - revealer = QBitmap.fromImage(revealer) - revealer = self.overlay_marks(revealer) - revealer = revealer.scaled(1014, 642) - self.filename = 'Revealer - ' - revealer.save(self.base_dir + self.filename + self.version+'_'+self.code_id + '.png') - self.toPdf(QImage(revealer)) - QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.abspath(self.base_dir + self.filename + self.version+'_'+ self.code_id + '.pdf'))) - - def make_cypherseed(self, img, rawnoise, calibration=False, is_seed = True): - img = img.convertToFormat(QImage.Format_Mono) - p = QPainter() - p.begin(img) - p.setCompositionMode(26) #xor - p.drawImage(0, 0, rawnoise) - p.end() - cypherseed = self.pixelcode_2x2(img) - cypherseed = QBitmap.fromImage(cypherseed) - cypherseed = cypherseed.scaled(self.f_size, Qt.KeepAspectRatio) - cypherseed = self.overlay_marks(cypherseed, True, calibration) - - if not is_seed: - self.filename = _('custom_secret')+'_' - self.was = _('Custom secret') - else: - self.filename = self.wallet_name+'_'+ _('seed')+'_' - self.was = self.wallet_name +' ' + _('seed') - - if not calibration: - self.toPdf(QImage(cypherseed)) - QDesktopServices.openUrl (QUrl.fromLocalFile(os.path.abspath(self.base_dir+self.filename+self.version+'_'+self.code_id+'.pdf'))) - cypherseed.save(self.base_dir + self.filename +self.version + '_'+ self.code_id + '.png') - self.bcrypt(self.c_dialog) - return cypherseed - - def calibration(self): - img = QImage(self.size[0],self.size[1], QImage.Format_Mono) - bitmap = QBitmap.fromImage(img, Qt.MonoOnly) - bitmap.fill(Qt.black) - self.make_calnoise() - img = self.overlay_marks(self.calnoise.scaledToHeight(self.f_size.height()), False, True) - self.calibration_pdf(img) - QDesktopServices.openUrl (QUrl.fromLocalFile(os.path.abspath(self.base_dir+_('calibration')+'.pdf'))) - return img - - def toPdf(self, image): - printer = QPrinter() - printer.setPaperSize(QSizeF(210, 297), QPrinter.Millimeter) - printer.setResolution(600) - printer.setOutputFormat(QPrinter.PdfFormat) - printer.setOutputFileName(self.base_dir+self.filename+self.version + '_'+self.code_id+'.pdf') - printer.setPageMargins(0,0,0,0,6) - painter = QPainter() - painter.begin(printer) - - delta_h = round(image.width()/self.abstand_v) - delta_v = round(image.height()/self.abstand_h) - - size_h = 2028+((int(self.calibration_h)*2028/(2028-(delta_h*2)+int(self.calibration_h)))/2) - size_v = 1284+((int(self.calibration_v)*1284/(1284-(delta_v*2)+int(self.calibration_v)))/2) - - image = image.scaled(size_h, size_v) - - painter.drawImage(553,533, image) - wpath = QPainterPath() - wpath.addRoundedRect(QRectF(553,533, size_h, size_v), 19, 19) - painter.setPen(QPen(Qt.black, 1)) - painter.drawPath(wpath) - painter.end() - - def calibration_pdf(self, image): - printer = QPrinter() - printer.setPaperSize(QSizeF(210, 297), QPrinter.Millimeter) - printer.setResolution(600) - printer.setOutputFormat(QPrinter.PdfFormat) - printer.setOutputFileName(self.base_dir+_('calibration')+'.pdf') - printer.setPageMargins(0,0,0,0,6) - - painter = QPainter() - painter.begin(printer) - painter.drawImage(553,533, image) - font = QFont('Source Sans Pro', 10, QFont.Bold) - painter.setFont(font) - painter.drawText(254,277, _("Calibration sheet")) - font = QFont('Source Sans Pro', 7, QFont.Bold) - painter.setFont(font) - painter.drawText(600,2077, _("Instructions:")) - font = QFont('Source Sans Pro', 7, QFont.Normal) - painter.setFont(font) - painter.drawText(700, 2177, _("1. Place this paper on a flat and well iluminated surface.")) - painter.drawText(700, 2277, _("2. Align your Revealer borderlines to the dashed lines on the top and left.")) - painter.drawText(700, 2377, _("3. Press slightly the Revealer against the paper and read the numbers that best " - "match on the opposite sides. ")) - painter.drawText(700, 2477, _("4. Type the numbers in the software")) - painter.end() - - def pixelcode_2x2(self, img): - result = QImage(img.width()*2, img.height()*2, QImage.Format_ARGB32 ) - white = qRgba(255,255,255,0) - black = qRgba(0,0,0,255) - - for x in range(img.width()): - for y in range(img.height()): - c = img.pixel(QPoint(x,y)) - colors = QColor(c).getRgbF() - if colors[0]: - result.setPixel(x*2+1,y*2+1, black) - result.setPixel(x*2,y*2+1, white) - result.setPixel(x*2+1,y*2, white) - result.setPixel(x*2, y*2, black) - - else: - result.setPixel(x*2+1,y*2+1, white) - result.setPixel(x*2,y*2+1, black) - result.setPixel(x*2+1,y*2, black) - result.setPixel(x*2, y*2, white) - return result - - def overlay_marks(self, img, is_cseed=False, calibration_sheet=False): - border_color = Qt.white - base_img = QImage(self.f_size.width(),self.f_size.height(), QImage.Format_ARGB32) - base_img.fill(border_color) - img = QImage(img) - - painter = QPainter() - painter.begin(base_img) - - total_distance_h = round(base_img.width() / self.abstand_v) - dist_v = round(total_distance_h) / 2 - dist_h = round(total_distance_h) / 2 - - img = img.scaledToWidth(base_img.width() - (2 * (total_distance_h))) - painter.drawImage(total_distance_h, - total_distance_h, - img) - - #frame around image - pen = QPen(Qt.black, 2) - painter.setPen(pen) - - #horz - painter.drawLine(0, total_distance_h, base_img.width(), total_distance_h) - painter.drawLine(0, base_img.height()-(total_distance_h), base_img.width(), base_img.height()-(total_distance_h)) - #vert - painter.drawLine(total_distance_h, 0, total_distance_h, base_img.height()) - painter.drawLine(base_img.width()-(total_distance_h), 0, base_img.width()-(total_distance_h), base_img.height()) - - #border around img - border_thick = 6 - Rpath = QPainterPath() - Rpath.addRect(QRectF((total_distance_h)+(border_thick/2), - (total_distance_h)+(border_thick/2), - base_img.width()-((total_distance_h)*2)-((border_thick)-1), - (base_img.height()-((total_distance_h))*2)-((border_thick)-1))) - pen = QPen(Qt.black, border_thick) - pen.setJoinStyle (Qt.MiterJoin) - - painter.setPen(pen) - painter.drawPath(Rpath) - - Bpath = QPainterPath() - Bpath.addRect(QRectF((total_distance_h), (total_distance_h), - base_img.width()-((total_distance_h)*2), (base_img.height()-((total_distance_h))*2))) - pen = QPen(Qt.black, 1) - painter.setPen(pen) - painter.drawPath(Bpath) - - pen = QPen(Qt.black, 1) - painter.setPen(pen) - painter.drawLine(0, base_img.height()/2, total_distance_h, base_img.height()/2) - painter.drawLine(base_img.width()/2, 0, base_img.width()/2, total_distance_h) - - painter.drawLine(base_img.width()-total_distance_h, base_img.height()/2, base_img.width(), base_img.height()/2) - painter.drawLine(base_img.width()/2, base_img.height(), base_img.width()/2, base_img.height() - total_distance_h) - - #print code - f_size = 37 - QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(__file__), 'DejaVuSansMono-Bold.ttf')) - font = QFont("DejaVu Sans Mono", f_size-11, QFont.Bold) - painter.setFont(font) - - if not calibration_sheet: - if is_cseed: #its a secret - painter.setPen(QPen(Qt.black, 1, Qt.DashDotDotLine)) - painter.drawLine(0, dist_v, base_img.width(), dist_v) - painter.drawLine(dist_h, 0, dist_h, base_img.height()) - painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v)) - painter.drawLine(base_img.width()-(dist_h), 0, base_img.width()-(dist_h), base_img.height()) - - painter.drawImage(((total_distance_h))+11, ((total_distance_h))+11, - QImage(':icons/electrumb.png').scaledToWidth(2.1*(total_distance_h), Qt.SmoothTransformation)) - - painter.setPen(QPen(Qt.white, border_thick*8)) - painter.drawLine(base_img.width()-((total_distance_h))-(border_thick*8)/2-(border_thick/2)-2, - (base_img.height()-((total_distance_h)))-((border_thick*8)/2)-(border_thick/2)-2, - base_img.width()-((total_distance_h))-(border_thick*8)/2-(border_thick/2)-2 - 77, - (base_img.height()-((total_distance_h)))-((border_thick*8)/2)-(border_thick/2)-2) - painter.setPen(QColor(0,0,0,255)) - painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick - 11, - base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.version + '_'+self.code_id) - painter.end() - - else: # revealer - - painter.setPen(QPen(border_color, 17)) - painter.drawLine(0, dist_v, base_img.width(), dist_v) - painter.drawLine(dist_h, 0, dist_h, base_img.height()) - painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v)) - painter.drawLine(base_img.width()-(dist_h), 0, base_img.width()-(dist_h), base_img.height()) - - painter.setPen(QPen(Qt.black, 2)) - painter.drawLine(0, dist_v, base_img.width(), dist_v) - painter.drawLine(dist_h, 0, dist_h, base_img.height()) - painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v)) - painter.drawLine(base_img.width()-(dist_h), 0, base_img.width()-(dist_h), base_img.height()) - logo = QImage(':icons/revealer_c.png').scaledToWidth(1.3*(total_distance_h)) - painter.drawImage((total_distance_h)+ (border_thick), ((total_distance_h))+ (border_thick), logo, Qt.SmoothTransformation) - - #frame around logo - painter.setPen(QPen(Qt.black, border_thick)) - painter.drawLine(total_distance_h+border_thick, total_distance_h+logo.height()+3*(border_thick/2), - total_distance_h+logo.width()+border_thick, total_distance_h+logo.height()+3*(border_thick/2)) - painter.drawLine(logo.width()+total_distance_h+3*(border_thick/2), total_distance_h+(border_thick), - total_distance_h+logo.width()+3*(border_thick/2), total_distance_h+logo.height()+(border_thick)) - - #frame around code/qr - qr_size = 179 - - painter.drawLine((base_img.width()-((total_distance_h))-(border_thick/2)-2)-qr_size, - (base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2, - (base_img.width()/2+(total_distance_h/2)-border_thick-(border_thick*8)/2)-qr_size, - (base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2) - - painter.drawLine((base_img.width()/2+(total_distance_h/2)-border_thick-(border_thick*8)/2)-qr_size, - (base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2, - base_img.width()/2 + (total_distance_h/2)-border_thick-(border_thick*8)/2-qr_size, - ((base_img.height()-((total_distance_h)))-(border_thick/2)-2)) - - painter.setPen(QPen(Qt.white, border_thick * 8)) - painter.drawLine( - base_img.width() - ((total_distance_h)) - (border_thick * 8) / 2 - (border_thick / 2) - 2, - (base_img.height() - ((total_distance_h))) - ((border_thick * 8) / 2) - (border_thick / 2) - 2, - base_img.width() / 2 + (total_distance_h / 2) - border_thick - qr_size, - (base_img.height() - ((total_distance_h))) - ((border_thick * 8) / 2) - (border_thick / 2) - 2) - - painter.setPen(QColor(0,0,0,255)) - painter.drawText(QRect(((base_img.width()/2) +21)-qr_size, base_img.height()-107, - base_img.width()-total_distance_h - border_thick -93, - base_img.height()-total_distance_h - border_thick), Qt.AlignLeft, self.hex_noise.upper()) - painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick -3 -qr_size, - base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.code_id) - - # draw qr code - qr_qt = self.paintQR(self.hex_noise.upper() +self.code_id) - target = QRectF(base_img.width()-65-qr_size, - base_img.height()-65-qr_size, - qr_size, qr_size ); - painter.drawImage(target, qr_qt); - painter.setPen(QPen(Qt.black, 4)) - painter.drawLine(base_img.width()-65-qr_size, - base_img.height()-65-qr_size, - base_img.width() - 65 - qr_size, - (base_img.height() - ((total_distance_h))) - ((border_thick * 8)) - (border_thick / 2) - 4 - ) - painter.drawLine(base_img.width()-65-qr_size, - base_img.height()-65-qr_size, - base_img.width() - 65, - base_img.height()-65-qr_size - ) - painter.end() - - else: # calibration only - painter.end() - cal_img = QImage(self.f_size.width() + 100, self.f_size.height() + 100, - QImage.Format_ARGB32) - cal_img.fill(Qt.white) - - cal_painter = QPainter() - cal_painter.begin(cal_img) - cal_painter.drawImage(0,0, base_img) - - #black lines in the middle of border top left only - cal_painter.setPen(QPen(Qt.black, 1, Qt.DashDotDotLine)) - cal_painter.drawLine(0, dist_v, base_img.width(), dist_v) - cal_painter.drawLine(dist_h, 0, dist_h, base_img.height()) - - pen = QPen(Qt.black, 2, Qt.DashDotDotLine) - cal_painter.setPen(pen) - n=15 - - cal_painter.setFont(QFont("DejaVu Sans Mono", 21, QFont.Bold)) - for x in range(-n,n): - #lines on bottom (vertical calibration) - cal_painter.drawLine((((base_img.width())/(n*2)) *(x))+ (base_img.width()/2)-13, - x+2+base_img.height()-(dist_v), - (((base_img.width())/(n*2)) *(x))+ (base_img.width()/2)+13, - x+2+base_img.height()-(dist_v)) - - num_pos = 9 - if x > 9 : num_pos = 17 - if x < 0 : num_pos = 20 - if x < -9: num_pos = 27 - - cal_painter.drawText((((base_img.width())/(n*2)) *(x))+ (base_img.width()/2)-num_pos, - 50+base_img.height()-(dist_v), - str(x)) - - #lines on the right (horizontal calibrations) - - cal_painter.drawLine(x+2+(base_img.width()-(dist_h)), - ((base_img.height()/(2*n)) *(x))+ (base_img.height()/n)+(base_img.height()/2)-13, - x+2+(base_img.width()-(dist_h)), - ((base_img.height()/(2*n)) *(x))+ (base_img.height()/n)+(base_img.height()/2)+13) - - - cal_painter.drawText(30+(base_img.width()-(dist_h)), - ((base_img.height()/(2*n)) *(x))+ (base_img.height()/2)+13, str(x)) - - cal_painter.end() - base_img = cal_img - - return base_img - - def paintQR(self, data): - if not data: - return - qr = qrcode.QRCode() - qr.add_data(data) - matrix = qr.get_matrix() - k = len(matrix) - border_color = Qt.white - base_img = QImage(k * 5, k * 5, QImage.Format_ARGB32) - base_img.fill(border_color) - qrpainter = QPainter() - qrpainter.begin(base_img) - boxsize = 5 - size = k * boxsize - left = (base_img.width() - size)/2 - top = (base_img.height() - size)/2 - qrpainter.setBrush(Qt.black) - qrpainter.setPen(Qt.black) - - for r in range(k): - for c in range(k): - if matrix[r][c]: - qrpainter.drawRect(left+c*boxsize, top+r*boxsize, boxsize - 1, boxsize - 1) - qrpainter.end() - return base_img - - def calibration_dialog(self, window): - d = WindowModalDialog(window, _("Revealer - Printer calibration settings")) - - d.setMinimumSize(100, 200) - - vbox = QVBoxLayout(d) - vbox.addWidget(QLabel(''.join(["<br/>", _("If you have an old printer, or want optimal precision"),"<br/>", - _("print the calibration pdf and follow the instructions "), "<br/>","<br/>", - ]))) - self.calibration_h = self.config.get('calibration_h') - self.calibration_v = self.config.get('calibration_v') - cprint = QPushButton(_("Open calibration pdf")) - cprint.clicked.connect(self.calibration) - vbox.addWidget(cprint) - - vbox.addWidget(QLabel(_('Calibration values:'))) - grid = QGridLayout() - vbox.addLayout(grid) - grid.addWidget(QLabel(_('Right side')), 0, 0) - horizontal = QLineEdit() - horizontal.setText(str(self.calibration_h)) - grid.addWidget(horizontal, 0, 1) - - grid.addWidget(QLabel(_('Bottom')), 1, 0) - vertical = QLineEdit() - vertical.setText(str(self.calibration_v)) - grid.addWidget(vertical, 1, 1) - - vbox.addStretch() - vbox.addSpacing(13) - vbox.addLayout(Buttons(CloseButton(d), OkButton(d))) - - if not d.exec_(): - return - - self.calibration_h = int(Decimal(horizontal.text())) - self.config.set_key('calibration_h', self.calibration_h) - self.calibration_v = int(Decimal(vertical.text())) - self.config.set_key('calibration_v', self.calibration_v) - - diff --git a/plugins/trezor/__init__.py b/plugins/trezor/__init__.py @@ -1,8 +0,0 @@ -from electrum.i18n import _ - -fullname = 'TREZOR Wallet' -description = _('Provides support for TREZOR hardware wallet') -requires = [('trezorlib','github.com/trezor/python-trezor')] -registers_keystore = ('hardware', 'trezor', _("TREZOR wallet")) -available_for = ['qt', 'cmdline'] - diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py @@ -1,11 +0,0 @@ -from trezorlib.client import proto, BaseClient, ProtocolMixin -from .clientbase import TrezorClientBase - -class TrezorClient(TrezorClientBase, ProtocolMixin, BaseClient): - def __init__(self, transport, handler, plugin): - BaseClient.__init__(self, transport=transport) - ProtocolMixin.__init__(self, transport=transport) - TrezorClientBase.__init__(self, handler, plugin, proto) - - -TrezorClientBase.wrap_methods(TrezorClient) diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py @@ -1,265 +0,0 @@ -import time -from struct import pack - -from electrum.i18n import _ -from electrum.util import PrintError, UserCancelled -from electrum.keystore import bip39_normalize_passphrase -from electrum.bitcoin import serialize_xpub - - -class GuiMixin(object): - # Requires: self.proto, self.device - - # ref: https://github.com/trezor/trezor-common/blob/44dfb07cfaafffada4b2ce0d15ba1d90d17cf35e/protob/types.proto#L89 - messages = { - 3: _("Confirm the transaction output on your {} device"), - 4: _("Confirm internal entropy on your {} device to begin"), - 5: _("Write down the seed word shown on your {}"), - 6: _("Confirm on your {} that you want to wipe it clean"), - 7: _("Confirm on your {} device the message to sign"), - 8: _("Confirm the total amount spent and the transaction fee on your " - "{} device"), - 10: _("Confirm wallet address on your {} device"), - 14: _("Choose on your {} device where to enter your passphrase"), - 'default': _("Check your {} device to continue"), - } - - def callback_Failure(self, msg): - # BaseClient's unfortunate call() implementation forces us to - # raise exceptions on failure in order to unwind the stack. - # However, making the user acknowledge they cancelled - # gets old very quickly, so we suppress those. The NotInitialized - # one is misnamed and indicates a passphrase request was cancelled. - if msg.code in (self.types.FailureType.PinCancelled, - self.types.FailureType.ActionCancelled, - self.types.FailureType.NotInitialized): - raise UserCancelled() - raise RuntimeError(msg.message) - - def callback_ButtonRequest(self, msg): - message = self.msg - if not message: - message = self.messages.get(msg.code, self.messages['default']) - self.handler.show_message(message.format(self.device), self.cancel) - return self.proto.ButtonAck() - - def callback_PinMatrixRequest(self, msg): - if msg.type == 2: - msg = _("Enter a new PIN for your {}:") - elif msg.type == 3: - msg = (_("Re-enter the new PIN for your {}.\n\n" - "NOTE: the positions of the numbers have changed!")) - else: - msg = _("Enter your current {} PIN:") - pin = self.handler.get_pin(msg.format(self.device)) - if len(pin) > 9: - self.handler.show_error(_('The PIN cannot be longer than 9 characters.')) - pin = '' # to cancel below - if not pin: - return self.proto.Cancel() - return self.proto.PinMatrixAck(pin=pin) - - def callback_PassphraseRequest(self, req): - if req and hasattr(req, 'on_device') and req.on_device is True: - return self.proto.PassphraseAck() - - if self.creating_wallet: - msg = _("Enter a passphrase to generate this wallet. Each time " - "you use this wallet your {} will prompt you for the " - "passphrase. If you forget the passphrase you cannot " - "access the bitcoins in the wallet.").format(self.device) - else: - msg = _("Enter the passphrase to unlock this wallet:") - passphrase = self.handler.get_passphrase(msg, self.creating_wallet) - if passphrase is None: - return self.proto.Cancel() - passphrase = bip39_normalize_passphrase(passphrase) - - ack = self.proto.PassphraseAck(passphrase=passphrase) - length = len(ack.passphrase) - if length > 50: - self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length)) - return self.proto.Cancel() - return ack - - def callback_PassphraseStateRequest(self, msg): - return self.proto.PassphraseStateAck() - - def callback_WordRequest(self, msg): - if (msg.type is not None - and msg.type in (self.types.WordRequestType.Matrix9, - self.types.WordRequestType.Matrix6)): - num = 9 if msg.type == self.types.WordRequestType.Matrix9 else 6 - char = self.handler.get_matrix(num) - if char == 'x': - return self.proto.Cancel() - return self.proto.WordAck(word=char) - - self.step += 1 - msg = _("Step {}/24. Enter seed word as explained on " - "your {}:").format(self.step, self.device) - word = self.handler.get_word(msg) - # Unfortunately the device can't handle self.proto.Cancel() - return self.proto.WordAck(word=word) - - -class TrezorClientBase(GuiMixin, PrintError): - - def __init__(self, handler, plugin, proto): - assert hasattr(self, 'tx_api') # ProtocolMixin already constructed? - self.proto = proto - self.device = plugin.device - self.handler = handler - self.tx_api = plugin - self.types = plugin.types - self.msg = None - self.creating_wallet = False - self.used() - - def __str__(self): - return "%s/%s" % (self.label(), self.features.device_id) - - def label(self): - '''The name given by the user to the device.''' - return self.features.label - - def is_initialized(self): - '''True if initialized, False if wiped.''' - return self.features.initialized - - def is_pairable(self): - return not self.features.bootloader_mode - - def has_usable_connection_with_device(self): - try: - res = self.ping("electrum pinging device") - assert res == "electrum pinging device" - except BaseException: - return False - return True - - def used(self): - self.last_operation = time.time() - - def prevent_timeouts(self): - self.last_operation = float('inf') - - def timeout(self, cutoff): - '''Time out the client if the last operation was before cutoff.''' - if self.last_operation < cutoff: - self.print_error("timed out") - self.clear_session() - - @staticmethod - def expand_path(n): - '''Convert bip32 path to list of uint32 integers with prime flags - 0/-1/1' -> [0, 0x80000001, 0x80000001]''' - # This code is similar to code in trezorlib where it unfortunately - # is not declared as a staticmethod. Our n has an extra element. - PRIME_DERIVATION_FLAG = 0x80000000 - path = [] - for x in n.split('/')[1:]: - prime = 0 - if x.endswith("'"): - x = x.replace('\'', '') - prime = PRIME_DERIVATION_FLAG - if x.startswith('-'): - prime = PRIME_DERIVATION_FLAG - path.append(abs(int(x)) | prime) - return path - - def cancel(self): - '''Provided here as in keepkeylib but not trezorlib.''' - self.transport.write(self.proto.Cancel()) - - def i4b(self, x): - return pack('>I', x) - - def get_xpub(self, bip32_path, xtype): - address_n = self.expand_path(bip32_path) - creating = False - node = self.get_public_node(address_n, creating).node - return serialize_xpub(xtype, node.chain_code, node.public_key, node.depth, self.i4b(node.fingerprint), self.i4b(node.child_num)) - - def toggle_passphrase(self): - if self.features.passphrase_protection: - self.msg = _("Confirm on your {} device to disable passphrases") - else: - self.msg = _("Confirm on your {} device to enable passphrases") - enabled = not self.features.passphrase_protection - self.apply_settings(use_passphrase=enabled) - - def change_label(self, label): - self.msg = _("Confirm the new label on your {} device") - self.apply_settings(label=label) - - def change_homescreen(self, homescreen): - self.msg = _("Confirm on your {} device to change your home screen") - self.apply_settings(homescreen=homescreen) - - def set_pin(self, remove): - if remove: - self.msg = _("Confirm on your {} device to disable PIN protection") - elif self.features.pin_protection: - self.msg = _("Confirm on your {} device to change your PIN") - else: - self.msg = _("Confirm on your {} device to set a PIN") - self.change_pin(remove) - - def clear_session(self): - '''Clear the session to force pin (and passphrase if enabled) - re-entry. Does not leak exceptions.''' - self.print_error("clear session:", self) - self.prevent_timeouts() - try: - super(TrezorClientBase, self).clear_session() - except BaseException as e: - # If the device was removed it has the same effect... - self.print_error("clear_session: ignoring error", str(e)) - - def get_public_node(self, address_n, creating): - self.creating_wallet = creating - return super(TrezorClientBase, self).get_public_node(address_n) - - def close(self): - '''Called when Our wallet was closed or the device removed.''' - self.print_error("closing client") - self.clear_session() - # Release the device - self.transport.close() - - def firmware_version(self): - f = self.features - return (f.major_version, f.minor_version, f.patch_version) - - def atleast_version(self, major, minor=0, patch=0): - return self.firmware_version() >= (major, minor, patch) - - def get_trezor_model(self): - """Returns '1' for Trezor One, 'T' for Trezor T.""" - return self.features.model - - @staticmethod - def wrapper(func): - '''Wrap methods to clear any message box they opened.''' - - def wrapped(self, *args, **kwargs): - try: - self.prevent_timeouts() - return func(self, *args, **kwargs) - finally: - self.used() - self.handler.finished() - self.creating_wallet = False - self.msg = None - - return wrapped - - @staticmethod - def wrap_methods(cls): - for method in ['apply_settings', 'change_pin', - 'get_address', 'get_public_node', - 'load_device_by_mnemonic', 'load_device_by_xprv', - 'recovery_device', 'reset_device', 'sign_message', - 'sign_tx', 'wipe_device']: - setattr(cls, method, cls.wrapper(getattr(cls, method))) diff --git a/plugins/trezor/cmdline.py b/plugins/trezor/cmdline.py @@ -1,14 +0,0 @@ -from electrum.plugins import hook -from .trezor import TrezorPlugin -from ..hw_wallet import CmdLineHandler - -class Plugin(TrezorPlugin): - handler = CmdLineHandler() - @hook - def init_keystore(self, keystore): - if not isinstance(keystore, self.keystore_class): - return - keystore.handler = self.handler - - def create_handler(self, window): - return self.handler diff --git a/plugins/trezor/qt.py b/plugins/trezor/qt.py @@ -1,613 +0,0 @@ -from functools import partial -import threading - -from PyQt5.Qt import Qt -from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton -from PyQt5.Qt import QVBoxLayout, QLabel - -from electrum_gui.qt.util import * -from electrum.i18n import _ -from electrum.plugins import hook, DeviceMgr -from electrum.util import PrintError, UserCancelled, bh2u -from electrum.wallet import Wallet, Standard_Wallet - -from ..hw_wallet.qt import QtHandlerBase, QtPluginBase -from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, - RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX) - - -PASSPHRASE_HELP_SHORT =_( - "Passphrases allow you to access new wallets, each " - "hidden behind a particular case-sensitive passphrase.") -PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _( - "You need to create a separate Electrum wallet for each passphrase " - "you use as they each generate different addresses. Changing " - "your passphrase does not lose other wallets, each is still " - "accessible behind its own passphrase.") -RECOMMEND_PIN = _( - "You should enable PIN protection. Your PIN is the only protection " - "for your bitcoins if your device is lost or stolen.") -PASSPHRASE_NOT_PIN = _( - "If you forget a passphrase you will be unable to access any " - "bitcoins in the wallet behind it. A passphrase is not a PIN. " - "Only change this if you are sure you understand it.") -MATRIX_RECOVERY = _( - "Enter the recovery words by pressing the buttons according to what " - "the device shows on its display. You can also use your NUMPAD.\n" - "Press BACKSPACE to go back a choice or word.\n") - - -class MatrixDialog(WindowModalDialog): - - def __init__(self, parent): - super(MatrixDialog, self).__init__(parent) - self.setWindowTitle(_("Trezor Matrix Recovery")) - self.num = 9 - self.loop = QEventLoop() - - vbox = QVBoxLayout(self) - vbox.addWidget(WWLabel(MATRIX_RECOVERY)) - - grid = QGridLayout() - grid.setSpacing(0) - self.char_buttons = [] - for y in range(3): - for x in range(3): - button = QPushButton('?') - button.clicked.connect(partial(self.process_key, ord('1') + y * 3 + x)) - grid.addWidget(button, 3 - y, x) - self.char_buttons.append(button) - vbox.addLayout(grid) - - self.backspace_button = QPushButton("<=") - self.backspace_button.clicked.connect(partial(self.process_key, Qt.Key_Backspace)) - self.cancel_button = QPushButton(_("Cancel")) - self.cancel_button.clicked.connect(partial(self.process_key, Qt.Key_Escape)) - buttons = Buttons(self.backspace_button, self.cancel_button) - vbox.addSpacing(40) - vbox.addLayout(buttons) - self.refresh() - self.show() - - def refresh(self): - for y in range(3): - self.char_buttons[3 * y + 1].setEnabled(self.num == 9) - - def is_valid(self, key): - return key >= ord('1') and key <= ord('9') - - def process_key(self, key): - self.data = None - if key == Qt.Key_Backspace: - self.data = '\010' - elif key == Qt.Key_Escape: - self.data = 'x' - elif self.is_valid(key): - self.char_buttons[key - ord('1')].setFocus() - self.data = '%c' % key - if self.data: - self.loop.exit(0) - - def keyPressEvent(self, event): - self.process_key(event.key()) - if not self.data: - QDialog.keyPressEvent(self, event) - - def get_matrix(self, num): - self.num = num - self.refresh() - self.loop.exec_() - - -class QtHandler(QtHandlerBase): - - pin_signal = pyqtSignal(object) - matrix_signal = pyqtSignal(object) - close_matrix_dialog_signal = pyqtSignal() - - def __init__(self, win, pin_matrix_widget_class, device): - super(QtHandler, self).__init__(win, device) - self.pin_signal.connect(self.pin_dialog) - self.matrix_signal.connect(self.matrix_recovery_dialog) - self.close_matrix_dialog_signal.connect(self._close_matrix_dialog) - self.pin_matrix_widget_class = pin_matrix_widget_class - self.matrix_dialog = None - - def get_pin(self, msg): - self.done.clear() - self.pin_signal.emit(msg) - self.done.wait() - return self.response - - def get_matrix(self, msg): - self.done.clear() - self.matrix_signal.emit(msg) - self.done.wait() - data = self.matrix_dialog.data - if data == 'x': - self.close_matrix_dialog() - return data - - def _close_matrix_dialog(self): - if self.matrix_dialog: - self.matrix_dialog.accept() - self.matrix_dialog = None - - def close_matrix_dialog(self): - self.close_matrix_dialog_signal.emit() - - def pin_dialog(self, msg): - # Needed e.g. when resetting a device - self.clear_dialog() - dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN")) - matrix = self.pin_matrix_widget_class() - vbox = QVBoxLayout() - vbox.addWidget(QLabel(msg)) - vbox.addWidget(matrix) - vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) - dialog.setLayout(vbox) - dialog.exec_() - self.response = str(matrix.get_value()) - self.done.set() - - def matrix_recovery_dialog(self, msg): - if not self.matrix_dialog: - self.matrix_dialog = MatrixDialog(self.top_level_window()) - self.matrix_dialog.get_matrix(msg) - self.done.set() - - -class QtPlugin(QtPluginBase): - # Derived classes must provide the following class-static variables: - # icon_file - # pin_matrix_widget_class - - def create_handler(self, window): - return QtHandler(window, self.pin_matrix_widget_class(), self.device) - - @hook - def receive_menu(self, menu, addrs, wallet): - if len(addrs) != 1: - return - for keystore in wallet.get_keystores(): - if type(keystore) == self.keystore_class: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) - menu.addAction(_("Show on {}").format(self.device), show_address) - break - - def show_settings_dialog(self, window, keystore): - device_id = self.choose_device(window, keystore) - if device_id: - SettingsDialog(window, self, keystore, device_id).exec_() - - def request_trezor_init_settings(self, wizard, method, model): - vbox = QVBoxLayout() - next_enabled = True - label = QLabel(_("Enter a label to name your device:")) - name = QLineEdit() - hl = QHBoxLayout() - hl.addWidget(label) - hl.addWidget(name) - hl.addStretch(1) - vbox.addLayout(hl) - - def clean_text(widget): - text = widget.toPlainText().strip() - return ' '.join(text.split()) - - if method in [TIM_NEW, TIM_RECOVER]: - gb = QGroupBox() - hbox1 = QHBoxLayout() - gb.setLayout(hbox1) - vbox.addWidget(gb) - gb.setTitle(_("Select your seed length:")) - bg_numwords = QButtonGroup() - for i, count in enumerate([12, 18, 24]): - rb = QRadioButton(gb) - rb.setText(_("%d words") % count) - bg_numwords.addButton(rb) - bg_numwords.setId(rb, i) - hbox1.addWidget(rb) - rb.setChecked(True) - cb_pin = QCheckBox(_('Enable PIN protection')) - cb_pin.setChecked(True) - else: - text = QTextEdit() - text.setMaximumHeight(60) - if method == TIM_MNEMONIC: - msg = _("Enter your BIP39 mnemonic:") - else: - msg = _("Enter the master private key beginning with xprv:") - def set_enabled(): - from electrum.keystore import is_xprv - wizard.next_button.setEnabled(is_xprv(clean_text(text))) - text.textChanged.connect(set_enabled) - next_enabled = False - - vbox.addWidget(QLabel(msg)) - vbox.addWidget(text) - pin = QLineEdit() - pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) - pin.setMaximumWidth(100) - hbox_pin = QHBoxLayout() - hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) - hbox_pin.addWidget(pin) - hbox_pin.addStretch(1) - - if method in [TIM_NEW, TIM_RECOVER]: - vbox.addWidget(WWLabel(RECOMMEND_PIN)) - vbox.addWidget(cb_pin) - else: - vbox.addLayout(hbox_pin) - - passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - cb_phrase = QCheckBox(_('Enable passphrases')) - cb_phrase.setChecked(False) - vbox.addWidget(passphrase_msg) - vbox.addWidget(passphrase_warning) - vbox.addWidget(cb_phrase) - - # ask for recovery type (random word order OR matrix) - if method == TIM_RECOVER and not model == 'T': - gb_rectype = QGroupBox() - hbox_rectype = QHBoxLayout() - gb_rectype.setLayout(hbox_rectype) - vbox.addWidget(gb_rectype) - gb_rectype.setTitle(_("Select recovery type:")) - bg_rectype = QButtonGroup() - - rb1 = QRadioButton(gb_rectype) - rb1.setText(_('Scrambled words')) - bg_rectype.addButton(rb1) - bg_rectype.setId(rb1, RECOVERY_TYPE_SCRAMBLED_WORDS) - hbox_rectype.addWidget(rb1) - rb1.setChecked(True) - - rb2 = QRadioButton(gb_rectype) - rb2.setText(_('Matrix')) - bg_rectype.addButton(rb2) - bg_rectype.setId(rb2, RECOVERY_TYPE_MATRIX) - hbox_rectype.addWidget(rb2) - else: - bg_rectype = None - - wizard.exec_layout(vbox, next_enabled=next_enabled) - - if method in [TIM_NEW, TIM_RECOVER]: - item = bg_numwords.checkedId() - pin = cb_pin.isChecked() - recovery_type = bg_rectype.checkedId() if bg_rectype else None - else: - item = ' '.join(str(clean_text(text)).split()) - pin = str(pin.text()) - recovery_type = None - - return (item, name.text(), pin, cb_phrase.isChecked(), recovery_type) - - -class Plugin(TrezorPlugin, QtPlugin): - icon_unpaired = ":icons/trezor_unpaired.png" - icon_paired = ":icons/trezor.png" - - @classmethod - def pin_matrix_widget_class(self): - from trezorlib.qt.pinmatrix import PinMatrixWidget - return PinMatrixWidget - - -class SettingsDialog(WindowModalDialog): - '''This dialog doesn't require a device be paired with a wallet. - We want users to be able to wipe a device even if they've forgotten - their PIN.''' - - def __init__(self, window, plugin, keystore, device_id): - title = _("{} Settings").format(plugin.device) - super(SettingsDialog, self).__init__(window, title) - self.setMaximumWidth(540) - - devmgr = plugin.device_manager() - config = devmgr.config - handler = keystore.handler - thread = keystore.thread - hs_rows, hs_cols = (64, 128) - - def invoke_client(method, *args, **kw_args): - unpair_after = kw_args.pop('unpair_after', False) - - def task(): - client = devmgr.client_by_id(device_id) - if not client: - raise RuntimeError("Device not connected") - if method: - getattr(client, method)(*args, **kw_args) - if unpair_after: - devmgr.unpair_id(device_id) - return client.features - - thread.add(task, on_success=update) - - def update(features): - self.features = features - set_label_enabled() - if features.bootloader_hash: - bl_hash = bh2u(features.bootloader_hash) - bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) - else: - bl_hash = "N/A" - noyes = [_("No"), _("Yes")] - endis = [_("Enable Passphrases"), _("Disable Passphrases")] - disen = [_("Disabled"), _("Enabled")] - setchange = [_("Set a PIN"), _("Change PIN")] - - version = "%d.%d.%d" % (features.major_version, - features.minor_version, - features.patch_version) - - device_label.setText(features.label) - pin_set_label.setText(noyes[features.pin_protection]) - passphrases_label.setText(disen[features.passphrase_protection]) - bl_hash_label.setText(bl_hash) - label_edit.setText(features.label) - device_id_label.setText(features.device_id) - initialized_label.setText(noyes[features.initialized]) - version_label.setText(version) - clear_pin_button.setVisible(features.pin_protection) - clear_pin_warning.setVisible(features.pin_protection) - pin_button.setText(setchange[features.pin_protection]) - pin_msg.setVisible(not features.pin_protection) - passphrase_button.setText(endis[features.passphrase_protection]) - language_label.setText(features.language) - - def set_label_enabled(): - label_apply.setEnabled(label_edit.text() != self.features.label) - - def rename(): - invoke_client('change_label', label_edit.text()) - - def toggle_passphrase(): - title = _("Confirm Toggle Passphrase Protection") - currently_enabled = self.features.passphrase_protection - if currently_enabled: - msg = _("After disabling passphrases, you can only pair this " - "Electrum wallet if it had an empty passphrase. " - "If its passphrase was not empty, you will need to " - "create a new wallet with the install wizard. You " - "can use this wallet again at any time by re-enabling " - "passphrases and entering its passphrase.") - else: - msg = _("Your current Electrum wallet can only be used with " - "an empty passphrase. You must create a separate " - "wallet with the install wizard for other passphrases " - "as each one generates a new set of addresses.") - msg += "\n\n" + _("Are you sure you want to proceed?") - if not self.question(msg, title=title): - return - invoke_client('toggle_passphrase', unpair_after=currently_enabled) - - def change_homescreen(): - dialog = QFileDialog(self, _("Choose Homescreen")) - filename, __ = dialog.getOpenFileName() - if not filename: - return # user cancelled - - if filename.endswith('.toif'): - img = open(filename, 'rb').read() - if img[:8] != b'TOIf\x90\x00\x90\x00': - handler.show_error('File is not a TOIF file with size of 144x144') - return - else: - from PIL import Image # FIXME - im = Image.open(filename) - if im.size != (128, 64): - handler.show_error('Image must be 128 x 64 pixels') - return - im = im.convert('1') - pix = im.load() - img = bytearray(1024) - for j in range(64): - for i in range(128): - if pix[i, j]: - o = (i + j * 128) - img[o // 8] |= (1 << (7 - o % 8)) - img = bytes(img) - invoke_client('change_homescreen', img) - - def clear_homescreen(): - invoke_client('change_homescreen', b'\x00') - - def set_pin(): - invoke_client('set_pin', remove=False) - - def clear_pin(): - invoke_client('set_pin', remove=True) - - def wipe_device(): - wallet = window.wallet - if wallet and sum(wallet.get_balance()): - title = _("Confirm Device Wipe") - msg = _("Are you SURE you want to wipe the device?\n" - "Your wallet still has bitcoins in it!") - if not self.question(msg, title=title, - icon=QMessageBox.Critical): - return - invoke_client('wipe_device', unpair_after=True) - - def slider_moved(): - mins = timeout_slider.sliderPosition() - timeout_minutes.setText(_("%2d minutes") % mins) - - def slider_released(): - config.set_session_timeout(timeout_slider.sliderPosition() * 60) - - # Information tab - info_tab = QWidget() - info_layout = QVBoxLayout(info_tab) - info_glayout = QGridLayout() - info_glayout.setColumnStretch(2, 1) - device_label = QLabel() - pin_set_label = QLabel() - passphrases_label = QLabel() - version_label = QLabel() - device_id_label = QLabel() - bl_hash_label = QLabel() - bl_hash_label.setWordWrap(True) - language_label = QLabel() - initialized_label = QLabel() - rows = [ - (_("Device Label"), device_label), - (_("PIN set"), pin_set_label), - (_("Passphrases"), passphrases_label), - (_("Firmware Version"), version_label), - (_("Device ID"), device_id_label), - (_("Bootloader Hash"), bl_hash_label), - (_("Language"), language_label), - (_("Initialized"), initialized_label), - ] - for row_num, (label, widget) in enumerate(rows): - info_glayout.addWidget(QLabel(label), row_num, 0) - info_glayout.addWidget(widget, row_num, 1) - info_layout.addLayout(info_glayout) - - # Settings tab - settings_tab = QWidget() - settings_layout = QVBoxLayout(settings_tab) - settings_glayout = QGridLayout() - - # Settings tab - Label - label_msg = QLabel(_("Name this {}. If you have multiple devices " - "their labels help distinguish them.") - .format(plugin.device)) - label_msg.setWordWrap(True) - label_label = QLabel(_("Device Label")) - label_edit = QLineEdit() - label_edit.setMinimumWidth(150) - label_edit.setMaxLength(plugin.MAX_LABEL_LEN) - label_apply = QPushButton(_("Apply")) - label_apply.clicked.connect(rename) - label_edit.textChanged.connect(set_label_enabled) - settings_glayout.addWidget(label_label, 0, 0) - settings_glayout.addWidget(label_edit, 0, 1, 1, 2) - settings_glayout.addWidget(label_apply, 0, 3) - settings_glayout.addWidget(label_msg, 1, 1, 1, -1) - - # Settings tab - PIN - pin_label = QLabel(_("PIN Protection")) - pin_button = QPushButton() - pin_button.clicked.connect(set_pin) - settings_glayout.addWidget(pin_label, 2, 0) - settings_glayout.addWidget(pin_button, 2, 1) - pin_msg = QLabel(_("PIN protection is strongly recommended. " - "A PIN is your only protection against someone " - "stealing your bitcoins if they obtain physical " - "access to your {}.").format(plugin.device)) - pin_msg.setWordWrap(True) - pin_msg.setStyleSheet("color: red") - settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) - - # Settings tab - Homescreen - homescreen_label = QLabel(_("Homescreen")) - homescreen_change_button = QPushButton(_("Change...")) - homescreen_clear_button = QPushButton(_("Reset")) - homescreen_change_button.clicked.connect(change_homescreen) - try: - import PIL - except ImportError: - homescreen_change_button.setDisabled(True) - homescreen_change_button.setToolTip( - _("Required package 'PIL' is not available - Please install it or use the Trezor website instead.") - ) - homescreen_clear_button.clicked.connect(clear_homescreen) - homescreen_msg = QLabel(_("You can set the homescreen on your " - "device to personalize it. You must " - "choose a {} x {} monochrome black and " - "white image.").format(hs_rows, hs_cols)) - homescreen_msg.setWordWrap(True) - settings_glayout.addWidget(homescreen_label, 4, 0) - settings_glayout.addWidget(homescreen_change_button, 4, 1) - settings_glayout.addWidget(homescreen_clear_button, 4, 2) - settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1) - - # Settings tab - Session Timeout - timeout_label = QLabel(_("Session Timeout")) - timeout_minutes = QLabel() - timeout_slider = QSlider(Qt.Horizontal) - timeout_slider.setRange(1, 60) - timeout_slider.setSingleStep(1) - timeout_slider.setTickInterval(5) - timeout_slider.setTickPosition(QSlider.TicksBelow) - timeout_slider.setTracking(True) - timeout_msg = QLabel( - _("Clear the session after the specified period " - "of inactivity. Once a session has timed out, " - "your PIN and passphrase (if enabled) must be " - "re-entered to use the device.")) - timeout_msg.setWordWrap(True) - timeout_slider.setSliderPosition(config.get_session_timeout() // 60) - slider_moved() - timeout_slider.valueChanged.connect(slider_moved) - timeout_slider.sliderReleased.connect(slider_released) - settings_glayout.addWidget(timeout_label, 6, 0) - settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3) - settings_glayout.addWidget(timeout_minutes, 6, 4) - settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1) - settings_layout.addLayout(settings_glayout) - settings_layout.addStretch(1) - - # Advanced tab - advanced_tab = QWidget() - advanced_layout = QVBoxLayout(advanced_tab) - advanced_glayout = QGridLayout() - - # Advanced tab - clear PIN - clear_pin_button = QPushButton(_("Disable PIN")) - clear_pin_button.clicked.connect(clear_pin) - clear_pin_warning = QLabel( - _("If you disable your PIN, anyone with physical access to your " - "{} device can spend your bitcoins.").format(plugin.device)) - clear_pin_warning.setWordWrap(True) - clear_pin_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(clear_pin_button, 0, 2) - advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5) - - # Advanced tab - toggle passphrase protection - passphrase_button = QPushButton() - passphrase_button.clicked.connect(toggle_passphrase) - passphrase_msg = WWLabel(PASSPHRASE_HELP) - passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) - passphrase_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(passphrase_button, 3, 2) - advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5) - advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5) - - # Advanced tab - wipe device - wipe_device_button = QPushButton(_("Wipe Device")) - wipe_device_button.clicked.connect(wipe_device) - wipe_device_msg = QLabel( - _("Wipe the device, removing all data from it. The firmware " - "is left unchanged.")) - wipe_device_msg.setWordWrap(True) - wipe_device_warning = QLabel( - _("Only wipe a device if you have the recovery seed written down " - "and the device wallet(s) are empty, otherwise the bitcoins " - "will be lost forever.")) - wipe_device_warning.setWordWrap(True) - wipe_device_warning.setStyleSheet("color: red") - advanced_glayout.addWidget(wipe_device_button, 6, 2) - advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5) - advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5) - advanced_layout.addLayout(advanced_glayout) - advanced_layout.addStretch(1) - - tabs = QTabWidget(self) - tabs.addTab(info_tab, _("Information")) - tabs.addTab(settings_tab, _("Settings")) - tabs.addTab(advanced_tab, _("Advanced")) - dialog_vbox = QVBoxLayout(self) - dialog_vbox.addWidget(tabs) - dialog_vbox.addLayout(Buttons(CloseButton(self))) - - # Update information - invoke_client(None) diff --git a/plugins/trezor/transport.py b/plugins/trezor/transport.py @@ -1,95 +0,0 @@ -from electrum.util import PrintError - - -class TrezorTransport(PrintError): - - @staticmethod - def all_transports(): - """Reimplemented trezorlib.transport.all_transports so that we can - enable/disable specific transports. - """ - try: - # only to detect trezorlib version - from trezorlib.transport import all_transports - except ImportError: - # old trezorlib. compat for trezorlib < 0.9.2 - transports = [] - #try: - # from trezorlib.transport_bridge import BridgeTransport - # transports.append(BridgeTransport) - #except BaseException: - # pass - try: - from trezorlib.transport_hid import HidTransport - transports.append(HidTransport) - except BaseException: - pass - try: - from trezorlib.transport_udp import UdpTransport - transports.append(UdpTransport) - except BaseException: - pass - try: - from trezorlib.transport_webusb import WebUsbTransport - transports.append(WebUsbTransport) - except BaseException: - pass - else: - # new trezorlib. - transports = [] - #try: - # from trezorlib.transport.bridge import BridgeTransport - # transports.append(BridgeTransport) - #except BaseException: - # pass - try: - from trezorlib.transport.hid import HidTransport - transports.append(HidTransport) - except BaseException: - pass - try: - from trezorlib.transport.udp import UdpTransport - transports.append(UdpTransport) - except BaseException: - pass - try: - from trezorlib.transport.webusb import WebUsbTransport - transports.append(WebUsbTransport) - except BaseException: - pass - return transports - return transports - - def enumerate_devices(self): - """Just like trezorlib.transport.enumerate_devices, - but with exception catching, so that transports can fail separately. - """ - devices = [] - for transport in self.all_transports(): - try: - new_devices = transport.enumerate() - except BaseException as e: - self.print_error('enumerate failed for {}. error {}' - .format(transport.__name__, str(e))) - else: - devices.extend(new_devices) - return devices - - def get_transport(self, path=None): - """Reimplemented trezorlib.transport.get_transport, - (1) for old trezorlib - (2) to be able to disable specific transports - (3) to call our own enumerate_devices that catches exceptions - """ - if path is None: - try: - return self.enumerate_devices()[0] - except IndexError: - raise Exception("No TREZOR device found") from None - - def match_prefix(a, b): - return a.startswith(b) or b.startswith(a) - transports = [t for t in self.all_transports() if match_prefix(path, t.PATH_PREFIX)] - if transports: - return transports[0].find_by_path(path) - raise Exception("Unknown path prefix '%s'" % path) diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py @@ -1,516 +0,0 @@ -from binascii import hexlify, unhexlify -import traceback -import sys - -from electrum.util import bfh, bh2u, versiontuple, UserCancelled -from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, deserialize_xpub, - TYPE_ADDRESS, TYPE_SCRIPT, is_address) -from electrum import constants -from electrum.i18n import _ -from electrum.plugins import BasePlugin, Device -from electrum.transaction import deserialize, Transaction -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey, xtype_from_derivation -from electrum.base_wizard import ScriptTypeNotSupported - -from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch - - -# TREZOR initialization methods -TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) -RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX = range(0, 2) - -# script "generation" -SCRIPT_GEN_LEGACY, SCRIPT_GEN_P2SH_SEGWIT, SCRIPT_GEN_NATIVE_SEGWIT = range(0, 3) - - -class TrezorKeyStore(Hardware_KeyStore): - hw_type = 'trezor' - device = 'TREZOR' - - def get_derivation(self): - return self.derivation - - def get_script_gen(self): - xtype = xtype_from_derivation(self.derivation) - if xtype in ('p2wpkh', 'p2wsh'): - return SCRIPT_GEN_NATIVE_SEGWIT - elif xtype in ('p2wpkh-p2sh', 'p2wsh-p2sh'): - return SCRIPT_GEN_P2SH_SEGWIT - else: - return SCRIPT_GEN_LEGACY - - def get_client(self, force_pair=True): - return self.plugin.get_client(self, force_pair) - - def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) - - def sign_message(self, sequence, message, password): - client = self.get_client() - address_path = self.get_derivation() + "/%d/%d"%sequence - address_n = client.expand_path(address_path) - msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) - return msg_sig.signature - - def sign_transaction(self, tx, password): - if tx.is_complete(): - return - # previous transactions used as inputs - prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} - for txin in tx.inputs(): - pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) - tx_hash = txin['prevout_hash'] - if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) - prev_tx[tx_hash] = txin['prev_tx'] - for x_pubkey in x_pubkeys: - if not is_xpubkey(x_pubkey): - continue - xpub, s = parse_xpubkey(x_pubkey) - if xpub == self.get_master_public_key(): - xpub_path[xpub] = self.get_derivation() - - self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) - - -class TrezorPlugin(HW_PluginBase): - # Derived classes provide: - # - # class-static variables: client_class, firmware_URL, handler_class, - # libraries_available, libraries_URL, minimum_firmware, - # wallet_class, types - - firmware_URL = 'https://wallet.trezor.io' - libraries_URL = 'https://github.com/trezor/python-trezor' - minimum_firmware = (1, 5, 2) - keystore_class = TrezorKeyStore - minimum_library = (0, 9, 0) - SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') - - MAX_LABEL_LEN = 32 - - def __init__(self, parent, config, name): - HW_PluginBase.__init__(self, parent, config, name) - - try: - # Minimal test if python-trezor is installed - import trezorlib - try: - library_version = trezorlib.__version__ - except AttributeError: - # python-trezor only introduced __version__ in 0.9.0 - library_version = 'unknown' - if library_version == 'unknown' or \ - versiontuple(library_version) < self.minimum_library: - self.libraries_available_message = ( - _("Library version for '{}' is too old.").format(name) - + '\nInstalled: {}, Needed: {}' - .format(library_version, self.minimum_library)) - self.print_stderr(self.libraries_available_message) - raise ImportError() - self.libraries_available = True - except ImportError: - self.libraries_available = False - return - - from . import client - from . import transport - import trezorlib.messages - self.client_class = client.TrezorClient - self.types = trezorlib.messages - self.DEVICE_IDS = ('TREZOR',) - - self.transport_handler = transport.TrezorTransport() - self.device_manager().register_enumerate_func(self.enumerate) - - def enumerate(self): - devices = self.transport_handler.enumerate_devices() - return [Device(d.get_path(), -1, d.get_path(), 'TREZOR', 0) for d in devices] - - def create_client(self, device, handler): - try: - self.print_error("connecting to device at", device.path) - transport = self.transport_handler.get_transport(device.path) - except BaseException as e: - self.print_error("cannot connect at", device.path, str(e)) - return None - - if not transport: - self.print_error("cannot connect at", device.path) - return - - self.print_error("connected to device at", device.path) - client = self.client_class(transport, handler, self) - - # Try a ping for device sanity - try: - client.ping('t') - except BaseException as e: - self.print_error("ping failed", str(e)) - return None - - if not client.atleast_version(*self.minimum_firmware): - msg = (_('Outdated {} firmware for device labelled {}. Please ' - 'download the updated firmware from {}') - .format(self.device, client.label(), self.firmware_URL)) - self.print_error(msg) - handler.show_error(msg) - return None - - return client - - def get_client(self, keystore, force_pair=True): - devmgr = self.device_manager() - handler = keystore.handler - with devmgr.hid_lock: - client = devmgr.client_for_keystore(self, handler, keystore, force_pair) - # returns the client for a given keystore. can use xpub - if client: - client.used() - return client - - def get_coin_name(self): - return "Testnet" if constants.net.TESTNET else "Bitcoin" - - def initialize_device(self, device_id, wizard, handler): - # Initialization method - msg = _("Choose how you want to initialize your {}.\n\n" - "The first two methods are secure as no secret information " - "is entered into your computer.\n\n" - "For the last two methods you input secrets on your keyboard " - "and upload them to your {}, and so you should " - "only do those on a computer you know to be trustworthy " - "and free of malware." - ).format(self.device, self.device) - choices = [ - # Must be short as QT doesn't word-wrap radio button text - (TIM_NEW, _("Let the device generate a completely new seed randomly")), - (TIM_RECOVER, _("Recover from a seed you have previously written down")), - (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), - (TIM_PRIVKEY, _("Upload a master private key")) - ] - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - model = client.get_trezor_model() - def f(method): - import threading - settings = self.request_trezor_init_settings(wizard, method, model) - t = threading.Thread(target=self._initialize_device_safe, args=(settings, method, device_id, wizard, handler)) - t.setDaemon(True) - t.start() - exit_code = wizard.loop.exec_() - if exit_code != 0: - # this method (initialize_device) was called with the expectation - # of leaving the device in an initialized state when finishing. - # signal that this is not the case: - raise UserCancelled() - wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f) - - def _initialize_device_safe(self, settings, method, device_id, wizard, handler): - exit_code = 0 - try: - self._initialize_device(settings, method, device_id, wizard, handler) - except UserCancelled: - exit_code = 1 - except BaseException as e: - traceback.print_exc(file=sys.stderr) - handler.show_error(str(e)) - exit_code = 1 - finally: - wizard.loop.exit(exit_code) - - def _initialize_device(self, settings, method, device_id, wizard, handler): - item, label, pin_protection, passphrase_protection, recovery_type = settings - - if method == TIM_RECOVER and recovery_type == RECOVERY_TYPE_SCRAMBLED_WORDS: - handler.show_error(_( - "You will be asked to enter 24 words regardless of your " - "seed's actual length. If you enter a word incorrectly or " - "misspell it, you cannot change it or go back - you will need " - "to start again from the beginning.\n\nSo please enter " - "the words carefully!"), - blocking=True) - - language = 'english' - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - - if method == TIM_NEW: - strength = 64 * (item + 2) # 128, 192 or 256 - u2f_counter = 0 - skip_backup = False - client.reset_device(True, strength, passphrase_protection, - pin_protection, label, language, - u2f_counter, skip_backup) - elif method == TIM_RECOVER: - word_count = 6 * (item + 2) # 12, 18 or 24 - client.step = 0 - if recovery_type == RECOVERY_TYPE_SCRAMBLED_WORDS: - recovery_type_trezor = self.types.RecoveryDeviceType.ScrambledWords - else: - recovery_type_trezor = self.types.RecoveryDeviceType.Matrix - client.recovery_device(word_count, passphrase_protection, - pin_protection, label, language, - type=recovery_type_trezor) - if recovery_type == RECOVERY_TYPE_MATRIX: - handler.close_matrix_dialog() - elif method == TIM_MNEMONIC: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_mnemonic(str(item), pin, - passphrase_protection, - label, language) - else: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_xprv(item, pin, passphrase_protection, - label, language) - - def _make_node_path(self, xpub, address_n): - _, depth, fingerprint, child_num, chain_code, key = deserialize_xpub(xpub) - node = self.types.HDNodeType( - depth=depth, - fingerprint=int.from_bytes(fingerprint, 'big'), - child_num=int.from_bytes(child_num, 'big'), - chain_code=chain_code, - public_key=key, - ) - return self.types.HDNodePathType(node=node, address_n=address_n) - - def setup_device(self, device_info, wizard, purpose): - devmgr = self.device_manager() - device_id = device_info.device.id_ - client = devmgr.client_by_id(device_id) - if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) - # fixme: we should use: client.handler = wizard - client.handler = self.create_handler(wizard) - if not device_info.initialized: - self.initialize_device(device_id, wizard, client.handler) - client.get_xpub('m', 'standard') - client.used() - - def get_xpub(self, device_id, derivation, xtype, wizard): - if xtype not in self.SUPPORTED_XTYPES: - raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - client.handler = wizard - xpub = client.get_xpub(derivation, xtype) - client.used() - return xpub - - def get_trezor_input_script_type(self, script_gen, is_multisig): - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - return self.types.InputScriptType.SPENDWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - return self.types.InputScriptType.SPENDP2SHWITNESS - else: - if is_multisig: - return self.types.InputScriptType.SPENDMULTISIG - else: - return self.types.InputScriptType.SPENDADDRESS - - def sign_transaction(self, keystore, tx, prev_tx, xpub_path): - self.prev_tx = prev_tx - self.xpub_path = xpub_path - client = self.get_client(keystore) - inputs = self.tx_inputs(tx, True, keystore.get_script_gen()) - outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.get_script_gen()) - signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0] - signatures = [(bh2u(x) + '01') for x in signatures] - tx.update_signatures(signatures) - - def show_address(self, wallet, address, keystore=None): - if keystore is None: - keystore = wallet.get_keystore() - if not self.show_address_helper(wallet, address, keystore): - return - client = self.get_client(keystore) - if not client.atleast_version(1, 3): - keystore.handler.show_error(_("Your device firmware is too old")) - return - change, index = wallet.get_address_index(address) - derivation = keystore.derivation - address_path = "%s/%d/%d"%(derivation, change, index) - address_n = client.expand_path(address_path) - xpubs = wallet.get_master_public_keys() - if len(xpubs) == 1: - script_gen = keystore.get_script_gen() - script_type = self.get_trezor_input_script_type(script_gen, is_multisig=False) - client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) - else: - def f(xpub): - return self._make_node_path(xpub, [change, index]) - pubkeys = wallet.get_public_keys(address) - # sort xpubs using the order of pubkeys - sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) - pubkeys = list(map(f, sorted_xpubs)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * wallet.n, - m=wallet.m, - ) - script_gen = keystore.get_script_gen() - script_type = self.get_trezor_input_script_type(script_gen, is_multisig=True) - client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type) - - def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY): - inputs = [] - for txin in tx.inputs(): - txinputtype = self.types.TxInputType() - if txin['type'] == 'coinbase': - prev_hash = "\0"*32 - prev_index = 0xffffffff # signed int -1 - else: - if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = parse_xpubkey(x_pubkey) - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype._extend_address_n(xpub_n + s) - txinputtype.script_type = self.get_trezor_input_script_type(script_gen, is_multisig=False) - else: - def f(x_pubkey): - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(0, bfh(x_pubkey)) - s = [] - return self._make_node_path(xpub, s) - pubkeys = list(map(f, x_pubkeys)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))), - m=txin.get('num_sig'), - ) - script_type = self.get_trezor_input_script_type(script_gen, is_multisig=True) - txinputtype = self.types.TxInputType( - script_type=script_type, - multisig=multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype._extend_address_n(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - if 'value' in txin: - txinputtype.amount = txin['value'] - txinputtype.prev_hash = prev_hash - txinputtype.prev_index = prev_index - - if txin.get('scriptSig') is not None: - script_sig = bfh(txin['scriptSig']) - txinputtype.script_sig = script_sig - - txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) - - inputs.append(txinputtype) - - return inputs - - def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY): - - def create_output_by_derivation(info): - index, xpubs, m = info - if len(xpubs) == 1: - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - script_type = self.types.OutputScriptType.PAYTOWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS - else: - script_type = self.types.OutputScriptType.PAYTOADDRESS - address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) - txoutputtype = self.types.TxOutputType( - amount=amount, - script_type=script_type, - address_n=address_n, - ) - else: - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - script_type = self.types.OutputScriptType.PAYTOWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS - else: - script_type = self.types.OutputScriptType.PAYTOMULTISIG - address_n = self.client_class.expand_path("/%d/%d" % index) - pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs] - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * len(pubkeys), - m=m) - txoutputtype = self.types.TxOutputType( - multisig=multisig, - amount=amount, - address_n=self.client_class.expand_path(derivation + "/%d/%d" % index), - script_type=script_type) - return txoutputtype - - def create_output_by_address(): - txoutputtype = self.types.TxOutputType() - txoutputtype.amount = amount - if _type == TYPE_SCRIPT: - txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN - txoutputtype.op_return_data = address[2:] - elif _type == TYPE_ADDRESS: - txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS - txoutputtype.address = address - return txoutputtype - - outputs = [] - has_change = False - any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - - for _type, address, amount in tx.outputs(): - use_create_by_derivation = False - - info = tx.output_info.get(address) - if info is not None and not has_change: - index, xpubs, m = info - on_change_branch = index[0] == 1 - # prioritise hiding outputs on the 'change' branch from user - # because no more than one change address allowed - # note: ^ restriction can be removed once we require fw - # that has https://github.com/trezor/trezor-mcu/pull/306 - if on_change_branch == any_output_on_change_branch: - use_create_by_derivation = True - has_change = True - - if use_create_by_derivation: - txoutputtype = create_output_by_derivation(info) - else: - txoutputtype = create_output_by_address() - outputs.append(txoutputtype) - - return outputs - - def electrum_tx_to_txtype(self, tx): - t = self.types.TransactionType() - if tx is None: - # probably for segwit input and we don't need this prev txn - return t - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] - inputs = self.tx_inputs(tx) - t._extend_inputs(inputs) - for vout in d['outputs']: - o = t._add_bin_outputs() - o.amount = vout['value'] - o.script_pubkey = bfh(vout['scriptPubKey']) - return t - - # This function is called from the TREZOR libraries (via tx_api) - def get_tx(self, tx_hash): - tx = self.prev_tx[tx_hash] - return self.electrum_tx_to_txtype(tx) diff --git a/plugins/trustedcoin/__init__.py b/plugins/trustedcoin/__init__.py @@ -1,11 +0,0 @@ -from electrum.i18n import _ - -fullname = _('Two Factor Authentication') -description = ''.join([ - _("This plugin adds two-factor authentication to your wallet."), '<br/>', - _("For more information, visit"), - " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" -]) -requires_wallet_type = ['2fa'] -registers_wallet_type = '2fa' -available_for = ['qt', 'cmdline', 'kivy'] diff --git a/plugins/trustedcoin/cmdline.py b/plugins/trustedcoin/cmdline.py @@ -1,45 +0,0 @@ -#!/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.plugins import hook -from .trustedcoin import TrustedCoinPlugin - - -class Plugin(TrustedCoinPlugin): - - def prompt_user_for_otp(self, wallet, tx): - if not isinstance(wallet, self.wallet_class): - return - if not wallet.can_sign_without_server(): - self.print_error("twofactor:sign_tx") - auth_code = None - if wallet.keystores['x3/'].get_tx_derivations(tx): - msg = _('Please enter your Google Authenticator code:') - auth_code = int(input(msg)) - else: - self.print_error("twofactor: xpub3 not needed") - wallet.auth_code = auth_code - diff --git a/plugins/trustedcoin/kivy.py b/plugins/trustedcoin/kivy.py @@ -1,110 +0,0 @@ -#!/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 functools import partial -from threading import Thread -import re -from decimal import Decimal - -from kivy.clock import Clock - -from electrum.i18n import _ -from electrum.plugins import hook -from .trustedcoin import TrustedCoinPlugin, server, KIVY_DISCLAIMER, TrustedCoinException, ErrorConnectingServer - - - -class Plugin(TrustedCoinPlugin): - - disclaimer_msg = KIVY_DISCLAIMER - - def __init__(self, parent, config, name): - super().__init__(parent, config, name) - - @hook - def load_wallet(self, wallet, window): - if not isinstance(wallet, self.wallet_class): - return - self.start_request_thread(wallet) - - def go_online_dialog(self, wizard): - # we skip this step on android - wizard.run('accept_terms_of_use') - - def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): - from electrum_gui.kivy.uix.dialogs.label_dialog import LabelDialog - msg = _('Please enter your Google Authenticator code') - d = LabelDialog(msg, '', lambda otp: self.on_otp(wallet, tx, otp, on_success, on_failure)) - d.open() - - def on_otp(self, wallet, tx, otp, on_success, on_failure): - try: - wallet.on_otp(tx, otp) - except TrustedCoinException as e: - if e.status_code == 400: # invalid OTP - Clock.schedule_once(lambda dt: on_failure(_('Invalid one-time password.'))) - else: - Clock.schedule_once(lambda dt, bound_e=e: on_failure(_('Error') + ':\n' + str(bound_e))) - except Exception as e: - Clock.schedule_once(lambda dt, bound_e=e: on_failure(_('Error') + ':\n' + str(bound_e))) - else: - on_success(tx) - - def accept_terms_of_use(self, wizard): - def handle_error(msg, e): - wizard.show_error(msg + ':\n' + str(e)) - wizard.terminate() - try: - tos = server.get_terms_of_service() - except ErrorConnectingServer as e: - Clock.schedule_once(lambda dt, bound_e=e: handle_error(_('Error connecting to server'), bound_e)) - except Exception as e: - Clock.schedule_once(lambda dt, bound_e=e: handle_error(_('Error'), bound_e)) - else: - f = lambda x: self.read_email(wizard) - wizard.tos_dialog(tos=tos, run_next=f) - - def read_email(self, wizard): - f = lambda x: self.create_remote_key(x, wizard) - wizard.email_dialog(run_next=f) - - def request_otp_dialog(self, wizard, short_id, otp_secret, xpub3): - f = lambda otp, reset: self.check_otp(wizard, short_id, otp_secret, xpub3, otp, reset) - wizard.otp_dialog(otp_secret=otp_secret, run_next=f) - - @hook - def abort_send(self, window): - wallet = window.wallet - if not isinstance(wallet, self.wallet_class): - return - if wallet.can_sign_without_server(): - return - if wallet.billing_info is None: - self.start_request_thread(wallet) - Clock.schedule_once( - lambda dt: window.show_error(_('Requesting account info from TrustedCoin server...') + '\n' + - _('Please try again.'))) - return True - return False diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py @@ -1,313 +0,0 @@ -#!/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 functools import partial -import threading -from threading import Thread -import re -from decimal import Decimal - -from PyQt5.QtGui import * -from PyQt5.QtCore import * - -from electrum_gui.qt.util import * -from electrum_gui.qt.qrcodewidget import QRCodeWidget -from electrum_gui.qt.amountedit import AmountEdit -from electrum_gui.qt.main_window import StatusBarButton -from electrum.i18n import _ -from electrum.plugins import hook -from electrum.util import PrintError, is_valid_email -from .trustedcoin import TrustedCoinPlugin, server - - -class TOS(QTextEdit): - tos_signal = pyqtSignal() - error_signal = pyqtSignal(object) - - -class HandlerTwoFactor(QObject, PrintError): - - def __init__(self, plugin, window): - super().__init__() - self.plugin = plugin - self.window = window - - def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): - if not isinstance(wallet, self.plugin.wallet_class): - return - if wallet.can_sign_without_server(): - return - if not wallet.keystores['x3/'].get_tx_derivations(tx): - self.print_error("twofactor: xpub3 not needed") - return - window = self.window.top_level_window() - auth_code = self.plugin.auth_dialog(window) - try: - wallet.on_otp(tx, auth_code) - except: - on_failure(sys.exc_info()) - return - on_success(tx) - -class Plugin(TrustedCoinPlugin): - - def __init__(self, parent, config, name): - super().__init__(parent, config, name) - - @hook - def on_new_window(self, window): - wallet = window.wallet - if not isinstance(wallet, self.wallet_class): - return - wallet.handler_2fa = HandlerTwoFactor(self, window) - if wallet.can_sign_without_server(): - msg = ' '.join([ - _('This wallet was restored from seed, and it contains two master private keys.'), - _('Therefore, two-factor authentication is disabled.') - ]) - action = lambda: window.show_message(msg) - else: - action = partial(self.settings_dialog, window) - button = StatusBarButton(QIcon(":icons/trustedcoin-status.png"), - _("TrustedCoin"), action) - window.statusBar().addPermanentWidget(button) - self.start_request_thread(window.wallet) - - def auth_dialog(self, window): - d = WindowModalDialog(window, _("Authorization")) - vbox = QVBoxLayout(d) - pw = AmountEdit(None, is_int = True) - msg = _('Please enter your Google Authenticator code') - vbox.addWidget(QLabel(msg)) - grid = QGridLayout() - grid.setSpacing(8) - grid.addWidget(QLabel(_('Code')), 1, 0) - grid.addWidget(pw, 1, 1) - vbox.addLayout(grid) - msg = _('If you have lost your second factor, you need to restore your wallet from seed in order to request a new code.') - label = QLabel(msg) - label.setWordWrap(1) - vbox.addWidget(label) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - if not d.exec_(): - return - return pw.get_amount() - - def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): - wallet.handler_2fa.prompt_user_for_otp(wallet, tx, on_success, on_failure) - - def waiting_dialog(self, window, on_finished=None): - task = partial(self.request_billing_info, window.wallet) - return WaitingDialog(window, 'Getting billing information...', task, - on_finished) - - @hook - def abort_send(self, window): - wallet = window.wallet - if not isinstance(wallet, self.wallet_class): - return - if wallet.can_sign_without_server(): - return - if wallet.billing_info is None: - self.start_request_thread(wallet) - window.show_error(_('Requesting account info from TrustedCoin server...') + '\n' + - _('Please try again.')) - return True - return False - - def settings_dialog(self, window): - self.waiting_dialog(window, partial(self.show_settings_dialog, window)) - - def show_settings_dialog(self, window, success): - if not success: - window.show_message(_('Server not reachable.')) - return - - wallet = window.wallet - d = WindowModalDialog(window, _("TrustedCoin Information")) - d.setMinimumSize(500, 200) - vbox = QVBoxLayout(d) - hbox = QHBoxLayout() - - logo = QLabel() - logo.setPixmap(QPixmap(":icons/trustedcoin-status.png")) - msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '<br/>'\ - + _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" - label = QLabel(msg) - label.setOpenExternalLinks(1) - - hbox.addStretch(10) - hbox.addWidget(logo) - hbox.addStretch(10) - hbox.addWidget(label) - hbox.addStretch(10) - - vbox.addLayout(hbox) - vbox.addStretch(10) - - msg = _('TrustedCoin charges a small fee to co-sign transactions. The fee depends on how many prepaid transactions you buy. An extra output is added to your transaction every time you run out of prepaid transactions.') + '<br/>' - label = QLabel(msg) - label.setWordWrap(1) - vbox.addWidget(label) - - vbox.addStretch(10) - grid = QGridLayout() - vbox.addLayout(grid) - - price_per_tx = wallet.price_per_tx - n_prepay = wallet.num_prepay(self.config) - i = 0 - for k, v in sorted(price_per_tx.items()): - if k == 1: - continue - grid.addWidget(QLabel("Pay every %d transactions:"%k), i, 0) - grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1) - b = QRadioButton() - b.setChecked(k == n_prepay) - b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, True)) - grid.addWidget(b, i, 2) - i += 1 - - n = wallet.billing_info.get('tx_remaining', 0) - grid.addWidget(QLabel(_("Your wallet has {} prepaid transactions.").format(n)), i, 0) - vbox.addLayout(Buttons(CloseButton(d))) - d.exec_() - - def on_buy(self, window, k, v, d): - d.close() - if window.pluginsdialog: - window.pluginsdialog.close() - wallet = window.wallet - uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000) - wallet.is_billing = True - window.pay_to_URI(uri) - window.payto_e.setFrozen(True) - window.message_e.setFrozen(True) - window.amount_e.setFrozen(True) - - def go_online_dialog(self, wizard): - msg = [ - _("Your wallet file is: {}.").format(os.path.abspath(wizard.storage.path)), - _("You need to be online in order to complete the creation of " - "your wallet. If you generated your seed on an offline " - 'computer, click on "{}" to close this window, move your ' - "wallet file to an online computer, and reopen it with " - "Electrum.").format(_('Cancel')), - _('If you are online, click on "{}" to continue.').format(_('Next')) - ] - msg = '\n\n'.join(msg) - wizard.stack = [] - wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use')) - - def accept_terms_of_use(self, window): - vbox = QVBoxLayout() - vbox.addWidget(QLabel(_("Terms of Service"))) - - tos_e = TOS() - tos_e.setReadOnly(True) - vbox.addWidget(tos_e) - tos_received = False - - vbox.addWidget(QLabel(_("Please enter your e-mail address"))) - email_e = QLineEdit() - vbox.addWidget(email_e) - - next_button = window.next_button - prior_button_text = next_button.text() - next_button.setText(_('Accept')) - - def request_TOS(): - try: - tos = server.get_terms_of_service() - except Exception as e: - import traceback - traceback.print_exc(file=sys.stderr) - tos_e.error_signal.emit(_('Could not retrieve Terms of Service:') - + '\n' + str(e)) - return - self.TOS = tos - tos_e.tos_signal.emit() - - def on_result(): - tos_e.setText(self.TOS) - nonlocal tos_received - tos_received = True - set_enabled() - - def on_error(msg): - window.show_error(str(msg)) - window.terminate() - - def set_enabled(): - next_button.setEnabled(tos_received and is_valid_email(email_e.text())) - - tos_e.tos_signal.connect(on_result) - tos_e.error_signal.connect(on_error) - t = Thread(target=request_TOS) - t.setDaemon(True) - t.start() - email_e.textChanged.connect(set_enabled) - email_e.setFocus(True) - window.exec_layout(vbox, next_enabled=False) - next_button.setText(prior_button_text) - email = str(email_e.text()) - self.create_remote_key(email, window) - - def request_otp_dialog(self, window, short_id, otp_secret, xpub3): - vbox = QVBoxLayout() - if otp_secret is not None: - uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret) - l = QLabel("Please scan the following QR code in Google Authenticator. You may as well use the following key: %s"%otp_secret) - l.setWordWrap(True) - vbox.addWidget(l) - qrw = QRCodeWidget(uri) - vbox.addWidget(qrw, 1) - msg = _('Then, enter your Google Authenticator code:') - else: - label = QLabel( - "This wallet is already registered with TrustedCoin. " - "To finalize wallet creation, please enter your Google Authenticator Code. " - ) - label.setWordWrap(1) - vbox.addWidget(label) - msg = _('Google Authenticator code:') - hbox = QHBoxLayout() - hbox.addWidget(WWLabel(msg)) - pw = AmountEdit(None, is_int = True) - pw.setFocus(True) - pw.setMaximumWidth(50) - hbox.addWidget(pw) - vbox.addLayout(hbox) - cb_lost = QCheckBox(_("I have lost my Google Authenticator account")) - cb_lost.setToolTip(_("Check this box to request a new secret. You will need to retype your seed.")) - vbox.addWidget(cb_lost) - cb_lost.setVisible(otp_secret is None) - def set_enabled(): - b = True if cb_lost.isChecked() else len(pw.text()) == 6 - window.next_button.setEnabled(b) - pw.textChanged.connect(set_enabled) - cb_lost.toggled.connect(set_enabled) - window.exec_layout(vbox, next_enabled=False, raise_on_cancel=False) - self.check_otp(window, short_id, otp_secret, xpub3, pw.get_amount(), cb_lost.isChecked()) diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py @@ -1,676 +0,0 @@ -#!/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 socket -import os -import requests -import json -import base64 -from urllib.parse import urljoin -from urllib.parse import quote - -import electrum -from electrum import bitcoin, ecc -from electrum import constants -from electrum import keystore -from electrum.bitcoin import * -from electrum.mnemonic import Mnemonic -from electrum import version -from electrum.wallet import Multisig_Wallet, Deterministic_Wallet -from electrum.i18n import _ -from electrum.plugins import BasePlugin, hook -from electrum.util import NotEnoughFunds -from electrum.storage import STO_EV_USER_PW - -# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server -def get_signing_xpub(): - if constants.net.TESTNET: - return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" - else: - return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" - -def get_billing_xpub(): - if constants.net.TESTNET: - return "tpubD6NzVbkrYhZ4X11EJFTJujsYbUmVASAYY7gXsEt4sL97AMBdypiH1E9ZVTpdXXEy3Kj9Eqd1UkxdGtvDt5z23DKsh6211CfNJo8bLLyem5r" - else: - return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" - -SEED_PREFIX = version.SEED_PREFIX_2FA - -DISCLAIMER = [ - _("Two-factor authentication is a service provided by TrustedCoin. " - "It uses a multi-signature wallet, where you own 2 of 3 keys. " - "The third key is stored on a remote server that signs transactions on " - "your behalf. To use this service, you will need a smartphone with " - "Google Authenticator installed."), - _("A small fee will be charged on each transaction that uses the " - "remote server. You may check and modify your billing preferences " - "once the installation is complete."), - _("Note that your coins are not locked in this service. You may withdraw " - "your funds at any time and at no cost, without the remote server, by " - "using the 'restore wallet' option with your wallet seed."), - _("The next step will generate the seed of your wallet. This seed will " - "NOT be saved in your computer, and it must be stored on paper. " - "To be safe from malware, you may want to do this on an offline " - "computer, and move your wallet later to an online computer."), -] - -KIVY_DISCLAIMER = [ - _("Two-factor authentication is a service provided by TrustedCoin. " - "To use it, you must have a separate device with Google Authenticator."), - _("This service uses a multi-signature wallet, where you own 2 of 3 keys. " - "The third key is stored on a remote server that signs transactions on " - "your behalf. A small fee will be charged on each transaction that uses the " - "remote server."), - _("Note that your coins are not locked in this service. You may withdraw " - "your funds at any time and at no cost, without the remote server, by " - "using the 'restore wallet' option with your wallet seed."), -] -RESTORE_MSG = _("Enter the seed for your 2-factor wallet:") - -class TrustedCoinException(Exception): - def __init__(self, message, status_code=0): - Exception.__init__(self, message) - self.status_code = status_code - - -class ErrorConnectingServer(Exception): - pass - - -class TrustedCoinCosignerClient(object): - def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/'): - self.base_url = base_url - self.debug = False - self.user_agent = user_agent - - def send_request(self, method, relative_url, data=None): - kwargs = {'headers': {}} - if self.user_agent: - kwargs['headers']['user-agent'] = self.user_agent - if method == 'get' and data: - kwargs['params'] = data - elif method == 'post' and data: - kwargs['data'] = json.dumps(data) - kwargs['headers']['content-type'] = 'application/json' - url = urljoin(self.base_url, relative_url) - if self.debug: - print('%s %s %s' % (method, url, data)) - try: - response = requests.request(method, url, **kwargs) - except Exception as e: - raise ErrorConnectingServer(e) - if self.debug: - print(response.text) - if response.status_code != 200: - message = str(response.text) - if response.headers.get('content-type') == 'application/json': - r = response.json() - if 'message' in r: - message = r['message'] - raise TrustedCoinException(message, response.status_code) - if response.headers.get('content-type') == 'application/json': - return response.json() - else: - return response.text - - def get_terms_of_service(self, billing_plan='electrum-per-tx-otp'): - """ - Returns the TOS for the given billing plan as a plain/text unicode string. - :param billing_plan: the plan to return the terms for - """ - payload = {'billing_plan': billing_plan} - return self.send_request('get', 'tos', payload) - - def create(self, xpubkey1, xpubkey2, email, billing_plan='electrum-per-tx-otp'): - """ - Creates a new cosigner resource. - :param xpubkey1: a bip32 extended public key (customarily the hot key) - :param xpubkey2: a bip32 extended public key (customarily the cold key) - :param email: a contact email - :param billing_plan: the billing plan for the cosigner - """ - payload = { - 'email': email, - 'xpubkey1': xpubkey1, - 'xpubkey2': xpubkey2, - 'billing_plan': billing_plan, - } - return self.send_request('post', 'cosigner', payload) - - def auth(self, id, otp): - """ - Attempt to authenticate for a particular cosigner. - :param id: the id of the cosigner - :param otp: the one time password - """ - payload = {'otp': otp} - return self.send_request('post', 'cosigner/%s/auth' % quote(id), payload) - - def get(self, id): - """ Get billing info """ - return self.send_request('get', 'cosigner/%s' % quote(id)) - - def get_challenge(self, id): - """ Get challenge to reset Google Auth secret """ - return self.send_request('get', 'cosigner/%s/otp_secret' % quote(id)) - - def reset_auth(self, id, challenge, signatures): - """ Reset Google Auth secret """ - payload = {'challenge':challenge, 'signatures':signatures} - return self.send_request('post', 'cosigner/%s/otp_secret' % quote(id), payload) - - def sign(self, id, transaction, otp): - """ - Attempt to authenticate for a particular cosigner. - :param id: the id of the cosigner - :param transaction: the hex encoded [partially signed] compact transaction to sign - :param otp: the one time password - """ - payload = { - 'otp': otp, - 'transaction': transaction - } - return self.send_request('post', 'cosigner/%s/sign' % quote(id), payload) - - def transfer_credit(self, id, recipient, otp, signature_callback): - """ - Transfer a cosigner's credits to another cosigner. - :param id: the id of the sending cosigner - :param recipient: the id of the recipient cosigner - :param otp: the one time password (of the sender) - :param signature_callback: a callback that signs a text message using xpubkey1/0/0 returning a compact sig - """ - payload = { - 'otp': otp, - 'recipient': recipient, - 'timestamp': int(time.time()), - - } - relative_url = 'cosigner/%s/transfer' % quote(id) - full_url = urljoin(self.base_url, relative_url) - headers = { - 'x-signature': signature_callback(full_url + '\n' + json.dumps(payload)) - } - return self.send_request('post', relative_url, payload, headers) - - -server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION) - -class Wallet_2fa(Multisig_Wallet): - - wallet_type = '2fa' - - def __init__(self, storage): - self.m, self.n = 2, 3 - Deterministic_Wallet.__init__(self, storage) - self.is_billing = False - self.billing_info = None - self._load_billing_addresses() - - def _load_billing_addresses(self): - billing_addresses = self.storage.get('trustedcoin_billing_addresses', {}) - self._billing_addresses = {} # index -> addr - # convert keys from str to int - for index, addr in list(billing_addresses.items()): - self._billing_addresses[int(index)] = addr - self._billing_addresses_set = set(self._billing_addresses.values()) # set of addrs - - def can_sign_without_server(self): - return not self.keystores['x2/'].is_watching_only() - - def get_user_id(self): - return get_user_id(self.storage) - - def min_prepay(self): - return min(self.price_per_tx.keys()) - - def num_prepay(self, config): - default = self.min_prepay() - n = config.get('trustedcoin_prepay', default) - if n not in self.price_per_tx: - n = default - return n - - def extra_fee(self, config): - if self.can_sign_without_server(): - return 0 - if self.billing_info is None: - self.plugin.start_request_thread(self) - return 0 - if self.billing_info.get('tx_remaining'): - return 0 - if self.is_billing: - return 0 - n = self.num_prepay(config) - price = int(self.price_per_tx[n]) - if price > 100000 * n: - raise Exception('too high trustedcoin fee ({} for {} txns)'.format(price, n)) - return price - - def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None, - change_addr=None, is_sweep=False): - mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction( - self, coins, o, config, fixed_fee, change_addr) - fee = self.extra_fee(config) if not is_sweep else 0 - if fee: - address = self.billing_info['billing_address'] - fee_output = (TYPE_ADDRESS, address, fee) - try: - tx = mk_tx(outputs + [fee_output]) - except NotEnoughFunds: - # TrustedCoin won't charge if the total inputs is - # lower than their fee - tx = mk_tx(outputs) - if tx.input_value() >= fee: - raise - self.print_error("not charging for this tx") - else: - tx = mk_tx(outputs) - return tx - - def on_otp(self, tx, otp): - if not otp: - self.print_error("sign_transaction: no auth code") - return - otp = int(otp) - long_user_id, short_id = self.get_user_id() - raw_tx = tx.serialize_to_network() - r = server.sign(short_id, raw_tx, otp) - if r: - raw_tx = r.get('transaction') - tx.update(raw_tx) - self.print_error("twofactor: is complete", tx.is_complete()) - # reset billing_info - self.billing_info = None - self.plugin.start_request_thread(self) - - def add_new_billing_address(self, billing_index: int, address: str): - saved_addr = self._billing_addresses.get(billing_index) - if saved_addr is not None: - if saved_addr == address: - return # already saved this address - else: - raise Exception('trustedcoin billing address inconsistency.. ' - 'for index {}, already saved {}, now got {}' - .format(billing_index, saved_addr, address)) - # do we have all prior indices? (are we synced?) - largest_index_we_have = max(self._billing_addresses) if self._billing_addresses else -1 - if largest_index_we_have + 1 < billing_index: # need to sync - for i in range(largest_index_we_have + 1, billing_index): - addr = make_billing_address(self, i) - self._billing_addresses[i] = addr - self._billing_addresses_set.add(addr) - # save this address; and persist to disk - self._billing_addresses[billing_index] = address - self._billing_addresses_set.add(address) - self.storage.put('trustedcoin_billing_addresses', self._billing_addresses) - # FIXME this often runs in a daemon thread, where storage.write will fail - self.storage.write() - - def is_billing_address(self, addr: str) -> bool: - return addr in self._billing_addresses_set - - -# Utility functions - -def get_user_id(storage): - def make_long_id(xpub_hot, xpub_cold): - return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold]))) - xpub1 = storage.get('x1/')['xpub'] - xpub2 = storage.get('x2/')['xpub'] - long_id = make_long_id(xpub1, xpub2) - short_id = hashlib.sha256(long_id).hexdigest() - return long_id, short_id - -def make_xpub(xpub, s): - version, _, _, _, c, cK = deserialize_xpub(xpub) - cK2, c2 = bitcoin._CKD_pub(cK, c, s) - return bitcoin.serialize_xpub(version, c2, cK2) - -def make_billing_address(wallet, num): - long_id, short_id = wallet.get_user_id() - xpub = make_xpub(get_billing_xpub(), long_id) - version, _, _, _, c, cK = deserialize_xpub(xpub) - cK, c = bitcoin.CKD_pub(cK, c, num) - return bitcoin.public_key_to_p2pkh(cK) - - -class TrustedCoinPlugin(BasePlugin): - wallet_class = Wallet_2fa - disclaimer_msg = DISCLAIMER - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.wallet_class.plugin = self - self.requesting = False - - @staticmethod - def is_valid_seed(seed): - return bitcoin.is_new_seed(seed, SEED_PREFIX) - - def is_available(self): - return True - - def is_enabled(self): - return True - - def can_user_disable(self): - return False - - @hook - def tc_sign_wrapper(self, wallet, tx, on_success, on_failure): - if not isinstance(wallet, self.wallet_class): - return - if tx.is_complete(): - return - if wallet.can_sign_without_server(): - return - if not wallet.keystores['x3/'].get_tx_derivations(tx): - self.print_error("twofactor: xpub3 not needed") - return - def wrapper(tx): - self.prompt_user_for_otp(wallet, tx, on_success, on_failure) - return wrapper - - @hook - def get_tx_extra_fee(self, wallet, tx): - if type(wallet) != Wallet_2fa: - return - for _type, addr, amount in tx.outputs(): - if _type == TYPE_ADDRESS and wallet.is_billing_address(addr): - return addr, amount - - def finish_requesting(func): - def f(self, *args, **kwargs): - try: - return func(self, *args, **kwargs) - finally: - self.requesting = False - return f - - @finish_requesting - def request_billing_info(self, wallet): - if wallet.can_sign_without_server(): - return - self.print_error("request billing info") - try: - billing_info = server.get(wallet.get_user_id()[1]) - except ErrorConnectingServer as e: - self.print_error('cannot connect to TrustedCoin server: {}'.format(e)) - return - billing_index = billing_info['billing_index'] - billing_address = make_billing_address(wallet, billing_index) - if billing_address != billing_info['billing_address']: - raise Exception('unexpected trustedcoin billing address: expected {}, received {}' - .format(billing_address, billing_info['billing_address'])) - wallet.add_new_billing_address(billing_index, billing_address) - wallet.billing_info = billing_info - wallet.price_per_tx = dict(billing_info['price_per_tx']) - wallet.price_per_tx.pop(1, None) - return True - - def start_request_thread(self, wallet): - from threading import Thread - if self.requesting is False: - self.requesting = True - t = Thread(target=self.request_billing_info, args=(wallet,)) - t.setDaemon(True) - t.start() - return t - - def make_seed(self): - return Mnemonic('english').make_seed(seed_type='2fa', num_bits=128) - - @hook - def do_clear(self, window): - window.wallet.is_billing = False - - def show_disclaimer(self, wizard): - wizard.set_icon(':icons/trustedcoin-wizard.png') - wizard.stack = [] - wizard.confirm_dialog(title='Disclaimer', message='\n\n'.join(self.disclaimer_msg), run_next = lambda x: wizard.run('choose_seed')) - - def choose_seed(self, wizard): - title = _('Create or restore') - message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') - choices = [ - ('create_seed', _('Create a new seed')), - ('restore_wallet', _('I already have a seed')), - ] - wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run) - - def create_seed(self, wizard): - seed = self.make_seed() - f = lambda x: wizard.request_passphrase(seed, x) - wizard.show_seed_dialog(run_next=f, seed_text=seed) - - @classmethod - def get_xkeys(self, seed, passphrase, derivation): - from electrum.mnemonic import Mnemonic - from electrum.keystore import bip32_root, bip32_private_derivation - bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase) - xprv, xpub = bip32_root(bip32_seed, 'standard') - xprv, xpub = bip32_private_derivation(xprv, "m/", derivation) - return xprv, xpub - - @classmethod - def xkeys_from_seed(self, seed, passphrase): - words = seed.split() - n = len(words) - # old version use long seed phrases - if n >= 20: - # note: pre-2.7 2fa seeds were typically 24-25 words, however they - # could probabilistically be arbitrarily shorter due to a bug. (see #3611) - # the probability of it being < 20 words is about 2^(-(256+12-19*11)) = 2^(-59) - if passphrase != '': - raise Exception('old 2fa seed cannot have passphrase') - xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/") - xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/") - elif n==12: - xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/") - xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/") - else: - raise Exception('unrecognized seed length: {} words'.format(n)) - return xprv1, xpub1, xprv2, xpub2 - - def create_keystore(self, wizard, seed, passphrase): - # this overloads the wizard's method - xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) - k1 = keystore.from_xprv(xprv1) - k2 = keystore.from_xpub(xpub2) - wizard.request_password(run_next=lambda pw, encrypt: self.on_password(wizard, pw, encrypt, k1, k2)) - - def on_password(self, wizard, password, encrypt_storage, k1, k2): - k1.update_password(None, password) - wizard.storage.set_keystore_encryption(bool(password)) - if encrypt_storage: - wizard.storage.set_password(password, enc_version=STO_EV_USER_PW) - wizard.storage.put('x1/', k1.dump()) - wizard.storage.put('x2/', k2.dump()) - wizard.storage.write() - self.go_online_dialog(wizard) - - def restore_wallet(self, wizard): - wizard.opt_bip39 = False - wizard.opt_ext = True - title = _("Restore two-factor Wallet") - f = lambda seed, is_bip39, is_ext: wizard.run('on_restore_seed', seed, is_ext) - wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed) - - def on_restore_seed(self, wizard, seed, is_ext): - f = lambda x: self.restore_choice(wizard, seed, x) - wizard.passphrase_dialog(run_next=f) if is_ext else f('') - - def restore_choice(self, wizard, seed, passphrase): - wizard.set_icon(':icons/trustedcoin-wizard.png') - wizard.stack = [] - title = _('Restore 2FA wallet') - msg = ' '.join([ - 'You are going to restore a wallet protected with two-factor authentication.', - 'Do you want to keep using two-factor authentication with this wallet,', - 'or do you want to disable it, and have two master private keys in your wallet?' - ]) - choices = [('keep', 'Keep'), ('disable', 'Disable')] - f = lambda x: self.on_choice(wizard, seed, passphrase, x) - wizard.choice_dialog(choices=choices, message=msg, title=title, run_next=f) - - def on_choice(self, wizard, seed, passphrase, x): - if x == 'disable': - f = lambda pw, encrypt: wizard.run('on_restore_pw', seed, passphrase, pw, encrypt) - wizard.request_password(run_next=f) - else: - self.create_keystore(wizard, seed, passphrase) - - def on_restore_pw(self, wizard, seed, passphrase, password, encrypt_storage): - storage = wizard.storage - xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) - k1 = keystore.from_xprv(xprv1) - k2 = keystore.from_xprv(xprv2) - k1.add_seed(seed) - k1.update_password(None, password) - k2.update_password(None, password) - storage.put('x1/', k1.dump()) - storage.put('x2/', k2.dump()) - long_user_id, short_id = get_user_id(storage) - xpub3 = make_xpub(get_signing_xpub(), long_user_id) - k3 = keystore.from_xpub(xpub3) - storage.put('x3/', k3.dump()) - - storage.set_keystore_encryption(bool(password)) - if encrypt_storage: - storage.set_password(password, enc_version=STO_EV_USER_PW) - - wizard.wallet = Wallet_2fa(storage) - wizard.create_addresses() - - - def create_remote_key(self, email, wizard): - xpub1 = wizard.storage.get('x1/')['xpub'] - xpub2 = wizard.storage.get('x2/')['xpub'] - # Generate third key deterministically. - long_user_id, short_id = get_user_id(wizard.storage) - xpub3 = make_xpub(get_signing_xpub(), long_user_id) - # secret must be sent by the server - try: - r = server.create(xpub1, xpub2, email) - except (socket.error, ErrorConnectingServer): - wizard.show_message('Server not reachable, aborting') - wizard.terminate() - return - except TrustedCoinException as e: - if e.status_code == 409: - r = None - else: - wizard.show_message(str(e)) - return - if r is None: - otp_secret = None - else: - otp_secret = r.get('otp_secret') - if not otp_secret: - wizard.show_message(_('Error')) - return - _xpub3 = r['xpubkey_cosigner'] - _id = r['id'] - if short_id != _id: - wizard.show_message("unexpected trustedcoin short_id: expected {}, received {}" - .format(short_id, _id)) - return - if xpub3 != _xpub3: - wizard.show_message("unexpected trustedcoin xpub3: expected {}, received {}" - .format(xpub3, _xpub3)) - return - self.request_otp_dialog(wizard, short_id, otp_secret, xpub3) - - def check_otp(self, wizard, short_id, otp_secret, xpub3, otp, reset): - if otp: - self.do_auth(wizard, short_id, otp, xpub3) - elif reset: - wizard.opt_bip39 = False - wizard.opt_ext = True - f = lambda seed, is_bip39, is_ext: wizard.run('on_reset_seed', short_id, seed, is_ext, xpub3) - wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed) - - def on_reset_seed(self, wizard, short_id, seed, is_ext, xpub3): - f = lambda passphrase: wizard.run('on_reset_auth', short_id, seed, passphrase, xpub3) - wizard.passphrase_dialog(run_next=f) if is_ext else f('') - - def do_auth(self, wizard, short_id, otp, xpub3): - try: - server.auth(short_id, otp) - except TrustedCoinException as e: - if e.status_code == 400: # invalid OTP - wizard.show_message(_('Invalid one-time password.')) - # ask again for otp - self.request_otp_dialog(wizard, short_id, None, xpub3) - else: - wizard.show_message(str(e)) - wizard.terminate() - except Exception as e: - wizard.show_message(str(e)) - wizard.terminate() - else: - k3 = keystore.from_xpub(xpub3) - wizard.storage.put('x3/', k3.dump()) - wizard.storage.put('use_trustedcoin', True) - wizard.storage.write() - wizard.wallet = Wallet_2fa(wizard.storage) - wizard.run('create_addresses') - - def on_reset_auth(self, wizard, short_id, seed, passphrase, xpub3): - xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) - if (wizard.storage.get('x1/')['xpub'] != xpub1 or - wizard.storage.get('x2/')['xpub'] != xpub2): - wizard.show_message(_('Incorrect seed')) - return - r = server.get_challenge(short_id) - challenge = r.get('challenge') - message = 'TRUSTEDCOIN CHALLENGE: ' + challenge - def f(xprv): - _, _, _, _, c, k = deserialize_xprv(xprv) - pk = bip32_private_key([0, 0], k, c) - key = ecc.ECPrivkey(pk) - sig = key.sign_message(message, True) - return base64.b64encode(sig).decode() - - signatures = [f(x) for x in [xprv1, xprv2]] - r = server.reset_auth(short_id, challenge, signatures) - new_secret = r.get('otp_secret') - if not new_secret: - wizard.show_message(_('Request rejected by server')) - return - self.request_otp_dialog(wizard, short_id, new_secret, xpub3) - - @hook - def get_action(self, storage): - if storage.get('wallet_type') != '2fa': - return - if not storage.get('x1/'): - return self, 'show_disclaimer' - if not storage.get('x2/'): - return self, 'show_disclaimer' - if not storage.get('x3/'): - return self, 'accept_terms_of_use' diff --git a/plugins/virtualkeyboard/__init__.py b/plugins/virtualkeyboard/__init__.py @@ -1,5 +0,0 @@ -from electrum.i18n import _ - -fullname = 'Virtual Keyboard' -description = '%s\n%s' % (_("Add an optional virtual keyboard to the password dialog."), _("Warning: do not use this if it makes you pick a weaker password.")) -available_for = ['qt'] diff --git a/plugins/virtualkeyboard/qt.py b/plugins/virtualkeyboard/qt.py @@ -1,61 +0,0 @@ -from PyQt5.QtGui import * -from PyQt5.QtWidgets import (QVBoxLayout, QGridLayout, QPushButton) -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ -import random - - -class Plugin(BasePlugin): - vkb = None - vkb_index = 0 - - @hook - def password_dialog(self, pw, grid, pos): - vkb_button = QPushButton(_("+")) - vkb_button.setFixedWidth(20) - vkb_button.clicked.connect(lambda: self.toggle_vkb(grid, pw)) - grid.addWidget(vkb_button, pos, 2) - self.kb_pos = 2 - self.vkb = None - - def toggle_vkb(self, grid, pw): - if self.vkb: - grid.removeItem(self.vkb) - self.vkb = self.virtual_keyboard(self.vkb_index, pw) - grid.addLayout(self.vkb, self.kb_pos, 0, 1, 3) - self.vkb_index += 1 - - def virtual_keyboard(self, i, pw): - i = i % 3 - if i == 0: - chars = 'abcdefghijklmnopqrstuvwxyz ' - elif i == 1: - chars = 'ABCDEFGHIJKLMNOPQRTSUVWXYZ ' - elif i == 2: - chars = '1234567890!?.,;:/%&()[]{}+-' - - n = len(chars) - s = [] - for i in range(n): - while True: - k = random.randint(0, n - 1) - if k not in s: - s.append(k) - break - - def add_target(t): - return lambda: pw.setText(str(pw.text()) + t) - - vbox = QVBoxLayout() - grid = QGridLayout() - grid.setSpacing(2) - for i in range(n): - l_button = QPushButton(chars[s[i]]) - l_button.setFixedWidth(25) - l_button.setFixedHeight(25) - l_button.clicked.connect(add_target(chars[s[i]])) - grid.addWidget(l_button, i // 6, i % 6) - - vbox.addLayout(grid) - - return vbox diff --git a/run_electrum b/run_electrum @@ -0,0 +1,473 @@ +#!/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__)) + '/electrum/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() + + +from electrum import bitcoin, util +from electrum import constants +from electrum import SimpleConfig, Network +from electrum import bitcoin, util, constants +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.plugin 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) diff --git a/scripts/bip70 b/scripts/bip70 @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# create a BIP70 payment request signed with a certificate - -import tlslite - -from electrum.transaction import Transaction -from electrum import paymentrequest -from electrum import paymentrequest_pb2 as pb2 - -chain_file = 'mychain.pem' -cert_file = 'mycert.pem' -amount = 1000000 -address = "18U5kpCAU4s8weFF8Ps5n8HAfpdUjDVF64" -memo = "blah" -out_file = "payreq" - - -with open(chain_file, 'r') as f: - chain = tlslite.X509CertChain() - chain.parsePemList(f.read()) - -certificates = pb2.X509Certificates() -certificates.certificate.extend(map(lambda x: str(x.bytes), chain.x509List)) - -with open(cert_file, 'r') as f: - rsakey = tlslite.utils.python_rsakey.Python_RSAKey.parsePEM(f.read()) - -script = Transaction.pay_script('address', address).decode('hex') - -pr_string = paymentrequest.make_payment_request(amount, script, memo, rsakey) - -with open(out_file,'wb') as f: - f.write(pr_string) - -print("Payment request was written to file '%s'"%out_file) diff --git a/scripts/block_headers b/scripts/block_headers @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 - -# A simple script that connects to a server and displays block headers - -import time -from electrum import SimpleConfig, Network -from electrum.util import print_msg, json_encode - -# start network -c = SimpleConfig() -network = Network(c) -network.start() - -# wait until connected -while network.is_connecting(): - time.sleep(0.1) - -if not network.is_connected(): - print_msg("daemon is not connected") - sys.exit(1) - -# 2. send the subscription -callback = lambda response: print_msg(json_encode(response.get('result'))) -network.send([('server.version',["block_headers script", "1.2"])], callback) -network.subscribe_to_headers(callback) - -# 3. wait for results -while network.is_connected(): - time.sleep(1) diff --git a/scripts/estimate_fee b/scripts/estimate_fee @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 -import util, json -from electrum.network import filter_protocol -peers = filter_protocol(util.get_peers()) -results = util.send_request(peers, 'blockchain.estimatefee', [2]) -print(json.dumps(results, indent=4)) diff --git a/scripts/get_history b/scripts/get_history @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 - -import sys -from electrum import Network -from electrum.util import json_encode, print_msg -from electrum import bitcoin - -try: - addr = sys.argv[1] -except Exception: - print("usage: get_history <bitcoin_address>") - sys.exit(1) - -n = Network() -n.start() -_hash = bitcoin.address_to_scripthash(addr) -h = n.get_history_for_scripthash(_hash) -print_msg(json_encode(h)) diff --git a/scripts/peers b/scripts/peers @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import util - -from electrum.network import filter_protocol -from electrum.blockchain import hash_header - -peers = util.get_peers() -peers = filter_protocol(peers, 's') - -results = util.send_request(peers, 'blockchain.headers.subscribe', []) - -for n,v in sorted(results.items(), key=lambda x:x[1].get('block_height')): - print("%60s"%n, v.get('block_height'), hash_header(v)) diff --git a/scripts/servers b/scripts/servers @@ -1,9 +0,0 @@ -#!/usr/bin/env python3 - -from electrum import set_verbosity -from electrum.network import filter_version -import util, json -set_verbosity(False) - -servers = filter_version(util.get_peers()) -print(json.dumps(servers, sort_keys = True, indent = 4)) diff --git a/scripts/txradar b/scripts/txradar @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -import util, sys -try: - tx = sys.argv[1] -except: - print("usage: txradar txid") - sys.exit(1) - -peers = util.get_peers() -results = util.send_request(peers, 'blockchain.transaction.get', [tx]) - -r1 = [] -r2 = [] - -for k, v in results.items(): - (r1 if v else r2).append(k) - -print("Received %d answers"%len(results)) -print("Propagation rate: %.1f percent" % (len(r1) *100./(len(r1)+ len(r2)))) diff --git a/scripts/util.py b/scripts/util.py @@ -1,87 +0,0 @@ -import select, time, queue -# import electrum -from electrum import Connection, Interface, SimpleConfig - -from electrum.network import parse_servers -from collections import defaultdict - -# electrum.util.set_verbosity(1) -def get_interfaces(servers, timeout=10): - '''Returns a map of servers to connected interfaces. If any - connections fail or timeout, they will be missing from the map. - ''' - assert type(servers) is list - socket_queue = queue.Queue() - config = SimpleConfig() - connecting = {} - for server in servers: - if server not in connecting: - connecting[server] = Connection(server, socket_queue, config.path) - interfaces = {} - timeout = time.time() + timeout - count = 0 - while time.time() < timeout and count < len(servers): - try: - server, socket = socket_queue.get(True, 0.3) - except queue.Empty: - continue - if socket: - interfaces[server] = Interface(server, socket) - count += 1 - return interfaces - -def wait_on_interfaces(interfaces, timeout=10): - '''Return a map of servers to a list of (request, response) tuples. - Waits timeout seconds, or until each interface has a response''' - result = defaultdict(list) - timeout = time.time() + timeout - while len(result) < len(interfaces) and time.time() < timeout: - rin = [i for i in interfaces.values()] - win = [i for i in interfaces.values() if i.unsent_requests] - rout, wout, xout = select.select(rin, win, [], 1) - for interface in wout: - interface.send_requests() - for interface in rout: - responses = interface.get_responses() - if responses: - result[interface.server].extend(responses) - return result - -def get_peers(): - config = SimpleConfig() - peers = {} - # 1. get connected interfaces - server = config.get('server') - if server is None: - print("You need to set a secure server, for example (for mainnet): 'electrum setconfig server helicarrier.bauerj.eu:50002:s'") - return [] - interfaces = get_interfaces([server]) - if not interfaces: - print("No connection to", server) - return [] - # 2. get list of peers - interface = interfaces[server] - interface.queue_request('server.peers.subscribe', [], 0) - responses = wait_on_interfaces(interfaces).get(server) - if responses: - response = responses[0][1] # One response, (req, response) tuple - peers = parse_servers(response.get('result')) - return peers - - -def send_request(peers, method, params): - print("Contacting %d servers"%len(peers)) - interfaces = get_interfaces(peers) - print("%d servers could be reached" % len(interfaces)) - for peer in peers: - if not peer in interfaces: - print("Connection failed:", peer) - for msg_id, i in enumerate(interfaces.values()): - i.queue_request(method, params, msg_id) - responses = wait_on_interfaces(interfaces) - for peer in interfaces: - if not peer in responses: - print(peer, "did not answer") - results = dict(zip(responses.keys(), [t[0][1].get('result') for t in responses.values()])) - print("%d answers"%len(results)) - return results diff --git a/scripts/watch_address b/scripts/watch_address @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import time -from electrum import bitcoin -from electrum import SimpleConfig, Network -from electrum.util import print_msg, json_encode - -try: - addr = sys.argv[1] -except Exception: - print("usage: watch_address <bitcoin_address>") - sys.exit(1) - -sh = bitcoin.address_to_scripthash(addr) - -# start network -c = SimpleConfig() -network = Network(c) -network.start() - -# wait until connected -while network.is_connecting(): - time.sleep(0.1) - -if not network.is_connected(): - print_msg("daemon is not connected") - sys.exit(1) - -# 2. send the subscription -callback = lambda response: print_msg(json_encode(response.get('result'))) -network.subscribe_to_address(addr, callback) - -# 3. wait for results -while network.is_connected(): - time.sleep(1) diff --git a/setup.py b/setup.py @@ -15,7 +15,7 @@ with open('contrib/requirements/requirements.txt') as f: with open('contrib/requirements/requirements-hw.txt') as f: requirements_hw = f.read().splitlines() -version = imp.load_source('version', 'lib/version.py') +version = imp.load_source('version', 'electrum/version.py') if sys.version_info[:3] < (3, 4, 0): sys.exit("Error: Electrum requires Python version >= 3.4.0...") @@ -55,36 +55,34 @@ setup( extras_require=extras_require, packages=[ 'electrum', - 'electrum_gui', - 'electrum_gui.qt', - 'electrum_plugins', - 'electrum_plugins.audio_modem', - 'electrum_plugins.cosigner_pool', - 'electrum_plugins.email_requests', - 'electrum_plugins.greenaddress_instant', - 'electrum_plugins.hw_wallet', - 'electrum_plugins.keepkey', - 'electrum_plugins.labels', - 'electrum_plugins.ledger', - 'electrum_plugins.revealer', - 'electrum_plugins.trezor', - 'electrum_plugins.digitalbitbox', - 'electrum_plugins.trustedcoin', - 'electrum_plugins.virtualkeyboard', + 'electrum.gui', + 'electrum.gui.qt', + 'electrum.plugins', + 'electrum.plugins.audio_modem', + 'electrum.plugins.cosigner_pool', + 'electrum.plugins.email_requests', + 'electrum.plugins.greenaddress_instant', + 'electrum.plugins.hw_wallet', + 'electrum.plugins.keepkey', + 'electrum.plugins.labels', + 'electrum.plugins.ledger', + 'electrum.plugins.revealer', + 'electrum.plugins.trezor', + 'electrum.plugins.digitalbitbox', + 'electrum.plugins.trustedcoin', + 'electrum.plugins.virtualkeyboard', ], package_dir={ - 'electrum': 'lib', - 'electrum_gui': 'gui', - 'electrum_plugins': 'plugins', + 'electrum': 'electrum' }, package_data={ '': ['*.txt', '*.json', '*.ttf', '*.otf'], 'electrum': [ - 'wordlist/*.txt', - 'locale/*/LC_MESSAGES/electrum.mo', + 'electrum/wordlist/*.txt', + 'electrum/locale/*/LC_MESSAGES/electrum.mo', ], }, - scripts=['electrum'], + scripts=['electrum/electrum'], data_files=data_files, description="Lightweight Bitcoin Wallet", author="Thomas Voegtlin", diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml @@ -19,5 +19,5 @@ parts: python-version: python3 stage-packages: [python3-pyqt5] build-packages: [pyqt5-dev-tools] - install: pyrcc5 icons.qrc -o $SNAPCRAFT_PART_INSTALL/lib/python3.5/site-packages/electrum_gui/qt/icons_rc.py + install: pyrcc5 icons.qrc -o $SNAPCRAFT_PART_INSTALL/lib/python3.5/site-packages/electrum/gui/qt/icons_rc.py after: [desktop-qt5] diff --git a/tox.ini b/tox.ini @@ -6,7 +6,7 @@ deps= pytest coverage commands= - coverage run --source=lib -m py.test -v + coverage run --source=electrum -m py.test -v coverage report extras= fast