Repository: BTCPrivate/electrum-btcp
Branch: master
Commit: 709449fc32f4
Files: 257
Total size: 1.8 MB
Directory structure:
gitextract_plcqy8dy/
├── .gitignore
├── .idea/
│ ├── electrum.iml
│ ├── misc.xml
│ ├── modules.xml
│ └── vcs.xml
├── .travis.yml
├── AUTHORS
├── Dockerfile
├── Info.plist
├── LICENCE
├── MANIFEST.in
├── README.rst
├── RELEASE-NOTES
├── app.fil
├── brewfile
├── build-docker.sh
├── clean.sh
├── config
├── contrib/
│ ├── build-wine/
│ │ ├── README.md
│ │ ├── build-electrum-git.sh
│ │ ├── build.sh
│ │ ├── deterministic.spec
│ │ ├── electrum.nsi
│ │ ├── prepare-hw.sh
│ │ ├── prepare-pyinstaller.sh
│ │ └── prepare-wine.sh
│ ├── freeze_packages.sh
│ ├── make_apk
│ ├── make_download
│ ├── make_locale
│ ├── make_packages
│ ├── requirements.txt
│ └── sign_packages
├── create-dmg.sh
├── docs/
│ └── release-tests.md
├── electrum-btcp
├── electrum-env
├── electrum.conf.sample
├── electrum.desktop
├── gui/
│ ├── __init__.py
│ ├── kivy/
│ │ ├── Makefile
│ │ ├── Readme.txt
│ │ ├── __init__.py
│ │ ├── data/
│ │ │ ├── fonts/
│ │ │ │ └── tron/
│ │ │ │ ├── License.txt
│ │ │ │ └── Readme.txt
│ │ │ ├── glsl/
│ │ │ │ ├── default.fs
│ │ │ │ ├── default.vs
│ │ │ │ ├── header.fs
│ │ │ │ └── header.vs
│ │ │ ├── images/
│ │ │ │ └── defaulttheme.atlas
│ │ │ ├── java-classes/
│ │ │ │ └── org/
│ │ │ │ └── electrum/
│ │ │ │ └── qr/
│ │ │ │ └── SimpleScannerActivity.java
│ │ │ └── style.kv
│ │ ├── i18n.py
│ │ ├── main.kv
│ │ ├── main_window.py
│ │ ├── nfc_scanner/
│ │ │ ├── __init__.py
│ │ │ ├── scanner_android.py
│ │ │ └── scanner_dummy.py
│ │ ├── tools/
│ │ │ ├── bitcoin_intent.xml
│ │ │ ├── blacklist.txt
│ │ │ └── buildozer.spec
│ │ └── uix/
│ │ ├── __init__.py
│ │ ├── combobox.py
│ │ ├── context_menu.py
│ │ ├── dialogs/
│ │ │ ├── __init__.py
│ │ │ ├── amount_dialog.py
│ │ │ ├── bump_fee_dialog.py
│ │ │ ├── checkbox_dialog.py
│ │ │ ├── choice_dialog.py
│ │ │ ├── fee_dialog.py
│ │ │ ├── fx_dialog.py
│ │ │ ├── installwizard.py
│ │ │ ├── label_dialog.py
│ │ │ ├── nfc_transaction.py
│ │ │ ├── password_dialog.py
│ │ │ ├── qr_dialog.py
│ │ │ ├── qr_scanner.py
│ │ │ ├── question.py
│ │ │ ├── seed_options.py
│ │ │ ├── settings.py
│ │ │ ├── tx_dialog.py
│ │ │ └── wallets.py
│ │ ├── drawer.py
│ │ ├── gridview.py
│ │ ├── menus.py
│ │ ├── qrcodewidget.py
│ │ ├── screens.py
│ │ └── ui_screens/
│ │ ├── about.kv
│ │ ├── address.kv
│ │ ├── history.kv
│ │ ├── invoice.kv
│ │ ├── invoices.kv
│ │ ├── network.kv
│ │ ├── proxy.kv
│ │ ├── receive.kv
│ │ ├── requests.kv
│ │ ├── send.kv
│ │ ├── server.kv
│ │ └── status.kv
│ ├── qt/
│ │ ├── __init__.py
│ │ ├── address_dialog.py
│ │ ├── address_list.py
│ │ ├── amountedit.py
│ │ ├── console.py
│ │ ├── contact_list.py
│ │ ├── fee_slider.py
│ │ ├── history_list.py
│ │ ├── installwizard.py
│ │ ├── invoice_list.py
│ │ ├── main_window.py
│ │ ├── network_dialog.py
│ │ ├── password_dialog.py
│ │ ├── paytoedit.py
│ │ ├── qrcodewidget.py
│ │ ├── qrtextedit.py
│ │ ├── qrwindow.py
│ │ ├── request_list.py
│ │ ├── seed_dialog.py
│ │ ├── transaction_dialog.py
│ │ ├── util.py
│ │ └── utxo_list.py
│ ├── stdio.py
│ └── text.py
├── icns-from-vector.sh
├── icons/
│ └── electrum.icns
├── icons.qrc
├── lib/
│ ├── __init__.py
│ ├── base_wizard.py
│ ├── bitcoin.py
│ ├── blockchain.py
│ ├── checkpoints.json
│ ├── checkpoints_testnet.json
│ ├── coinchooser.py
│ ├── commands.py
│ ├── contacts.py
│ ├── currencies.json
│ ├── daemon.py
│ ├── dnssec.py
│ ├── equihash.py
│ ├── exchange_rate.py
│ ├── i18n.py
│ ├── interface.py
│ ├── jsonrpc.py
│ ├── keystore.py
│ ├── mnemonic.py
│ ├── msqr.py
│ ├── network.py
│ ├── old_mnemonic.py
│ ├── paymentrequest.proto
│ ├── paymentrequest.py
│ ├── paymentrequest_pb2.py
│ ├── pem.py
│ ├── plot.py
│ ├── plugins.py
│ ├── qrscanner.py
│ ├── ripemd.py
│ ├── rsakey.py
│ ├── segwit_addr.py
│ ├── servers-orig.json
│ ├── servers.json
│ ├── servers_testnet.json
│ ├── simple_config.py
│ ├── storage.py
│ ├── synchronizer.py
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── test_bitcoin.py
│ │ ├── test_interface.py
│ │ ├── test_mnemonic.py
│ │ ├── test_simple_config.py
│ │ ├── test_storage_upgrade.py
│ │ ├── test_transaction.py
│ │ ├── test_util.py
│ │ ├── test_wallet.py
│ │ └── test_wallet_vertical.py
│ ├── transaction.py
│ ├── util.py
│ ├── verifier.py
│ ├── version.py
│ ├── wallet.py
│ ├── websockets.py
│ ├── wordlist/
│ │ ├── chinese_simplified.txt
│ │ ├── english.txt
│ │ ├── japanese.txt
│ │ ├── portuguese.txt
│ │ └── spanish.txt
│ └── x509.py
├── packages.txt
├── plugins/
│ ├── README
│ ├── __init__.py
│ ├── audio_modem/
│ │ ├── __init__.py
│ │ └── qt.py
│ ├── cosigner_pool/
│ │ ├── __init__.py
│ │ └── qt.py
│ ├── digitalbitbox/
│ │ ├── __init__.py
│ │ ├── cmdline.py
│ │ ├── digitalbitbox.py
│ │ └── qt.py
│ ├── email_requests/
│ │ ├── __init__.py
│ │ └── qt.py
│ ├── greenaddress_instant/
│ │ ├── __init__.py
│ │ └── qt.py
│ ├── hw_wallet/
│ │ ├── __init__.py
│ │ ├── cmdline.py
│ │ ├── plugin.py
│ │ └── qt.py
│ ├── keepkey/
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── clientbase.py
│ │ ├── cmdline.py
│ │ ├── keepkey.py
│ │ ├── plugin.py
│ │ ├── qt.py
│ │ └── qt_generic.py
│ ├── labels/
│ │ ├── __init__.py
│ │ ├── kivy.py
│ │ ├── labels.py
│ │ └── qt.py
│ ├── ledger/
│ │ ├── __init__.py
│ │ ├── auth2fa.py
│ │ ├── cmdline.py
│ │ ├── ledger.py
│ │ └── qt.py
│ ├── trezor/
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── clientbase.py
│ │ ├── cmdline.py
│ │ ├── plugin.py
│ │ ├── qt.py
│ │ ├── qt_generic.py
│ │ └── trezor.py
│ ├── trustedcoin/
│ │ ├── __init__.py
│ │ ├── cmdline.py
│ │ ├── qt.py
│ │ └── trustedcoin.py
│ └── virtualkeyboard/
│ ├── __init__.py
│ └── qt.py
├── pubkeys/
│ ├── Animazing.asc
│ ├── ThomasV.asc
│ ├── kyuupichan.asc
│ └── wozz.asc
├── requirements.txt
├── requirements_travis.txt
├── run-docker.sh
├── scripts/
│ ├── bip70
│ ├── block_headers
│ ├── estimate_fee
│ ├── get_history
│ ├── peers
│ ├── servers
│ ├── txradar
│ ├── util.py
│ └── watch_address
├── setup-mac.sh
├── setup-release.py
├── setup.py
├── snap/
│ └── snapcraft.yaml
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
####-*.patch
*.pyc
*.swp
build/
dist/
*.egg/
/electrum.py
.DS_Store
contrib/pyinstaller/
Electrum_BTCP.egg-info/
gui/qt/icons_rc.py
locale/
.devlocaltmp/
*_trial_temp
packages
env/
.tox/
.buildozer/
bin/
# tox files
.cache/
.coverage
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
================================================
FILE: .idea/electrum.iml
================================================
================================================
FILE: .idea/misc.xml
================================================
================================================
FILE: .idea/modules.xml
================================================
================================================
FILE: .idea/vcs.xml
================================================
================================================
FILE: .travis.yml
================================================
sudo: false
language: python
python:
- 3.5
- 3.6
install:
- pip install -r requirements_travis.txt
cache:
- pip
script:
- tox
after_success:
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi
- coveralls
================================================
FILE: AUTHORS
================================================
ThomasV - Creator and maintainer.
Animazing / Tachikoma - Styled the new GUI. Mac version.
Azelphur - GUI stuff.
Coblee - Alternate coin support and py2app support.
Deafboy - Ubuntu packages.
EagleTM - Bugfixes.
ErebusBat - Mac distribution.
Genjix - Porting pro-mode functionality to lite-gui and worked on server
Slush - Work on the server. Designed the original Stratum spec.
Julian Toash (Tuxavant) - Various fixes to the client.
rdymac - Website and translations.
kyuupichan - Miscellaneous.
================================================
FILE: Dockerfile
================================================
FROM ubuntu:18.04
ENV VERSION 1.0.0
RUN set -x \
&& apt-get update \
&& apt-get install -y curl \
&& curl -sL https://github.com/BTCPrivate/electrum-btcp/archive/P!${VERSION}.tar.gz |tar xvz \
&& mv electrum-btcp-P-${VERSION} electrum-btcp \
&& cd electrum-btcp \
&& apt-get install -y $(grep -vE "^\s*#" packages.txt | tr "\n" " ") \
&& pip3 install -r requirements.txt \
&& pip3 install pyblake2 \
&& protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto \
&& pyrcc5 icons.qrc -o gui/qt/icons_rc.py \
&& ./contrib/make_locale
WORKDIR /electrum-btcp
ENV DISPLAY :0
CMD ./electrum
================================================
FILE: Info.plist
================================================
CFBundleURLTypes
CFBundleURLName
bitcoinprivate
CFBundleURLSchemes
bitcoinprivate
LSArchitecturePriority
x86_64
i386
================================================
FILE: LICENCE
================================================
The MIT License (MIT)
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.
================================================
FILE: MANIFEST.in
================================================
include LICENCE RELEASE-NOTES AUTHORS
include README.rst
include electrum.conf.sample
include electrum.desktop
include *.py
include electrum
recursive-include lib *.py
recursive-include gui *.py
recursive-include plugins *.py
recursive-include packages *.py
recursive-include packages cacert.pem
include app.fil
include icons.qrc
recursive-include icons *
recursive-include scripts *
================================================
FILE: README.rst
================================================
BTCP Electrum - Lightweight Bitcoin Private Wallet
==========================================
.. image:: https://opencollective.com/electrum-btcp/backers/badge.svg
:alt: Backers on Open Collective
:target: #backers
.. image:: https://opencollective.com/electrum-btcp/sponsors/badge.svg
:alt: Sponsors on Open Collective
:target: #sponsors
Download the current Release: https://github.com/BTCPrivate/electrum-btcp/releases/
Viewing & Sending from Z addresses is not yet supported on this wallet.
Know about your data directory:
Linux & Mac: ~/.electrum-btcp/
Windows: C:\Users\YourUserName\AppData\Roaming\Electrum-btcp\
~/.electrum-btcp/wallets/ has your wallet files - BACK UP THIS FOLDER
You can also use the 'Export Private Keys' and 'Show Seed' functions from inside the application to write down and safely store your the keys to your funds.
Please use the issue tracker for bug reports, feature requests, and other mission-critical information. It is actively monitored by the Zclassic development team. For general support, please visit our Discord: https://discord.gg/2PRZ5q
Development Version
===================
First, clone from Github::
git clone git://github.com/BTCPrivate/electrum-btcp.git
cd electrum-btcp
For Mac:
--------
Using Homebrew::
# Setup Homebrew
./setup-mac
# Install Homebrew dependencies
brew bundle
# Install Python dependencies
pip3 install -r requirements.txt
# Build icons
pyrcc5 icons.qrc -o gui/qt/icons_rc.py
# Compile the protobuf description file
protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto
# Build .app, .dmg
./create-dmg
# Run
./electrum-btcp
For Linux:
----------
Install Dependencies::
sudo apt-get install $(grep -vE "^\s*#" packages.txt | tr "\n" " ")
pip install -r requirements.txt
// ^ pip3 for newer version
(Ubuntu with ledger wallet)
ln -s /lib/x86_64-linux-gnu/libudev.so.1 /lib/x86_64-linux-gnu/libudev.so
# For yum installations (no apt-get), or for a clean python env, use Anaconda with Python 3:
#https://poweruphosting.com/blog/install-anaconda-python-ubuntu-16-04/
Compile the icons file for Qt::
pyrcc5 icons.qrc -o gui/qt/icons_rc.py
For the Linux app launcher (start menu) icon::
sudo desktop-file-install electrum.desktop
Compile the protobuf description file::
protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto
Create translations (optional)::
./contrib/make_locale
Run::
./electrum-btcp
For Ubuntu 18.04 including Docker install:
----------------------
Update apt package index and upgrade packages as needed
sudo apt-get update && apt-get upgrade
Install Docker package from Ubuntu repository
sudo apt install docker.io
Build the docker image::
sudo ./build-docker.sh
Run the docker image::
./run-docker.sh
For Linux with docker:
----------------------
Build the docker image::
./build-docker.sh
Run the docker image::
./run-docker.sh
Building Releases
=================
MacOS
------
Simply - ::
./setup-mac.sh
sudo ./create-dmg.sh
Windows
-------
See `contrib/build-wine/README` file.
Android
-------
See `gui/kivy/Readme.txt` file.
UPSTREAM PATCH: https://github.com/spesmilo/electrum/blob/master/gui/kivy/Readme.md
---
To just create binaries, create the 'packages/' directory::
./contrib/make_packages
(This directory contains the Python dependencies used by Electrum.)
BTCP Hints and Debug
===================
There are several useful scripts in:
scripts
This is a good initial check to determine whether things are working:
cd scripts
python3 block_headers
--
The Zclassic Wiki is located at: https://github.com/z-classic/zclassic/wiki. Please use this as a reference and feel free to contribute.
~/.electrum-btcp/
~/.electrum-btcp/wallets/ has your wallet files - ** back up this folder **
~/.electrum-btcp/config has your Electrum connection object.
Credits
+++++++
Contributors
------------
This project exists thanks to all the people who contribute!
.. image:: https://opencollective.com/electrum-btcp/contributors.svg?width=890&button=false
Backers
-------
Thank you to all our backers! `Become a backer`__.
.. image:: https://opencollective.com/electrum-btcp/backers.svg?width=890
:target: https://opencollective.com/electrum-btcp#backers
__ Backer_
.. _Backer: https://opencollective.com/electrum-btcp#backer
Sponsors
--------
Support us by becoming a sponsor. Your logo will show up here with a link to your website. `Become a sponsor`__.
.. image:: https://opencollective.com/electrum-btcp/sponsor/0/avatar.svg
:target: https://opencollective.com/electrum-btcp/sponsor/0/website
__ Sponsor_
.. _Sponsor: https://opencollective.com/electrum-btcp#sponsor
Original Project Info
---------------------
::
Forked from **spesmilo/electrum**: https://github.com/spesmilo/electrum
Licence: MIT Licence
Author: Thomas Voegtlin
Language: Python (GUI: Qt, Kivy)
Platforms: Windows, Mac, Linux, Android
Homepage: https://electrum.org/
.. image:: https://travis-ci.org/spesmilo/electrum.svg?branch=master
:target: https://travis-ci.org/spesmilo/electrum
:alt: Build Status
.. image:: https://coveralls.io/repos/github/spesmilo/electrum/badge.svg?branch=master
:target: https://coveralls.io/github/spesmilo/electrum?branch=master
:alt: Test coverage statistics
---
The Bitcoin Private Team
================================================
FILE: RELEASE-NOTES
================================================
# Release 3.0.5 : (Security update)
This is a follow-up to the 3.0.4 release, which did not completely fix
issue #3374. Users should upgrade to 3.0.5.
* The JSONRPC interface is password protected
* JSONRPC commands are disabled if the GUI is running, except 'ping',
which is used to determine if a GUI is already running
# Release 3.0.4 : (Security update)
* Fix a vulnerability caused by Cross-Origin Resource Sharing (CORS)
in the JSONRPC interface. Previous versions of Electrum are
vulnerable to port scanning and deanonimization attacks from
malicious websites. Wallets that are not password-protected are
vulnerable to theft.
* Bundle QR scanner with Android app
* Minor bug fixes
# Release 3.0.3
* Qt GUI: sweeping now uses the Send tab, allowing fees to be set
* Windows: if using the installer binary, there is now a separate shortcut
for "Electrum Testnet"
* Digital Bitbox: added suport for p2sh-segwit
* OS notifications for incoming transactions
* better transaction size estimation:
- fees for segwit txns were somewhat underestimated (#3347)
- some multisig txns were underestimated
- handle uncompressed pubkeys
* fix #3321: testnet for Windows binaries
* fix #3264: Ledger/dbb signing on some platforms
* fix #3407: KeepKey sending to p2sh output
* other minor fixes and usability improvements
# Release 3.0.2
* Android: replace requests tab with address tab, with access to
private keys
* sweeping minikeys: search for both compressed and uncompressed
pubkeys
* fix wizard crash when attempting to reset Google Authenticator
* fix #3248: fix Ledger+segwit signing
* fix #3262: fix SSL payment request signing
* other minor fixes.
# Release 3.0.1
* minor bug and usability fixes
# Release 3.0 - Uncanny Valley (November 1st, 2017)
* The project was migrated to Python3 and Qt5. Python2 is no longer
supported. If you cloned the source repository, you will need to
run "python3 setup.py install" in order to install the new
dependencies.
* Segwit support:
- Native segwit scripts are supported using a new type of
seed. The version number for segwit seeds is 0x100. The install
wizard will not create segwit seeds by default; users must
opt-in with the segwit option.
- Native segwit scripts are represented using bech32 addresses,
following BIP173. Please note that BIP173 is still in draft
status, and that other wallets/websites may not support
it. Thus, you should keep a non-segwit wallet in order to be
able to receive bitcoins during the transition period. If BIP173
ends up being rejected or substantially modified, your wallet
may have to be restored from seed. This will not affect funds
sent to bech32 addresses, and it will not affect the capacity of
Electrum to spend these funds.
- Segwit scripts embedded in p2sh are supported with hardware
wallets or bip39 seeds. To create a segwit-in-p2sh wallet,
trezor/ledger users will need to enter a BIP49 derivation path.
- The BIP32 master keys of segwit wallets are serialized using new
version numbers. The new version numbers encode the script type,
and they result in the following prefixes:
* xpub/xprv : p2pkh or p2sh
* ypub/yprv : p2wpkh-in-p2sh
* Ypub/Yprv : p2wsh-in-p2sh
* zpub/zprv : p2wpkh
* Zpub/Zprv : p2wsh
These values are identical for mainnet and testnet; tpub/tprv
prefixes are no longer used in testnet wallets.
- The Wallet Import Format (WIF) is similarly extended for segwit
scripts. After a base58-encoded key is decoded to binary, its
first byte encodes the script type:
* 128 + 0: p2pkh
* 128 + 1: p2wpkh
* 128 + 2: p2wpkh-in-p2sh
* 128 + 5: p2sh
* 128 + 6: p2wsh
* 128 + 7: p2wsh-in-p2sh
The distinction between p2sh and p2pkh in private key means that
it is not possible to import a p2sh private key and associate it
to a p2pkh address.
* A new version of the Electrum protocol is required by the client
(version 1.1). Servers using older versions of the protocol will
not be displayed in the GUI.
* By default, transactions are time-locked to the height of the
current block. Other values of locktime may be passed using the
command line.
# Release 2.9.3
* fix configuration file issue #2719
* fix ledger signing of non-RBF transactions
* disable 'spend confirmed only' option by default
# Release 2.9.2
* force headers download if headers file is corrupted
* add websocket to windows builds
# Release 2.9.1
* fix initial headers download
* validate contacts on import
* command-line option for locktime
# Release 2.9 - Independence (July 27th, 2017)
* Multiple Chain Validation: Electrum will download and validate
block headers sent by servers that may follow different branches
of a fork in the Bitcoin blockchain. Instead of a linear sequence,
block headers are organized in a tree structure. Branching points
are located efficiently using binary search. The purpose of MCV is
to detect and handle blockchain forks that are invisible to the
classical SPV model.
* The desired branch of a blockchain fork can be selected using the
network dialog. Branches are identified by the hash and height of
the diverging block. Coin splitting is possible using RBF
transaction (a tutorial will be added).
* Multibit support: If the user enters a BIP39 seed (or uses a
hardware wallet), the full derivation path is configurable in the
install wizard.
* Option to send only confirmed coins
* Qt GUI:
- Network dialog uses tabs and gets updated by network events.
- The gui tabs use icons
* Kivy GUI:
- separation between network dialog and wallet settings dialog.
- option for manual server entry
- proxy configuration
* Daemon: The wallet password can be passed as parameter to the
JSONRPC API.
* Various other bugfixes and improvements.
# Release 2.8.3
* Fix crash on reading older wallet formats.
* TrustedCoin: remove pay-per-tx option
# Release 2.8.2
* show paid invoices in history tab
* improve CPFP dialog
* fixes for trezor, keepkey
* other minor bugfixes
# Release 2.8.1
* fix Digital Bitbox plugin
* fix daemon jsonrpc
* fix trustedcoin wallet creation
* other minor bugfixes
# Release 2.8.0 (March 9, 2017)
* Wallet file encryption using ECIES: A keypair is derived from the
wallet password. Once the wallet is decrypted, only the public key
is retained in memory, in order to save the encrypted file.
* The daemon requires wallets to be explicitly loaded before
commands can use them. Wallets can be loaded using: 'electrum
daemon load_wallet [-w path]'. This command will require a
password if the wallet is encrypted.
* Invoices and contacts are stored in the wallet file and are no
longer shared between wallets. Previously created invoices and
contacts files may be imported from the menu.
* Fees improvements:
- Dynamic fees are enabled by default.
- Child Pays For Parent (CPFP) dialog in the GUI.
- RBF is automatically proposed for low fee transactions.
* Support for Segregated Witness (testnet only).
* Support for Digital Bitbox hardware wallet.
* The GUI shows a blue icon when connected using a proxy.
# Release 2.7.18
* enforce https on exchange rate APIs
* use hardcoded list of exchanges
* move 'Freeze' menu to Coins (utxo) tab
* various bugfixes
# Release 2.7.17
* fix a few minor regressions in the Qt GUI
# Release 2.7.16
* add Testnet support (fix #541)
* allow daemon to be launched in the foreground (fix #1873)
* Qt: use separate tabs for addresses and UTXOs
* Qt: update fee slider with a network callback
* Ledger: new ui and mobile 2fa validation (neocogent)
# Release 2.7.15
* Use fee slider for both static and dynamic fees.
* Add fee slider to RBF dialog (fix #2083).
* Simplify fee preferences.
* Critical: Fix password update issue (#2097). This bug prevents
password updates in multisig and 2FA wallets. It may also cause
wallet corruption if the wallet contains several master private
keys (such as 2FA wallets that have been restored from
seed). Affected wallets will need to be restored again.
# Release 2.7.14
* Merge exchange_rate plugin with main code
* Faster synchronization and transaction creation
* Fix bugs #2096, #2016
# Release 2.7.13
* fix message signing with imported keys
* add size to transaction details window
* move plot plugin to main code
* minor bugfixes
# Release 2.7.12
various bugfixes
# Release 2.7.11
* fix offline signing (issue #195)
* fix android crashes caused by threads
# Release 2.7.10
* various fixes for hardware wallets
* improve fee bumping
* separate sign and broadcast buttons in Qt tx dialog
* allow spaces in private keys
# Release 2.7.9
* Fix a bug with the ordering of pubkeys in recent multisig wallets.
Affected wallets will regenerate their public keys when opened for
the first time. This bug does not affect address generation.
* Fix hardware wallet issues #1975, #1976
# Release 2.7.8
* Fix a bug with fee bumping
* Fix crash when parsing request (issue #1969)
# Release 2.7.7
* Fix utf8 encoding bug with old wallet seeds (issue #1967)
* Fix delete request from menu (isue #1968)
# Release 2.7.6
* Fixes a critical bug with imported private keys (issue #1966). Keys
imported in Electrum 2.7.x were not encrypted, even if the wallet
had a password. If you imported private keys using Electrum 2.7.x,
you will need to import those keys again. If you imported keys in
2.6 and converted with 2.7.x, you don't need to do anything, but
you still need to upgrade in order to be able to spend.
* Wizard: Hide seed options in a popup dialog.
# Release 2.7.5
* Add number of confirmations to request status. (issue #1757)
* In the GUI, refer to passphrase as 'seed extension'.
* Fix bug with utf8 encoded passphrases.
* Kivy wizard: add a dialog for seed options.
* Kivy wizard: add current word to suggestions, because some users
don't see the space key.
# Release 2.7.4
* Fix private key import in wizard
* Fix Ledger display (issue #1961)
* Fix old watching-only wallets (issue #1959)
* Fix Android compatibility (issue #1947)
# Release 2.7.3
* fix Trezor and Keepkey support in Windows builds
* fix sweep private key dialog
* minor fixes: #1958, #1959
# Release 2.7.2
* fix bug in password update (issue #1954)
* fix fee slider (issue #1953)
# Release 2.7.1
* fix wizard crash with old seeds
* fix issue #1948: fee slider
# Release 2.7.0 (Oct 2 2016)
* The wallet file format has been upgraded. This upgrade is not
backward compatible, which means that a wallet upgraded to the 2.7
format will not be readable by earlier versions of
Electrum. Multiple accounts inside the same wallet are not
supported in the new format; the Qt GUI will propose to split any
wallet that has several accounts. Make sure that you have saved
your seed phrase before you upgrade Electrum.
* This version introduces a separation between wallets types and
keystores types. 'Wallet type' defines the type of Bitcoin contract
used in the wallet, while 'keystore type' refers to the method used
to store private keys. Therefore, so-called 'hardware wallets' will
be referred to as 'hardware keystores'.
* Hardware keystores:
- The Ledger Nano S is supported.
- Hardware keystores can be used as cosigners in multi-signature
wallets.
- Multiple hardware cosigners can be used in the same multisig
wallet. One icon per keystore is displayed in the satus bar. Each
connected device will co-sign the transaction.
* Replace-By-Fee: RBF transactions are supported in both Qt and
Android. A warning is displayed in the history for transactions
that are replaceable, have unconfirmed parents, or that have very
low fees.
* Dynamic fees: Dynamic fees are enabled by default. A slider allows
the user to select the expected confirmation time of their
transaction. The expected confirmation times of incoming
transactions is also displayed in the history.
* The install wizards of Qt and Kivy have been unified.
* Qt GUI (Desktop):
- A fee slider is visible in the in send tab
- The Address tab is hidden by default, can be shown with Ctrl-A
- UTXOs are displayed in the Address tab
* Kivy GUI (Android):
- The GUI displays the complete transaction history.
- Multisig wallets are supported.
- Wallets can be created and deleted in the GUI.
* Seed phrases can be extended with a user-chosen passphrase. The
length of seed phrases is standardized to 12 words, using 132 bits
of entropy (including 2FA seeds). In the wizard, the type of the
seed is displayed in the seed input dialog.
* TrustedCoin users can request a reset of their Google Authenticator
account, if they still have their seed.
# Release 2.6.4 (bugfixes)
* fix coinchooser bug (#1703)
* fix daemon JSONRPC (#1731)
* fix command-line broadcast (#1728)
* QT: add colors to labels
# Release 2.6.3 (bugfixes)
* fix command line parsing of transactions
* fix signtransaction --privkey (#1715)
# Release 2.6.2 (bugfixes)
* fix Trustedcoin restore from seed (bug #1704)
* small improvements to kivy GUI
# Release 2.6.1 (bugfixes)
* fix broadcast command (bug #1688)
* fix tx dialog (bug #1690)
* kivy: support old-type seed phrases in wizard
# Release 2.6
* The source code is relicensed under the MIT Licence
* First official release of the Kivy GUI, with android APK
* The old 'android' and 'gtk' GUIs are deprecated
* Separation between plugins and GUIs
* The command line uses jsonrpc to communicate with the daemon
* New command: 'notify
'
* Alternative coin selection policy, designed to help preserve user
privacy. Enable it by setting the Coin Selection preference to
Privacy.
* The install wizard has been rewritten and improved
* Support minikeys as used in Casascius coins for private key import
and sweeping
* Much improved support for TREZOR and KeepKey devices:
- full device information display
- initialize a new or wiped device in 4 ways:
1) device generates a new wallet
2) you enter a seed
3) you enter a BIP39 mnemonic to generate the seed
4) you enter a master private key
- KeepKey secure seed recovery (KeepKey only)
- change / set / disable PIN
- set homescreen (TREZOR only)
- set a session timeout. Once a session has timed out, further use
of the device requires your PIN and passhphrase to be re-entered
- enable / disable passphrases
- device wipe
- multiple device support
# Release 2.5.4
* increase MIN_RELAY_TX_FEE to avoid dust transactions
# Release 2.5.3 (bugfixes)
* installwizard: do not allow direct copy-paste of the seed
* installwizard: fix bug #1531 (starting offline)
# Release 2.5.2 (bugfixes)
* fix bug #1513 (client tries to broadcast transaction while not connected)
* fix synchronization bug (#1520)
* fix command line bug (#1494)
* fixes for exchange rate plugin
# Release 2.5.1 (bugfixes)
* signatures in transactions were still using the old class
* make sure that setup.py uses python2
* fix wizard crash with trustedcoin plugin
* fix socket infinite loop
* fix history bug #1479
# Release 2.5
* Low-S values are used in signatures (BIP 62).
* The Kivy GUI has been merged into master.
* The Qt GUI supports multiple windows in the same process. When a
new Electrum instance is started, it checks for an already running
Electrum process, and connects to it.
* The network layer uses select(), so all server communication is
handled by a single thread. Moreover, the synchronizer, verifier,
and exchange rate plugin now run as separate jobs within the
networking thread instead of as their own threads.
* Plugins are revamped, particularly the exchange rate plugin.
# Release 2.4.4
* Fix bug with TrustedCoin plugin
# Release 2.4.3
* Support for KeepKey hardware wallet
* Simplified Chinese wordlist
* Minor bugfixes and GUI tweaks
# Release 2.4.2
* Command line can read arguments from stdin (pipe)
* Speedup fee computation for large transactions
* Various bugfixes
# Release 2.4.1
* Use ssl.PROTOCOL_TLSv1
* Fix DNSSEC issues with ECDSA signatures
* Replace TLSLite dependency with minimal RSA implementation
* Dynamic Fees: using estimatefee value returned by server
* Various GUI improvements
# Release 2.4
* Payment to DNS names storing a Bitcoin addresses (OpenAlias) is
supported directly, without activating a plugin. The verification
uses DNSSEC.
* The DNSSEC verification code was rewritten. The previous code,
which was part of the OpenAlias plugin, is vulnerable and should
not be trusted (Electrum 2.0 to 2.3).
* Payment requests can be signed using Bitcoin addresses stored
in DNS (OpenAlias). The identity of the requestor is verified using
DNSSEC.
* Payment requests signed with OpenAlias keys can be shared as
bitcoin: URIs, if they are simple (a single address-type
output). The BIP21 URI scheme is extended with 'name', 'sig',
'time', 'exp'.
* Arbitrary m-of-n multisig wallets are supported (n<=15).
* Multisig transactions can be signed with TREZOR. When you create
the multisig wallet, just enter the xpub of your existing TREZOR
wallet.
* Transaction fees set manually in the GUI are retained, including
when the user uses the '!' shortcut.
* New 'email' plugin, that enables sending and receiving payment
requests by email.
* The daemon supports Websocket notifications of payments.
# Release 2.3.3
* fix proxy settings (issue #1309)
* improvements to the transaction dialog:
- request password after showing transaction
- show change addresses in yellow color
# Release 2.3.2
* minor bugfixes
* updated ledger plugin
* sort inputs/outputs lexicographically (BIP-LI01)
# Release 2.3.1
* patch a bug with payment requests
# Release 2.3
* Improved logic for the network layer.
* More efficient coin selection. Spend oldest coins first, and
minimize the number of transaction inputs.
* Plugins are loaded independently of the GUI. As a result, Openalias,
TrustedCoin and TREZOR wallets can be used with the command
line. Example: 'electrum payto '
* The command line has been refactored:
- Arguments are parsed with argparse.
- The inline help includes a description of options.
- Some commands have been renamed. Notably, 'mktx' and 'payto' have
been merged into a single command, with a --broadcast option.
Type 'electrum --help' for a complete overview.
* The command line accepts the '!' syntax to send the maximum
amount available. It can be combined with the '--from' option.
Example: 'payto ! --from '
* The command line also accepts a '?' shortcut for private keys
arguments, that triggers a prompt.
* Payment requests can be managed with the command line, using the
following commands: 'addrequest', 'rmrequest', 'listrequests'.
Payment requests can be signed with a SSL certificate, and published
as bip70 files in a public web directory. To see the relevant
configuration variables, type 'electrum addrequest --help'
* Commands can be called with jsonrpc, using the 'jsonrpc' gui. The
jsonrpc interface may be called by php.
# Release 2.2
* Show amounts (thousands separators and decimal point)
according to locale in GUI
* Show unmatured coins in balance
* Fix exchange rates plugin
* Network layer: refactoring and fixes
# Release 2.1.1
* patch a bug that prevents new wallet creation.
* fix connection issue on osx binaries
# Release 2.1
* Faster startup, thanks to the following optimizations:
1. Transaction input/outputs are cached in the wallet file
2. Fast X509 certificate parser, not using pyasn1 anymore.
3. The Label Sync plugin only requests modified labels.
* The 'Invoices' and 'Send' tabs have been merged.
* Contacts are stored in a separate file, shared between wallets.
* A Search Box is available in the GUI (Ctrl-S)
* Payment requests have an expiration date and can be exported to
BIP70 files.
* file: scheme support in BIP72 URIs: "bitcoin:?r=file:///..."
* Own addresses are shown in green in the Transaction dialog.
* Address History dialog.
* The OpenAlias plugin was improved.
* Various bug fixes and GUI improvements.
* A new LabelSync backend is being used an import of the old
database was made but since the release came later it's
recommended that you do a full push when you upgrade.
# Release 2.0.4 - Minor GUI improvements
* The password dialog will ask for password again if the user enters
a wrong password
* The Master Public Key dialog displays which keys belong to the
wallet, and which are cosigners
* The transaction dialog will ask to save unsaved transaction
received from cosigner pool, when user clicks on 'Close'
* The multisig restore dialog accepts xprv keys.
* The network daemon must be started explicitly before using commands
that require a connection
Example:
electrum daemon start
electrum getaddressunspent
electrum daemon status
electrum daemon stop
If a daemon is running, the GUI will use it.
# Release 2.0.3 - bugfixes and minor GUI improvements
* Do not use daemon threads (fix #960)
* Add a zoom button to receive tab
* Add exchange rate conversion to receive tab
* Use Tor's default port number in default proxy config
# Release 2.0.2 - bugfixes
* Fix transaction sweep (#1066)
* Fix thread timing bug (#1054)
# Release 2.0.1 - bugfixes
* Fix critical bug in TREZOR address derivation: passphrases were not
NFKD normalized. TREZOR users who created a wallet protected by a
passphrase containing utf-8 characters with diacritics are
affected. These users will have to open their wallet with version
2.0 and to move their funds to a new wallet.
* Use a file socket for the daemon (fixes network dialog issues)
* Fix crash caused by QR scanner icon when zbar not installed.
* Fix CosignerPool plugin
* Label Sync plugin: Fix label sharing between multisig wallets
# Release 2.0
* Before you upgrade, make sure you have saved your wallet seed on
paper.
* Documentation is now hosted on a wiki: http://electrum.orain.org
* New seed derivation method (not compatible with BIP39). The seed
phrase includes a version number, that refers to the wallet
structure. The version number also serves as a checksum, and it
will prevent the import of seeds from incompatible wallets. Old
Electrum seeds are still supported.
* New address derivation (BIP32). Standard wallets are single account
and use a gap limit of 20.
* Support for Multisig wallets using parallel BIP32 derivations and
P2SH addresses ("2 of 2", "2 of 3").
* Compact serialization format for unsigned or partially signed
transactions, that includes the BIP32 master public key and
derivation needed to sign inputs. Serialized transactions can be
sent to cosigners or to cold storage using QR codes (using Andreas
Schildbach's base 43 idea).
* Support for BIP70 payment requests:
- Verification of the chain of signatures uses tlslite.
- In the GUI, payment requests are shown in the 'Invoices' tab.
* Support for hardware wallets: TREZOR (SatoshiLabs) and Btchip (Ledger).
* Two-factor authentication service by TrustedCoin. This service uses
"2 of 3" multisig wallets and Google Authenticator. Note that
wallets protected by this service can be deterministically restored
from seed, without Trustedcoin's server.
* Cosigner Pool plugin: encrypted communication channel for multisig
wallets, to send and receive partially signed transactions.
* Audio Modem plugin: send and receive transactions by sound.
* OpenAlias plugin: send bitcoins to aliases verified using DNSSEC.
* New 'Receive' tab in the GUI:
- create and manage payment requests, with QR Codes
- the former 'Receive' tab was renamed to 'Addresses'
- the former Point of Sale plugin is replaced by a resizeable
window that pops up if you click on the QR code
* The 'Send' tab in the Qt GUI supports transactions with multiple
outputs, and raw hexadecimal scripts.
* The GUI can connect to the Electrum daemon: "electrum -d" will
start the daemon if it is not already running, and the GUI will
connect to it. The daemon can serve several clients. It times out
if no client uses if for more than 5 minutes.
* The install wizard can be used to import addresses or private
keys. A watching-only wallet is created by entering a list of
addresses in the wizard dialog.
* New file format: Wallets files are saved as JSON. Note that new
wallet files cannot be read by older versions of Electrum. Old
wallet files will be converted to the new format; this operation
may take some time, because public keys will be derived for each
address of your wallet.
* The client accepts servers with a CA-signed SSL certificate.
* ECIES encrypt/decrypt methods, availabe in the GUI and using
the command line:
encrypt
decrypt
* The Android GUI has received various updates and it is much more
stable. Another script was added to Android, called Authenticator,
that works completely offline: it reads an unsigned transaction
shown as QR code, signs it and shows the result as a QR code.
# Release 1.9.8
* Electrum servers were upgraded to version 0.9. The new server stores
a Patrica tree of all UTXOs, an idea proposed by Alan Reiner in the
bitcointalk forum. This property allows the client to directly
request the balance of any address. The new commands are:
1. getaddressbalance
2. getaddressunspent
3. getutxoaddress
* Command-line commands that require a connection to the network spawn
a daemon, that remains connected and handles subsequent
commands. The daemon terminates itself if it remains unused for more
than one minute. The purpose of this is to make scripting more
efficient. For example, a bash script using many electrum commands
will open only one connection.
# Release 1.9.7
* Fix for offline signing
* Various bugfixes
* GUI usability improvements
* Coinbase Buyback plugin
# Release 1.9.6
* During wallet creation, do not write seed to disk until it is encrypted.
* Confirmation dialog if the transaction fee is higher than 1mBTC.
* bugfixes
# Release 1.9.5
* Coin control: select addresses to send from
* Put addresses that have been used in a minimized section (Qt GUI)
* Allow non ascii chars in passwords
# Release 1.9.4
bugfixes: offline transactions
# Release 1.9.3
bugfixes: connection problems, transactions staying unverified
# Release 1.9.2
* fix a syntax error
# Release 1.9.1
* fix regression with --offline mode
* fix regression with --portable mode: use a dedicated directory
# Release 1.9
* The client connects to multiple servers in order to retrieve block headers and find the longest chain
* SSL certificate validation (to prevent MITM)
* Deterministic signatures (RFC 6979)
* Menu to create/restore/open wallets
* Create transactions with multiple outputs from CSV (comma separated values)
* New text gui: stdio
* Plugins are no longer tied to the qt GUI, they can reach all GUIs
* Proxy bugs have been fixed
# Release 1.8.1
* Notification option when receiving new tranactions
* Confirm dialogue before sending large amounts
* Alternative datafile location for non-windows systems
* Fix offline wallet creation
* Remove enforced tx fee
* Tray icon improvements
* Various bugfixes
# Release 1.8
* Menubar in classic gui
* Updated the QR Code plugin to enable offline/online wallets to transmit unsigned/signed transactions via QR code.
* Fixed bug where never-confirmed transactions prevented further spending
# Release 1.7.4
* Increase default fee
* fix create and restore in command line
* fix verify message in the gui
# Release 1.7.3:
* Classic GUI can display amounts in mBTC
* Account selector in the classic GUI
* Changed the way the portable flag uses without supplying a -w argument
* Classic GUI asks users to enter their seed on wallet creation
# Release 1.7.2:
* Transactions that are in the same block are displayed in chronological order in the history.
* The client computes transaction priority and rejects zero-fee transactions that need a fee.
* The default fee was lowered to 200 uBTC per kb.
* Due to an internal format change, your history may be pruned when
you open your wallet for the first time after upgrading to 1.7.2. If
this is the case, please visit a full server to restore your full
history. You will only need to do that once.
# Release 1.7.1: bugfixes.
# Release 1.7
* The Classic GUI can be extended with plugins. Developers who want to
add new features or third-party services to Electrum are invited to
write plugins. Some previously existing and non-essential features of
Electrum (point-of-sale mode, qrcode scanner) were removed from the
core and are now available as plugins.
* The wallet waits for 2 confirmations before creating new
addresses. This makes recovery from seed more robust. Note that it
might create unwanted gaps if you use Electrum 1.7 together with older
versions of Electrum.
* An interactive Python console replaces the 'Wall' tab. The provided
python environment gives users access to the wallet and gui. Most
electrum commands are available as python function in the
console. Custom scripts an be loaded with a "run(filename)"
command. Tab-completions are available.
* The location of the Electrum folder in Windows changed from
LOCALAPPDATA to APPDATA. Discussion on this topic can be found here:
https://bitcointalk.org/index.php?topic=144575.0
* Private keys can be exported from within the classic GUI:
For a single address, use the address menu (right-click).
To export the keys of your entire wallet, use the settings dialog (import/export tab).
* It is possible to create, sign and redeem multisig transaction using the
command line interface. This is made possible by the following new commands:
dumpprivkey, listunspent, createmultisig, createrawtransaction, decoderawtransaction, signrawtransaction
The syntax of these commands is similar to their bitcoind counterpart.
For an example, see Gavin's tutorial: https://gist.github.com/gavinandresen/3966071
* Offline wallets now work in a way similar to Armory:
1. user creates an unsigned transaction using the online (watching-only) wallet.
2. unsigned transaction is copied to the offline computer, and signed by the offline wallet.
3. signed transaction is copied to the online computer, broadcasted by the online client.
4. All these steps can be done via the command line interface or the classic GUI.
* Many command line commands have been renamed in order to make the syntax consistent with bitcoind.
# Release 1.6.2
== Classic GUI
* Added new version notification
# Release 1.6.1 (11-01-2013)
== Core
* It is now possible to restore a wallet from MPK (this will create a watching-only wallet)
* A switch button allows to easily switch between Lite and Classic GUI.
== Classic GUI
* Seed and MPK help dialogs were rewritten
* Point of Sale: requested amounts can be expressed in other currencies and are converted to bitcoin.
== Lite GUI
* The receiving button was removed in favor of a menu item to keep it consistent with the history toggle.
# Release 1.6.0 (07-01-2013)
== Core
* (Feature) Add support for importing, signing and verifiying compressed keys
* (Feature) Auto reconnect to random server on disconnect
* (Feature) Ultimate fallback to HTTP port 80 if TCP doesn't work on any server
* (Bug) Under rare circumstances changing password with incorrect password could damage wallet
== Lite GUI
* (Chore) Use blockchain.info for exchange rate data
* (Feature) added currency conversion for BRL, CNY, RUB
* (Feature) Saraha theme
* (Feature) csv import/export for transactions including labels
== Classic GUI
* (Chore) pruning servers now called "p", full servers "f" to avoid confusion with terms
* (Feature) Debits in history shown in red
* (Feature) csv import/export for transactions including labels
# Release 1.5.8 (02-01-2013)
== Core
* (Bug) Fix pending address balance on received coins for pruning servers
* (Bug) Fix history command line option to show output again (regression by SPV)
* (Chore) Add timeout to blockchain headers file download by HTTP
* (Feature) new option: -L, --language: default language used in GUI.
== Lite GUI
* (Bug) Sending to auto-completed contacts works again
* (Chore) Added version number to title bar
== Classic GUI
* (Feature) Language selector in options.
# Release 1.5.7 (18-12-2012)
== Core
* The blockchain headers file is no longer included in the packages, it is downloaded on startup.
* New command line option: -P or --portable, for portable wallets. With this flag, all preferences are saved to the wallet file, and the blockchain headers file is in the same directory as the wallet
== Lite GUI
* (Feature) Added the ability to export your transactions to a CSV file.
* (Feature) Added a label dialog after sending a transaction.
* (Feature) Reworked receiving addresses; instead of a random selection from one of your receiving addresses a new widget will show listing unused addresses.
* (Chore) Removed server selection. With all the new server options a simple menu item does not suffice anymore.
================================================
FILE: app.fil
================================================
gui/qt/__init__.py
gui/qt/main_window.py
gui/qt/history_list.py
gui/qt/contact_list.py
gui/qt/invoice_list.py
gui/qt/request_list.py
gui/qt/installwizard.py
gui/qt/network_dialog.py
gui/qt/password_dialog.py
gui/qt/util.py
gui/qt/seed_dialog.py
gui/qt/transaction_dialog.py
gui/qt/address_dialog.py
gui/qt/qrcodewidget.py
gui/qt/qrtextedit.py
gui/qt/qrwindow.py
gui/kivy/main.kv
gui/kivy/main_window.py
gui/kivy/uix/dialogs/__init__.py
gui/kivy/uix/dialogs/fee_dialog.py
gui/kivy/uix/dialogs/installwizard.py
gui/kivy/uix/dialogs/settings.py
gui/kivy/uix/dialogs/wallets.py
gui/kivy/uix/ui_screens/history.kv
gui/kivy/uix/ui_screens/receive.kv
gui/kivy/uix/ui_screens/send.kv
plugins/labels/qt.py
plugins/trezor/qt.py
plugins/virtualkeyboard/qt.py
================================================
FILE: brewfile
================================================
tap "caskroom/cask"
brew "python3"
brew "protobuf"
# brew "zbar" -
brew "gmp"
# required by gmpy
brew "gettext"
tap "brewsci/science"
brew "matplotlib"
# required for matplotlib used in Plot History
brew "libusb-compat"
# ledger wallet
================================================
FILE: build-docker.sh
================================================
#!/bin/bash
docker build -t electrum-btcp:latest .
================================================
FILE: clean.sh
================================================
#!/bin/bash
sudo rm -rf build/
sudo rm -rf dist/
sudo rm -rf __pycache__
sudo rm -rf Electrum_BTCP.egg-info
sudo rm -rf /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/Electrum_BTCP-*
================================================
FILE: config
================================================
{
"server": "35.224.186.7:50001:t"
}
================================================
FILE: contrib/build-wine/README.md
================================================
Windows Binary Builds
=====================
These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine.
Produced binaries are deterministic so you should be able to generate binaries that match the official releases.
Usage:
1. Install the following dependencies:
- dirmngr
- gpg
- Wine (>= v2)
For example:
```
$ sudo apt-get install wine-development dirmngr gnupg2
$ sudo ln -sf /usr/bin/wine-development /usr/local/bin/wine
$ wine --version
wine-2.0 (Debian 2.0-3+b2)
```
or
```
$ pacman -S wine gnupg
$ wine --version
wine-2.21
```
2. Make sure `/opt` is writable by the current user.
3. Run `build.sh`.
4. The generated binaries are in `./dist`.
================================================
FILE: contrib/build-wine/build-electrum-git.sh
================================================
#!/bin/bash
NAME_ROOT=electrum
PYTHON_VERSION=3.5.4
# These settings probably don't need any change
export WINEPREFIX=/opt/wine64
export PYTHONDONTWRITEBYTECODE=1
export PYTHONHASHSEED=22
PYHOME=c:/python$PYTHON_VERSION
PYTHON="wine $PYHOME/python.exe -OO -B"
# Let's begin!
cd `dirname $0`
set -e
cd tmp
for repo in electrum electrum-locale electrum-icons; do
if [ -d $repo ]; then
cd $repo
git pull
git checkout master
cd ..
else
URL=https://github.com/spesmilo/$repo.git
git clone -b master $URL $repo
fi
done
pushd electrum-locale
for i in ./locale/*; do
dir=$i/LC_MESSAGES
mkdir -p $dir
msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true
done
popd
pushd electrum
if [ ! -z "$1" ]; then
git checkout $1
fi
VERSION=`git describe --tags`
echo "Last commit: $VERSION"
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
popd
rm -rf $WINEPREFIX/drive_c/electrum
cp -r electrum $WINEPREFIX/drive_c/electrum
cp electrum/LICENCE .
cp -r electrum-locale/locale $WINEPREFIX/drive_c/electrum/lib/
cp electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/
# Install frozen dependencies
$PYTHON -m pip install -r ../../requirements.txt
pushd $WINEPREFIX/drive_c/electrum
$PYTHON setup.py install
popd
cd ..
rm -rf dist/
# build standalone and portable versions
wine "C:/python$PYTHON_VERSION/scripts/pyinstaller.exe" --noconfirm --ascii --name $NAME_ROOT-$VERSION -w deterministic.spec
# set timestamps in dist, in order to make the installer reproducible
pushd dist
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
popd
# build NSIS installer
# $VERSION could be passed to the electrum.nsi script, but this would require some rewriting in the script iself.
wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" /DPRODUCT_VERSION=$VERSION electrum.nsi
cd dist
mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe
cd ..
echo "Done."
md5sum dist/electrum*exe
================================================
FILE: contrib/build-wine/build.sh
================================================
#!/bin/bash
# Lucky number
export PYTHONHASHSEED=22
if [ ! -z "$1" ]; then
to_build="$1"
fi
here=$(dirname "$0")
echo "Clearing $here/build and $here/dist..."
rm $here/build/* -rf
rm $here/dist/* -rf
$here/prepare-wine.sh && \
$here/prepare-pyinstaller.sh && \
$here/prepare-hw.sh || exit 1
echo "Resetting modification time in C:\Python..."
# (Because of some bugs in pyinstaller)
pushd /opt/wine64/drive_c/python*
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
popd
ls -l /opt/wine64/drive_c/python*
$here/build-electrum-git.sh $to_build && \
echo "Done."
================================================
FILE: contrib/build-wine/deterministic.spec
================================================
# -*- mode: python -*-
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
import sys
import os
for i, x in enumerate(sys.argv):
if x == '--name':
cmdline_name = sys.argv[i+1]
break
else:
raise BaseException('no name')
home = os.getcwd()+'\\'
# see https://github.com/pyinstaller/pyinstaller/issues/2005
hiddenimports = []
# hiddenimports += collect_submodules('trezorlib')
# hiddenimports += collect_submodules('btchip')
# hiddenimports += collect_submodules('keepkeylib')
datas = [
(home+'lib/currencies.json', 'electrum'),
(home+'lib/servers.json', 'electrum'),
(home+'lib/checkpoints.json', 'electrum'),
(home+'lib/servers_testnet.json', 'electrum'),
(home+'lib/checkpoints_testnet.json', 'electrum'),
(home+'lib/wordlist/english.txt', 'electrum/wordlist'),
# (home+'lib/locale', 'electrum/locale'),
(home+'plugins', 'electrum_plugins'),
]
# datas += collect_data_files('trezorlib')
# 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-btcp',
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',
#home+'packages/requests/utils.py'
],
datas=datas,
#pathex=[home+'lib', home+'gui', home+'plugins'],
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
# hotfix for #3171 (pre-Win10 binaries)
a.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\windows')]
pyz = PYZ(a.pure)
#####
# "standalone" exe with all dependencies packed into it
exe_standalone = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
name=os.path.join('build\\pyi.win32\\electrum-btcp', cmdline_name + ".exe"),
debug=False,
strip=None,
upx=False,
icon=home+'icons/electrum.ico',
console=False)
# console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used
# exe_portable = EXE(
# pyz,
# a.scripts,
# a.binaries,
# a.datas,
# name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + "-portable.exe"),
# debug=False,
# strip=None,
# upx=False,
# icon=home+'icons/electrum.ico',
# console=False)
# #####
# # exe and separate files that NSIS uses to build installer "setup" exe
# exe_dependent = EXE(
# pyz,
# a.scripts,
# exclude_binaries=True,
# name=os.path.join('build\\pyi.win32\\electrum', cmdline_name),
# debug=False,
# strip=None,
# upx=False,
# icon=home+'icons/electrum.ico',
# console=False)
# coll = COLLECT(
# exe_dependent,
# a.binaries,
# a.zipfiles,
# a.datas,
# strip=None,
# upx=True,
# debug=False,
# icon=home+'icons/electrum.ico',
# console=False,
# name=os.path.join('dist', 'electrum'))
================================================
FILE: contrib/build-wine/electrum.nsi
================================================
;--------------------------------
;Include Modern UI
!include "TextFunc.nsh" ;Needed for the $GetSize fuction. I know, doesn't sound logical, it isn't.
!include "MUI2.nsh"
;--------------------------------
;Variables
!define PRODUCT_NAME "Electrum"
!define PRODUCT_WEB_SITE "https://github.com/spesmilo/electrum"
!define PRODUCT_PUBLISHER "Electrum Technologies GmbH"
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
;--------------------------------
;General
;Name and file
Name "${PRODUCT_NAME}"
OutFile "dist/electrum-setup.exe"
;Default installation folder
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
;Get installation folder from registry if available
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" ""
;Request application privileges for Windows Vista
RequestExecutionLevel admin
;Specifies whether or not the installer will perform a CRC on itself before allowing an install
CRCCheck on
;Sets whether or not the details of the install are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them.
ShowInstDetails show
;Sets whether or not the details of the uninstall are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them.
ShowUninstDetails show
;Sets the colors to use for the install info screen (the default is 00FF00 000000. Use the form RRGGBB (in hexadecimal, as in HTML, only minus the leading '#', since # can be used for comments). Note that if "/windows" is specified as the only parameter, the default windows colors will be used.
InstallColors /windows
;This command sets the compression algorithm used to compress files/data in the installer. (http://nsis.sourceforge.net/Reference/SetCompressor)
SetCompressor /SOLID lzma
;Sets the dictionary size in megabytes (MB) used by the LZMA compressor (default is 8 MB).
SetCompressorDictSize 64
;Sets the text that is shown (by default it is 'Nullsoft Install System vX.XX') in the bottom of the install window. Setting this to an empty string ("") uses the default; to set the string to blank, use " " (a space).
BrandingText "${PRODUCT_NAME} Installer v${PRODUCT_VERSION}"
;Sets what the titlebars of the installer will display. By default, it is 'Name Setup', where Name is specified with the Name command. You can, however, override it with 'MyApp Installer' or whatever. If you specify an empty string (""), the default will be used (you can however specify " " to achieve a blank string)
Caption "${PRODUCT_NAME}"
;Adds the Product Version on top of the Version Tab in the Properties of the file.
VIProductVersion 1.0.0.0
;VIAddVersionKey - Adds a field in the Version Tab of the File Properties. This can either be a field provided by the system or a user defined field.
VIAddVersionKey ProductName "${PRODUCT_NAME} Installer"
VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}"
VIAddVersionKey CompanyName "${PRODUCT_NAME}"
VIAddVersionKey LegalCopyright "2013-2016 ${PRODUCT_PUBLISHER}"
VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer"
VIAddVersionKey FileVersion ${PRODUCT_VERSION}
VIAddVersionKey ProductVersion ${PRODUCT_VERSION}
VIAddVersionKey InternalName "${PRODUCT_NAME} Installer"
VIAddVersionKey LegalTrademarks "${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}"
VIAddVersionKey OriginalFilename "${PRODUCT_NAME}.exe"
;--------------------------------
;Interface Settings
!define MUI_ABORTWARNING
!define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?"
!define MUI_ICON "tmp\electrum\icons\electrum.ico"
;--------------------------------
;Pages
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
;--------------------------------
;Languages
!insertmacro MUI_LANGUAGE "English"
;--------------------------------
;Installer Sections
;Check if we have Administrator rights
Function .onInit
UserInfo::GetAccountType
pop $0
${If} $0 != "admin" ;Require admin rights on NT4+
MessageBox mb_iconstop "Administrator rights required!"
SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED
Quit
${EndIf}
FunctionEnd
Section
SetOutPath $INSTDIR
;Uninstall previous version files
RMDir /r "$INSTDIR\*.*"
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*"
;Files to pack into the installer
File /r "dist\electrum\*.*"
File "..\..\icons\electrum.ico"
;Store installation folder
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR
;Create uninstaller
DetailPrint "Creating uninstaller..."
WriteUninstaller "$INSTDIR\Uninstall.exe"
;Create desktop shortcut
DetailPrint "Creating desktop shortcut..."
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" ""
;Create start-menu items
DetailPrint "Creating start-menu items..."
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0
;Links bitcoinprivate: URI's to Electrum
WriteRegStr HKCU "Software\Classes\bitcoin" "" "URL:bitcoinprivate Protocol"
WriteRegStr HKCU "Software\Classes\bitcoin" "URL Protocol" ""
WriteRegStr HKCU "Software\Classes\bitcoin" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\""
;Adds an uninstaller possibilty to Windows Uninstall or change a program section
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\electrum.ico"
;Fixes Windows broken size estimates
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "EstimatedSize" "$0"
SectionEnd
;--------------------------------
;Descriptions
;--------------------------------
;Uninstaller Section
Section "Uninstall"
RMDir /r "$INSTDIR\*.*"
RMDir "$INSTDIR"
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*"
RMDir "$SMPROGRAMS\${PRODUCT_NAME}"
DeleteRegKey HKCU "Software\Classes\bitcoin"
DeleteRegKey HKCU "Software\${PRODUCT_NAME}"
DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}"
SectionEnd
================================================
FILE: contrib/build-wine/prepare-hw.sh
================================================
#!/bin/bash
TREZOR_GIT_URL=https://github.com/trezor/python-trezor.git
KEEPKEY_GIT_URL=https://github.com/keepkey/python-keepkey.git
BTCHIP_GIT_URL=https://github.com/LedgerHQ/btchip-python.git
BRANCH=master
PYTHON_VERSION=3.5.4
# These settings probably don't need any change
export WINEPREFIX=/opt/wine64
PYHOME=c:/python$PYTHON_VERSION
PYTHON="wine $PYHOME/python.exe -OO -B"
# Let's begin!
cd `dirname $0`
set -e
cd tmp
$PYTHON -m pip install setuptools --upgrade
$PYTHON -m pip install cython --upgrade
$PYTHON -m pip install trezor==0.7.16 --upgrade
$PYTHON -m pip install keepkey==4.0.0 --upgrade
$PYTHON -m pip install btchip-python==0.1.23 --upgrade
================================================
FILE: contrib/build-wine/prepare-pyinstaller.sh
================================================
#!/bin/bash
PYTHON_VERSION=3.5.4
PYINSTALLER_GIT_URL=https://github.com/ecdsa/pyinstaller.git
BRANCH=fix_2952
export WINEPREFIX=/opt/wine64
PYHOME=c:/python$PYTHON_VERSION
PYTHON="wine $PYHOME/python.exe -OO -B"
cd `dirname $0`
set -e
cd tmp
if [ ! -d "pyinstaller" ]; then
git clone -b $BRANCH $PYINSTALLER_GIT_URL pyinstaller
fi
cd pyinstaller
git pull
git checkout $BRANCH
$PYTHON setup.py install
cd ..
wine "C:/python$PYTHON_VERSION/scripts/pyinstaller.exe" -v
================================================
FILE: contrib/build-wine/prepare-wine.sh
================================================
#!/bin/bash
# Please update these carefully, some versions won't work under Wine
NSIS_URL=https://prdownloads.sourceforge.net/nsis/nsis-3.02.1-setup.exe?download
NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e
PYTHON_VERSION=3.5.4
## These settings probably don't need change
export WINEPREFIX=/opt/wine64
#export WINEARCH='win32'
PYHOME=c:/python$PYTHON_VERSION
PYTHON="wine $PYHOME/python.exe -OO -B"
# based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg
verify_signature() {
local file=$1 keyring=$2 out=
if out=$(gpg --no-default-keyring --keyring "$keyring" --status-fd 1 --verify "$file" 2>/dev/null) &&
echo "$out" | grep -qs "^\[GNUPG:\] VALIDSIG "; then
return 0
else
echo "$out" >&2
exit 0
fi
}
verify_hash() {
local file=$1 expected_hash=$2 out=
actual_hash=$(sha256sum $file | awk '{print $1}')
if [ "$actual_hash" == "$expected_hash" ]; then
return 0
else
echo "$file $actual_hash (unexpected hash)" >&2
exit 0
fi
}
# Let's begin!
cd `dirname $0`
set -e
# Clean up Wine environment
echo "Cleaning $WINEPREFIX"
rm -rf $WINEPREFIX
echo "done"
wine 'wineboot'
echo "Cleaning tmp"
rm -rf tmp
mkdir -p tmp
echo "done"
cd tmp
# Install Python
# note: you might need "sudo apt-get install dirmngr" for the following
# keys from https://www.python.org/downloads/#pubkeys
KEYRING_PYTHON_DEV=keyring-electrum-build-python-dev.gpg
gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --recv-keys 531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5
for msifile in core dev exe lib pip tools; do
echo "Installing $msifile..."
wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi"
wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc"
verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV
wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION
done
# upgrade pip
$PYTHON -m pip install pip --upgrade
# Install PyWin32
$PYTHON -m pip install pypiwin32
# Install PyQt
$PYTHON -m pip install PyQt5
## Install pyinstaller
#$PYTHON -m pip install pyinstaller==3.3
# Install ZBar
#wget -q -O zbar.exe "https://sourceforge.net/projects/zbar/files/zbar/0.10/zbar-0.10-setup.exe/download"
#wine zbar.exe
# install Cryptodome
$PYTHON -m pip install pycryptodomex
# install PySocks
$PYTHON -m pip install win_inet_pton
# install websocket (python2)
$PYTHON -m pip install websocket-client
# Upgrade setuptools (so Electrum can be installed later)
$PYTHON -m pip install setuptools --upgrade
# Install NSIS installer
wget -q -O nsis.exe "$NSIS_URL"
verify_hash nsis.exe $NSIS_SHA256
wine nsis.exe /S
# Install UPX
#wget -O upx.zip "https://downloads.sourceforge.net/project/upx/upx/3.08/upx308w.zip"
#unzip -o upx.zip
#cp upx*/upx.exe .
# add dlls needed for pyinstaller:
cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/
echo "Wine is configured. Please run prepare-pyinstaller.sh"
================================================
FILE: contrib/freeze_packages.sh
================================================
#!/bin/bash
# Run this after a new release to update dependencies
venv_dir=~/.electrum-venv
contrib=$(dirname "$0")
which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; }
rm $venv_dir -rf
virtualenv $venv_dir
source $venv_dir/bin/activate
echo "Installing dependencies"
pushd $contrib/..
python setup.py install
popd
pip freeze | sed '/^Electrum/ d' > $contrib/requirements.txt
echo "Updated requirements"
================================================
FILE: contrib/make_apk
================================================
#!/bin/bash
pushd lib
VERSION=$(python -c "import version; print version.ELECTRUM_VERSION")".0"
popd
echo $VERSION
echo $VERSION > contrib/apk_version
pushd ./gui/kivy/; make apk; popd
================================================
FILE: contrib/make_download
================================================
#!/usr/bin/python2
import re
import os
from versions import version, version_win, version_mac, version_android, version_apk
from versions import download_template, download_page
with open(download_template) as f:
string = f.read()
string = string.replace("##VERSION##", version)
string = string.replace("##VERSION_WIN##", version_win)
string = string.replace("##VERSION_MAC##", version_mac)
string = string.replace("##VERSION_ANDROID##", version_android)
string = string.replace("##VERSION_APK##", version_apk)
files = {
'tgz': "Electrum-%s.tar.gz" % version,
'zip': "Electrum-%s.zip" % version,
'mac': "electrum-%s.dmg" % version_mac,
'win': "electrum-%s.exe" % version_win,
'win_setup': "electrum-%s-setup.exe" % version_win,
'win_portable': "electrum-%s-portable.exe" % version_win,
}
for k, n in files.items():
path = "dist/%s"%n
link = "https://download.electrum.org/%s/%s"%(version,n)
if not os.path.exists(path):
os.system("wget -q %s -O %s" % (link, path))
if not os.path.getsize(path):
os.unlink(path)
string = re.sub("(.*?)
"%k, '', string, flags=re.DOTALL + re.MULTILINE)
continue
sigpath = path + '.asc'
siglink = link + '.asc'
if not os.path.exists(sigpath):
os.system("wget -q %s -O %s" % (siglink, sigpath))
if not os.path.getsize(sigpath):
os.unlink(sigpath)
string = re.sub("(.*?)
"%k, '', string, flags=re.DOTALL + re.MULTILINE)
continue
if os.system("gpg --verify %s"%sigpath) != 0:
raise BaseException(sigpath)
string = string.replace("##link_%s##"%k, link)
with open(download_page,'w') as f:
f.write(string)
================================================
FILE: contrib/make_locale
================================================
#!/usr/bin/env python3
import os
import io
import zipfile
import requests
os.chdir(os.path.dirname(os.path.realpath(__file__)))
os.chdir('..')
# Generate fresh translation template
if not os.path.exists('lib/locale'):
os.mkdir('lib/locale')
cmd = 'xgettext -s --no-wrap -f app.fil --output=lib/locale/messages.pot'
print('Generate template')
os.system(cmd)
os.chdir('lib')
crowdin_identifier = 'electrum'
crowdin_file_name = 'electrum-client/messages.pot'
locale_file_name = 'locale/messages.pot'
crowdin_api_key = None
filename = '~/.crowdin_api_key'
if os.path.exists(filename):
with open(filename) as f:
crowdin_api_key = f.read().strip()
if "crowdin_api_key" in os.environ:
crowdin_api_key = os.environ["crowdin_api_key"]
if crowdin_api_key:
# Push to Crowdin
print('Push to Crowdin')
url = ('https://api.crowdin.com/api/project/' + crowdin_identifier + '/update-file?key=' + crowdin_api_key)
with open(locale_file_name,'rb') as f:
files = {crowdin_file_name: f}
requests.request('POST', url, files=files)
# Build translations
print('Build translations')
response = requests.request('GET', 'https://api.crowdin.com/api/project/' + crowdin_identifier + '/export?key=' + crowdin_api_key).content
print(response)
# Download & unzip
print('Download translations')
s = requests.request('GET', 'https://crowdin.com/download/project/' + crowdin_identifier + '.zip').content
zfobj = zipfile.ZipFile(io.BytesIO(s))
print('Unzip translations')
for name in zfobj.namelist():
if not name.startswith('electrum-client/locale'):
continue
if name.endswith('/'):
if not os.path.exists(name[16:]):
os.mkdir(name[16:])
else:
with open(name[16:], 'wb') as output:
output.write(zfobj.read(name))
# Convert .po to .mo
print('Installing')
for lang in os.listdir('locale'):
if lang.startswith('messages'):
continue
# Check LC_MESSAGES folder
mo_dir = 'locale/%s/LC_MESSAGES' % lang
if not os.path.exists(mo_dir):
os.mkdir(mo_dir)
cmd = 'msgfmt --output-file="%s/electrum.mo" "locale/%s/electrum.po"' % (mo_dir,lang)
print('Installing', lang)
os.system(cmd)
================================================
FILE: contrib/make_packages
================================================
#!/bin/bash
contrib=$(dirname "$0")
whereis pip3
if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi
rm $contrib/../packages/ -r
#Install pure python modules in electrum directory
pip3 install -r $contrib/requirements.txt -t $contrib/../packages
================================================
FILE: contrib/requirements.txt
================================================
certifi==2017.11.5
chardet==3.0.4
dnspython==1.15.0
ecdsa==0.13.3
idna==2.6
jsonrpclib-pelix==0.3.1
pbkdf2==1.3
protobuf==3.5.0.post1
pyaes==1.6.1
PySocks==1.6.7
qrcode==5.3
requests==2.18.4
six==1.11.0
urllib3==1.22
================================================
FILE: contrib/sign_packages
================================================
#!/usr/bin/python2
import os
import getpass
if __name__ == '__main__':
os.chdir("dist")
password = getpass.getpass("Password:")
for f in os.listdir('.'):
if f.endswith('asc'):
continue
os.system( "gpg --sign --armor --detach --passphrase \"%s\" %s"%(password, f) )
os.chdir("..")
================================================
FILE: create-dmg.sh
================================================
#!/bin/sh
echo "Cleaning..."
sudo sh ./clean.sh
VERSION=$(python3 -c "from lib import version; print(version.ELECTRUM_VERSION)")
VERSION=${VERSION//ELECTRUM_VERSION=/}
echo "Creating package $VERSION"
echo "Running brew install"
brew bundle
echo "Running pip3 install"
pip3 install -r requirements.txt
echo "Building icons"
pyrcc5 icons.qrc -o gui/qt/icons_rc.py
echo "Compiling the protobuf description file"
protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto
echo "Compiling translations"
./contrib/make_locale
echo "Creating package $VERSION"
sudo python3 setup.py sdist
echo "Creating .app from python using py2app"
sudo ARCHFLAGS="-arch i386 -arch x86_64" sudo python3 setup-release.py py2app --includes sip
sudo mkdir dist/installer-mac/
sudo mv "dist/Electrum BTCP.app" "dist/installer-mac/"
sudo touch "dist/installer-mac/To install, copy it into Applications"
echo "Creating .dmg"
sudo hdiutil create -fs HFS+ -volname "Electrum BTCP - Installer" -srcfolder "dist/installer-mac" dist/electrum-btcp-$VERSION-macosx.dmg
echo "Done! .dmg and .app are in dist/"
================================================
FILE: docs/release-tests.md
================================================
## BTCP Electrum Wallet
**Tests to perform before each release**
### 1. Connection
1. User can connect for the first time (Green Dot).
1. User can connect without having a BTCPrivate directory
2. User can connect having an existing, matching, BTCPrivate directory
2. User can connect consecutively. This includes being able to resume from previous blockchain headers sync
### 2. User can import b addr private key
1. User can import b addr private key (from ZCL, BTC, and BTCP)
2. User can export b addr private key (from ZCL, BTC, and BTCP)
3. User can Sweep from a b addr private key (from ZCL, BTC, and BTCP)
### 3. Transactions
1. User can send from b->b
2. User can receive from z->b
3. User can receive from b->b
4. User can send from bx (multisig)
5. User can send to bx (multisig)
6. All transactions become Verified
7. Coinbase transaction error handling is correctly done ('e.g. needs to send full amount to Z').
================================================
FILE: electrum-btcp
================================================
#!/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
# from https://gist.github.com/tito/09c42fb4767721dc323d
import threading
try:
import jnius
except:
jnius = None
if jnius:
orig_thread_run = threading.Thread.run
def thread_check_run(*args, **kwargs):
try:
return orig_thread_run(*args, **kwargs)
finally:
jnius.detach()
threading.Thread.run = thread_check_run
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
is_macOS = sys.platform == 'darwin'
# 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 '"%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
if is_bundle and is_macOS:
requests.utils.DEFAULT_CA_BUNDLE_PATH = os.path.join(os.path.dirname(__file__), 'cacert.pem')
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 or is_macOS:
import imp
imp.load_module('electrum', *imp.find_module('lib'))
imp.load_module('electrum_gui', *imp.find_module('gui'))
imp.load_module('electrum_plugins', *imp.find_module('plugins'))
from electrum import bitcoin
from electrum import SimpleConfig, Network
from electrum.wallet import Wallet, Imported_Wallet
from electrum.storage import WalletStorage
from electrum.util import print_msg, print_stderr, json_encode, json_decode
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
import electrum_plugins
# 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()
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 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
# instanciate 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 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 run_offline_command(config, config_options):
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():
storage.decrypt(password)
wallet = Wallet(storage)
else:
wallet = None
# check password
if cmd.requires_password and storage.get('use_encryption'):
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
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__':
# on osx, 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 BaseException('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 hasattr(sys, '_MEIPASS') 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'):
bitcoin.NetworkConstants.set_testnet()
# 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:
init_plugins(config, 'cmdline')
result = run_offline_command(config, config_options)
# 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)
================================================
FILE: electrum-env
================================================
#!/bin/bash
#
# This script creates a virtualenv named 'env' and installs all
# python dependencies before activating the env and running Electrum.
# If 'env' already exists, it is activated and Electrum is started
# without any installations. Additionally, the PYTHONPATH environment
# variable is set properly before running Electrum.
#
# python-qt and its dependencies will still need to be installed with
# your package manager.
if [ -e ./env/bin/activate ]; then
source ./env/bin/activate
else
virtualenv env -p `which python3`
source ./env/bin/activate
python3 setup.py install
fi
export PYTHONPATH="/usr/local/lib/python3.5/site-packages:$PYTHONPATH"
./electrum-btcp "$@"
deactivate
================================================
FILE: electrum.conf.sample
================================================
# Configuration file for the electrum client
# Settings defined here are shared across wallets
#
# copy this file to /etc/electrum.conf if you want read-only settings
[client]
server = electrum.novit.ro:50001:t
proxy = None
gap_limit = 5
# booleans use python syntax
use_change = True
gui = qt
num_zeros = 2
# default transaction fee is in Satoshis
fee = 10000
winpos-qt = [799, 226, 877, 435]
================================================
FILE: electrum.desktop
================================================
# If you want electrum to appear in a linux app launcher ("start menu"), install this by doing:
# sudo desktop-file-install electrum.desktop
[Desktop Entry]
Comment=Lightweight Bitcoin Private Client
Exec=electrum-btcp %u
GenericName[en_US]=Bitcoin Private Wallet
GenericName=Bitcoin Private Wallet
Icon=electrum
Name[en_US]=Bitcoin Private Electrum Wallet
Name=Bitcoin Private Electrum Wallet
Categories=Finance;Network;
StartupNotify=false
Terminal=false
Type=Application
MimeType=x-scheme-handler/bitcoin;
================================================
FILE: gui/__init__.py
================================================
# To create a new GUI, please add its code to this directory.
# Three objects are passed to the ElectrumGui: config, daemon and plugins
# The Wallet object is instanciated by the GUI
# Notifications about network events are sent to the GUI by using network.register_callback()
================================================
FILE: gui/kivy/Makefile
================================================
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
================================================
FILE: gui/kivy/Readme.txt
================================================
Before compiling, create packages: `contrib/make_packages`
Commands::
`make theming` to make a atlas out of a list of pngs
`make apk` to make a apk
If something in included modules like kivy or any other module changes
then you need to rebuild the distribution. To do so:
rm -rf .buildozer/android/platform/python-for-android/dist
how to build with ssl:
rm -rf .buildozer/android/platform/build/
./contrib/make_apk
pushd /opt/electrum/.buildozer/android/platform/build/build/libs_collections/Electrum/armeabi-v7a
cp libssl1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libssl.so
cp libcrypto1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libcrypto.so
popd
./contrib/make_apk
================================================
FILE: gui/kivy/__init__.py
================================================
#!/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.
#
# Kivy GUI
import sys
import os
try:
sys.argv = ['']
import kivy
except ImportError:
# This error ideally shouldn't raised with pre-built packages
sys.exit("Error: Could not import kivy. Please install it using the" + \
"instructions mentioned here `http://kivy.org/#download` .")
# minimum required version for kivy
kivy.require('1.8.0')
from kivy.logger import Logger
class ElectrumGui:
def __init__(self, config, daemon, plugins):
Logger.debug('ElectrumGUI: initialising')
self.daemon = daemon
self.network = daemon.network
self.config = config
self.plugins = plugins
def main(self):
from .main_window import ElectrumWindow
self.config.open_last_wallet()
w = ElectrumWindow(config=self.config,
network=self.network,
plugins = self.plugins,
gui_object=self)
w.run()
if w.wallet:
self.config.save_last_wallet(w.wallet)
================================================
FILE: gui/kivy/data/fonts/tron/License.txt
================================================
Copyright (c) 2010-2011, Jeff Bell [www.randombell.com] | [jeffbell@randombell.com].
This font may be distributed freely however must retain this document as well as the Readme.txt file.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is available with a FAQ at: http://scripts.sil.org/OFL
================================================
FILE: gui/kivy/data/fonts/tron/Readme.txt
================================================
TR2N v1.3
ABOUT THE FONT:
A font based upon the poster text for TRON LEGACY.
The font is different from the pre-existing TRON font currently on the web. Similar in minor aspects but different in most. Style based upon text from different region posters.
UPDATE HISTORY:
3/7/11 - Adjusted the letter B (both lowercase and uppercase), capped off the ends of T, P and R, added a few more punctuation marks, as well as added the TR and TP ligature to allow for the solid bar connect as in the poster art.
1/22/11 - Made minor corrections to all previous letters and punctuation. Corrected issue with number 8's top filling in.
ABOUT THE AUTHOR:
Jeff Bell has produced fonts before, but this is the first one in over 10 years. His original 3 fonts were under the name DJ-JOHNNYRKA and include "CASPER", "BEVERLY HILLS COP", "THE GODFATHER" and "FIDDUMS FAMILY".
For more information on Jeff Bell and his work can be found online:
www.randombell.com
www.damovieman.deviantart.com
http://www.imdb.com/name/nm3983081/
http://www.vimeo.com/user4004969/videos
================================================
FILE: gui/kivy/data/glsl/default.fs
================================================
$HEADER$
void main (void){
gl_FragColor = frag_color * texture2D(texture0, tex_coord0);
}
================================================
FILE: gui/kivy/data/glsl/default.vs
================================================
$HEADER$
void main (void) {
frag_color = color * vec4(1.0, 1.0, 1.0, opacity);
tex_coord0 = vTexCoords0;
gl_Position = projection_mat * modelview_mat * vec4(vPosition.xy, 0.0, 1.0);
}
================================================
FILE: gui/kivy/data/glsl/header.fs
================================================
#ifdef GL_ES
precision highp float;
#endif
/* Outputs from the vertex shader */
varying vec4 frag_color;
varying vec2 tex_coord0;
/* uniform texture samplers */
uniform sampler2D texture0;
================================================
FILE: gui/kivy/data/glsl/header.vs
================================================
#ifdef GL_ES
precision highp float;
#endif
/* Outputs to the fragment shader */
varying vec4 frag_color;
varying vec2 tex_coord0;
/* vertex attributes */
attribute vec2 vPosition;
attribute vec2 vTexCoords0;
/* uniform variables */
uniform mat4 modelview_mat;
uniform mat4 projection_mat;
uniform vec4 color;
uniform float opacity;
================================================
FILE: gui/kivy/data/images/defaulttheme.atlas
================================================
{"defaulttheme-0.png": {"progressbar_background": [391, 227, 24, 24], "tab_btn_disabled": [264, 137, 32, 32], "tab_btn_pressed": [366, 137, 32, 32], "image-missing": [152, 171, 48, 48], "splitter_h": [174, 123, 32, 7], "splitter_down": [501, 253, 7, 32], "splitter_disabled_down": [503, 291, 7, 32], "vkeyboard_key_down": [468, 137, 32, 32], "vkeyboard_disabled_key_down": [400, 137, 32, 32], "selector_right": [248, 223, 55, 62], "player-background": [2, 287, 103, 103], "selector_middle": [191, 223, 55, 62], "spinner": [235, 82, 29, 37], "tab_btn_disabled_pressed": [298, 137, 32, 32], "switch-button_disabled": [277, 291, 43, 32], "textinput_disabled_active": [372, 326, 64, 64], "splitter_grip": [36, 50, 12, 26], "vkeyboard_key_normal": [2, 44, 32, 32], "button_disabled": [80, 82, 29, 37], "media-playback-stop": [302, 171, 48, 48], "splitter": [501, 87, 7, 32], "splitter_down_h": [140, 123, 32, 7], "sliderh_background_disabled": [72, 132, 41, 37], "modalview-background": [464, 456, 45, 54], "button": [142, 82, 29, 37], "splitter_disabled": [502, 137, 7, 32], "checkbox_radio_disabled_on": [433, 87, 32, 32], "slider_cursor": [402, 171, 48, 48], "vkeyboard_disabled_background": [68, 221, 64, 64], "checkbox_disabled_on": [297, 87, 32, 32], "sliderv_background_disabled": [2, 78, 37, 41], "button_disabled_pressed": [111, 82, 29, 37], "audio-volume-muted": [102, 171, 48, 48], "close": [417, 231, 20, 20], "action_group_disabled": [452, 171, 33, 48], "vkeyboard_background": [2, 221, 64, 64], "checkbox_off": [331, 87, 32, 32], "tab_disabled": [305, 253, 96, 32], "sliderh_background": [115, 132, 41, 37], "switch-button": [322, 291, 43, 32], "tree_closed": [439, 231, 20, 20], "bubble_btn_pressed": [435, 291, 32, 32], "selector_left": [134, 223, 55, 62], "filechooser_file": [174, 326, 64, 64], "checkbox_radio_disabled_off": [399, 87, 32, 32], "checkbox_radio_on": [196, 137, 32, 32], "checkbox_on": [365, 87, 32, 32], "button_pressed": [173, 82, 29, 37], "audio-volume-high": [464, 406, 48, 48], "audio-volume-low": [2, 171, 48, 48], "progressbar": [305, 227, 32, 24], "previous_normal": [487, 187, 19, 32], "separator": [504, 342, 5, 48], "filechooser_folder": [240, 326, 64, 64], "checkbox_radio_off": [467, 87, 32, 32], "textinput_active": [306, 326, 64, 64], "textinput": [438, 326, 64, 64], "player-play-overlay": [122, 395, 117, 115], "media-playback-pause": [202, 171, 48, 48], "sliderv_background": [41, 78, 37, 41], "ring": [354, 402, 108, 108], "bubble_arrow": [487, 175, 16, 10], "slider_cursor_disabled": [352, 171, 48, 48], "checkbox_disabled_off": [469, 291, 32, 32], "action_group_down": [2, 121, 33, 48], "spinner_disabled": [204, 82, 29, 37], "splitter_disabled_h": [106, 123, 32, 7], "bubble": [107, 325, 65, 65], "media-playback-start": [252, 171, 48, 48], "vkeyboard_disabled_key_normal": [434, 137, 32, 32], "overflow": [230, 137, 32, 32], "tree_opened": [461, 231, 20, 20], "action_item": [339, 227, 24, 24], "bubble_btn": [401, 291, 32, 32], "audio-volume-medium": [52, 171, 48, 48], "action_group": [37, 121, 33, 48], "spinner_pressed": [266, 82, 29, 37], "filechooser_selected": [2, 392, 118, 118], "tab": [403, 253, 96, 32], "action_bar": [158, 133, 36, 36], "action_view": [365, 227, 24, 24], "tab_btn": [332, 137, 32, 32], "switch-background": [192, 291, 83, 32], "splitter_disabled_down_h": [72, 123, 32, 7], "action_item_down": [367, 291, 32, 32], "switch-background_disabled": [107, 291, 83, 32], "textinput_disabled": [241, 399, 111, 111], "splitter_grip_h": [483, 239, 26, 12]}}
================================================
FILE: gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java
================================================
package org.electrum.qr;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.content.Intent;
import java.util.Arrays;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
import com.google.zxing.Result;
import com.google.zxing.BarcodeFormat;
public class SimpleScannerActivity extends Activity implements ZXingScannerView.ResultHandler {
private ZXingScannerView mScannerView;
final String TAG = "org.electrum.SimpleScannerActivity";
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view
mScannerView.setFormats(Arrays.asList(BarcodeFormat.QR_CODE));
setContentView(mScannerView); // Set the scanner view as the content view
}
@Override
public void onResume() {
super.onResume();
mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results.
mScannerView.startCamera(); // Start camera on resume
}
@Override
public void onPause() {
super.onPause();
mScannerView.stopCamera(); // Stop camera on pause
}
@Override
public void handleResult(Result rawResult) {
Intent resultIntent = new Intent();
resultIntent.putExtra("text", rawResult.getText());
resultIntent.putExtra("format", rawResult.getBarcodeFormat().toString());
setResult(Activity.RESULT_OK, resultIntent);
this.finish();
}
}
================================================
FILE: gui/kivy/data/style.kv
================================================
#:kivy 1.0
:
canvas:
Color:
rgba: self.disabled_color if self.disabled else (self.color if not self.markup else (1, 1, 1, 1))
Rectangle:
texture: self.texture
size: self.texture_size
pos: int(self.center_x - self.texture_size[0] / 2.), int(self.center_y - self.texture_size[1] / 2.)
<-Button,-ToggleButton>:
state_image: self.background_normal if self.state == 'normal' else self.background_down
disabled_image: self.background_disabled_normal if self.state == 'normal' else self.background_disabled_down
canvas:
Color:
rgba: self.background_color
BorderImage:
border: self.border
pos: self.pos
size: self.size
source: self.disabled_image if self.disabled else self.state_image
Color:
rgba: self.disabled_color if self.disabled else self.color
Rectangle:
texture: self.texture
size: self.texture_size
pos: int(self.center_x - self.texture_size[0] / 2.), int(self.center_y - self.texture_size[1] / 2.)
opacity: .7 if self.disabled else 1
rows: 1
canvas:
Color:
rgba: self.parent.background_color if self.parent else (1, 1, 1, 1)
BorderImage:
border: self.parent.border if self.parent else (16, 16, 16, 16)
texture: root.parent._bk_img.texture if root.parent else None
size: self.size
pos: self.pos
:
background_normal: 'atlas://data/images/defaulttheme/bubble_btn'
background_down: 'atlas://data/images/defaulttheme/bubble_btn_pressed'
background_disabled_normal: 'atlas://data/images/defaulttheme/bubble_btn'
background_disabled_down: 'atlas://data/images/defaulttheme/bubble_btn_pressed'
border: (0, 0, 0, 0)
:
canvas:
Color:
rgb: 1, 1, 1
BorderImage:
border: (0, 18, 0, 18) if self.orientation == 'horizontal' else (18, 0, 18, 0)
pos: (self.x + self.padding, self.center_y - sp(18)) if self.orientation == 'horizontal' else (self.center_x - 18, self.y + self.padding)
size: (self.width - self.padding * 2, sp(36)) if self.orientation == 'horizontal' else (sp(36), self.height - self.padding * 2)
source: 'atlas://data/images/defaulttheme/slider{}_background{}'.format(self.orientation[0], '_disabled' if self.disabled else '')
Rectangle:
pos: (self.value_pos[0] - sp(16), self.center_y - sp(17)) if self.orientation == 'horizontal' else (self.center_x - (16), self.value_pos[1] - sp(16))
size: (sp(32), sp(32))
source: 'atlas://data/images/defaulttheme/slider_cursor{}'.format('_disabled' if self.disabled else '')
:
canvas.before:
PushMatrix
Translate:
xy: self.pos
canvas.after:
PopMatrix
:
canvas:
Color:
rgba: self.color
Rectangle:
texture: self.texture
size: self.norm_image_size
pos: self.center_x - self.norm_image_size[0] / 2., self.center_y - self.norm_image_size[1] / 2.
rows: 1
padding: 3
canvas:
Color:
rgba: self.parent.background_color if self.parent else (1, 1, 1, 1)
BorderImage:
border: self.parent.border if self.parent else (16, 16, 16, 16)
source: (root.parent.background_disabled_image if self.disabled else root.parent.background_image) if root.parent else None
size: self.size
pos: self.pos
rows: 1
padding: '2dp', '2dp', '2dp', '2dp'
canvas.before:
BorderImage:
pos: self.pos
size: self.size
border: root.border
source: root.background_image
:
halign: 'center'
valign: 'middle'
background_normal: 'atlas://data/images/defaulttheme/tab_btn'
background_disabled_normal: 'atlas://data/images/defaulttheme/tab_btn_disabled'
background_down: 'atlas://data/images/defaulttheme/tab_btn_pressed'
background_disabled_down: 'atlas://data/images/defaulttheme/tab_btn_pressed'
border: (8, 8, 8, 8)
font_size: '15sp'
allow_stretch: True
:
canvas.before:
Color:
rgba: self.background_color
BorderImage:
border: self.border
pos: self.pos
size: self.size
source: (self.background_disabled_active if self.disabled else self.background_active) if self.focus else (self.background_disabled_normal if self.disabled else self.background_normal)
Color:
rgba: (self.cursor_color if self.focus and not self.cursor_blink else (0, 0, 0, 0))
Rectangle:
pos: [int(x) for x in self.cursor_pos]
size: 1, -self.line_height
Color:
rgba: self.disabled_foreground_color if self.disabled else (self.hint_text_color if not self.text and not self.focus else self.foreground_color)
:
but_cut: cut.__self__
but_copy: copy.__self__
but_paste: paste.__self__
but_selectall: selectall.__self__
size_hint: None, None
size: '150sp', '50sp'
BubbleButton:
id: cut
text: 'Cut'
on_release: root.do('cut')
BubbleButton:
id: copy
text: 'Copy'
on_release: root.do('copy')
BubbleButton:
id: paste
text: 'Paste'
on_release: root.do('paste')
BubbleButton:
id: selectall
text: 'Select All'
on_release: root.do('selectall')
:
font_name: 'data/fonts/RobotoMono-Regular.ttf'
:
canvas.before:
Color:
rgba: self.color_selected if self.is_selected else self.odd_color if self.odd else self.even_color
Rectangle:
pos: [self.parent.x, self.y] if self.parent else [0, 0]
size: [self.parent.width, self.height] if self.parent else [1, 1]
Color:
rgba: 1, 1, 1, int(not self.is_leaf)
Rectangle:
source: 'atlas://data/images/defaulttheme/tree_%s' % ('opened' if self.is_open else 'closed')
size: 16, 16
pos: self.x - 20, self.center_y - 8
canvas.after:
Color:
rgba: .5, .5, .5, .2
Line:
points: [self.parent.x, self.y, self.parent.right, self.y] if self.parent else []
:
width: self.texture_size[0]
height: max(self.texture_size[1] + dp(10), dp(24))
text_size: self.width, None
:
canvas.before:
StencilPush
Rectangle:
pos: self.pos
size: self.size
StencilUse
canvas.after:
StencilUnUse
Rectangle:
pos: self.pos
size: self.size
StencilPop
:
on_entry_added: treeview.add_node(args[1])
on_entries_cleared: treeview.root.nodes = []
on_subentry_to_entry: not args[2].locked and treeview.add_node(args[1], args[2])
on_remove_subentry: args[2].nodes = []
BoxLayout:
pos: root.pos
size: root.size
size_hint: None, None
orientation: 'vertical'
BoxLayout:
size_hint_y: None
height: 30
orientation: 'horizontal'
Widget:
# Just for spacing
width: 10
size_hint_x: None
Label:
text: 'Name'
text_size: self.size
halign: 'left'
bold: True
Label:
text: 'Size'
text_size: self.size
size_hint_x: None
halign: 'right'
bold: True
Widget:
# Just for spacing
width: 10
size_hint_x: None
ScrollView:
id: scrollview
do_scroll_x: False
Scatter:
do_rotation: False
do_scale: False
do_translation: False
size: treeview.size
size_hint_y: None
TreeView:
id: treeview
hide_root: True
size_hint_y: None
width: scrollview.width
height: self.minimum_height
on_node_expand: root.controller.entry_subselect(args[1])
on_node_collapse: root.controller.close_subselection(args[1])
:
layout: layout
FileChooserListLayout:
id: layout
controller: root
[FileListEntry@FloatLayout+TreeViewNode]:
locked: False
entries: []
path: ctx.path
# FIXME: is_selected is actually a read_only treeview property. In this
# case, however, we're doing this because treeview only has single-selection
# hardcoded in it. The fix to this would be to update treeview to allow
# multiple selection.
is_selected: self.path in ctx.controller().selection
orientation: 'horizontal'
size_hint_y: None
height: '48dp' if dp(1) > 1 else '24dp'
# Don't allow expansion of the ../ node
is_leaf: not ctx.isdir or ctx.name.endswith('..' + ctx.sep) or self.locked
on_touch_down: self.collide_point(*args[1].pos) and ctx.controller().entry_touched(self, args[1])
on_touch_up: self.collide_point(*args[1].pos) and ctx.controller().entry_released(self, args[1])
BoxLayout:
pos: root.pos
Label:
id: filename
text_size: self.width, None
halign: 'left'
shorten: True
text: ctx.name
Label:
text_size: self.width, None
size_hint_x: None
halign: 'right'
text: '{}'.format(ctx.get_nice_size())
:
on_entry_added: stacklayout.add_widget(args[1])
on_entries_cleared: stacklayout.clear_widgets()
ScrollView:
id: scrollview
pos: root.pos
size: root.size
size_hint: None, None
do_scroll_x: False
Scatter:
do_rotation: False
do_scale: False
do_translation: False
size_hint_y: None
height: stacklayout.height
StackLayout:
id: stacklayout
width: scrollview.width
size_hint_y: None
height: self.minimum_height
spacing: '10dp'
padding: '10dp'
:
layout: layout
FileChooserIconLayout:
id: layout
controller: root
[FileIconEntry@Widget]:
locked: False
path: ctx.path
selected: self.path in ctx.controller().selection
size_hint: None, None
on_touch_down: self.collide_point(*args[1].pos) and ctx.controller().entry_touched(self, args[1])
on_touch_up: self.collide_point(*args[1].pos) and ctx.controller().entry_released(self, args[1])
size: '100dp', '100dp'
canvas:
Color:
rgba: 1, 1, 1, 1 if self.selected else 0
BorderImage:
border: 8, 8, 8, 8
pos: root.pos
size: root.size
source: 'atlas://data/images/defaulttheme/filechooser_selected'
Image:
size: '48dp', '48dp'
source: 'atlas://data/images/defaulttheme/filechooser_%s' % ('folder' if ctx.isdir else 'file')
pos: root.x + dp(24), root.y + dp(40)
Label:
text: ctx.name
text_size: (root.width, self.height)
halign: 'center'
shorten: True
size: '100dp', '16dp'
pos: root.x, root.y + dp(16)
Label:
text: '{}'.format(ctx.get_nice_size())
font_size: '11sp'
color: .8, .8, .8, 1
size: '100dp', '16sp'
pos: root.pos
halign: 'center'
:
pos_hint: {'x': 0, 'y': 0}
canvas:
Color:
rgba: 0, 0, 0, .8
Rectangle:
pos: self.pos
size: self.size
Label:
pos_hint: {'x': .2, 'y': .6}
size_hint: .6, .2
text: 'Opening %s' % root.path
FloatLayout:
pos_hint: {'x': .2, 'y': .4}
size_hint: .6, .2
ProgressBar:
id: pb
pos_hint: {'x': 0, 'center_y': .5}
max: root.total
value: root.index
Label:
pos_hint: {'x': 0}
text: '%d / %d' % (root.index, root.total)
size_hint_y: None
height: self.texture_size[1]
y: pb.center_y - self.height - 8
font_size: '13sp'
color: (.8, .8, .8, .8)
AnchorLayout:
pos_hint: {'x': .2, 'y': .2}
size_hint: .6, .2
Button:
text: 'Cancel'
size_hint: None, None
size: 150, 44
on_release: root.cancel()
# Switch widget
:
active_norm_pos: max(0., min(1., (int(self.active) + self.touch_distance / sp(41))))
canvas:
Color:
rgb: 1, 1, 1
Rectangle:
source: 'atlas://data/images/defaulttheme/switch-background{}'.format('_disabled' if self.disabled else '')
size: sp(83), sp(32)
pos: int(self.center_x - sp(41)), int(self.center_y - sp(16))
Rectangle:
source: 'atlas://data/images/defaulttheme/switch-button{}'.format('_disabled' if self.disabled else '')
size: sp(43), sp(32)
pos: int(self.center_x - sp(41) + self.active_norm_pos * sp(41)), int(self.center_y - sp(16))
# ModalView widget
:
canvas:
Color:
rgba: root.background_color[:3] + [root.background_color[-1] * self._anim_alpha]
Rectangle:
size: self._window.size if self._window else (0, 0)
Color:
rgb: 1, 1, 1
BorderImage:
source: root.background
border: root.border
pos: self.pos
size: self.size
# Popup widget
:
_container: container
GridLayout:
padding: '12dp'
cols: 1
size_hint: None, None
pos: root.pos
size: root.size
Label:
text: root.title
color: root.title_color
size_hint_y: None
height: self.texture_size[1] + dp(16)
text_size: self.width - dp(16), None
font_size: root.title_size
font_name: root.title_font
halign: root.title_align
Widget:
size_hint_y: None
height: dp(4)
canvas:
Color:
rgba: root.separator_color
Rectangle:
pos: self.x, self.y + root.separator_height / 2.
size: self.width, root.separator_height
BoxLayout:
id: container
# =============================================================================
# Spinner widget
# =============================================================================
:
size_hint_y: None
height: '48dp'
:
background_normal: 'atlas://data/images/defaulttheme/spinner'
background_disabled_normal: 'atlas://data/images/defaulttheme/spinner_disabled'
background_down: 'atlas://data/images/defaulttheme/spinner_pressed'
# =============================================================================
# ActionBar widget
# =============================================================================
:
height: '48dp'
size_hint_y: None
spacing: '4dp'
canvas:
Color:
rgba: self.background_color
BorderImage:
border: root.border
pos: self.pos
size: self.size
source: self.background_image
:
orientation: 'horizontal'
canvas:
Color:
rgba: self.background_color
BorderImage:
pos: self.pos
size: self.size
source: self.background_image
:
size_hint_x: None
minimum_width: '2sp'
width: self.minimum_width
canvas:
Rectangle:
pos: self.x, self.y + sp(4)
size: self.width, self.height - sp(8)
source: self.background_image
:
background_normal: 'atlas://data/images/defaulttheme/' + ('action_bar' if self.inside_group else 'action_item')
background_down: 'atlas://data/images/defaulttheme/action_item_down'
size_hint_x: None if not root.inside_group else 1
width: [dp(48) if (root.icon and not root.inside_group) else max(dp(48), (self.texture_size[0] + dp(32))), self.size_hint_x][0]
color: self.color[:3] + [0 if (root.icon and not root.inside_group) else 1]
Image:
allow_stretch: True
opacity: 1 if (root.icon and not root.inside_group) else 0
source: root.icon
mipmap: root.mipmap
pos: root.x + dp(4), root.y + dp(4)
size: root.width - dp(8), root.height - sp(8)
:
size_hint_x: None if not root.inside_group else 1
width: self.texture_size[0] + dp(32)
:
size_hint_x: None
width: self.texture_size[0] + dp(32)
:
background_normal: 'atlas://data/images/defaulttheme/action_bar' if self.inside_group else 'atlas://data/images/defaulttheme/action_item'
:
temp_width: 0
temp_height: 0
:
background_normal: 'atlas://data/images/defaulttheme/action_item'
background_down: 'atlas://data/images/defaulttheme/action_item_down'
:
size_hint_x: 1
minimum_width: layout.minimum_width + min(sp(100), title.width)
important: True
GridLayout:
id: layout
rows: 1
pos: root.pos
size_hint_x: None
width: self.minimum_width
ActionPreviousButton:
on_press: root.dispatch('on_press')
on_release: root.dispatch('on_release')
size_hint_x: None
width: prevlayout.width
GridLayout:
id: prevlayout
rows: 1
width: self.minimum_width
height: self.parent.height
pos: self.parent.pos
ActionPreviousImage:
id: prev_icon_image
source: root.previous_image
opacity: 1 if root.with_previous else 0
allow_stretch: True
size_hint_x: None
temp_width: root.previous_image_width or dp(prev_icon_image.texture_size[0])
temp_height: root.previous_image_height or dp(prev_icon_image.texture_size[1])
width:
(self.temp_width if self.temp_height <= self.height else \
self.temp_width * (self.height / self.temp_height)) \
if self.texture else dp(8)
mipmap: root.mipmap
ActionPreviousImage:
id: app_icon_image
source: root.app_icon
allow_stretch: True
size_hint_x: None
temp_width: root.app_icon_width or dp(app_icon_image.texture_size[0])
temp_height: root.app_icon_height or dp(app_icon_image.texture_size[1])
width:
(self.temp_width if self.temp_height <= self.height else \
self.temp_width * (self.height / self.temp_height)) \
if self.texture else dp(8)
mipmap: root.mipmap
Widget:
size_hint_x: None
width: '5sp'
Label:
id: title
text: root.title
text_size: self.size
color: root.color
shorten: True
shorten_from: 'right'
halign: 'left'
valign: 'middle'
:
background_normal: 'atlas://data/images/defaulttheme/action_group'
background_down: 'atlas://data/images/defaulttheme/action_group_down'
background_disabled_normal: 'atlas://data/images/defaulttheme/action_group_disabled'
border: 30, 30, 3, 3
ActionSeparator:
pos: root.pos
size: root.separator_width, root.height
opacity: 1 if root.use_separator else 0
background_image: root.separator_image if root.use_separator else 'action_view'
:
border: 3, 3, 3, 3
background_normal: 'atlas://data/images/defaulttheme/action_item'
background_down: 'atlas://data/images/defaulttheme/action_item_down'
background_disabled_normal: 'atlas://data/images/defaulttheme/button_disabled'
size_hint_x: None
minimum_width: '48sp'
width: self.texture_size[0] if self.texture else self.minimum_width
canvas.after:
Color:
rgb: 1, 1, 1
Rectangle:
pos: root.center_x - sp(16), root.center_y - sp(16)
size: sp(32), sp(32)
source: root.overflow_image
:
auto_width: False
# =============================================================================
# Accordion widget
# =============================================================================
[AccordionItemTitle@Label]:
text: ctx.title
normal_background: ctx.item.background_normal if ctx.item.collapse else ctx.item.background_selected
disabled_background: ctx.item.background_disabled_normal if ctx.item.collapse else ctx.item.background_disabled_selected
canvas.before:
Color:
rgba: self.disabled_color if self.disabled else self.color
BorderImage:
source: self.disabled_background if self.disabled else self.normal_background
pos: self.pos
size: self.size
PushMatrix
Translate:
xy: self.center_x, self.center_y
Rotate:
angle: 90 if ctx.item.orientation == 'horizontal' else 0
axis: 0, 0, 1
Translate:
xy: -self.center_x, -self.center_y
canvas.after:
PopMatrix
:
container: container
container_title: container_title
BoxLayout:
orientation: root.orientation
pos: root.pos
BoxLayout:
size_hint_x: None if root.orientation == 'horizontal' else 1
size_hint_y: None if root.orientation == 'vertical' else 1
width: root.min_space if root.orientation == 'horizontal' else 100
height: root.min_space if root.orientation == 'vertical' else 100
id: container_title
StencilView:
id: sv
BoxLayout:
id: container
pos: sv.pos
size: root.content_size
:
canvas.after:
Color:
rgba: self._bar_color if (self.do_scroll_y and self.viewport_size[1] > self.height) else [0, 0, 0, 0]
Rectangle:
pos: (self.right - self.bar_width - self.bar_margin) if self.bar_pos_y == 'right' else (self.x + self.bar_margin), self.y + self.height * self.vbar[0]
size: min(self.bar_width, self.width), self.height * self.vbar[1]
Color:
rgba: self._bar_color if (self.do_scroll_x and self.viewport_size[0] > self.width) else [0, 0, 0, 0]
Rectangle:
pos: self.x + self.width * self.hbar[0], (self.y + self.bar_margin) if self.bar_pos_x == 'bottom' else (self.top - self.bar_margin - self.bar_width)
size: self.width * self.hbar[1], min(self.bar_width, self.height)
:
_checkbox_state_image:
self.background_checkbox_down \
if self.active else self.background_checkbox_normal
_checkbox_disabled_image:
self.background_checkbox_disabled_down \
if self.active else self.background_checkbox_disabled_normal
_radio_state_image:
self.background_radio_down \
if self.active else self.background_radio_normal
_radio_disabled_image:
self.background_radio_disabled_down \
if self.active else self.background_radio_disabled_normal
_checkbox_image:
self._checkbox_disabled_image \
if self.disabled else self._checkbox_state_image
_radio_image:
self._radio_disabled_image \
if self.disabled else self._radio_state_image
canvas:
Color:
rgb: 1, 1, 1
Rectangle:
source: self._radio_image if self.group else self._checkbox_image
size: sp(32), sp(32)
pos: int(self.center_x - sp(16)), int(self.center_y - sp(16))
# =============================================================================
# Screen Manager
# =============================================================================
:
canvas.before:
StencilPush
Rectangle:
pos: self.pos
size: self.size
StencilUse
canvas.after:
StencilUnUse
Rectangle:
pos: self.pos
size: self.size
StencilPop
================================================
FILE: gui/kivy/i18n.py
================================================
import gettext
class _(str):
observers = set()
lang = None
def __new__(cls, s, *args, **kwargs):
if _.lang is None:
_.switch_lang('en')
t = _.translate(s, *args, **kwargs)
o = super(_, cls).__new__(cls, t)
o.source_text = s
return o
@staticmethod
def translate(s, *args, **kwargs):
return _.lang(s).format(args, kwargs)
@staticmethod
def bind(label):
try:
_.observers.add(label)
except:
pass
# garbage collection
new = set()
for label in _.observers:
try:
new.add(label)
except:
pass
_.observers = new
@staticmethod
def switch_lang(lang):
# get the right locales directory, and instanciate a gettext
from electrum.i18n import LOCALE_DIR
locales = gettext.translation('electrum', LOCALE_DIR, languages=[lang], fallback=True)
_.lang = locales.gettext
for label in _.observers:
try:
label.text = _(label.text.source_text)
except:
pass
================================================
FILE: gui/kivy/main.kv
================================================
#:import Clock kivy.clock.Clock
#:import Window kivy.core.window.Window
#:import Factory kivy.factory.Factory
#:import _ electrum_gui.kivy.i18n._
###########################
# Global Defaults
###########################
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)
on_focus: app._focused_widget = root
font_size: '18sp'
on_parent: self.MIN_STATE_TIME = 0.1
font_size: '12sp'
:
canvas.before:
Color:
rgba: 0.1, 0.1, 0.1, 1
Rectangle:
size: self.size
pos: self.pos
:
canvas.before:
Color:
rgba: 0.1, 0.1, 0.1, 1
Rectangle:
size: self.size
pos: self.pos
# Custom Global Widgets
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
color: (0.8, 0.8, 0.8, 1)
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
:
rows: 1
size_hint: 1, None
height: self.minimum_height
:
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
#########################
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
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'
height: self.minimum_height
size_hint_y: None
cols: 1
spacing: '10dp'
padding: '10dp'
canvas.before:
Color:
rgb: .3, .3, .3
Rectangle:
size: self.size
pos: self.pos
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
data: ''
text: ' '.join(map(''.join, zip(*[iter(self.data)]*4))) if self.data else ''
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)
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
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
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
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
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.3, 0.3, 0.3, 1)
Rectangle:
size: self.size
pos: self.pos
:
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
:
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
:
size_hint: 1, None
height: '48dp'
on_release:
self.parent.update_amount(self.text)
padding: 0, 0, 0, 0
:
on_parent:
if self.parent: self.parent.bar_width = 0
if self.parent: self.parent.scroll_x = 0.5
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
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'
:
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)
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
TabbedCarousel:
id: panel
tab_height: '48dp'
tab_width: panel.width/3
strip_border: 0, 0, 0, 0
InvoicesScreen:
id: invoices_screen
tab: invoices_tab
SendScreen:
id: send_screen
tab: send_tab
HistoryScreen:
id: history_screen
tab: history_tab
ReceiveScreen:
id: receive_screen
tab: receive_tab
AddressScreen:
id: address_screen
tab: address_tab
CleanHeader:
id: invoices_tab
text: _('Invoices')
slide: 0
CleanHeader:
id: send_tab
text: _('Send')
slide: 1
CleanHeader:
id: history_tab
text: _('History')
slide: 2
CleanHeader:
id: receive_tab
text: _('Receive')
slide: 3
CleanHeader:
id: address_tab
text: _('Addresses')
slide: 4
#on_release:
# fixme: the following line was commented out because it does no 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
================================================
FILE: gui/kivy/main_window.py
================================================
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
from .uix.dialogs import OutputList, OutputItem
#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
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('')
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.bitcoin import NetworkConstants
pp = servers.get(host, NetworkConstants.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 == self.network.get_blockchain_name(b):
self.network.follow_chain(index)
#self.block
names = [self.network.blockchains[b].get_name() for b in chains]
if len(names) >1:
ChoiceDialog(_('Choose your chain'), names, '', 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):
return self.electrum_config.get('base_unit', 'BTCP')
def _set_bu(self, value):
assert value in base_units.keys()
self.electrum_config.set_key('base_unit', value, 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 ''
rate = self.fx.exchange_rate()
if not rate:
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 not rate:
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
App.__init__(self)#, **kwargs)
title = _('Bitcoin Private Electrum')
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 updation 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
def wallet_name(self):
return os.path.basename(self.wallet.storage.path) if self.wallet else ' '
def on_pr(self, pr):
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):
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)
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()
self.load_wallet_by_name(self.electrum_config.get_wallet_path())
# 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_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history'])
# 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, instance, wallet):
if wallet:
wallet.start_threads(self.daemon.network)
self.daemon.add_wallet(wallet)
self.load_wallet(wallet)
self.on_resume()
def load_wallet_by_name(self, path):
if not path:
return
wallet = self.daemon.load_wallet(path, None)
if wallet:
if wallet != self.wallet:
self.stop_wallet()
self.load_wallet(wallet)
self.on_resume()
else:
Logger.debug('Electrum: Wallet not found. Launching install wizard')
storage = WalletStorage(path)
wizard = Factory.InstallWizard(self.electrum_config, storage)
wizard.bind(on_wizard_complete=self.on_wizard_complete)
action = wizard.storage.get_action()
wizard.run(action)
def on_stop(self):
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()
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()
if self.network.interface:
self.server_host = self.network.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):
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 (%d blocks)"%server_lag)
else:
c, u, x = self.wallet.get_balance()
text = self.format_amount(c+x+u)
status = str(text.strip() + ' ' + self.base_unit)
else:
status = _("Disconnected")
n = self.wallet.basename()
self.status = '[size=15dp]%s[/size]\n%s' %(n, status)
#fiat_balance = self.fx.format_amount_and_units(c+u+x) or ''
def get_max_amount(self):
inputs = self.wallet.get_spendable_coins(None, self.electrum_config)
addr = str(self.send_screen.screen.address) or self.wallet.dummy_address()
outputs = [(TYPE_ADDRESS, addr, '!')]
tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config)
amount = tx.output_value()
return format_satoshis_plain(amount, self.decimal_point())
def format_amount(self, x, is_diff=False, whitespaces=False):
return format_satoshis(x, is_diff, 0, self.decimal_point(), 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):
# pause nfc
if self.nfcscanner:
self.nfcscanner.nfc_disable()
return True
def on_resume(self):
if self.nfcscanner:
self.nfcscanner.nfc_enable()
# workaround p4a bug:
# show an empty info bubble, to refresh the display
self.show_info_bubble('', duration=0.1, pos=(0,0), width=1, arrow_pos=None)
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 a 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 a 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 a 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
Clock.schedule_once(lambda dt: on_success(tx))
def _broadcast_thread(self, tx, on_complete):
ok, txid = self.network.broadcast(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()
@profiler
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 protected(self, msg, f, args):
if self.wallet.has_password():
self.password_dialog(msg, f, args)
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 = os.path.basename(self.wallet.storage.path)
self.protected(_("Enter your PIN code to confirm deletion of %s") % 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:" + basename)
d = os.listdir(dirname)
name = 'default_wallet'
new_path = os.path.join(dirname, name)
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 change_password(self, cb):
if self.wallet.has_password():
self.protected(_("Changing PIN code.") + '\n' + _("Enter your current PIN:"), self._change_password, (cb,))
else:
self._change_password(cb, None)
def _change_password(self, cb, old_password):
if self.wallet.has_password():
if old_password is None:
return
try:
self.wallet.check_password(old_password)
except InvalidPassword:
self.show_error("Invalid PIN")
return
self.password_dialog(_('Enter new PIN'), self._change_password2, (cb, old_password,))
def _change_password2(self, cb, old_password, new_password):
self.password_dialog(_('Confirm new PIN'), self._change_password3, (cb, old_password, new_password))
def _change_password3(self, cb, old_password, new_password, confirmed_password):
if new_password == confirmed_password:
self.wallet.update_password(old_password, new_password)
cb()
else:
self.show_error("PIN numbers do not match")
def password_dialog(self, msg, f, args):
from .uix.dialogs.password_dialog import PasswordDialog
def callback(pw):
Clock.schedule_once(lambda x: f(*(args + (pw,))), 0.1)
if self._password_dialog is None:
self._password_dialog = PasswordDialog()
self._password_dialog.init(msg, callback)
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
key = str(self.wallet.export_private_key(addr, password)[0])
pk_label.data = key
self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))
================================================
FILE: gui/kivy/nfc_scanner/__init__.py
================================================
__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
accissible 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 implementtation 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')
================================================
FILE: gui/kivy/nfc_scanner/scanner_android.py
================================================
'''This is the Android implementatoin 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 interace with the
Android NFC adapter. See Module Documentation for deatils.
'''
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 whan 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 functions 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 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()
================================================
FILE: gui/kivy/nfc_scanner/scanner_dummy.py
================================================
''' 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')
================================================
FILE: gui/kivy/tools/bitcoin_intent.xml
================================================
================================================
FILE: gui/kivy/tools/blacklist.txt
================================================
# eggs
*.egg-info
# unit test
unittest/*
# python config
config/makesetup
# unused pygame files
pygame/_camera_*
pygame/camera.pyo
pygame/*.html
pygame/*.bmp
pygame/*.svg
pygame/cdrom.so
pygame/pygame_icon.icns
pygame/LGPL
pygame/threads/Py25Queue.pyo
pygame/*.ttf
pygame/mac*
pygame/_numpy*
pygame/sndarray.pyo
pygame/surfarray.pyo
pygame/_arraysurfarray.pyo
# unused kivy files (platform specific)
kivy/input/providers/wm_*
kivy/input/providers/mactouch*
kivy/input/providers/probesysfs*
kivy/input/providers/mtdev*
kivy/input/providers/hidinput*
kivy/core/camera/camera_videocapture*
kivy/core/spelling/*osx*
kivy/core/video/video_pyglet*
kivy/adapters
kivy/modules
kivy/uix/sandbox
kivy/uix/pagelayout
kivy/uix/video
kivy/uix/vkeyboard
kivy/uix/videoplayer
# unused encodings
lib-dynload/*codec*
encodings/cp*.pyo
encodings/tis*
encodings/shift*
encodings/bz2*
encodings/iso*
encodings/undefined*
encodings/johab*
encodings/p*
encodings/m*
encodings/euc*
encodings/k*
encodings/unicode_internal*
encodings/quo*
encodings/gb*
encodings/big5*
encodings/hp*
encodings/hz*
# unused python modules
bsddb/*
wsgiref/*
hotshot/*
pydoc_data/*
tty.pyo
#anydbm.pyo
nturl2path.pyo
LICENCE.txt
macurl2path.pyo
dummy_threading.pyo
audiodev.pyo
antigravity.pyo
#dumbdbm.pyo
sndhdr.pyo
__phello__.foo.pyo
sunaudio.pyo
os2emxpath.pyo
multiprocessing/dummy*
# unused binaries python modules
lib-dynload/termios.so
lib-dynload/_lsprof.so
lib-dynload/*audioop.so
#lib-dynload/mmap.so
lib-dynload/_hotshot.so
#lib-dynload/_csv.so
lib-dynload/future_builtins.so
lib-dynload/_heapq.so
lib-dynload/_json.so
lib-dynload/grp.so
lib-dynload/resource.so
lib-dynload/pyexpat.so
# odd files
plat-linux3/regen
#>sqlite3
# conditionnal include depending if some recipes are included or not.
#sqlite3/*
#lib-dynload/_sqlite3.so
# tag
android.manifest.intent_filters = gui/kivy/tools/bitcoin_intent.xml
# (list) Android additionnal libraries to copy into libs/armeabi
#android.add_libs_armeabi = lib/android/*.so
# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False
# (list) Android application meta-data to set (key=value format)
#android.meta_data =
# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =
android.whitelist = lib-dynload/_csv.so
# local version that merges branch 866
p4a.source_dir = /opt/python-for-android
#
# iOS specific
#
# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: ()"
# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s
[buildozer]
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2
# -----------------------------------------------------------------------------
# List as sections
#
# You can define all the "list" as [section:key].
# Each line will be considered as a option to the list.
# Let's take [app] / source.exclude_patterns.
# Instead of doing:
#
# [app]
# source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
# This can be translated into:
#
# [app:source.exclude_patterns]
# license
# data/audio/*.wav
# data/images/original/*
#
# -----------------------------------------------------------------------------
# Profiles
#
# You can extend section / key with a profile
# For example, you want to deploy a demo version of your application without
# HD content. You could first change the title to add "(demo)" in the name
# and extend the excluded directories to remove the HD content.
#
# [app@demo]
# title = My Application (demo)
#
# [app:source.exclude_patterns@demo]
# images/hd/*
#
# Then, invoke the command line with the "demo" profile:
#
# buildozer --profile demo android debug
================================================
FILE: gui/kivy/uix/__init__.py
================================================
================================================
FILE: gui/kivy/uix/combobox.py
================================================
'''
ComboBox
=======
Based on Spinner
'''
__all__ = ('ComboBox', 'ComboBoxOption')
from kivy.properties import ListProperty, ObjectProperty, BooleanProperty
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
from kivy.lang import Builder
Builder.load_string('''
:
size_hint_y: None
height: 44
:
background_normal: 'atlas://data/images/defaulttheme/spinner'
background_down: 'atlas://data/images/defaulttheme/spinner_pressed'
on_key:
if self.items: x, y = zip(*self.items); self.text = y[x.index(args[1])]
''')
class ComboBoxOption(Button):
pass
class ComboBox(Button):
items = ListProperty()
key = ObjectProperty()
option_cls = ObjectProperty(ComboBoxOption)
dropdown_cls = ObjectProperty(DropDown)
is_open = BooleanProperty(False)
def __init__(self, **kwargs):
self._dropdown = None
super(ComboBox, self).__init__(**kwargs)
self.items_dict = dict(self.items)
self.bind(
on_release=self._toggle_dropdown,
dropdown_cls=self._build_dropdown,
option_cls=self._build_dropdown,
items=self._update_dropdown,
key=self._update_text)
self._build_dropdown()
self._update_text()
def _update_text(self, *largs):
try:
self.text = self.items_dict[self.key]
except KeyError:
pass
def _build_dropdown(self, *largs):
if self._dropdown:
self._dropdown.unbind(on_select=self._on_dropdown_select)
self._dropdown.dismiss()
self._dropdown = None
self._dropdown = self.dropdown_cls()
self._dropdown.bind(on_select=self._on_dropdown_select)
self._update_dropdown()
def _update_dropdown(self, *largs):
dp = self._dropdown
cls = self.option_cls
dp.clear_widgets()
for key, value in self.items:
item = cls(text=value)
# extra attribute
item.key = key
item.bind(on_release=lambda option: dp.select(option.key))
dp.add_widget(item)
def _toggle_dropdown(self, *largs):
self.is_open = not self.is_open
def _on_dropdown_select(self, instance, data, *largs):
self.key = data
self.is_open = False
def on_is_open(self, instance, value):
if value:
self._dropdown.open(self)
else:
self._dropdown.dismiss()
================================================
FILE: gui/kivy/uix/context_menu.py
================================================
#!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('''
background_normal: ''
background_color: (0.192, .498, 0.745, 1)
height: '48dp'
size_hint: 1, None
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: self.hide(), 0.1)
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()
================================================
FILE: gui/kivy/uix/dialogs/__init__.py
================================================
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 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 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 = 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(Factory.BoxLayout):
pass
class OutputList(Factory.GridLayout):
def __init__(self, **kwargs):
super(Factory.GridLayout, self).__init__(**kwargs)
self.app = App.get_running_app()
def update(self, outputs):
self.clear_widgets()
for (type, address, amount) in outputs:
self.add_output(address, amount)
def add_output(self, address, amount):
b = Factory.OutputItem()
b.address = address
b.value = self.app.format_amount_and_units(amount)
self.add_widget(b)
================================================
FILE: gui/kivy/uix/dialogs/amount_dialog.py
================================================
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('''
id: popup
title: _('Amount')
AnchorLayout:
anchor_x: 'center'
BoxLayout:
orientation: 'vertical'
size_hint: 0.8, 1
BoxLayout:
size_hint: 1, None
height: '80dp'
Label:
id: a
btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else ''
fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else ''
text1: ((self.fiat_text if kb.is_fiat else self.btc_text) if app.fiat_unit else self.btc_text) if self.btc_text else ''
text2: ((self.btc_text if kb.is_fiat else self.fiat_text) if app.fiat_unit else '') if self.btc_text else ''
text: self.text1 + "\\n" + "[color=#8888ff]" + self.text2 + "[/color]"
halign: 'right'
size_hint: 1, None
font_size: '22dp'
height: '80dp'
Widget:
size_hint: 1, 0.2
GridLayout:
id: kb
amount: ''
fiat_amount: ''
is_fiat: False
on_fiat_amount: if self.is_fiat: self.amount = app.fiat_to_btc(self.fiat_amount)
on_amount: if not self.is_fiat: self.fiat_amount = app.btc_to_fiat(self.amount)
size_hint: 1, None
update_amount: popup.update_amount
height: '300dp'
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: '.'
KButton:
text: '0'
KButton:
text: '<'
Button:
id: but_max
opacity: 1 if root.show_max else 0
disabled: not root.show_max
size_hint: 1, None
height: '48dp'
text: 'Max'
on_release:
kb.is_fiat = False
kb.amount = app.get_max_amount()
Button:
id: button_fiat
size_hint: 1, None
height: '48dp'
text: (app.base_unit if not kb.is_fiat else app.fiat_unit) if app.fiat_unit else ''
on_release:
if app.fiat_unit: popup.toggle_fiat(kb)
Button:
size_hint: 1, None
height: '48dp'
text: 'Clear'
on_release:
kb.amount = ''
kb.fiat_amount = ''
Widget:
size_hint: 1, 0.2
BoxLayout:
size_hint: 1, None
height: '48dp'
Widget:
size_hint: 1, None
height: '48dp'
Button:
size_hint: 1, None
height: '48dp'
text: _('OK')
on_release:
root.callback(a.btc_text)
popup.dismiss()
''')
from kivy.properties import BooleanProperty
class AmountDialog(Factory.Popup):
show_max = BooleanProperty(False)
def __init__(self, show_max, amount, cb):
Factory.Popup.__init__(self)
self.show_max = show_max
self.callback = cb
if amount:
self.ids.kb.amount = amount
def toggle_fiat(self, a):
a.is_fiat = not a.is_fiat
def update_amount(self, c):
kb = self.ids.kb
amount = kb.fiat_amount if kb.is_fiat else kb.amount
if c == '<':
amount = amount[:-1]
elif c == '.' and amount in ['0', '']:
amount = '0.'
elif amount == '0':
amount = c
else:
try:
Decimal(amount+c)
amount += c
except:
pass
if kb.is_fiat:
kb.fiat_amount = amount
else:
kb.amount = amount
================================================
FILE: gui/kivy/uix/dialogs/bump_fee_dialog.py
================================================
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from electrum.util import fee_levels
from electrum_gui.kivy.i18n import _
Builder.load_string('''
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: tooltip
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.fee_step = self.config.max_fee_rate() / 10
self.dynfees = self.config.get('dynamic_fees', True) and self.app.network
self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee)
self.update_slider()
self.update_text()
def update_text(self):
value = int(self.ids.slider.value)
self.ids.new_fee.value = self.app.format_amount_and_units(self.get_fee())
if self.dynfees:
value = int(self.ids.slider.value)
self.ids.tooltip.text = fee_levels[value]
def update_slider(self):
slider = self.ids.slider
if self.dynfees:
slider.range = (0, 4)
slider.step = 1
slider.value = 3
else:
slider.range = (1, 10)
slider.step = 1
rate = self.init_fee*1000//self.tx_size
slider.value = min( rate * 2 // self.fee_step, 10)
def get_fee(self):
value = int(self.ids.slider.value)
if self.dynfees:
if self.config.has_fee_estimates():
dynfee = self.config.dynfee(value)
return int(dynfee * self.tx_size // 1000)
else:
return int(value*self.fee_step * 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()
def on_checkbox(self, b):
self.dynfees = b
self.update_text()
================================================
FILE: gui/kivy/uix/dialogs/checkbox_dialog.py
================================================
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
Builder.load_string('''
id: popup
title: ''
size_hint: 0.8, 0.8
pos_hint: {'top':0.9}
BoxLayout:
orientation: 'vertical'
Label:
id: description
text: ''
halign: 'left'
text_size: self.width, None
size: self.texture_size
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.2
Label:
text: _('Enable')
CheckBox:
id:cb
Widget:
size_hint: 1, 0.1
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(cb.active)
popup.dismiss()
''')
class CheckBoxDialog(Factory.Popup):
def __init__(self, title, text, status, callback):
Factory.Popup.__init__(self)
self.ids.cb.active = status
self.ids.description.text = text
self.callback = callback
self.title = title
================================================
FILE: gui/kivy/uix/dialogs/choice_dialog.py
================================================
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
Builder.load_string('''
id: popup
title: ''
size_hint: 0.8, 0.8
pos_hint: {'top':0.9}
BoxLayout:
orientation: 'vertical'
Widget:
size_hint: 1, 0.1
ScrollView:
orientation: 'vertical'
size_hint: 1, 0.8
GridLayout:
row_default_height: '48dp'
orientation: 'vertical'
id: choices
cols: 2
size_hint: 1, None
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.value)
popup.dismiss()
''')
class ChoiceDialog(Factory.Popup):
def __init__(self, title, choices, key, callback):
Factory.Popup.__init__(self)
print(choices, type(choices))
if type(choices) is list:
choices = dict(map(lambda x: (x,x), choices))
layout = self.ids.choices
layout.bind(minimum_height=layout.setter('height'))
for k, v in sorted(choices.items()):
l = Label(text=v)
l.height = '48dp'
l.size_hint_x = 4
cb = CheckBox(group='choices')
cb.value = k
cb.height = '48dp'
cb.size_hint_x = 1
def f(cb, x):
if x: self.value = cb.value
cb.bind(active=f)
if k == key:
cb.active = True
layout.add_widget(l)
layout.add_widget(cb)
layout.add_widget(Widget(size_hint_y=1))
self.callback = callback
self.title = title
self.value = key
================================================
FILE: gui/kivy/uix/dialogs/fee_dialog.py
================================================
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from electrum.util import fee_levels
from electrum_gui.kivy.i18n import _
Builder.load_string('''
id: popup
title: _('Transaction Fees')
size_hint: 0.8, 0.8
pos_hint: {'top':0.9}
BoxLayout:
orientation: 'vertical'
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
id: fee_per_kb
text: ''
Slider:
id: slider
range: 0, 4
step: 1
on_value: root.on_slider(self.value)
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('Dynamic Fees')
CheckBox:
id: dynfees
on_active: root.on_checkbox(self.active)
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: 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.fee_rate = self.config.fee_per_kb()
self.callback = callback
self.dynfees = self.config.get('dynamic_fees', True)
self.ids.dynfees.active = self.dynfees
self.update_slider()
self.update_text()
def update_text(self):
value = int(self.ids.slider.value)
self.ids.fee_per_kb.text = self.get_fee_text(value)
def update_slider(self):
slider = self.ids.slider
if self.dynfees:
slider.range = (0, 4)
slider.step = 1
slider.value = self.config.get('fee_level', 2)
else:
slider.range = (0, 9)
slider.step = 1
slider.value = self.config.static_fee_index(self.fee_rate)
def get_fee_text(self, value):
if self.ids.dynfees.active:
tooltip = fee_levels[value]
if self.config.has_fee_estimates():
dynfee = self.config.dynfee(value)
tooltip += '\n' + (self.app.format_amount_and_units(dynfee)) + '/kB'
else:
fee_rate = self.config.static_fee(value)
tooltip = self.app.format_amount_and_units(fee_rate) + '/kB'
if self.config.has_fee_estimates():
i = self.config.reverse_dynfee(fee_rate)
tooltip += '\n' + (_('low fee') if i < 0 else 'Within %d blocks'%i)
return tooltip
def on_ok(self):
value = int(self.ids.slider.value)
self.config.set_key('dynamic_fees', self.dynfees, False)
if self.dynfees:
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()
def on_checkbox(self, b):
self.dynfees = b
self.update_slider()
self.update_text()
================================================
FILE: gui/kivy/uix/dialogs/fx_dialog.py
================================================
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
Builder.load_string('''
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
Clock.schedule_once(lambda dt: self.add_exchanges())
================================================
FILE: gui/kivy/uix/dialogs/installwizard.py
================================================
from functools import partial
import threading
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 . 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_xpub = "xpub661MyMwAqRbcEbvVtRRSjqxVnaWVUMewVzMiURAKyYratih4TtBpMypzzefmv8zUNebmNVzB3PojdC5sV2P9bDgMoo9B3SARw1MXUUfU1GL"
Builder.load_string('''
#:import Window kivy.core.window.Window
#:import _ electrum_gui.kivy.i18n._
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'
:
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)
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: 0, 0, 0, .9
Rectangle:
size: Window.size
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 == ''
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 %d cosigners')%n.value
Slider:
id: n
range: 2, 5
step: 1
value: 2
Label:
color: root.text_color
text: _('Require %d signatures')%m.value
Slider:
id: m
range: 1, n.value
step: 1
value: 2
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
:
size_hint: 1, None
height: '33dp'
on_release:
self.parent.update_amount(self.text)
:
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)
:
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
:
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
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: '<'
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()
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()
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
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 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 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):
'''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)
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 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_error(self, msg):
app = App.get_running_app()
Clock.schedule_once(lambda dt: app.show_error(msg))
def password_dialog(self, message, callback):
popup = PasswordDialog()
popup.init(message, callback)
popup.open()
def request_password(self, run_next):
def callback(pin):
if pin:
self.run('confirm_password', pin, run_next)
else:
run_next(None, None)
self.password_dialog('Choose a PIN code', callback)
def confirm_password(self, pin, run_next):
def callback(conf):
if conf == pin:
run_next(pin, False)
else:
self.show_error(_('PIN mismatch'))
self.run('request_password', run_next)
self.password_dialog('Confirm your PIN code', callback)
def action_dialog(self, action, run_next):
f = getattr(self, action)
f()
================================================
FILE: gui/kivy/uix/dialogs/label_dialog.py
================================================
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
Builder.load_string('''
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
================================================
FILE: gui/kivy/uix/dialogs/nfc_transaction.py
================================================
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)
================================================
FILE: gui/kivy/uix/dialogs/password_dialog.py
================================================
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
Builder.load_string('''
id: popup
title: _('PIN Code')
message: ''
size_hint: 0.9, 0.9
BoxLayout:
orientation: 'vertical'
Widget:
size_hint: 1, 1
Label:
text: root.message
text_size: self.width, None
size: self.texture_size
Widget:
size_hint: 1, 1
Label:
id: a
text: ' * '*len(kb.password) + ' o '*(6-len(kb.password))
Widget:
size_hint: 1, 1
GridLayout:
id: kb
update_amount: popup.update_password
password: ''
on_password: popup.on_password(self.password)
size_hint: 1, None
height: '200dp'
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: '<'
BoxLayout:
size_hint: 1, None
height: '48dp'
Widget:
size_hint: 0.5, None
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Cancel')
on_release:
popup.dismiss()
popup.callback(None)
''')
class PasswordDialog(Factory.Popup):
#def __init__(self, message, callback):
# Factory.Popup.__init__(self)
def init(self, message, callback):
self.message = message
self.callback = callback
self.ids.kb.password = ''
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:
self.dismiss()
Clock.schedule_once(lambda dt: self.callback(pw), 0.1)
================================================
FILE: gui/kivy/uix/dialogs/qr_dialog.py
================================================
from kivy.factory import Factory
from kivy.lang import Builder
Builder.load_string('''
id: popup
title: ''
data: ''
shaded: False
show_text: False
AnchorLayout:
anchor_x: 'center'
BoxLayout:
orientation: 'vertical'
size_hint: 1, 1
padding: '10dp'
spacing: '10dp'
QRCodeWidget:
id: qr
TopLabel:
text: root.data if root.show_text else ''
Widget:
size_hint: 1, 0.2
BoxLayout:
size_hint: 1, None
height: '48dp'
Widget:
size_hint: 1, None
height: '48dp'
Button:
size_hint: 1, None
height: '48dp'
text: _('Close')
on_release:
popup.dismiss()
''')
class QRDialog(Factory.Popup):
def __init__(self, title, data, show_text):
Factory.Popup.__init__(self)
self.title = title
self.data = data
self.show_text = show_text
def on_open(self):
self.ids.qr.set_data(self.data)
================================================
FILE: gui/kivy/uix/dialogs/qr_scanner.py
================================================
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('''
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)
''')
================================================
FILE: gui/kivy/uix/dialogs/question.py
================================================
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('''
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
================================================
FILE: gui/kivy/uix/dialogs/seed_options.py
================================================
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
Builder.load_string('''
id: popup
title: _('Seed Options')
size_hint: 0.8, 0.8
pos_hint: {'top':0.9}
BoxLayout:
orientation: 'vertical'
Label:
id: description
text: _('You may extend your seed with custom words')
halign: 'left'
text_size: self.width, None
size: self.texture_size
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.2
Label:
text: _('Extend Seed')
CheckBox:
id:cb
Widget:
size_hint: 1, 0.1
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(cb.active)
popup.dismiss()
''')
class SeedOptionsDialog(Factory.Popup):
def __init__(self, status, callback):
Factory.Popup.__init__(self)
self.ids.cb.active = status
self.callback = callback
================================================
FILE: gui/kivy/uix/dialogs/settings.py
================================================
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
from electrum.i18n import languages
from electrum_gui.kivy.i18n import _
from electrum.plugins import run_hook
from electrum import coinchooser
from electrum.util import fee_levels
from .choice_dialog import ChoiceDialog
Builder.load_string('''
#:import partial functools.partial
#:import _ electrum_gui.kivy.i18n._
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:
status: '' if root.disable_pin else ('ON' if root.use_encryption else 'OFF')
disabled: root.disable_pin
title: _('PIN code') + ': ' + self.status
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 BTCP amounts.")
action: partial(root.unit_dialog, self)
CardSeparator
SettingsItem:
status: root.fee_status()
title: _('Fees') + ': ' + self.status
description: _("Fees paid to the BTCP miners.")
action: partial(root.fee_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 possiblity, 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._fee_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'), list(base_units.keys()), self.app.base_unit, cb)
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):
if self.config.get('dynamic_fees', True):
return fee_levels[self.config.get('fee_level', 2)]
else:
return self.app.format_amount_and_units(self.config.fee_per_kb()) + '/kB'
def fee_dialog(self, label, dt):
if self._fee_dialog is None:
from .fee_dialog import FeeDialog
def cb():
label.status = self.fee_status()
self._fee_dialog = FeeDialog(self.app, self.config, cb)
self._fee_dialog.open()
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()
================================================
FILE: gui/kivy/uix/dialogs/tx_dialog.py
================================================
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('''
id: popup
title: _('Transaction')
is_mine: True
can_sign: False
can_broadcast: False
can_rbf: False
fee_str: ''
date_str: ''
amount_str: ''
tx_hash: ''
status_str: ''
description: ''
outputs_str: ''
BoxLayout:
orientation: 'vertical'
ScrollView:
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: _('Date') if root.date_str else ''
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: _('Outputs') + ':'
OutputList:
height: self.minimum_height
size_hint: 1, None
id: output_list
TopLabel:
text: _('Transaction ID') + ':' if root.tx_hash else ''
TxHashLabel:
data: root.tx_hash
name: _('Transaction ID')
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_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
elif exp_n:
self.date_str = _('Within %d blocks') % exp_n if exp_n > 0 else _('unknown (low fee)')
else:
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)
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)
================================================
FILE: gui/kivy/uix/dialogs/wallets.py
================================================
import os
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
from ...i18n import _
from .label_dialog import LabelDialog
Builder.load_string('''
#:import os os
:
title: _('Wallets')
id: popup
path: os.path.dirname(app.get_wallet_path())
BoxLayout:
orientation: 'vertical'
padding: '10dp'
FileChooserListView:
id: wallet_selector
dirselect: False
filter_dirs: True
filter: '*.*'
path: root.path
rootpath: root.path
size_hint_y: 0.6
Widget
size_hint_y: 0.1
GridLayout:
cols: 3
size_hint_y: 0.1
Button:
id: open_button
size_hint: 0.1, None
height: '48dp'
text: _('New')
on_release:
popup.dismiss()
root.new_wallet(app, wallet_selector.path)
Button:
id: open_button
size_hint: 0.1, None
height: '48dp'
text: _('Open')
disabled: not wallet_selector.selection
on_release:
popup.dismiss()
root.open_wallet(app)
''')
class WalletDialog(Factory.Popup):
def new_wallet(self, app, dirname):
def cb(text):
if text:
app.load_wallet_by_name(os.path.join(dirname, text))
d = LabelDialog(_('Enter wallet name'), '', cb)
d.open()
def open_wallet(self, app):
app.load_wallet_by_name(self.ids.wallet_selector.selection[0])
================================================
FILE: gui/kivy/uix/drawer.py
================================================
'''Drawer Widget to hold the main window and the menu/hidden section that
can be swiped in from the left. This Menu would be only hidden in phone mode
and visible in Tablet Mode.
This class is specifically in lined to save on start up speed(minimize i/o).
'''
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import OptionProperty, NumericProperty, ObjectProperty
from kivy.clock import Clock
from kivy.lang import Builder
import gc
# delayed imports
app = None
class Drawer(Factory.RelativeLayout):
'''Drawer Widget to hold the main window and the menu/hidden section that
can be swiped in from the left. This Menu would be only hidden in phone mode
and visible in Tablet Mode.
'''
state = OptionProperty('closed',
options=('closed', 'open', 'opening', 'closing'))
'''This indicates the current state the drawer is in.
:attr:`state` is a `OptionProperty` defaults to `closed`. Can be one of
`closed`, `open`, `opening`, `closing`.
'''
scroll_timeout = NumericProperty(200)
'''Timeout allowed to trigger the :data:`scroll_distance`,
in milliseconds. If the user has not moved :data:`scroll_distance`
within the timeout, the scrolling will be disabled and the touch event
will go to the children.
:data:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty`
and defaults to 200 (milliseconds)
'''
scroll_distance = NumericProperty('9dp')
'''Distance to move before scrolling the :class:`Drawer` in pixels.
As soon as the distance has been traveled, the :class:`Drawer` will
start to scroll, and no touch event will go to children.
It is advisable that you base this value on the dpi of your target
device's screen.
:data:`scroll_distance` is a :class:`~kivy.properties.NumericProperty`
and defaults to 20dp.
'''
drag_area = NumericProperty('9dp')
'''The percentage of area on the left edge that triggers the opening of
the drawer. from 0-1
:attr:`drag_area` is a `NumericProperty` defaults to 2
'''
hidden_widget = ObjectProperty(None)
''' This is the widget that is hidden in phone mode on the left side of
drawer or displayed on the left of the overlay widget in tablet mode.
:attr:`hidden_widget` is a `ObjectProperty` defaults to None.
'''
overlay_widget = ObjectProperty(None)
'''This a pointer to the default widget that is overlayed either on top or
to the right of the hidden widget.
'''
def __init__(self, **kwargs):
super(Drawer, self).__init__(**kwargs)
self._triigger_gc = Clock.create_trigger(self._re_enable_gc, .2)
def toggle_drawer(self):
if app.ui_mode[0] == 't':
return
Factory.Animation.cancel_all(self.overlay_widget)
anim = Factory.Animation(x=self.hidden_widget.width
if self.state in ('opening', 'closed') else 0,
d=.1, t='linear')
anim.bind(on_complete = self._complete_drawer_animation)
anim.start(self.overlay_widget)
def _re_enable_gc(self, dt):
global gc
gc.enable()
def on_touch_down(self, touch):
if self.disabled:
return
if not self.collide_point(*touch.pos):
return
touch.grab(self)
# disable gc for smooth interaction
# This is still not enough while wallet is synchronising
# look into pausing all background tasks while ui interaction like this
gc.disable()
global app
if not app:
app = App.get_running_app()
# skip on tablet mode
if app.ui_mode[0] == 't':
return super(Drawer, self).on_touch_down(touch)
state = self.state
touch.ud['send_touch_down'] = False
start = 0 #if state[0] == 'c' else self.hidden_widget.right
drag_area = self.drag_area\
if self.state[0] == 'c' else\
(self.overlay_widget.x)
if touch.x < start or touch.x > drag_area:
if self.state == 'open':
self.toggle_drawer()
return
return super(Drawer, self).on_touch_down(touch)
self._touch = touch
Clock.schedule_once(self._change_touch_mode,
self.scroll_timeout/1000.)
touch.ud['in_drag_area'] = True
touch.ud['send_touch_down'] = True
return
def on_touch_move(self, touch):
if not touch.grab_current is self:
return
self._touch = False
# skip on tablet mode
if app.ui_mode[0] == 't':
return super(Drawer, self).on_touch_move(touch)
if not touch.ud.get('in_drag_area', None):
return super(Drawer, self).on_touch_move(touch)
ov = self.overlay_widget
ov.x=min(self.hidden_widget.width,
max(ov.x + touch.dx*2, 0))
#_anim = Animation(x=x, duration=1/2, t='in_out_quart')
#_anim.cancel_all(ov)
#_anim.start(ov)
if abs(touch.x - touch.ox) < self.scroll_distance:
return
touch.ud['send_touch_down'] = False
Clock.unschedule(self._change_touch_mode)
self._touch = None
self.state = 'opening' if touch.dx > 0 else 'closing'
touch.ox = touch.x
return
def _change_touch_mode(self, *args):
if not self._touch:
return
touch = self._touch
touch.ungrab(self)
touch.ud['in_drag_area'] = False
touch.ud['send_touch_down'] = False
self._touch = None
super(Drawer, self).on_touch_down(touch)
return
def on_touch_up(self, touch):
if not touch.grab_current is self:
return
self._triigger_gc()
touch.ungrab(self)
touch.grab_current = None
# skip on tablet mode
get = touch.ud.get
if app.ui_mode[0] == 't':
return super(Drawer, self).on_touch_up(touch)
self.old_x = [1, ] * 10
self.speed = sum((
(self.old_x[x + 1] - self.old_x[x]) for x in range(9))) / 9.
if get('send_touch_down', None):
# touch up called before moving
Clock.unschedule(self._change_touch_mode)
self._touch = None
Clock.schedule_once(
lambda dt: super(Drawer, self).on_touch_down(touch))
if get('in_drag_area', None):
if abs(touch.x - touch.ox) < self.scroll_distance:
anim_to = (0 if self.state[0] == 'c'
else self.hidden_widget.width)
Factory.Animation(x=anim_to, d=.1).start(self.overlay_widget)
return
touch.ud['in_drag_area'] = False
if not get('send_touch_down', None):
self.toggle_drawer()
Clock.schedule_once(lambda dt: super(Drawer, self).on_touch_up(touch))
def _complete_drawer_animation(self, *args):
self.state = 'open' if self.state in ('opening', 'closed') else 'closed'
def add_widget(self, widget, index=1):
if not widget:
return
iget = self.ids.get
if not iget('hidden_widget') or not iget('overlay_widget'):
super(Drawer, self).add_widget(widget)
return
if not self.hidden_widget:
self.hidden_widget = self.ids.hidden_widget
if not self.overlay_widget:
self.overlay_widget = self.ids.overlay_widget
if self.overlay_widget.children and self.hidden_widget.children:
Logger.debug('Drawer: Accepts only two widgets. discarding rest')
return
if not self.hidden_widget.children:
self.hidden_widget.add_widget(widget)
else:
self.overlay_widget.add_widget(widget)
widget.x = 0
def remove_widget(self, widget):
if self.overlay_widget.children[0] == widget:
self.overlay_widget.clear_widgets()
return
if widget == self.hidden_widget.children:
self.hidden_widget.clear_widgets()
return
def clear_widgets(self):
self.overlay_widget.clear_widgets()
self.hidden_widget.clear_widgets()
if __name__ == '__main__':
from kivy.app import runTouchApp
from kivy.lang import Builder
runTouchApp(Builder.load_string('''
Drawer:
Button:
Button
'''))
================================================
FILE: gui/kivy/uix/gridview.py
================================================
from kivy.uix.boxlayout import BoxLayout
from kivy.adapters.dictadapter import DictAdapter
from kivy.adapters.listadapter import ListAdapter
from kivy.properties import ObjectProperty, ListProperty, AliasProperty
from kivy.uix.listview import (ListItemButton, ListItemLabel, CompositeListItem,
ListView)
from kivy.lang import Builder
from kivy.metrics import dp, sp
Builder.load_string('''
header_view: header_view
content_view: content_view
BoxLayout:
orientation: 'vertical'
padding: '0dp', '2dp'
BoxLayout:
id: header_box
orientation: 'vertical'
size_hint: 1, None
height: '30dp'
ListView:
id: header_view
BoxLayout:
id: content_box
orientation: 'vertical'
ListView:
id: content_view
<-HorizVertGrid>
header_view: header_view
content_view: content_view
ScrollView:
id: scrl
do_scroll_y: False
RelativeLayout:
size_hint_x: None
width: max(scrl.width, dp(sum(root.widths)))
BoxLayout:
orientation: 'vertical'
padding: '0dp', '2dp'
BoxLayout:
id: header_box
orientation: 'vertical'
size_hint: 1, None
height: '30dp'
ListView:
id: header_view
BoxLayout:
id: content_box
orientation: 'vertical'
ListView:
id: content_view
''')
class GridView(BoxLayout):
"""Workaround solution for grid view by using 2 list view.
Sometimes the height of lines is shown properly."""
def _get_hd_adpt(self):
return self.ids.header_view.adapter
header_adapter = AliasProperty(_get_hd_adpt, None)
'''
'''
def _get_cnt_adpt(self):
return self.ids.content_view.adapter
content_adapter = AliasProperty(_get_cnt_adpt, None)
'''
'''
headers = ListProperty([])
'''
'''
widths = ListProperty([])
'''
'''
data = ListProperty([])
'''
'''
getter = ObjectProperty(lambda item, i: item[i])
'''
'''
on_context_menu = ObjectProperty(None)
def __init__(self, **kwargs):
self._from_widths = False
super(GridView, self).__init__(**kwargs)
#self.on_headers(self, self.headers)
def on_widths(self, instance, value):
if not self.get_root_window():
return
self._from_widths = True
self.on_headers(instance, self.headers)
self._from_widths = False
def on_headers(self, instance, value):
if not self._from_widths:
return
if not (value and self.canvas and self.headers):
return
widths = self.widths
if len(self.widths) != len(value):
return
#if widths is not None:
# widths = ['%sdp' % i for i in widths]
def generic_args_converter(row_index,
item,
is_header=True,
getter=self.getter):
cls_dicts = []
_widths = self.widths
getter = self.getter
on_context_menu = self.on_context_menu
for i, header in enumerate(self.headers):
kwargs = {
'padding': ('2dp','2dp'),
'halign': 'center',
'valign': 'middle',
'size_hint_y': None,
'shorten': True,
'height': '30dp',
'text_size': (_widths[i], dp(30)),
'text': getter(item, i),
}
kwargs['font_size'] = '9sp'
if is_header:
kwargs['deselected_color'] = kwargs['selected_color'] =\
[0, 1, 1, 1]
else: # this is content
kwargs['deselected_color'] = 1, 1, 1, 1
if on_context_menu is not None:
kwargs['on_press'] = on_context_menu
if widths is not None: # set width manually
kwargs['size_hint_x'] = None
kwargs['width'] = widths[i]
cls_dicts.append({
'cls': ListItemButton,
'kwargs': kwargs,
})
return {
'id': item[-1],
'size_hint_y': None,
'height': '30dp',
'cls_dicts': cls_dicts,
}
def header_args_converter(row_index, item):
return generic_args_converter(row_index, item)
def content_args_converter(row_index, item):
return generic_args_converter(row_index, item, is_header=False)
self.ids.header_view.adapter = ListAdapter(data=[self.headers],
args_converter=header_args_converter,
selection_mode='single',
allow_empty_selection=False,
cls=CompositeListItem)
self.ids.content_view.adapter = ListAdapter(data=self.data,
args_converter=content_args_converter,
selection_mode='single',
allow_empty_selection=False,
cls=CompositeListItem)
self.content_adapter.bind_triggers_to_view(self.ids.content_view._trigger_reset_populate)
class HorizVertGrid(GridView):
pass
if __name__ == "__main__":
from kivy.app import App
class MainApp(App):
def build(self):
data = []
for i in range(90):
data.append((str(i), str(i)))
self.data = data
return Builder.load_string('''
BoxLayout:
orientation: 'vertical'
HorizVertGrid:
on_parent: if args[1]: self.content_adapter.data = app.data
headers:['Address', 'Previous output']
widths: [400, 500]
font_size: '16sp'
''')
MainApp().run()
================================================
FILE: gui/kivy/uix/menus.py
================================================
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.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)
================================================
FILE: gui/kivy/uix/qrcodewidget.py
================================================
''' Kivy Widget that accepts data and displays qrcode
'''
from threading import Thread
from functools import partial
import qrcode
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics.texture import Texture
from kivy.properties import StringProperty
from kivy.properties import ObjectProperty, StringProperty, ListProperty,\
BooleanProperty
from kivy.lang import Builder
from kivy.clock import Clock
Builder.load_string('''
canvas.before:
# Draw white Rectangle
Color:
rgba: root.background_color
Rectangle:
size: self.size
pos: self.pos
canvas.after:
Color:
rgba: root.foreground_color
Rectangle:
size: self.size
pos: self.pos
Image
id: qrimage
pos_hint: {'center_x': .5, 'center_y': .5}
allow_stretch: True
size_hint: None, None
size: root.width * .9, root.height * .9
''')
class QRCodeWidget(FloatLayout):
data = StringProperty(None, allow_none=True)
background_color = ListProperty((1, 1, 1, 1))
foreground_color = ListProperty((0, 0, 0, 0))
def __init__(self, **kwargs):
super(QRCodeWidget, self).__init__(**kwargs)
self.data = None
self.qr = None
self._qrtexture = None
def on_data(self, instance, value):
if not (self.canvas or value):
return
self.update_qr()
def set_data(self, data):
if self.data == data:
return
MinSize = 210 if len(data) < 128 else 500
self.setMinimumSize((MinSize, MinSize))
self.data = data
self.qr = None
def update_qr(self):
if not self.data and self.qr:
return
L = qrcode.constants.ERROR_CORRECT_L
data = self.data
self.qr = qr = qrcode.QRCode(
version=None,
error_correction=L,
box_size=10,
border=0,
)
qr.add_data(data)
qr.make(fit=True)
self.update_texture()
def setMinimumSize(self, size):
# currently unused, do we need this?
self._texture_size = size
def _create_texture(self, k):
self._qrtexture = texture = Texture.create(size=(k,k), colorfmt='rgb')
# don't interpolate texture
texture.min_filter = 'nearest'
texture.mag_filter = 'nearest'
def update_texture(self):
if not self.qr:
return
matrix = self.qr.get_matrix()
k = len(matrix)
# create the texture
self._create_texture(k)
buff = []
bext = buff.extend
cr, cg, cb, ca = self.background_color[:]
cr, cg, cb = cr*255, cg*255, cb*255
for r in range(k):
for c in range(k):
bext([0, 0, 0] if matrix[k-1-r][c] else [cr, cg, cb])
# then blit the buffer
buff = bytes(buff)
# update texture
self._upd_texture(buff)
def _upd_texture(self, buff):
texture = self._qrtexture
texture.blit_buffer(buff, colorfmt='rgb', bufferfmt='ubyte')
img = self.ids.qrimage
img.anim_delay = -1
img.texture = texture
img.canvas.ask_update()
if __name__ == '__main__':
from kivy.app import runTouchApp
import sys
data = str(sys.argv[1:])
runTouchApp(QRCodeWidget(data=data))
================================================
FILE: gui/kivy/uix/screens.py
================================================
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.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
from electrum import bitcoin
from electrum.util import timestamp_to_datetime
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
from .context_menu import ContextMenu
from electrum_gui.kivy.i18n import _
class EmptyLabel(Factory.Label):
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)
TX_ICONS = [
"close",
"close",
"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')
date = timestamp_to_datetime(timestamp)
ri = self.cards.get(tx_hash)
if ri is None:
ri = Factory.HistoryItem()
ri.screen = self
ri.tx_hash = tx_hash
self.cards[tx_hash] = ri
ri.icon = icon
ri.date = status_str
ri.message = label
ri.value = value or 0
ri.amount = self.app.format_amount(value, True) if value is not None else '--'
ri.confirmations = conf
if self.app.fiat_unit and date:
rate = self.app.fx.history_rate(date)
if rate:
s = self.app.fx.value_str(value, rate)
ri.quote_text = '' if s is None else s + ' ' + self.app.fiat_unit
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
history_card.clear_widgets()
count = 0
for item in history:
ri = self.get_card(*item)
count += 1
history_card.add_widget(ri)
if count == 0:
msg = _('This screen shows your list of transactions. It is currently empty.')
history_card.add_widget(EmptyLabel(text=msg))
class SendScreen(CScreen):
kvname = 'send'
payment_request = None
def set_URI(self, text):
import electrum
try:
uri = electrum.util.parse_URI(text, self.app.on_pr)
except:
self.app.show_info(_("Not a BTCP 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):
pass
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 sould 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.update_tab('invoices')
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 BTCP address or a payment request'))
return
if not bitcoin.is_address(address):
self.app.show_error(_('Invalid BTCP 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),
]
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 BTCP 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
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)
self.app.wallet.add_payment_request(req, self.app.electrum_config)
self.app.update_tab('requests')
def on_amount_or_message(self):
self.save_request()
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.'))
else:
self.save_request()
self.app.show_info(_('New request added to your list.'))
invoice_text = {
PR_UNPAID:_('Pending'),
PR_UNKNOWN:_('Unknown'),
PR_PAID:_('Paid'),
PR_EXPIRED:_('Expired')
}
request_text = {
PR_UNPAID: _('Pending'),
PR_UNKNOWN: _('Unknown'),
PR_PAID: _('Received'),
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 InvoicesScreen(CScreen):
kvname = 'invoices'
cards = {}
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.screen.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)
if not _list:
msg = _('This screen shows the list of payment requests that have been sent to you. You may also use it to store contact addresses.')
invoices_list.add_widget(EmptyLabel(text=msg))
def do_pay(self, obj):
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 .dialogs.question import Question
def cb(result):
if result:
self.app.wallet.invoices.remove(obj.key)
self.app.update_tab('invoices')
d = Question(_('Delete invoice?'), cb)
d.open()
address_icon = {
'Pending' : 'atlas://gui/kivy/theming/light/important',
'Paid' : 'atlas://gui/kivy/theming/light/confirmed'
}
class AddressScreen(CScreen):
kvname = 'address'
cards = {}
def get_card(self, addr, balance, is_used, label):
ci = self.cards.get(addr)
if ci is None:
ci = Factory.AddressItem()
ci.screen = self
ci.address = addr
self.cards[addr] = ci
ci.memo = label
ci.amount = self.app.format_amount_and_units(balance)
request = self.app.wallet.get_payment_request(addr, self.app.electrum_config)
if is_used:
ci.status = _('Used')
elif request:
status, conf = self.app.wallet.get_request_status(addr)
requested_amount = request.get('amount')
# make sure that requested amount is > 0
if status == PR_PAID:
s = _('Request paid')
elif status == PR_UNPAID:
s = _('Request pending')
elif status == PR_EXPIRED:
s = _('Request expired')
else:
s = ''
ci.status = s + ': ' + self.app.format_amount_and_units(requested_amount)
else:
ci.status = _('Funded') if balance>0 else _('Unused')
return ci
def update(self):
self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)]
wallet = self.app.wallet
_list = wallet.get_change_addresses() if self.screen.show_change else wallet.get_receiving_addresses()
search = self.screen.message
container = self.screen.ids.search_container
container.clear_widgets()
n = 0
for address in _list:
label = wallet.labels.get(address, '')
balance = sum(wallet.get_addr_balance(address))
is_used = wallet.is_used(address)
if self.screen.show_used == 1 and (balance or is_used):
continue
if self.screen.show_used == 2 and balance == 0:
continue
if self.screen.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
container.add_widget(card)
n += 1
if not n:
msg = _('No address matching your search')
container.add_widget(EmptyLabel(text=msg))
def do_show(self, obj):
self.app.show_request(obj.address)
def do_view(self, obj):
req = self.app.wallet.get_payment_request(obj.address, self.app.electrum_config)
if req:
c, u, x = self.app.wallet.get_addr_balance(obj.address)
balance = c + u + x
if balance > 0:
req['fund'] = balance
status = req.get('status')
amount = req.get('amount')
address = req['address']
if amount:
status = req.get('status')
status = request_text[status]
else:
received_amount = self.app.wallet.get_addr_received(address)
status = self.app.format_amount_and_units(received_amount)
self.app.show_pr_details(req, status, False)
else:
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 do_delete(self, obj):
from .dialogs.question import Question
def cb(result):
if result:
self.app.wallet.remove_payment_request(obj.address, self.app.electrum_config)
self.update()
d = Question(_('Delete request?'), cb)
d.open()
def ext_search(self, card, search):
return card.memo.find(search) >= 0 or card.amount.find(search) >= 0
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)
================================================
FILE: gui/kivy/uix/ui_screens/about.kv
================================================
#:import VERSION electrum.version.ELECTRUM_VERSION
Popup:
title: _("About Electrum")
BoxLayout:
orientation: 'vertical'
spacing: '10dp'
padding: '10dp'
GridLayout:
cols: 2
spacing: '10dp'
TopLabel:
text: _('Version')
size_hint_x: 0.4
TopLabel:
text: VERSION
size_hint_x: 0.6
TopLabel:
text: _('Licence')
size_hint_x: 0.4
TopLabel:
text: "MIT Licence"
size_hint_x: 0.6
TopLabel:
text: _('Homepage')
size_hint_x: 0.4
TopLabel:
markup: True
text: '[color=6666ff][ref=x]https://electrum.org[/ref][/color]'
on_ref_press:
import webbrowser
webbrowser.open("https://electrum.org")
size_hint_x: 0.6
TopLabel:
text: _('Developers')
size_hint_x: 0.4
TopLabel:
text: '\n'.join(['Thomas Voegtlin', 'Neil Booth', 'Akshay Arora'])
size_hint_x: 0.6
TopLabel:
text: _('Distributed by Electrum Technologies GmbH')
padding: '0dp', '20dp'
Widget:
size_hint: None, 0.5
BoxLayout:
size_hint: 1, None
height: '48dp'
Widget:
size_hint: 0.5, None
height: '48dp'
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Close')
on_release: root.dismiss()
================================================
FILE: gui/kivy/uix/ui_screens/address.kv
================================================
#: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'
text_size: self.width, None
halign: 'left'
valign: 'top'
address: ''
memo: ''
amount: ''
status: ''
BoxLayout:
spacing: '8dp'
height: '32dp'
orientation: 'vertical'
Widget
AddressLabel:
text: root.address
shorten: True
Widget
AddressLabel:
text: root.memo
color: .699, .699, .699, 1
font_size: '13sp'
shorten: True
Widget
BoxLayout:
spacing: '8dp'
height: '32dp'
orientation: 'vertical'
Widget
AddressLabel:
text: root.amount
halign: 'right'
font_size: '15sp'
Widget
AddressLabel:
text: root.status
halign: 'right'
font_size: '13sp'
color: .699, .699, .699, 1
AddressScreen:
id: addr_screen
name: 'address'
message: ''
pr_status: 'Pending'
show_change: False
show_used: 0
on_message:
self.parent.update()
BoxLayout
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: _('Change') if root.show_change else _('Receiving')
on_release:
root.show_change = not root.show_change
Clock.schedule_once(lambda dt: app.address_screen.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: app.address_screen.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(addr_screen))
ScrollView:
GridLayout:
cols: 1
id: search_container
size_hint_y: None
height: self.minimum_height
spacing: '2dp'
================================================
FILE: gui/kivy/uix/ui_screens/history.kv
================================================
#: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)
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'
icon: 'atlas://gui/kivy/theming/light/important'
message: ''
value: 0
amount: '--'
amount_color: '#FF6657' if self.value < 0 else '#2EA442'
confirmations: 0
date: ''
quote_text: ''
spacing: '9dp'
Image:
id: icon
source: root.icon
size_hint: None, 1
width: self.height *.54
mipmap: True
BoxLayout:
orientation: 'vertical'
Widget
CardLabel:
text: root.date
font_size: '14sp'
CardLabel:
color: .699, .699, .699, 1
font_size: '13sp'
shorten: True
text: root.message
Widget
CardLabel:
halign: 'right'
font_size: '15sp'
size_hint: None, 1
width: '110sp'
markup: True
font_name: font_light
text:
u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\
u'[color=#B2B3B3][size=13sp]{qt}[/size]'\
u'[/color]'.format(amount_color=root.amount_color,\
amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\
unit=app.base_unit)
HistoryScreen:
name: 'history'
content: content
ScrollView:
id: content
do_scroll_x: False
GridLayout
id: history_container
cols: 1
size_hint: 1, None
height: self.minimum_height
padding: '12dp'
spacing: '2dp'
================================================
FILE: gui/kivy/uix/ui_screens/invoice.kv
================================================
#:import Decimal decimal.Decimal
Popup:
id: popup
is_invoice: True
amount: 0
requestor: ''
exp: ''
description: ''
status: ''
signature: ''
isaddr: ''
fund: 0
pk: ''
title: _('Invoice') if popup.is_invoice else _('Request')
tx_hash: ''
BoxLayout:
orientation: 'vertical'
ScrollView:
GridLayout:
cols: 1
height: self.minimum_height
size_hint_y: None
padding: '10dp'
spacing: '10dp'
GridLayout:
cols: 1
size_hint_y: None
height: self.minimum_height
spacing: '10dp'
BoxLabel:
text: (_('Status') if popup.amount or popup.is_invoice or popup.isaddr == 'y' else _('Amount received')) if root.status else ''
value: root.status
BoxLabel:
text: _('Request Amount') if root.amount else ''
value: app.format_amount_and_units(root.amount) if root.amount else ''
BoxLabel:
text: _('Requested By') if popup.is_invoice else _('Address')
value: root.requestor
BoxLabel:
text: _('Signature') if root.signature else ''
value: root.signature
BoxLabel:
text: _('Expiration') if root.exp else ''
value: root.exp
BoxLabel:
text: _('Description') if root.description else ''
value: root.description
BoxLabel:
text: _('Balance') if popup.fund else ''
value: app.format_amount_and_units(root.fund) if root.fund else ''
TopLabel:
text: _('Private Key')
RefLabel:
id: pk_label
touched: True if not self.touched else True
data: root.pk
TopLabel:
text: _('Outputs') if popup.is_invoice else ''
OutputList:
id: output_list
TopLabel:
text: _('Transaction ID') if popup.tx_hash else ''
TxHashLabel:
data: popup.tx_hash
name: _('Transaction ID')
Widget:
size_hint: 1, 0.1
BoxLayout:
size_hint: 1, None
height: '48dp'
Widget:
size_hint: 0.5, None
height: '48dp'
Button:
size_hint: 2, None
height: '48dp'
text: _('Close')
on_release: popup.dismiss()
Button:
size_hint: 2, None
height: '48dp'
text: _('Hide private key') if pk_label.data else _('Export private key')
on_release:
setattr(pk_label, 'data', '') if pk_label.data else popup.export(pk_label, popup.requestor)
================================================
FILE: gui/kivy/uix/ui_screens/invoices.kv
================================================
#color: .305, .309, .309, 1
text_size: self.width, None
halign: 'left'
valign: 'top'
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
InvoicesScreen:
name: 'invoices'
BoxLayout:
orientation: 'vertical'
spacing: '1dp'
ScrollView:
GridLayout:
cols: 1
id: invoices_container
size_hint: 1, None
height: self.minimum_height
spacing: '2dp'
padding: '12dp'
================================================
FILE: gui/kivy/uix/ui_screens/network.kv
================================================
Popup:
id: nd
title: _('Network')
BoxLayout:
orientation: 'vertical'
ScrollView:
GridLayout:
id: scrollviewlayout
cols:1
size_hint: 1, None
height: self.minimum_height
padding: '10dp'
SettingsItem:
value: _("%d connections.")% app.num_nodes if app.num_nodes else _("Not connected")
title: _("Status") + ': ' + self.value
description: _("Connections with Electrum servers")
action: lambda x: None
CardSeparator
SettingsItem:
title: _("Server") + ': ' + app.server_host
description: _("Server used to query your history.")
action: lambda x: app.popup_dialog('server')
CardSeparator
SettingsItem:
proxy: app.proxy_config.get('mode')
host: app.proxy_config.get('host')
port: app.proxy_config.get('port')
title: _("Proxy") + ': ' + ((self.host +':' + self.port) if self.proxy else _('None'))
description: _('Proxy configuration')
action: lambda x: app.popup_dialog('proxy')
CardSeparator
SettingsItem:
title: _("Auto-connect") + ': ' + ('ON' if app.auto_connect else 'OFF')
description: _("Select your server automatically")
action: app.toggle_auto_connect
CardSeparator
SettingsItem:
value: "%d blocks" % app.num_blocks
title: _("Blockchain") + ': ' + self.value
description: _('Verified block headers')
action: lambda x: x
CardSeparator
SettingsItem:
title: _('Fork detected at block %d')%app.blockchain_checkpoint if app.num_chains>1 else _('No fork detected')
fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name
description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain')
action: app.choose_blockchain_dialog
disabled: app.num_chains == 1
================================================
FILE: gui/kivy/uix/ui_screens/proxy.kv
================================================
Popup:
id: nd
title: _('Proxy')
BoxLayout:
orientation: 'vertical'
padding: '10dp'
spacing: '10dp'
GridLayout:
cols: 2
Label:
text: _('Proxy mode')
Spinner:
id: mode
height: '48dp'
size_hint_y: None
text: app.proxy_config.get('mode', 'none')
values: ['none', 'socks4', 'socks5', 'http']
Label:
text: _('Host')
TextInput:
id: host
multiline: False
height: '48dp'
size_hint_y: None
text: app.proxy_config.get('host', '')
disabled: mode.text == 'none'
Label:
text: _('Port')
TextInput:
id: port
multiline: False
input_type: 'number'
height: '48dp'
size_hint_y: None
text: app.proxy_config.get('port', '')
disabled: mode.text == 'none'
Label:
text: _('Username')
TextInput:
id: user
multiline: False
height: '48dp'
size_hint_y: None
text: app.proxy_config.get('user', '')
disabled: mode.text == 'none'
Label:
text: _('Password')
TextInput:
id: password
multiline: False
password: True
height: '48dp'
size_hint_y: None
text: app.proxy_config.get('password', '')
disabled: mode.text == 'none'
Widget:
size_hint: 1, 0.1
BoxLayout:
Widget:
size_hint: 0.5, None
Button:
size_hint: 0.5, None
height: '48dp'
text: _('OK')
on_release:
host, port, protocol, proxy, auto_connect = app.network.get_parameters()
proxy = {}
proxy['mode']=str(root.ids.mode.text).lower()
proxy['host']=str(root.ids.host.text)
proxy['port']=str(root.ids.port.text)
proxy['user']=str(root.ids.user.text)
proxy['password']=str(root.ids.password.text)
if proxy['mode']=='none': proxy = None
app.network.set_parameters(host, port, protocol, proxy, auto_connect)
app.proxy_config = proxy if proxy else {}
nd.dismiss()
================================================
FILE: gui/kivy/uix/ui_screens/receive.kv
================================================
#: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 _('BTCP Address')
shorten: True
disabled: True
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'
Button:
text: _('Copy')
size_hint: 1, None
height: '48dp'
on_release: s.parent.do_copy()
Button:
text: _('Share')
size_hint: 1, None
height: '48dp'
on_release: s.parent.do_share()
Button:
text: _('New')
size_hint: 1, None
height: '48dp'
on_release: Clock.schedule_once(lambda dt: s.parent.do_new())
================================================
FILE: gui/kivy/uix/ui_screens/requests.kv
================================================
#color: .305, .309, .309, 1
text_size: self.width, None
halign: 'left'
valign: 'top'
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
RequestsScreen:
name: 'requests'
BoxLayout:
orientation: 'vertical'
spacing: '1dp'
ScrollView:
GridLayout:
cols: 1
id: requests_container
size_hint_y: None
height: self.minimum_height
spacing: '2dp'
padding: '12dp'
================================================
FILE: gui/kivy/uix/ui_screens/send.kv
================================================
#: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.')))
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))
BoxLayout:
size_hint: 1, None
height: '48dp'
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'
Button:
text: _('Paste')
on_release: s.parent.do_paste()
Button:
text: _('Clear')
on_release: s.parent.do_clear()
IconButton:
size_hint: 0.6, 1
on_release: s.parent.do_save()
icon: 'atlas://gui/kivy/theming/light/save'
BoxLayout:
size_hint: 1, None
height: '48dp'
Widget:
size_hint: 2, 1
Button:
text: _('Pay')
size_hint: 1, 1
on_release: s.parent.do_send()
Widget:
size_hint: 1, 1
================================================
FILE: gui/kivy/uix/ui_screens/server.kv
================================================
Popup:
id: nd
title: _('Server')
BoxLayout:
orientation: 'vertical'
padding: '10dp'
spacing: '10dp'
TopLabel:
text: _("Electrum requests your transaction history from a single server. The returned history is checked against blockchain headers sent by other nodes, using Simple Payment Verification (SPV).")
font_size: '6pt'
Widget:
size_hint: 1, 0.8
GridLayout:
cols: 2
Label:
height: '36dp'
size_hint_x: 1
size_hint_y: None
text: _('Host') + ':'
TextInput:
id: host
multiline: False
height: '36dp'
size_hint_x: 3
size_hint_y: None
text: app.server_host
Label:
height: '36dp'
size_hint_x: 1
size_hint_y: None
text: _('Port') + ':'
TextInput:
id: port
multiline: False
input_type: 'number'
height: '36dp'
size_hint_x: 3
size_hint_y: None
text: app.server_port
Widget
Button:
id: chooser
text: _('Choose from peers')
height: '36dp'
size_hint_x: 0.5
size_hint_y: None
on_release:
app.choose_server_dialog(root)
Widget:
size_hint: 1, 0.1
BoxLayout:
Widget:
size_hint: 0.5, None
Button:
size_hint: 0.5, None
height: '48dp'
text: _('OK')
on_release:
host, port, protocol, proxy, auto_connect = app.network.get_parameters()
host = str(root.ids.host.text)
port = str(root.ids.port.text)
app.network.set_parameters(host, port, protocol, proxy, auto_connect)
nd.dismiss()
================================================
FILE: gui/kivy/uix/ui_screens/status.kv
================================================
Popup:
title: "Electrum"
confirmed: 0
unconfirmed: 0
unmatured: 0
watching_only: app.wallet.is_watching_only()
on_parent:
self.confirmed, self.unconfirmed, self.unmatured = app.wallet.get_balance()
BoxLayout:
orientation: 'vertical'
ScrollView:
GridLayout:
cols: 1
height: self.minimum_height
size_hint_y: None
padding: '10dp'
spacing: '10dp'
padding: '10dp'
spacing: '10dp'
GridLayout:
cols: 1
size_hint_y: None
height: self.minimum_height
spacing: '10dp'
BoxLabel:
text: _('Wallet Name')
value: app.wallet_name()
BoxLabel:
text: _("Wallet type:")
value: app.wallet.wallet_type
BoxLabel:
text: _("Balance") + ':'
value: app.format_amount_and_units(root.confirmed + root.unconfirmed + root.unmatured)
BoxLabel:
text: _("Confirmed") + ':'
opacity: 1 if root.confirmed else 0
value: app.format_amount_and_units(root.confirmed)
opacity: 1 if root.confirmed else 0
BoxLabel:
text: _("Unconfirmed") + ':'
opacity: 1 if root.unconfirmed else 0
value: app.format_amount_and_units(root.unconfirmed)
BoxLabel:
text: _("Unmatured") + ':'
opacity: 1 if root.unmatured else 0
value: app.format_amount_and_units(root.unmatured)
opacity: 1 if root.unmatured else 0
TopLabel:
text: _('Master Public Key')
RefLabel:
data: app.wallet.get_master_public_key() or 'None'
name: _('Master Public Key')
TopLabel:
id: seed_label
text: _('This wallet is watching-only') if root.watching_only else ''
BoxLayout:
size_hint: 1, None
height: '48dp'
Button:
size_hint: 0.5, None
height: '48dp'
text: '' if root.watching_only else (_('Hide seed') if seed_label.text else _('Show seed'))
disabled: root.watching_only
on_release:
setattr(seed_label, 'text', '') if seed_label.text else app.show_seed(seed_label)
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Delete')
on_release:
root.dismiss()
app.delete_wallet()
================================================
FILE: gui/qt/__init__.py
================================================
#!/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
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.synchronizer import Synchronizer
# from electrum.verifier import SPV
# from electrum.util import DebugMem
from electrum.util import UserCancelled, print_error
# from electrum.wallet import Abstract_Wallet
from .installwizard import InstallWizard, GoBack
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)
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)
run_hook('init_qt', self)
ColorScheme.update_from_widget(QWidget())
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):
'''Raises the window for the wallet if it is open. Otherwise
opens the wallet and creates a new window for it.'''
for w in self.windows:
if w.wallet.storage.path == path:
w.bring_to_top()
break
else:
try:
wallet = self.daemon.load_wallet(path, None)
except BaseException as e:
d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e))
d.exec_()
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()
except UserCancelled:
pass
except GoBack as e:
print_error('[start_new_window] Exception caught (GoBack)', e)
wizard.terminate()
if not wallet:
return
wallet.start_threads(self.daemon.network)
self.daemon.add_wallet(wallet)
w = self.create_window_for_wallet(wallet)
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:
import traceback
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')):
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()
================================================
FILE: gui/qt/address_dialog.py
================================================
#!/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.
from electrum.i18n import _
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from .util import *
from .history_list import HistoryList
from .qrtextedit import ShowQRTextEdit
class AddressDialog(WindowModalDialog):
def __init__(self, parent, address):
WindowModalDialog.__init__(self, parent, _("Address"))
self.address = address
self.parent = parent
self.config = parent.config
self.wallet = parent.wallet
self.app = parent.app
self.saved = True
self.setMinimumWidth(700)
vbox = QVBoxLayout()
self.setLayout(vbox)
vbox.addWidget(QLabel(_("Address:")))
self.addr_e = ButtonsLineEdit(self.address)
self.addr_e.addCopyButton(self.app)
icon = ":icons/qrcode_white.png" if ColorScheme.dark_scheme else ":icons/qrcode.png"
self.addr_e.addButton(icon, self.show_qr, _("Show QR Code"))
self.addr_e.setReadOnly(True)
vbox.addWidget(self.addr_e)
try:
pubkeys = self.wallet.get_public_keys(address)
except BaseException as e:
pubkeys = None
if pubkeys:
vbox.addWidget(QLabel(_("Public keys") + ':'))
for pubkey in pubkeys:
pubkey_e = ButtonsLineEdit(pubkey)
pubkey_e.addCopyButton(self.app)
vbox.addWidget(pubkey_e)
try:
redeem_script = self.wallet.pubkeys_to_redeem_script(pubkeys)
except BaseException as e:
redeem_script = None
if redeem_script:
vbox.addWidget(QLabel(_("Redeem Script") + ':'))
redeem_e = ShowQRTextEdit(text=redeem_script)
redeem_e.addCopyButton(self.app)
vbox.addWidget(redeem_e)
vbox.addWidget(QLabel(_("History")))
self.hw = HistoryList(self.parent)
self.hw.get_domain = self.get_domain
vbox.addWidget(self.hw)
vbox.addLayout(Buttons(CloseButton(self)))
self.format_amount = self.parent.format_amount
self.hw.update()
def get_domain(self):
return [self.address]
def show_qr(self):
text = self.address
try:
self.parent.show_qrcode(text, 'Address', parent=self)
except Exception as e:
self.show_message(str(e))
================================================
FILE: gui/qt/address_list.py
================================================
#!/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 .util import *
from electrum.i18n import _
from electrum.util import block_explorer_URL
from electrum.plugins import run_hook
from electrum.bitcoin import is_address
class AddressList(MyTreeWidget):
filter_columns = [0, 1, 2] # Address, Label, Balance
def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [], 1)
self.refresh_headers()
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.show_change = False
self.show_used = 0
self.change_button = QComboBox(self)
self.change_button.currentIndexChanged.connect(self.toggle_change)
for t in [_('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_list_header(self):
return QLabel(_("Filter ")), self.change_button, self.used_button
def refresh_headers(self):
headers = [ _('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, show):
show = bool(show)
if show == self.show_change:
return
self.show_change = show
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
addr_list = self.wallet.get_change_addresses() if self.show_change else self.wallet.get_receiving_addresses()
self.clear()
for address in addr_list:
num = len(self.wallet.history.get(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)
fx = self.parent.fx
if fx and fx.get_fiat_address_config():
rate = fx.exchange_rate()
fiat_balance = fx.value_str(balance, rate)
address_item = QTreeWidgetItem([address, label, balance_text, fiat_balance, "%d"%num])
address_item.setTextAlignment(3, Qt.AlignRight)
else:
address_item = QTreeWidgetItem([address, label, balance_text, "%d"%num])
address_item.setTextAlignment(2, Qt.AlignRight)
address_item.setFont(0, QFont(MONOSPACE_FONT))
address_item.setData(0, Qt.UserRole, address)
address_item.setData(0, Qt.UserRole+1, True) # label can be edited
if self.wallet.is_frozen(address):
address_item.setBackground(0, ColorScheme.BLUE.as_color(True))
if self.wallet.is_beyond_limit(address, self.show_change):
address_item.setBackground(0, ColorScheme.RED.as_color(True))
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(0) 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 %s")%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 %s")%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
================================================
FILE: gui/qt/amountedit.py
================================================
# -*- coding: utf-8 -*-
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame)
from decimal import Decimal
from electrum.util import format_satoshis_plain
class MyLineEdit(QLineEdit):
frozen = pyqtSignal()
def setFrozen(self, b):
self.setReadOnly(b)
self.setFrame(not b)
self.frozen.emit()
class AmountEdit(MyLineEdit):
shortcut = pyqtSignal()
def __init__(self, base_unit, is_int = False, parent=None):
QLineEdit.__init__(self, parent)
# This seems sufficient for hundred-BTC amounts with 8 decimals
self.setFixedWidth(140)
self.base_unit = base_unit
self.textChanged.connect(self.numbify)
self.is_int = is_int
self.is_shortcut = False
self.help_palette = QPalette()
def decimal_point(self):
return 8
def numbify(self):
text = self.text().strip()
if text == '!':
self.shortcut.emit()
return
pos = self.cursorPosition()
chars = '0123456789'
if not self.is_int: chars +='.'
s = ''.join([i for i in text if i in chars])
if not self.is_int:
if '.' in s:
p = s.find('.')
s = s.replace('.','')
s = s[:p] + '.' + s[p:p+self.decimal_point()]
self.setText(s)
# setText sets Modified to False. Instead we want to remember
# if updates were because of user modification.
self.setModified(self.hasFocus())
self.setCursorPosition(pos)
def paintEvent(self, event):
QLineEdit.paintEvent(self, event)
if self.base_unit:
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(self.help_palette.brush(QPalette.Disabled, QPalette.Text).color())
painter.drawText(textRect, Qt.AlignRight | Qt.AlignVCenter, self.base_unit())
def get_amount(self):
try:
return (int if self.is_int else Decimal)(str(self.text()))
except:
return None
def setAmount(self, x):
self.setText("%d"%x)
class BTCAmountEdit(AmountEdit):
def __init__(self, decimal_point, is_int = False, parent=None):
AmountEdit.__init__(self, self._base_unit, is_int, parent)
self.decimal_point = decimal_point
def _base_unit(self):
p = self.decimal_point()
if p == 8:
return 'BTCP'
if p == 5:
return 'mBTCP'
if p == 2:
return 'bits'
raise Exception('Unknown base unit')
def get_amount(self):
try:
x = Decimal(str(self.text()))
except:
return None
p = pow(10, self.decimal_point())
return int( p * x )
def setAmount(self, amount):
if amount is None:
self.setText(" ") # Space forces repaint in case units changed
else:
self.setText(format_satoshis_plain(amount, self.decimal_point()))
class FeerateEdit(BTCAmountEdit):
def _base_unit(self):
p = self.decimal_point()
if p == 2:
return 'mBTCP/kB'
if p == 0:
return 'sat/byte'
raise Exception('Unknown base unit')
def get_amount(self):
sat_per_byte_amount = BTCAmountEdit.get_amount(self)
if sat_per_byte_amount is None:
return None
return 1000 * sat_per_byte_amount
================================================
FILE: gui/qt/console.py
================================================
# source: http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget
import sys, os, re
import traceback, platform
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from electrum import util
if platform.system() == 'Windows':
MONOSPACE_FONT = 'Lucida Console'
elif platform.system() == 'Darwin':
MONOSPACE_FONT = 'Monaco'
else:
MONOSPACE_FONT = 'monospace'
class Console(QtWidgets.QPlainTextEdit):
def __init__(self, prompt='>> ', startup_message='', parent=None):
QtWidgets.QPlainTextEdit.__init__(self, parent)
self.prompt = prompt
self.history = []
self.namespace = {}
self.construct = []
self.setGeometry(50, 75, 600, 400)
self.setWordWrapMode(QtGui.QTextOption.WrapAnywhere)
self.setUndoRedoEnabled(False)
self.document().setDefaultFont(QtGui.QFont(MONOSPACE_FONT, 10, QtGui.QFont.Normal))
self.showMessage(startup_message)
self.updateNamespace({'run':self.run_script})
self.set_json(False)
def set_json(self, b):
self.is_json = b
def run_script(self, filename):
with open(filename) as f:
script = f.read()
# eval is generally considered bad practice. use it wisely!
result = eval(script, self.namespace, self.namespace)
def updateNamespace(self, namespace):
self.namespace.update(namespace)
def showMessage(self, message):
self.appendPlainText(message)
self.newPrompt()
def clear(self):
self.setPlainText('')
self.newPrompt()
def newPrompt(self):
if self.construct:
prompt = '.' * len(self.prompt)
else:
prompt = self.prompt
self.completions_pos = self.textCursor().position()
self.completions_visible = False
self.appendPlainText(prompt)
self.moveCursor(QtGui.QTextCursor.End)
def getCommand(self):
doc = self.document()
curr_line = doc.findBlockByLineNumber(doc.lineCount() - 1).text()
curr_line = curr_line.rstrip()
curr_line = curr_line[len(self.prompt):]
return curr_line
def setCommand(self, command):
if self.getCommand() == command:
return
doc = self.document()
curr_line = doc.findBlockByLineNumber(doc.lineCount() - 1).text()
self.moveCursor(QtGui.QTextCursor.End)
for i in range(len(curr_line) - len(self.prompt)):
self.moveCursor(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor)
self.textCursor().removeSelectedText()
self.textCursor().insertText(command)
self.moveCursor(QtGui.QTextCursor.End)
def show_completions(self, completions):
if self.completions_visible:
self.hide_completions()
c = self.textCursor()
c.setPosition(self.completions_pos)
completions = map(lambda x: x.split('.')[-1], completions)
t = '\n' + ' '.join(completions)
if len(t) > 500:
t = t[:500] + '...'
c.insertText(t)
self.completions_end = c.position()
self.moveCursor(QtGui.QTextCursor.End)
self.completions_visible = True
def hide_completions(self):
if not self.completions_visible:
return
c = self.textCursor()
c.setPosition(self.completions_pos)
l = self.completions_end - self.completions_pos
for x in range(l): c.deleteChar()
self.moveCursor(QtGui.QTextCursor.End)
self.completions_visible = False
def getConstruct(self, command):
if self.construct:
prev_command = self.construct[-1]
self.construct.append(command)
if not prev_command and not command:
ret_val = '\n'.join(self.construct)
self.construct = []
return ret_val
else:
return ''
else:
if command and command[-1] == (':'):
self.construct.append(command)
return ''
else:
return command
def getHistory(self):
return self.history
def setHisory(self, history):
self.history = history
def addToHistory(self, command):
if command[0:1] == ' ':
return
if command and (not self.history or self.history[-1] != command):
self.history.append(command)
self.history_index = len(self.history)
def getPrevHistoryEntry(self):
if self.history:
self.history_index = max(0, self.history_index - 1)
return self.history[self.history_index]
return ''
def getNextHistoryEntry(self):
if self.history:
hist_len = len(self.history)
self.history_index = min(hist_len, self.history_index + 1)
if self.history_index < hist_len:
return self.history[self.history_index]
return ''
def getCursorPosition(self):
c = self.textCursor()
return c.position() - c.block().position() - len(self.prompt)
def setCursorPosition(self, position):
self.moveCursor(QtGui.QTextCursor.StartOfLine)
for i in range(len(self.prompt) + position):
self.moveCursor(QtGui.QTextCursor.Right)
def register_command(self, c, func):
methods = { c: func}
self.updateNamespace(methods)
def runCommand(self):
command = self.getCommand()
self.addToHistory(command)
command = self.getConstruct(command)
if command:
tmp_stdout = sys.stdout
class stdoutProxy():
def __init__(self, write_func):
self.write_func = write_func
self.skip = False
def flush(self):
pass
def write(self, text):
if not self.skip:
stripped_text = text.rstrip('\n')
self.write_func(stripped_text)
QtCore.QCoreApplication.processEvents()
self.skip = not self.skip
if type(self.namespace.get(command)) == type(lambda:None):
self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command))
self.newPrompt()
return
sys.stdout = stdoutProxy(self.appendPlainText)
try:
try:
# eval is generally considered bad practice. use it wisely!
result = eval(command, self.namespace, self.namespace)
if result != None:
if self.is_json:
util.print_msg(util.json_encode(result))
else:
self.appendPlainText(repr(result))
except SyntaxError:
# exec is generally considered bad practice. use it wisely!
exec(command, self.namespace, self.namespace)
except SystemExit:
self.close()
except Exception:
traceback_lines = traceback.format_exc().split('\n')
# Remove traceback mentioning this file, and a linebreak
for i in (3,2,1,-1):
traceback_lines.pop(i)
self.appendPlainText('\n'.join(traceback_lines))
sys.stdout = tmp_stdout
self.newPrompt()
self.set_json(False)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Tab:
self.completions()
return
self.hide_completions()
if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.runCommand()
return
if event.key() == QtCore.Qt.Key_Home:
self.setCursorPosition(0)
return
if event.key() == QtCore.Qt.Key_PageUp:
return
elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Backspace):
if self.getCursorPosition() == 0:
return
elif event.key() == QtCore.Qt.Key_Up:
self.setCommand(self.getPrevHistoryEntry())
return
elif event.key() == QtCore.Qt.Key_Down:
self.setCommand(self.getNextHistoryEntry())
return
elif event.key() == QtCore.Qt.Key_L and event.modifiers() == QtCore.Qt.ControlModifier:
self.clear()
super(Console, self).keyPressEvent(event)
def completions(self):
cmd = self.getCommand()
lastword = re.split(' |\(|\)',cmd)[-1]
beginning = cmd[0:-len(lastword)]
path = lastword.split('.')
ns = self.namespace.keys()
if len(path) == 1:
ns = ns
prefix = ''
else:
obj = self.namespace.get(path[0])
prefix = path[0] + '.'
ns = dir(obj)
completions = []
for x in ns:
if x[0] == '_':continue
xx = prefix + x
if xx.startswith(lastword):
completions.append(xx)
completions.sort()
if not completions:
self.hide_completions()
elif len(completions) == 1:
self.hide_completions()
self.setCommand(beginning + completions[0])
else:
# find common prefix
p = os.path.commonprefix(completions)
if len(p)>len(lastword):
self.hide_completions()
self.setCommand(beginning + p)
else:
self.show_completions(completions)
welcome_message = '''
---------------------------------------------------------------
Welcome to a primitive Python interpreter.
---------------------------------------------------------------
'''
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
console = Console(startup_message=welcome_message)
console.updateNamespace({'myVar1' : app, 'myVar2' : 1234})
console.show()
sys.exit(app.exec_())
================================================
FILE: gui/qt/contact_list.py
================================================
#!/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
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):
wallet_folder = self.parent.get_wallet_folder()
filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
if not filename:
return
self.parent.contacts.import_file(filename)
self.on_update()
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())
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 %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
if column in self.editable_columns:
item = self.currentItem()
menu.addAction(_("Edit %s")%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)
================================================
FILE: gui/qt/fee_slider.py
================================================
from electrum.i18n import _
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QSlider, QToolTip
import threading
class FeeSlider(QSlider):
def __init__(self, window, config, callback):
QSlider.__init__(self, Qt.Horizontal)
self.config = config
self.window = window
self.callback = callback
self.dyn = False
self.lock = threading.RLock()
self.update()
self.valueChanged.connect(self.moved)
def moved(self, pos):
with self.lock:
fee_rate = self.config.dynfee(pos) if self.dyn else self.config.static_fee(pos)
tooltip = self.get_tooltip(pos, fee_rate)
QToolTip.showText(QCursor.pos(), tooltip, self)
self.setToolTip(tooltip)
self.callback(self.dyn, pos, fee_rate)
def get_tooltip(self, pos, fee_rate):
from electrum.util import fee_levels
rate_str = self.window.format_fee_rate(fee_rate) if fee_rate else _('unknown')
if self.dyn:
tooltip = fee_levels[pos] + '\n' + rate_str
else:
tooltip = 'Fixed rate: ' + rate_str
if self.config.has_fee_estimates():
i = self.config.reverse_dynfee(fee_rate)
tooltip += '\n' + (_('Low fee') if i < 0 else 'Within %d blocks'%i)
return tooltip
def update(self):
with self.lock:
self.dyn = self.config.is_dynfee()
if self.dyn:
pos = self.config.get('fee_level', 2)
fee_rate = self.config.dynfee(pos)
self.setRange(0, 4)
self.setValue(pos)
else:
fee_rate = self.config.fee_per_kb()
pos = self.config.static_fee_index(fee_rate)
self.setRange(0, 9)
self.setValue(pos)
tooltip = self.get_tooltip(pos, fee_rate)
self.setToolTip(tooltip)
def activate(self):
self.setStyleSheet('')
def deactivate(self):
# TODO it would be nice to find a platform-independent solution
# that makes the slider look as if it was disabled
self.setStyleSheet(
"""
QSlider::groove:horizontal {
border: 1px solid #999999;
height: 8px;
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #B1B1B1, stop:1 #B1B1B1);
margin: 2px 0;
}
QSlider::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
border: 1px solid #5c5c5c;
width: 12px;
margin: -2px 0;
border-radius: 3px;
}
"""
)
================================================
FILE: gui/qt/history_list.py
================================================
#!/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 .util import *
from electrum.i18n import _
from electrum.util import block_explorer_URL
from electrum.util import timestamp_to_datetime, profiler
TX_ICONS = [
"warning.png",
"warning.png",
"warning.png",
"unconfirmed.png",
"unconfirmed.png",
"clock1.png",
"clock2.png",
"clock3.png",
"clock4.png",
"clock5.png",
"confirmed.png",
]
class HistoryList(MyTreeWidget):
filter_columns = [2, 3, 4] # Date, Description, Amount
def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
self.refresh_headers()
self.setColumnHidden(1, True)
def refresh_headers(self):
headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')]
fx = self.parent.fx
if fx and fx.show_history():
headers.extend(['%s '%fx.ccy + _('Amount'), '%s '%fx.ccy + _('Balance')])
self.update_headers(headers)
def get_domain(self):
'''Replaced in address_dialog.py'''
return self.wallet.get_addresses()
@profiler
def on_update(self):
self.wallet = self.parent.wallet
h = self.wallet.get_history(self.get_domain())
item = self.currentItem()
current_tx = item.data(0, Qt.UserRole) if item else None
self.clear()
fx = self.parent.fx
if fx: fx.history_used_spot = False
for h_item in h:
tx_hash, height, conf, timestamp, value, balance = h_item
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
has_invoice = self.wallet.invoices.paid.get(tx_hash)
icon = QIcon(":icons/" + TX_ICONS[status])
v_str = self.parent.format_amount(value, True, whitespaces=True)
balance_str = self.parent.format_amount(balance, whitespaces=True)
label = self.wallet.get_label(tx_hash)
entry = ['', tx_hash, status_str, label, v_str, balance_str]
if fx and fx.show_history():
date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp)
for amount in [value, balance]:
text = fx.historical_value_str(amount, date)
entry.append(text)
item = QTreeWidgetItem(entry)
item.setIcon(0, icon)
item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else ""))
if has_invoice:
item.setIcon(3, QIcon(":icons/seal"))
for i in range(len(entry)):
if i>3:
item.setTextAlignment(i, Qt.AlignRight)
if i!=2:
item.setFont(i, QFont(MONOSPACE_FONT))
if value and value < 0:
item.setForeground(3, QBrush(QColor("#BC1E1E")))
item.setForeground(4, QBrush(QColor("#BC1E1E")))
if tx_hash:
item.setData(0, Qt.UserRole, tx_hash)
self.insertTopLevelItem(0, item)
if current_tx == tx_hash:
self.setCurrentItem(item)
def on_doubleclick(self, item, column):
if self.permit_edit(item, column):
super(HistoryList, self).on_doubleclick(item, column)
else:
tx_hash = item.data(0, Qt.UserRole)
tx = self.wallet.transactions.get(tx_hash)
self.parent.show_transaction(tx)
def update_labels(self):
root = self.invisibleRootItem()
child_count = root.childCount()
for i in range(child_count):
item = root.child(i)
txid = item.data(0, Qt.UserRole)
label = self.wallet.get_label(txid)
item.setText(3, label)
def update_item(self, tx_hash, height, conf, timestamp):
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
icon = QIcon(":icons/" + TX_ICONS[status])
items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1)
if items:
item = items[0]
item.setIcon(0, icon)
item.setText(2, status_str)
def create_menu(self, position):
self.selectedIndexes()
item = self.currentItem()
if not item:
return
column = self.currentColumn()
tx_hash = item.data(0, Qt.UserRole)
if not tx_hash:
return
if column is 0:
column_title = "ID"
column_data = tx_hash
else:
column_title = self.headerItem().text(column)
column_data = item.text(column)
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
tx = self.wallet.transactions.get(tx_hash)
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
is_unconfirmed = height <= 0
pr_key = self.wallet.invoices.paid.get(tx_hash)
menu = QMenu()
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
if column in self.editable_columns:
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
if is_unconfirmed and tx:
rbf = is_mine and not tx.is_final()
if rbf:
menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx))
else:
child_tx = self.wallet.cpfp(tx, 0)
if child_tx:
menu.addAction(_("Child pays for parent"), lambda: self.parent.cpfp(tx, child_tx))
if pr_key:
menu.addAction(QIcon(":icons/seal"), _("View invoice"), lambda: self.parent.show_invoice(pr_key))
if tx_URL:
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
menu.exec_(self.viewport().mapToGlobal(position))
================================================
FILE: gui/qt/installwizard.py
================================================
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
from electrum.i18n import _
from .seed_dialog import SeedLayout, KeysLayout
from .network_dialog import NetworkChoiceLayout
from .util import *
from .password_dialog import PasswordLayout, PW_NEW
class GoBack(Exception):
pass
MSG_GENERATING_WAIT = _("Generating your addresses, please wait...")
MSG_ENTER_ANYTHING = _("Please enter a seed phrase, a master key, a list of "
"BTCP addresses, or a list of private keys")
MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or xprv):")
MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:")
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
+ _("Leave this field empty if you want to disable encryption.")
MSG_RESTORE_PASSPHRASE = \
_("Please enter your seed derivation passphrase. "
"Note: this is NOT your encryption password. "
"Leave this field empty if you did not use one or are unsure.")
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%s"%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
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, title=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):
return self.text_input(title, message, 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):
playout = PasswordLayout(None, msg, kind, self.next_button)
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):
"""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)
def show_restore(self, wallet, network):
# FIXME: these messages are shown after the install wizard is
# finished and the window closed. On MacOSX 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):
self.please_wait.setText(MSG_GENERATING_WAIT)
self.refresh_gui()
t = threading.Thread(target = task)
t.start()
t.join()
@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 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.setMaximumWidth(150)
hbox = QHBoxLayout()
hbox.addWidget(button, 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)
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)
================================================
FILE: gui/qt/invoice_list.py
================================================
#!/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 .util import *
from electrum.i18n import _
from electrum.util import format_time
class InvoiceList(MyTreeWidget):
filter_columns = [0, 1, 2, 3] # Date, Requested By, Description, Amount
def __init__(self, parent):
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Expires'), _('Requested By'), _('Description'), _('Amount'), _('Status')], 2)
self.setSortingEnabled(True)
self.header().setSectionResizeMode(1, QHeaderView.Interactive)
self.setColumnWidth(1, 200)
def on_update(self):
inv_list = self.parent.invoices.unpaid_invoices()
self.clear()
for pr in inv_list:
key = pr.get_id()
status = self.parent.invoices.get_status(key)
requestor = pr.get_requestor()
exp = pr.get_expiration_date()
date_str = format_time(exp) if exp else _('Never')
item = QTreeWidgetItem([date_str, requestor, pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')])
item.setIcon(4, QIcon(pr_icons.get(status)))
item.setData(0, Qt.UserRole, key)
item.setFont(1, QFont(MONOSPACE_FONT))
item.setFont(3, QFont(MONOSPACE_FONT))
self.addTopLevelItem(item)
self.setCurrentItem(self.topLevelItem(0))
self.setVisible(len(inv_list))
self.parent.invoices_label.setVisible(len(inv_list))
def import_invoices(self):
wallet_folder = self.parent.get_wallet_folder()
filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
if not filename:
return
self.parent.invoices.import_file(filename)
self.on_update()
def create_menu(self, position):
menu = QMenu()
item = self.itemAt(position)
if not item:
return
key = item.data(0, Qt.UserRole)
column = self.currentColumn()
column_title = self.headerItem().text(column)
column_data = item.text(column)
pr = self.parent.invoices.get(key)
status = self.parent.invoices.get_status(key)
if column_data:
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
if status == PR_UNPAID:
menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(key))
menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key))
menu.exec_(self.viewport().mapToGlobal(position))
================================================
FILE: gui/qt/main_window.py
================================================
#!/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.QtCore import Qt
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from electrum.util import bh2u, bfh
from electrum import keystore, simple_config
from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS, NetworkConstants
from electrum.plugins import run_hook
from electrum.i18n import _
from electrum.util import (format_time, format_satoshis, PrintError,
format_satoshis_plain, NotEnoughFunds,
UserCancelled, NoDynamicFeeEstimates)
from electrum import Transaction
from electrum import util, bitcoin, commands, coinchooser
from electrum import paymentrequest
from electrum.wallet import Multisig_Wallet
try:
from electrum.plot import plot_history
except:
plot_history = None
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 *
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.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', 8)
self.fee_unit = config.get('fee_unit', 0)
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 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):
'''Do the right thing in the presence of tx dialog windows'''
override = self.tl_windows[-1] if self.tl_windows else None
return self.top_level_window_recurse(override)
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):
traceback.print_exception(*exc_info)
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()
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)
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 = "[TESTNET] Bitcoin Private Electrum" if NetworkConstants.TESTNET else "Bitcoin Private 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.can_change_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 BTCP with it."),
_("Make sure you own the seed phrase or the private keys, before you request BTCP to be sent to this wallet.")
])
self.show_warning(msg, title=_('Information'))
def open_wallet(self):
wallet_folder = self.get_wallet_folder()
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 (IOError, os.error) 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):
wallet_folder = self.get_wallet_folder()
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 Opened"))
file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
file_menu.addAction(_("&Backup Wallet"), 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()
labels_menu = wallet_menu.addMenu(_("&Labels"))
labels_menu.addAction(_("&Import"), self.do_import_labels)
labels_menu.addAction(_("&Export"), self.do_export_labels)
contacts_menu = wallet_menu.addMenu(_("Contacts"))
contacts_menu.addAction(_("&New"), self.new_contact_dialog)
contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts())
invoices_menu = wallet_menu.addMenu(_("Invoices"))
invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices())
hist_menu = wallet_menu.addMenu(_("&History"))
hist_menu.addAction("Plot", self.plot_history_dialog).setEnabled(plot_history is not None)
hist_menu.addAction("Export", self.export_history_dialog)
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 OSX 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 Multiple Addresses"), 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://btcprivate.org"))
help_menu.addSeparator()
help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("https://github.com/z-classic/zclassic/wiki")).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 BTCP",
_("Version")+" %s" % (self.wallet.electrum_version) + "\n\n" +
_("Electrum BTCP's focus is speed, with low resource usage and simplifying Bitcoin Private. 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 Private 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: "),
"https://github.com/BTCPrivate/electrum-btcp/issues ",
_("Before reporting a bug, upgrade to the most recent version of Electrum-ZCL (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 BTCP - " + _("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 more then three
tx_amount = len(self.tx_notifications)
if(tx_amount >= 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(_("%(txs)s new transactions received: Total amount received in the new transactions %(amount)s") \
% { 'txs' : tx_amount, 'amount' : 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(_("Inbound Transaction - %(amount)s") % { 'amount' : self.format_amount_and_units(v)})
def notify(self, message):
if self.tray:
try:
# this requires Qt 5.9
self.tray.showMessage("Electrum BTCP", message, QIcon(":icons/electrum_dark_icon"), 20000)
except TypeError:
self.tray.showMessage("Electrum BTCP", 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, is_diff, self.num_zeros, self.decimal_point, 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 text and x:
text += ' (%s)'%x
return text
def format_fee_rate(self, fee_rate):
if self.fee_unit == 0:
return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' sat/byte'
else:
return self.format_amount(fee_rate) + ' ' + self.base_unit() + '/kB'
def get_decimal_point(self):
return self.decimal_point
def base_unit(self):
assert self.decimal_point in [2, 5, 8]
if self.decimal_point == 2:
return 'bits'
if self.decimal_point == 5:
return 'mBTCP'
if self.decimal_point == 8:
return 'BTCP'
raise Exception('Unknown base unit')
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 None
if rate is None 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 (%d blocks)"%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, True).strip())
if x:
text += " [%s unmatured]"%(self.format_amount(x, 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:
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
return l
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 = _('BTCP address where the payment should be received. Note that each payment request uses a different BTCP 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.NoFocus)
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 BTCP 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 = self.password_dialog(msg)
if password:
try:
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
except Exception as e:
self.show_error(str(e))
return
else:
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)
self.wallet.add_payment_request(req, self.config)
self.sign_payment_request(addr)
self.request_list.update()
self.address_list.update()
self.save_request_button.setEnabled(False)
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 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 BTCP 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 BTCP 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.setCompleter(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 = _('BTCP 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:
self.config.set_key('fee_level', pos, False)
else:
self.config.set_key('fee_per_kb', fee_rate, False)
if fee_rate:
self.feerate_e.setAmount(fee_rate // 1000)
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 not edit_changed.get_amount():
# 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: 2 if self.fee_unit else 0)
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))
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)
vbox_feecontrol = QVBoxLayout()
vbox_feecontrol.addWidget(self.fee_adv_controls)
vbox_feecontrol.addWidget(self.fee_slider)
grid.addLayout(vbox_feecontrol, 5, 1, 1, 3)
if not self.config.get('show_fee', True):
self.fee_adv_controls.setVisible(False)
self.preview_button = EnterButton(_("Preview"), self.do_preview)
self.preview_button.setToolTip(_('Display the details of your transactions 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(t):
self.is_max = False
self.max_button.setEnabled(not bool(t))
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):
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.
'''
if not self.config.get('offline') and self.config.is_dynfee() and not self.config.has_fee_estimates():
self.statusBar().showMessage(_('Waiting for fee estimates...'))
return False
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:
self.not_enough_funds = True
if not freeze_fee:
self.fee_e.setAmount(None)
return
except NoDynamicFeeEstimates:
tx = make_tx(0)
size = tx.estimated_size()
self.size_e.setAmount(size)
return
except BaseException:
traceback.print_exc(file=sys.stderr)
return
size = tx.estimated_size()
self.size_e.setAmount(size)
fee = tx.get_fee()
if not freeze_fee:
fee = None if self.not_enough_funds else fee
self.fee_e.setAmount(fee)
if not freeze_feerate:
fee_rate = fee // size if fee is not None else None
self.feerate_e.setAmount(fee_rate)
if self.is_max:
amount = tx.output_value()
self.amount_e.setAmount(amount)
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_password():
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()
amount = 0 if amount is None else float(amount)
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 "%s" could not be validated via an additional security check, DNSSEC, and thus may not be correct.'%alias) + '\n'
msg += _('Do you wish to continue?')
if not self.question(msg):
return
if not outputs:
self.show_error(_('No Recipients'))
return
for _type, addr, amount in outputs:
if addr is None:
self.show_error(_('No BTCP Address'))
return
if _type == TYPE_ADDRESS and not bitcoin.is_address(addr):
self.show_error(_('Invalid BTCP 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(_("This transaction requires a higher fee, or it will not be propagated by the network"))
return
if preview:
self.show_transaction(tx, tx_desc)
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 = 2 * self.config.max_fee_rate()
if fee > confirm_rate * tx.estimated_size() / 1000:
msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
if self.wallet.has_password():
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_signed(result):
callback(True)
def on_failed(exc_info):
self.on_error(exc_info)
callback(False)
if self.tx_external_keypairs:
# can sign directly
task = partial(Transaction.sign, tx, self.tx_external_keypairs)
else:
# call hook to see if plugin needs gui interaction
run_hook('sign_tx', self, tx)
task = partial(self.wallet.sign_transaction, tx, password)
WaitingDialog(self, _('Signing transaction...'), task,
on_signed, on_failed)
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(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()
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.amount_e, self.message_e]:
e.setFrozen(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 or not isinstance(URI, str):
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.size_e.setAmount(0)
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, list_header=None):
w = QWidget()
w.searchable_list = l
vbox = QVBoxLayout()
w.setLayout(vbox)
vbox.setContentsMargins(0, 0, 0, 0)
vbox.setSpacing(0)
if list_header:
hbox = QHBoxLayout()
for b in list_header:
hbox.addWidget(b)
hbox.addStretch()
vbox.addLayout(hbox)
vbox.addWidget(l)
return w
def create_addresses_tab(self):
from .address_list import AddressList
self.address_list = l = AddressList(self)
return self.create_list_tab(l, l.get_list_header())
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")+" %s "%addr +_("from your wallet?")):
self.wallet.delete_address(addr)
self.address_list.update()
self.history_list.update()
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 %s from your list of contacts?")
% " + ".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)
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(_("Requested By") + ':'), 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.can_change_password())
self.send_button.setVisible(not self.wallet.is_watching_only())
def change_password_dialog(self):
from .password_dialog import ChangePasswordDialog
d = ChangePasswordDialog(self, self.wallet)
ok, password, new_password, encrypt_file = d.run()
if not ok:
return
try:
self.wallet.update_password(password, new_password, encrypt_file)
except BaseException as e:
self.show_error(str(e))
return
except:
traceback.print_exc(file=sys.stdout)
self.show_error(_('Failed to update password'))
return
msg = _('Password was updated successfully') if new_password else _('Password is disabled, this wallet is not protected')
self.show_message(msg, title=_("Success"))
self.update_lock_icon()
def toggle_search(self):
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:" + 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)
if xtype in ['p2wpkh', 'p2wsh', 'p2wpkh-p2sh', 'p2wsh-p2sh']:
vbox.addWidget(WWLabel(_("Warning: the format of private keys associated to segwit addresses may not be compatible with other wallets")))
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 BTCP address.'))
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):
signature.setText(base64.b64encode(sig).decode('ascii'))
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 BTCP address.'))
return
try:
# This can throw on invalid base64
sig = base64.b64decode(str(signature.toPlainText()))
verified = bitcoin.verify_message(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):
cyphertext = encrypted_e.toPlainText()
task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password)
self.wallet.thread.add(task, on_success=lambda text: message_e.setText(text.decode('utf-8')))
def do_encrypt(self, message_e, pubkey_e, encrypted_e):
message = message_e.toPlainText()
message = message.encode('utf-8')
try:
encrypted = bitcoin.encrypt_message(message, pubkey_e.text())
encrypted_e.setText(encrypted.decode('ascii'))
except BaseException as e:
traceback.print_exc(file=sys.stdout)
self.show_warning(str(e))
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
data = bh2u(bitcoin.base_decode(data, length=None, base=43))
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):
from electrum.transaction import SerializationError
text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
if not text:
return
try:
tx = self.tx_from_text(text)
if tx:
self.show_transaction(tx)
except SerializationError as e:
self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
def do_process_from_file(self):
from electrum.transaction import SerializationError
try:
tx = self.read_tx_from_file()
if tx:
self.show_transaction(tx)
except SerializationError as e:
self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
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.synchronous_get(('blockchain.transaction.get',[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 can not be "backed up" by simply exporting these private keys.'))
d = WindowModalDialog(self, _('Private keys'))
d.setMinimumSize(850, 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):
labelsFile = self.getOpenFileName(_("Open Labels File"), "*.json")
if not labelsFile: return
try:
with open(labelsFile, 'r') as f:
data = f.read()
for key, value in json.loads(data).items():
self.wallet.set_label(key, value)
self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile))
except (IOError, os.error) as reason:
self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason))
self.address_list.update()
self.history_list.update()
def do_export_labels(self):
labels = self.wallet.labels
try:
fileName = self.getSaveFileName(_("Select destination file for your labels:"), 'electrum_labels.json', "*.json")
if fileName:
with open(fileName, 'w+') as f:
json.dump(labels, f, indent=4, sort_keys=True)
self.show_message(_("Your labels were exported to") + " '%s'" % str(fileName))
except (IOError, os.error) as reason:
self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason))
def export_history_dialog(self):
d = WindowModalDialog(self, _('Export History'))
d.setMinimumSize(400, 200)
vbox = QVBoxLayout(d)
defaultname = os.path.expanduser('~/electrum-history.csv')
select_msg = _('Select destination file for your wallet transaction history:')
hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
vbox.addLayout(hbox)
vbox.addStretch(1)
hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
vbox.addLayout(hbox)
run_hook('export_history_dialog', self, hbox)
self.update()
if not d.exec_():
return
filename = filename_e.text()
if not filename:
return
try:
self.do_export_history(self.wallet, filename, csv_button.isChecked())
except (IOError, os.error) as reason:
export_error_label = _("Electrum was unable to produce a transaction export.")
self.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
return
self.show_message(_("Your wallet history has been successfully exported."))
def plot_history_dialog(self):
if plot_history is None:
return
wallet = self.wallet
history = wallet.get_history()
if len(history) > 0:
plt = plot_history(self.wallet, history)
plt.show()
def do_export_history(self, wallet, fileName, is_csv):
history = wallet.get_history()
lines = []
for item in history:
tx_hash, height, confirmations, timestamp, value, balance = item
if height>0:
if timestamp is not None:
time_string = format_time(timestamp)
else:
time_string = _("unverified")
else:
time_string = _("unconfirmed")
if value is not None:
value_string = format_satoshis(value, True)
else:
value_string = '--'
if tx_hash:
label = wallet.get_label(tx_hash)
else:
label = ""
if is_csv:
lines.append([tx_hash, label, confirmations, value_string, time_string])
else:
lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
with open(fileName, "w+") as f:
if is_csv:
transaction = csv.writer(f, lineterminator='\n')
transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
for line in lines:
transaction.writerow(line)
else:
import json
f.write(json.dumps(lines, indent = 4))
def sweep_key_dialog(self):
d = WindowModalDialog(self, title=_('Sweep private keys'))
d.setMinimumSize(600, 300)
vbox = QVBoxLayout(d)
vbox.addWidget(QLabel(_("Enter private keys:")))
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, msg, func):
text = text_dialog(self, title, msg + ' :', _('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, msg = _('Import Private Keys'), _("Enter private keys")
self._do_import(title, msg, 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()))
try:
index = languages.keys().index(self.config.get("language",''))
except Exception:
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))
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 possiblity, 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))
self.fee_unit = self.config.get('fee_unit', 0)
fee_unit_label = HelpLabel(_('Fee Unit') + ':', '')
fee_unit_combo = QComboBox()
fee_unit_combo.addItems([_('sat/byte'), _('mBTCP/kB')])
fee_unit_combo.setCurrentIndex(self.fee_unit)
def on_fee_unit(x):
self.fee_unit = x
self.config.set_key('fee_unit', x)
self.fee_slider.update()
fee_unit_combo.currentIndexChanged.connect(on_fee_unit)
fee_widgets.append((fee_unit_label, fee_unit_combo))
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 http://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 = ['BTCP', 'mBTCP', 'bits']
msg = _('Base unit of your wallet.')\
+ '\n1BTCP=1000mBTCP.\n' \
+ _(' These settings affects the fields in the Send tab')+' '
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):
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]
if unit_result == 'BTCP':
self.decimal_point = 8
elif unit_result == 'mBTCP':
self.decimal_point = 5
elif unit_result == 'bits':
self.decimal_point = 2
else:
raise Exception('Unknown base unit')
self.config.set_key('decimal_point', self.decimal_point, True)
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(on_unit)
gui_widgets.append((unit_label, unit_combo))
block_explorers = sorted(util.block_explorer_info().keys())
msg = _('Choose which block explorer to use for transaction info')
block_ex_label = HelpLabel(_('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))
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))
# Fiat Currency
hist_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_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
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_fiat_address_cb()
update_exchanges()
ccy_combo.currentIndexChanged.connect(on_currency)
hist_checkbox.stateChanged.connect(on_history)
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 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)
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)
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 BaseException as e:
self.show_error(str(e))
return
if is_final:
new_tx.set_rbf(False)
self.show_transaction(new_tx, tx_label)
================================================
FILE: gui/qt/network_dialog.py
================================================
#!/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 socket
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import PyQt5.QtCore as QtCore
from electrum.i18n import _
from electrum.bitcoin import NetworkConstants
from electrum.util import print_error
from .util import *
protocol_names = ['TCP', 'SSL']
protocol_letters = 'ts'
class NetworkDialog(QDialog):
def __init__(self, network, config, network_updated_signal_obj):
QDialog.__init__(self)
self.setWindowTitle(_('Network'))
self.setMinimumSize(500, 20)
self.nlayout = NetworkChoiceLayout(network, config)
self.network_updated_signal_obj = network_updated_signal_obj
vbox = QVBoxLayout(self)
vbox.addLayout(self.nlayout.layout())
vbox.addLayout(Buttons(CloseButton(self)))
self.network_updated_signal_obj.network_updated_signal.connect(
self.on_update)
network.register_callback(self.on_network, ['updated', 'interfaces'])
def on_network(self, event, *args):
self.network_updated_signal_obj.network_updated_signal.emit(event, args)
def on_update(self):
self.nlayout.update()
class NodesListWidget(QTreeWidget):
def __init__(self, parent):
QTreeWidget.__init__(self)
self.parent = parent
self.setHeaderLabels([_('Connected Node'), _('Height')])
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.create_menu)
def create_menu(self, position):
item = self.currentItem()
if not item:
return
is_server = not bool(item.data(0, Qt.UserRole))
menu = QMenu()
if is_server:
server = item.data(1, Qt.UserRole)
menu.addAction(_("Use as server"), lambda: self.parent.follow_server(server))
else:
index = item.data(1, Qt.UserRole)
menu.addAction(_("Follow this branch"), lambda: self.parent.follow_branch(index))
menu.exec_(self.viewport().mapToGlobal(position))
def keyPressEvent(self, event):
if event.key() in [ Qt.Key_F2, Qt.Key_Return ]:
self.on_activated(self.currentItem(), self.currentColumn())
else:
QTreeWidget.keyPressEvent(self, event)
def on_activated(self, item, column):
# on 'enter' we show the menu
pt = self.visualItemRect(item).bottomLeft()
pt.setX(50)
self.customContextMenuRequested.emit(pt)
def update(self, network):
self.clear()
self.addChild = self.addTopLevelItem
chains = network.get_blockchains()
n_chains = len(chains)
for k, items in chains.items():
b = network.blockchains[k]
name = b.get_name()
if n_chains >1:
x = QTreeWidgetItem([name + '@%d'%b.get_checkpoint(), '%d'%b.height()])
x.setData(0, Qt.UserRole, 1)
x.setData(1, Qt.UserRole, b.checkpoint)
else:
x = self
for i in items:
star = ' *' if i == network.interface else ''
item = QTreeWidgetItem([i.host + star, '%d'%i.tip])
item.setData(0, Qt.UserRole, 0)
item.setData(1, Qt.UserRole, i.server)
x.addChild(item)
if n_chains>1:
self.addTopLevelItem(x)
x.setExpanded(True)
h = self.header()
h.setStretchLastSection(False)
h.setSectionResizeMode(0, QHeaderView.Stretch)
h.setSectionResizeMode(1, QHeaderView.ResizeToContents)
class ServerListWidget(QTreeWidget):
def __init__(self, parent):
QTreeWidget.__init__(self)
self.parent = parent
self.setHeaderLabels([_('Host'), _('Port')])
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.create_menu)
def create_menu(self, position):
item = self.currentItem()
if not item:
return
menu = QMenu()
server = item.data(1, Qt.UserRole)
menu.addAction(_("Use as server"), lambda: self.set_server(server))
menu.exec_(self.viewport().mapToGlobal(position))
def set_server(self, s):
host, port, protocol = s.split(':')
self.parent.server_host.setText(host)
self.parent.server_port.setText(port)
self.parent.set_server()
def keyPressEvent(self, event):
if event.key() in [ Qt.Key_F2, Qt.Key_Return ]:
self.on_activated(self.currentItem(), self.currentColumn())
else:
QTreeWidget.keyPressEvent(self, event)
def on_activated(self, item, column):
# on 'enter' we show the menu
pt = self.visualItemRect(item).bottomLeft()
pt.setX(50)
self.customContextMenuRequested.emit(pt)
def update(self, servers, protocol, use_tor):
self.clear()
for _host, d in sorted(servers.items()):
if _host.endswith('.onion') and not use_tor:
continue
port = d.get(protocol)
if port:
x = QTreeWidgetItem([_host, port])
server = _host+':'+port+':'+protocol
x.setData(1, Qt.UserRole, server)
self.addTopLevelItem(x)
h = self.header()
h.setStretchLastSection(False)
h.setSectionResizeMode(0, QHeaderView.Stretch)
h.setSectionResizeMode(1, QHeaderView.ResizeToContents)
class NetworkChoiceLayout(object):
def __init__(self, network, config, wizard=False):
self.network = network
self.config = config
self.protocol = None
self.tor_proxy = None
self.tabs = tabs = QTabWidget()
server_tab = QWidget()
proxy_tab = QWidget()
blockchain_tab = QWidget()
tabs.addTab(blockchain_tab, _('Overview'))
tabs.addTab(server_tab, _('Server'))
tabs.addTab(proxy_tab, _('Proxy'))
# server tab
grid = QGridLayout(server_tab)
grid.setSpacing(8)
self.server_host = QLineEdit()
self.server_host.setFixedWidth(200)
self.server_port = QLineEdit()
self.server_port.setFixedWidth(60)
self.autoconnect_cb = QCheckBox(_('Select server automatically'))
self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect'))
self.server_host.editingFinished.connect(self.set_server)
self.server_port.editingFinished.connect(self.set_server)
self.autoconnect_cb.clicked.connect(self.set_server)
self.autoconnect_cb.clicked.connect(self.update)
msg = ' '.join([
_("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
_("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
])
grid.addWidget(self.autoconnect_cb, 0, 0, 1, 3)
grid.addWidget(HelpButton(msg), 0, 4)
grid.addWidget(QLabel(_('Server') + ':'), 1, 0)
grid.addWidget(self.server_host, 1, 1, 1, 2)
grid.addWidget(self.server_port, 1, 3)
label = _('Server Peers') if network.is_connected() else _('Default Servers')
grid.addWidget(QLabel(label + ':'), 2, 0, 1, 5)
self.servers_list = ServerListWidget(self)
grid.addWidget(self.servers_list, 3, 0, 1, 5)
# Proxy tab
grid = QGridLayout(proxy_tab)
grid.setSpacing(8)
# proxy setting
self.proxy_cb = QCheckBox(_('Use Proxy'))
self.proxy_cb.clicked.connect(self.check_disable_proxy)
self.proxy_cb.clicked.connect(self.set_proxy)
self.proxy_mode = QComboBox()
self.proxy_mode.addItems(['SOCKS4', 'SOCKS5', 'HTTP'])
self.proxy_host = QLineEdit()
self.proxy_host.setFixedWidth(200)
self.proxy_port = QLineEdit()
self.proxy_port.setFixedWidth(60)
self.proxy_user = QLineEdit()
self.proxy_user.setPlaceholderText(_("Proxy user"))
self.proxy_password = QLineEdit()
self.proxy_password.setPlaceholderText(_("Password"))
self.proxy_password.setEchoMode(QLineEdit.Password)
self.proxy_password.setFixedWidth(60)
self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
self.proxy_host.editingFinished.connect(self.set_proxy)
self.proxy_port.editingFinished.connect(self.set_proxy)
self.proxy_user.editingFinished.connect(self.set_proxy)
self.proxy_password.editingFinished.connect(self.set_proxy)
self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
self.proxy_host.textEdited.connect(self.proxy_settings_changed)
self.proxy_port.textEdited.connect(self.proxy_settings_changed)
self.proxy_user.textEdited.connect(self.proxy_settings_changed)
self.proxy_password.textEdited.connect(self.proxy_settings_changed)
self.tor_cb = QCheckBox(_("Use Tor Proxy"))
self.tor_cb.setIcon(QIcon(":icons/tor_logo.png"))
self.tor_cb.hide()
self.tor_cb.clicked.connect(self.use_tor_proxy)
grid.addWidget(self.tor_cb, 1, 0, 1, 3)
grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
grid.addWidget(self.proxy_mode, 4, 1)
grid.addWidget(self.proxy_host, 4, 2)
grid.addWidget(self.proxy_port, 4, 3)
grid.addWidget(self.proxy_user, 5, 2)
grid.addWidget(self.proxy_password, 5, 3)
grid.setRowStretch(7, 1)
# Blockchain Tab
grid = QGridLayout(blockchain_tab)
msg = ' '.join([
_("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
_("This blockchain is used to verify the transactions sent by your transaction server.")
])
self.status_label = QLabel('')
grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
grid.addWidget(self.status_label, 0, 1, 1, 3)
grid.addWidget(HelpButton(msg), 0, 4)
self.server_label = QLabel('')
msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
grid.addWidget(QLabel(_('Server') + ':'), 1, 0)
grid.addWidget(self.server_label, 1, 1, 1, 3)
grid.addWidget(HelpButton(msg), 1, 4)
self.height_label = QLabel('')
msg = _('This is the height of your local copy of the blockchain.')
grid.addWidget(QLabel(_('Blockchain') + ':'), 2, 0)
grid.addWidget(self.height_label, 2, 1)
grid.addWidget(HelpButton(msg), 2, 4)
self.split_label = QLabel('')
grid.addWidget(self.split_label, 3, 0, 1, 3)
self.nodes_list_widget = NodesListWidget(self)
grid.addWidget(self.nodes_list_widget, 5, 0, 1, 5)
vbox = QVBoxLayout()
vbox.addWidget(tabs)
self.layout_ = vbox
# tor detector
self.td = td = TorDetector()
td.found_proxy.connect(self.suggest_proxy)
td.start()
self.fill_in_proxy_settings()
self.update()
def check_disable_proxy(self, b):
if not self.config.is_modifiable('proxy'):
b = False
for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
w.setEnabled(b)
def enable_set_server(self):
if self.config.is_modifiable('server'):
enabled = not self.autoconnect_cb.isChecked()
self.server_host.setEnabled(enabled)
self.server_port.setEnabled(enabled)
self.servers_list.setEnabled(enabled)
else:
for w in [self.autoconnect_cb, self.server_host, self.server_port, self.servers_list]:
w.setEnabled(False)
def update(self):
host, port, protocol, proxy_config, auto_connect = self.network.get_parameters()
self.server_host.setText(host)
self.server_port.setText(port)
self.autoconnect_cb.setChecked(auto_connect)
host = self.network.interface.host if self.network.interface else _('None')
self.server_label.setText(host)
self.set_protocol(protocol)
self.servers = self.network.get_servers()
self.servers_list.update(self.servers, self.protocol, self.tor_cb.isChecked())
self.enable_set_server()
height_str = "%d "%(self.network.get_local_height()) + _('blocks')
self.height_label.setText(height_str)
n = len(self.network.get_interfaces())
status = _("Connected to {0} nodes.").format(n) if n else _("Not connected")
self.status_label.setText(status)
chains = self.network.get_blockchains()
if len(chains)>1:
chain = self.network.blockchain()
checkpoint = chain.get_checkpoint()
name = chain.get_name()
msg = _('Chain split detected at block {0}').format(checkpoint) + '\n'
msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
else:
msg = ''
self.split_label.setText(msg)
self.nodes_list_widget.update(self.network)
def fill_in_proxy_settings(self):
host, port, protocol, proxy_config, auto_connect = self.network.get_parameters()
if not proxy_config:
proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
b = proxy_config.get('mode') != "none"
self.check_disable_proxy(b)
if b:
self.proxy_cb.setChecked(True)
self.proxy_mode.setCurrentIndex(
self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
self.proxy_host.setText(proxy_config.get("host"))
self.proxy_port.setText(proxy_config.get("port"))
self.proxy_user.setText(proxy_config.get("user", ""))
self.proxy_password.setText(proxy_config.get("password", ""))
def layout(self):
return self.layout_
def set_protocol(self, protocol):
if protocol != self.protocol:
self.protocol = protocol
def change_protocol(self, use_ssl):
p = 's' if use_ssl else 't'
host = self.server_host.text()
pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS)
if p not in pp.keys():
p = list(pp.keys())[0]
port = pp[p]
self.server_host.setText(host)
self.server_port.setText(port)
self.set_protocol(p)
self.set_server()
def follow_branch(self, index):
self.network.follow_chain(index)
self.update()
def follow_server(self, server):
self.network.switch_to_interface(server)
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
host, port, protocol = server.split(':')
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
self.update()
def server_changed(self, x):
if x:
self.change_server(str(x.text(0)), self.protocol)
def change_server(self, host, protocol):
pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS)
if protocol and protocol not in protocol_letters:
protocol = None
if protocol:
port = pp.get(protocol)
if port is None:
protocol = None
if not protocol:
if 's' in pp.keys():
protocol = 's'
port = pp.get(protocol)
else:
protocol = list(pp.keys())[0]
port = pp.get(protocol)
self.server_host.setText(host)
self.server_port.setText(port)
def accept(self):
pass
def set_server(self):
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
host = str(self.server_host.text())
port = str(self.server_port.text())
protocol = 't' if self.config.get('nossl') else 's'
auto_connect = self.autoconnect_cb.isChecked()
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
def set_proxy(self):
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
if self.proxy_cb.isChecked():
proxy = { 'mode':str(self.proxy_mode.currentText()).lower(),
'host':str(self.proxy_host.text()),
'port':str(self.proxy_port.text()),
'user':str(self.proxy_user.text()),
'password':str(self.proxy_password.text())}
else:
proxy = None
self.tor_cb.setChecked(False)
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
def suggest_proxy(self, found_proxy):
self.tor_proxy = found_proxy
self.tor_cb.setText("Use Tor proxy at port " + str(found_proxy[1]))
if self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5') \
and self.proxy_host.text() == "127.0.0.1" \
and self.proxy_port.text() == str(found_proxy[1]):
self.tor_cb.setChecked(True)
self.tor_cb.show()
def use_tor_proxy(self, use_it):
if not use_it:
self.proxy_cb.setChecked(False)
else:
socks5_mode_index = self.proxy_mode.findText('SOCKS5')
if socks5_mode_index == -1:
print_error("[network_dialog] can't find proxy_mode 'SOCKS5'")
return
self.proxy_mode.setCurrentIndex(socks5_mode_index)
self.proxy_host.setText("127.0.0.1")
self.proxy_port.setText(str(self.tor_proxy[1]))
self.proxy_user.setText("")
self.proxy_password.setText("")
self.tor_cb.setChecked(True)
self.proxy_cb.setChecked(True)
self.check_disable_proxy(use_it)
self.set_proxy()
def proxy_settings_changed(self):
self.tor_cb.setChecked(False)
class TorDetector(QThread):
found_proxy = pyqtSignal(object)
def __init__(self):
QThread.__init__(self)
def run(self):
# Probable ports for Tor to listen at
ports = [9050, 9150]
for p in ports:
if TorDetector.is_tor_port(p):
self.found_proxy.emit(("127.0.0.1", p))
return
@staticmethod
def is_tor_port(port):
try:
s = (socket._socketobject if hasattr(socket, "_socketobject") else socket.socket)(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.1)
s.connect(("127.0.0.1", port))
# Tor responds uniquely to HTTP-like requests
s.send(b"GET\n")
if b"Tor is not an HTTP Proxy" in s.recv(1024):
return True
except socket.error:
pass
return False
================================================
FILE: gui/qt/password_dialog.py
================================================
#!/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):
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))
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()))
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") + ": " + "" + strength + " ")
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 ChangePasswordDialog(WindowModalDialog):
def __init__(self, parent, wallet):
WindowModalDialog.__init__(self, parent)
is_encrypted = wallet.storage.is_encrypted()
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.')
OK_button = OkButton(self)
self.playout = PasswordLayout(wallet, msg, PW_CHANGE, 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 or not wallet.has_password())
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 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()
================================================
FILE: gui/qt/paytoedit.py
================================================
#!/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.
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import QCompleter, QPlainTextEdit
from .qrtextedit import ScanQRTextEdit
import re
from decimal import Decimal
from electrum import bitcoin
from . import util
RE_ADDRESS = '[1-9A-HJ-NP-Za-km-z]{26,}'
RE_ALIAS = '(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>'
frozen_style = "QWidget { background-color:none; border:none;}"
normal_style = "QPlainTextEdit { }"
class PayToEdit(ScanQRTextEdit):
def __init__(self, win):
ScanQRTextEdit.__init__(self)
self.win = win
self.amount_edit = win.amount_e
self.document().contentsChanged.connect(self.update_size)
self.heightMin = 0
self.heightMax = 150
self.c = None
self.textChanged.connect(self.check_text)
self.outputs = []
self.errors = []
self.is_pr = False
self.is_alias = False
self.scan_f = win.pay_to_URI
self.update_size()
self.payto_address = None
self.previous_payto = ''
def setFrozen(self, b):
self.setReadOnly(b)
self.setStyleSheet(frozen_style if b else normal_style)
for button in self.buttons:
button.setHidden(b)
def setGreen(self):
self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True))
def setExpired(self):
self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True))
def parse_address_and_amount(self, line):
x, y = line.split(',')
out_type, out = self.parse_output(x)
amount = self.parse_amount(y)
return out_type, out, amount
def parse_output(self, x):
try:
address = self.parse_address(x)
return bitcoin.TYPE_ADDRESS, address
except:
script = self.parse_script(x)
return bitcoin.TYPE_SCRIPT, script
def parse_script(self, x):
from electrum.transaction import opcodes, push_script
script = ''
for word in x.split():
if word[0:3] == 'OP_':
assert word in opcodes.lookup
script += chr(opcodes.lookup[word])
else:
script += push_script(word).decode('hex')
return script
def parse_amount(self, x):
if x.strip() == '!':
return '!'
p = pow(10, self.amount_edit.decimal_point())
return int(p * Decimal(x.strip()))
def parse_address(self, line):
r = line.strip()
m = re.match('^'+RE_ALIAS+'$', r)
address = str(m.group(2) if m else r)
assert bitcoin.is_address(address)
return address
def check_text(self):
self.errors = []
if self.is_pr:
return
# filter out empty lines
lines = [i for i in self.lines() if i]
outputs = []
total = 0
self.payto_address = None
if len(lines) == 1:
data = lines[0]
if data.startswith("bitcoin:"):
self.scan_f(data)
return
try:
self.payto_address = self.parse_output(data)
except:
pass
if self.payto_address:
self.win.lock_amount(False)
return
is_max = False
for i, line in enumerate(lines):
try:
_type, to_address, amount = self.parse_address_and_amount(line)
except:
self.errors.append((i, line.strip()))
continue
outputs.append((_type, to_address, amount))
if amount == '!':
is_max = True
else:
total += amount
self.win.is_max = is_max
self.outputs = outputs
self.payto_address = None
if self.win.is_max:
self.win.do_update_fee()
else:
self.amount_edit.setAmount(total if outputs else None)
self.win.lock_amount(total or len(lines)>1)
def get_errors(self):
return self.errors
def get_recipient(self):
return self.payto_address
def get_outputs(self, is_max):
if self.payto_address:
if is_max:
amount = '!'
else:
amount = self.amount_edit.get_amount()
_type, addr = self.payto_address
self.outputs = [(_type, addr, amount)]
return self.outputs[:]
def lines(self):
return self.toPlainText().split('\n')
def is_multiline(self):
return len(self.lines()) > 1
def paytomany(self):
self.setText("\n\n\n")
self.update_size()
def update_size(self):
lineHeight = QFontMetrics(self.document().defaultFont()).height()
docHeight = self.document().size().height()
h = docHeight * lineHeight + 11
if self.heightMin <= h <= self.heightMax:
self.setMinimumHeight(h)
self.setMaximumHeight(h)
self.verticalScrollBar().hide()
def setCompleter(self, completer):
self.c = completer
self.c.setWidget(self)
self.c.setCompletionMode(QCompleter.PopupCompletion)
self.c.activated.connect(self.insertCompletion)
def insertCompletion(self, completion):
if self.c.widget() != self:
return
tc = self.textCursor()
extra = len(completion) - len(self.c.completionPrefix())
tc.movePosition(QTextCursor.Left)
tc.movePosition(QTextCursor.EndOfWord)
tc.insertText(completion[-extra:])
self.setTextCursor(tc)
def textUnderCursor(self):
tc = self.textCursor()
tc.select(QTextCursor.WordUnderCursor)
return tc.selectedText()
def keyPressEvent(self, e):
if self.isReadOnly():
return
if self.c.popup().isVisible():
if e.key() in [Qt.Key_Enter, Qt.Key_Return]:
e.ignore()
return
if e.key() in [Qt.Key_Tab]:
e.ignore()
return
if e.key() in [Qt.Key_Down, Qt.Key_Up] and not self.is_multiline():
e.ignore()
return
QPlainTextEdit.keyPressEvent(self, e)
ctrlOrShift = e.modifiers() and (Qt.ControlModifier or Qt.ShiftModifier)
if self.c is None or (ctrlOrShift and not e.text()):
return
eow = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="
hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift
completionPrefix = self.textUnderCursor()
if hasModifier or not e.text() or len(completionPrefix) < 1 or eow.find(e.text()[-1]) >= 0:
self.c.popup().hide()
return
if completionPrefix != self.c.completionPrefix():
self.c.setCompletionPrefix(completionPrefix)
self.c.popup().setCurrentIndex(self.c.completionModel().index(0, 0))
cr = self.cursorRect()
cr.setWidth(self.c.popup().sizeHintForColumn(0) + self.c.popup().verticalScrollBar().sizeHint().width())
self.c.complete(cr)
def qr_input(self):
data = super(PayToEdit,self).qr_input()
if data.startswith("bitcoin:"):
self.scan_f(data)
# TODO: update fee
def resolve(self):
self.is_alias = False
if self.hasFocus():
return
if self.is_multiline(): # only supports single line entries atm
return
if self.is_pr:
return
key = str(self.toPlainText())
if key == self.previous_payto:
return
self.previous_payto = key
if not (('.' in key) and (not '<' in key) and (not ' ' in key)):
return
parts = key.split(sep=',') # assuming single line
if parts and len(parts) > 0 and bitcoin.is_address(parts[0]):
return
try:
data = self.win.contacts.resolve(key)
except:
return
if not data:
return
self.is_alias = True
address = data.get('address')
name = data.get('name')
new_url = key + ' <' + address + '>'
self.setText(new_url)
self.previous_payto = new_url
#if self.win.config.get('openalias_autoadd') == 'checked':
self.win.contacts[key] = ('openalias', name)
self.win.contact_list.on_update()
self.setFrozen(True)
if data.get('type') == 'openalias':
self.validated = data.get('validated')
if self.validated:
self.setGreen()
else:
self.setExpired()
else:
self.validated = None
================================================
FILE: gui/qt/qrcodewidget.py
================================================
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import PyQt5.QtGui as QtGui
from PyQt5.QtWidgets import (
QApplication, QVBoxLayout, QTextEdit, QHBoxLayout, QPushButton, QWidget)
import os
import qrcode
import electrum
from electrum.i18n import _
from .util import WindowModalDialog
class QRCodeWidget(QWidget):
def __init__(self, data = None, fixedSize=False):
QWidget.__init__(self)
self.data = None
self.qr = None
self.fixedSize=fixedSize
if fixedSize:
self.setFixedSize(fixedSize, fixedSize)
self.setData(data)
def setData(self, data):
if self.data != data:
self.data = data
if self.data:
self.qr = qrcode.QRCode()
self.qr.add_data(self.data)
if not self.fixedSize:
k = len(self.qr.get_matrix())
self.setMinimumSize(k*5,k*5)
else:
self.qr = None
self.update()
def paintEvent(self, e):
if not self.data:
return
black = QColor(0, 0, 0, 255)
white = QColor(255, 255, 255, 255)
if not self.qr:
qp = QtGui.QPainter()
qp.begin(self)
qp.setBrush(white)
qp.setPen(white)
r = qp.viewport()
qp.drawRect(0, 0, r.width(), r.height())
qp.end()
return
matrix = self.qr.get_matrix()
k = len(matrix)
qp = QtGui.QPainter()
qp.begin(self)
r = qp.viewport()
margin = 10
framesize = min(r.width(), r.height())
boxsize = int( (framesize - 2*margin)/k )
size = k*boxsize
left = (r.width() - size)/2
top = (r.height() - size)/2
# Make a white margin around the QR in case of dark theme use
qp.setBrush(white)
qp.setPen(white)
qp.drawRect(left-margin, top-margin, size+(margin*2), size+(margin*2))
qp.setBrush(black)
qp.setPen(black)
for r in range(k):
for c in range(k):
if matrix[r][c]:
qp.drawRect(left+c*boxsize, top+r*boxsize, boxsize - 1, boxsize - 1)
qp.end()
class QRDialog(WindowModalDialog):
def __init__(self, data, parent=None, title = "", show_text=False):
WindowModalDialog.__init__(self, parent, title)
vbox = QVBoxLayout()
qrw = QRCodeWidget(data)
qscreen = QApplication.primaryScreen()
vbox.addWidget(qrw, 1)
if show_text:
text = QTextEdit()
text.setText(data)
text.setReadOnly(True)
vbox.addWidget(text)
hbox = QHBoxLayout()
hbox.addStretch(1)
config = electrum.get_config()
if config:
filename = os.path.join(config.path, "qrcode.png")
def print_qr():
p = qscreen.grabWindow(qrw.winId())
p.save(filename, 'png')
self.show_message(_("QR code saved to file") + " " + filename)
def copy_to_clipboard():
p = qscreen.grabWindow(qrw.winId())
QApplication.clipboard().setPixmap(p)
self.show_message(_("QR code copied to clipboard"))
b = QPushButton(_("Copy"))
hbox.addWidget(b)
b.clicked.connect(copy_to_clipboard)
b = QPushButton(_("Save"))
hbox.addWidget(b)
b.clicked.connect(print_qr)
b = QPushButton(_("Close"))
hbox.addWidget(b)
b.clicked.connect(self.accept)
b.setDefault(True)
vbox.addLayout(hbox)
self.setLayout(vbox)
================================================
FILE: gui/qt/qrtextedit.py
================================================
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
with open(fileName, "r") as f:
data = f.read()
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())
================================================
FILE: gui/qt/qrwindow.py
================================================
#!/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 = "%s " % address if address else ""
self.address_label.setText(address_text)
if amount:
amount = self.win.format_amount(amount)
amount_text = "%s %s " % (amount, self.win.base_unit())
else:
amount_text = ''
self.amount_label.setText(amount_text)
label_text = "%s " % message if message else ""
self.label_label.setText(label_text)
self.qrw.setData(url)
================================================
FILE: gui/qt/request_list.py
================================================
#!/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[addr]
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, QIcon(":icons/seal.png"))
item.setToolTip(2, 'signed by '+ requestor)
if status is not PR_UNKNOWN:
item.setIcon(6, QIcon(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[addr]
column = self.currentColumn()
column_title = self.headerItem().text(column)
column_data = item.text(column)
menu = QMenu(self)
menu.addAction(_("Copy %s")%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))
================================================
FILE: gui/qt/seed_dialog.py
================================================
#!/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.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from electrum.i18n import _
from .util import *
from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
def seed_warning_msg(seed):
return ''.join([
"",
_("Please save these {0} words on paper (order is important). "),
_("This seed will allow you to recover your wallet in case "
"of computer failure."),
"
",
"" + _("WARNING") + ": ",
"",
"" + _("Never disclose your seed.") + " ",
"" + _("Never type it on a website.") + " ",
"" + _("Do not store it electronically.") + " ",
" "
]).format(len(seed.split()))
class SeedLayout(QVBoxLayout):
#options
is_bip39 = False
is_ext = False
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([
'' + _('Warning') + ': ',
_('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):
QVBoxLayout.__init__(self)
self.parent = parent
self.options = options
if title:
self.addWidget(WWLabel(title))
self.seed_e = ButtonsTextEdit()
if seed:
self.seed_e.setText(seed)
else:
self.seed_e.setTabChangesFocus(True)
self.is_seed = is_seed
self.saved_is_seed = self.is_seed
self.seed_e.textChanged.connect(self.on_edit)
self.seed_e.setMaximumHeight(75)
hbox = QHBoxLayout()
if icon:
logo = QLabel()
logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(64))
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)
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 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)
class KeysLayout(QVBoxLayout):
def __init__(self, parent=None, title=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)
self.addWidget(WWLabel(title))
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)
vbox.addLayout(Buttons(CloseButton(self)))
================================================
FILE: gui/qt/transaction_dialog.py
================================================
#!/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
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.util import bfh
from .util import *
dialogs = [] # Otherwise python randomly garbage collects the dialogs...
def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
d = TxDialog(tx, parent, desc, prompt_if_unsaved)
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 = copy.deepcopy(tx)
self.tx.deserialize()
self.main_window = parent
self.wallet = parent.wallet
self.prompt_if_unsaved = prompt_if_unsaved
self.saved = False
self.desc = desc
self.setMinimumWidth(750)
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"))
b.clicked.connect(self.save)
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.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()
dialogs.remove(self)
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):
if success:
self.prompt_if_unsaved = True
self.saved = False
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):
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 saved 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: %s")%time_str)
self.date_label.show()
elif exp_n:
text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)')
self.date_label.setText(_('Expected confirmation time') + ': ' + 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_str += ' ( %s ) '% self.main_window.format_fee_rate(fee/size*1000)
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(QColor("yellow")))
chg.setToolTip(_("Wallet change address"))
def text_format(addr):
if self.wallet.is_mine(addr):
return chg if self.wallet.is_change(addr) else rec
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[0:8] + '...', ext)
cursor.insertText(prevout_hash[-8:] + ":%-4d " % prevout_n, ext)
addr = x.get('address')
if addr == "(pubkey)":
_addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n)
if _addr:
addr = _addr
if addr is None:
addr = _('Unknown')
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)
================================================
FILE: gui/qt/util.py
================================================
import os.path
import time
import sys
import platform
import queue
from collections import namedtuple
from functools import partial
from electrum.i18n import _
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
if platform.system() == 'Windows':
MONOSPACE_FONT = 'Lucida Console'
elif platform.system() == 'Darwin':
MONOSPACE_FONT = 'Monaco'
else:
MONOSPACE_FONT = 'monospace'
dialogs = []
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
pr_icons = {
PR_UNPAID:":icons/unpaid.png",
PR_PAID:":icons/confirmed.png",
PR_EXPIRED:":icons/expired.png"
}
pr_tooltips = {
PR_UNPAID:_('Pending'),
PR_PAID:_('Paid'),
PR_EXPIRED:_('Expired')
}
expiration_values = [
(_('1 hour'), 60*60),
(_('1 day'), 24*60*60),
(_('1 week'), 7*24*60*60),
(_('Never'), None)
]
class Timer(QThread):
stopped = False
timer_signal = pyqtSignal()
def run(self):
while not self.stopped:
self.timer_signal.emit()
time.sleep(0.5)
def stop(self):
self.stopped = True
self.wait()
class EnterButton(QPushButton):
def __init__(self, text, func):
QPushButton.__init__(self, text)
self.func = func
self.clicked.connect(func)
def keyPressEvent(self, e):
if e.key() == Qt.Key_Return:
self.func()
class ThreadedButton(QPushButton):
def __init__(self, text, task, on_success=None, on_error=None):
QPushButton.__init__(self, text)
self.task = task
self.on_success = on_success
self.on_error = on_error
self.clicked.connect(self.run_task)
def run_task(self):
self.setEnabled(False)
self.thread = TaskThread(self)
self.thread.add(self.task, self.on_success, self.done, self.on_error)
def done(self):
self.setEnabled(True)
self.thread.stop()
class WWLabel(QLabel):
def __init__ (self, text="", parent=None):
QLabel.__init__(self, text, parent)
self.setWordWrap(True)
class HelpLabel(QLabel):
def __init__(self, text, help_text):
QLabel.__init__(self, text)
self.help_text = help_text
self.app = QCoreApplication.instance()
self.font = QFont()
def mouseReleaseEvent(self, x):
QMessageBox.information(self, 'Help', self.help_text)
def enterEvent(self, event):
self.font.setUnderline(True)
self.setFont(self.font)
self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
return QLabel.enterEvent(self, event)
def leaveEvent(self, event):
self.font.setUnderline(False)
self.setFont(self.font)
self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
return QLabel.leaveEvent(self, event)
class HelpButton(QPushButton):
def __init__(self, text):
QPushButton.__init__(self, '?')
self.help_text = text
self.setFocusPolicy(Qt.NoFocus)
self.setFixedWidth(20)
self.clicked.connect(self.onclick)
def onclick(self):
QMessageBox.information(self, 'Help', self.help_text)
class Buttons(QHBoxLayout):
def __init__(self, *buttons):
QHBoxLayout.__init__(self)
self.addStretch(1)
for b in buttons:
self.addWidget(b)
class CloseButton(QPushButton):
def __init__(self, dialog):
QPushButton.__init__(self, _("Close"))
self.clicked.connect(dialog.close)
self.setDefault(True)
class CopyButton(QPushButton):
def __init__(self, text_getter, app):
QPushButton.__init__(self, _("Copy"))
self.clicked.connect(lambda: app.clipboard().setText(text_getter()))
class CopyCloseButton(QPushButton):
def __init__(self, text_getter, app, dialog):
QPushButton.__init__(self, _("Copy and Close"))
self.clicked.connect(lambda: app.clipboard().setText(text_getter()))
self.clicked.connect(dialog.close)
self.setDefault(True)
class OkButton(QPushButton):
def __init__(self, dialog, label=None):
QPushButton.__init__(self, label or _("OK"))
self.clicked.connect(dialog.accept)
self.setDefault(True)
class CancelButton(QPushButton):
def __init__(self, dialog, label=None):
QPushButton.__init__(self, label or _("Cancel"))
self.clicked.connect(dialog.reject)
class MessageBoxMixin(object):
def top_level_window_recurse(self, window=None):
window = window or self
classes = (WindowModalDialog, QMessageBox)
for n, child in enumerate(window.children()):
# Test for visibility as old closed dialogs may not be GC-ed
if isinstance(child, classes) and child.isVisible():
return self.top_level_window_recurse(child)
return window
def top_level_window(self):
return self.top_level_window_recurse()
def question(self, msg, parent=None, title=None, icon=None):
Yes, No = QMessageBox.Yes, QMessageBox.No
return self.msg_box(icon or QMessageBox.Question,
parent, title or '',
msg, buttons=Yes|No, defaultButton=No) == Yes
def show_warning(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Warning, parent,
title or _('Warning'), msg)
def show_error(self, msg, parent=None):
return self.msg_box(QMessageBox.Warning, parent,
_('Error'), msg)
def show_critical(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Critical, parent,
title or _('Critical Error'), msg)
def show_message(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Information, parent,
title or _('Information'), msg)
def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok,
defaultButton=QMessageBox.NoButton):
parent = parent or self.top_level_window()
d = QMessageBox(icon, title, str(text), buttons, parent)
d.setWindowModality(Qt.WindowModal)
d.setDefaultButton(defaultButton)
return d.exec_()
class WindowModalDialog(QDialog, MessageBoxMixin):
'''Handy wrapper; window modal dialogs are better for our multi-window
daemon model as other wallet windows can still be accessed.'''
def __init__(self, parent, title=None):
QDialog.__init__(self, parent)
self.setWindowModality(Qt.WindowModal)
if title:
self.setWindowTitle(title)
class WaitingDialog(WindowModalDialog):
'''Shows a please wait dialog whilst runnning a task. It is not
necessary to maintain a reference to this dialog.'''
def __init__(self, parent, message, task, on_success=None, on_error=None):
assert parent
if isinstance(parent, MessageBoxMixin):
parent = parent.top_level_window()
WindowModalDialog.__init__(self, parent, _("Please wait..."))
vbox = QVBoxLayout(self)
vbox.addWidget(QLabel(message))
self.accepted.connect(self.on_accepted)
self.show()
self.thread = TaskThread(self)
self.thread.add(task, on_success, self.accept, on_error)
def wait(self):
self.thread.wait()
def on_accepted(self):
self.thread.stop()
def line_dialog(parent, title, label, ok_label, default=None):
dialog = WindowModalDialog(parent, title)
dialog.setMinimumWidth(500)
l = QVBoxLayout()
dialog.setLayout(l)
l.addWidget(QLabel(label))
txt = QLineEdit()
if default:
txt.setText(default)
l.addWidget(txt)
l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label)))
if dialog.exec_():
return txt.text()
def text_dialog(parent, title, label, ok_label, default=None, allow_multi=False):
from .qrtextedit import ScanQRTextEdit
dialog = WindowModalDialog(parent, title)
dialog.setMinimumWidth(500)
l = QVBoxLayout()
dialog.setLayout(l)
l.addWidget(QLabel(label))
txt = ScanQRTextEdit(allow_multi=allow_multi)
if default:
txt.setText(default)
l.addWidget(txt)
l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label)))
if dialog.exec_():
return txt.toPlainText()
class ChoicesLayout(object):
def __init__(self, msg, choices, on_clicked=None, checked_index=0):
vbox = QVBoxLayout()
if len(msg) > 50:
vbox.addWidget(WWLabel(msg))
msg = ""
gb2 = QGroupBox(msg)
vbox.addWidget(gb2)
vbox2 = QVBoxLayout()
gb2.setLayout(vbox2)
self.group = group = QButtonGroup()
for i,c in enumerate(choices):
button = QRadioButton(gb2)
button.setText(c)
vbox2.addWidget(button)
group.addButton(button)
group.setId(button, i)
if i==checked_index:
button.setChecked(True)
if on_clicked:
group.buttonClicked.connect(partial(on_clicked, self))
self.vbox = vbox
def layout(self):
return self.vbox
def selected_index(self):
return self.group.checkedId()
def address_field(addresses):
hbox = QHBoxLayout()
address_e = QLineEdit()
if addresses and len(addresses) > 0:
address_e.setText(addresses[0])
else:
addresses = []
def func():
try:
i = addresses.index(str(address_e.text())) + 1
i = i % len(addresses)
address_e.setText(addresses[i])
except ValueError:
# the user might have changed address_e to an
# address not in the wallet (or to something that isn't an address)
if addresses and len(addresses) > 0:
address_e.setText(addresses[0])
button = QPushButton(_('Address'))
button.clicked.connect(func)
hbox.addWidget(button)
hbox.addWidget(address_e)
return hbox, address_e
def filename_field(parent, config, defaultname, select_msg):
vbox = QVBoxLayout()
vbox.addWidget(QLabel(_("Export Format")))
gb = QGroupBox("format", parent)
b1 = QRadioButton(gb)
b1.setText(_("CSV"))
b1.setChecked(True)
b2 = QRadioButton(gb)
b2.setText(_("JSON"))
vbox.addWidget(b1)
vbox.addWidget(b2)
hbox = QHBoxLayout()
directory = config.get('io_dir', os.path.expanduser('~'))
path = os.path.join( directory, defaultname )
filename_e = QLineEdit()
filename_e.setText(path)
def func():
text = filename_e.text()
_filter = "*.csv" if text.endswith(".csv") else "*.json" if text.endswith(".json") else None
p, __ = QFileDialog.getSaveFileName(None, select_msg, text, _filter)
if p:
filename_e.setText(p)
button = QPushButton(_('File'))
button.clicked.connect(func)
hbox.addWidget(button)
hbox.addWidget(filename_e)
vbox.addLayout(hbox)
def set_csv(v):
text = filename_e.text()
text = text.replace(".json",".csv") if v else text.replace(".csv",".json")
filename_e.setText(text)
b1.clicked.connect(lambda: set_csv(True))
b2.clicked.connect(lambda: set_csv(False))
return vbox, filename_e, b1
class ElectrumItemDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
return self.parent().createEditor(parent, option, index)
class MyTreeWidget(QTreeWidget):
def __init__(self, parent, create_menu, headers, stretch_column=None,
editable_columns=None):
QTreeWidget.__init__(self, parent)
self.parent = parent
self.config = self.parent.config
self.stretch_column = stretch_column
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(create_menu)
self.setUniformRowHeights(True)
# extend the syntax for consistency
self.addChild = self.addTopLevelItem
self.insertChild = self.insertTopLevelItem
# Control which columns are editable
self.editor = None
self.pending_update = False
if editable_columns is None:
editable_columns = [stretch_column]
self.editable_columns = editable_columns
self.setItemDelegate(ElectrumItemDelegate(self))
self.itemDoubleClicked.connect(self.on_doubleclick)
self.update_headers(headers)
self.current_filter = ""
def update_headers(self, headers):
self.setColumnCount(len(headers))
self.setHeaderLabels(headers)
self.header().setStretchLastSection(False)
for col in range(len(headers)):
sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
self.header().setSectionResizeMode(col, sm)
def editItem(self, item, column):
if column in self.editable_columns:
self.editing_itemcol = (item, column, item.text(column))
# Calling setFlags causes on_changed events for some reason
item.setFlags(item.flags() | Qt.ItemIsEditable)
QTreeWidget.editItem(self, item, column)
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
def keyPressEvent(self, event):
if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None:
self.on_activated(self.currentItem(), self.currentColumn())
else:
QTreeWidget.keyPressEvent(self, event)
def permit_edit(self, item, column):
return (column in self.editable_columns
and self.on_permit_edit(item, column))
def on_permit_edit(self, item, column):
return True
def on_doubleclick(self, item, column):
if self.permit_edit(item, column):
self.editItem(item, column)
def on_activated(self, item, column):
# on 'enter' we show the menu
pt = self.visualItemRect(item).bottomLeft()
pt.setX(50)
self.customContextMenuRequested.emit(pt)
def createEditor(self, parent, option, index):
self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(),
parent, option, index)
self.editor.editingFinished.connect(self.editing_finished)
return self.editor
def editing_finished(self):
# Long-time QT bug - pressing Enter to finish editing signals
# editingFinished twice. If the item changed the sequence is
# Enter key: editingFinished, on_change, editingFinished
# Mouse: on_change, editingFinished
# This mess is the cleanest way to ensure we make the
# on_edited callback with the updated item
if self.editor:
(item, column, prior_text) = self.editing_itemcol
if self.editor.text() == prior_text:
self.editor = None # Unchanged - ignore any 2nd call
elif item.text(column) == prior_text:
pass # Buggy first call on Enter key, item not yet updated
else:
# What we want - the updated item
self.on_edited(*self.editing_itemcol)
self.editor = None
# Now do any pending updates
if self.editor is None and self.pending_update:
self.pending_update = False
self.on_update()
def on_edited(self, item, column, prior):
'''Called only when the text actually changes'''
key = item.data(0, Qt.UserRole)
text = item.text(column)
self.parent.wallet.set_label(key, text)
self.parent.history_list.update_labels()
self.parent.update_completions()
def update(self):
# Defer updates if editing
if self.editor:
self.pending_update = True
else:
self.setUpdatesEnabled(False)
self.on_update()
self.setUpdatesEnabled(True)
if self.current_filter:
self.filter(self.current_filter)
def on_update(self):
pass
def get_leaves(self, root):
child_count = root.childCount()
if child_count == 0:
yield root
for i in range(child_count):
item = root.child(i)
for x in self.get_leaves(item):
yield x
def filter(self, p):
columns = self.__class__.filter_columns
p = p.lower()
self.current_filter = p
for item in self.get_leaves(self.invisibleRootItem()):
item.setHidden(all([item.text(column).lower().find(p) == -1
for column in columns]))
class ButtonsWidget(QWidget):
def __init__(self):
super(QWidget, self).__init__()
self.buttons = []
def resizeButtons(self):
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
x = self.rect().right() - frameWidth
y = self.rect().bottom() - frameWidth
for button in self.buttons:
sz = button.sizeHint()
x -= sz.width()
button.move(x, y - sz.height())
def addButton(self, icon_name, on_click, tooltip):
button = QToolButton(self)
button.setIcon(QIcon(icon_name))
button.setStyleSheet("QToolButton { border: none; hover {border: 1px} pressed {border: 1px} padding: 0px; }")
button.setVisible(True)
button.setToolTip(tooltip)
button.clicked.connect(on_click)
self.buttons.append(button)
return button
def addCopyButton(self, app):
self.app = app
self.addButton(":icons/copy.png", self.on_copy, _("Copy to clipboard"))
def on_copy(self):
self.app.clipboard().setText(self.text())
QToolTip.showText(QCursor.pos(), _("Text copied to clipboard"), self)
class ButtonsLineEdit(QLineEdit, ButtonsWidget):
def __init__(self, text=None):
QLineEdit.__init__(self, text)
self.buttons = []
def resizeEvent(self, e):
o = QLineEdit.resizeEvent(self, e)
self.resizeButtons()
return o
class ButtonsTextEdit(QPlainTextEdit, ButtonsWidget):
def __init__(self, text=None):
QPlainTextEdit.__init__(self, text)
self.setText = self.setPlainText
self.text = self.toPlainText
self.buttons = []
def resizeEvent(self, e):
o = QPlainTextEdit.resizeEvent(self, e)
self.resizeButtons()
return o
class TaskThread(QThread):
'''Thread that runs background tasks. Callbacks are guaranteed
to happen in the context of its parent.'''
Task = namedtuple("Task", "task cb_success cb_done cb_error")
doneSig = pyqtSignal(object, object, object)
def __init__(self, parent, on_error=None):
super(TaskThread, self).__init__(parent)
self.on_error = on_error
self.tasks = queue.Queue()
self.doneSig.connect(self.on_done)
self.start()
def add(self, task, on_success=None, on_done=None, on_error=None):
on_error = on_error or self.on_error
self.tasks.put(TaskThread.Task(task, on_success, on_done, on_error))
def run(self):
while True:
task = self.tasks.get()
if not task:
break
try:
result = task.task()
self.doneSig.emit(result, task.cb_done, task.cb_success)
except BaseException:
self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
def on_done(self, result, cb_done, cb):
# This runs in the parent's thread.
if cb_done:
cb_done()
if cb:
cb(result)
def stop(self):
self.tasks.put(None)
class ColorSchemeItem:
def __init__(self, fg_color, bg_color):
self.colors = (fg_color, bg_color)
def _get_color(self, background):
return self.colors[(int(background) + int(ColorScheme.dark_scheme)) % 2]
def as_stylesheet(self, background=False):
css_prefix = "background-" if background else ""
color = self._get_color(background)
return "QWidget {{ {}color:{}; }}".format(css_prefix, color)
def as_color(self, background=False):
color = self._get_color(background)
return QColor(color)
class ColorScheme:
dark_scheme = False
GREEN = ColorSchemeItem("#117c11", "#8af296")
RED = ColorSchemeItem("#7c1111", "#f18c8c")
BLUE = ColorSchemeItem("#123b7c", "#8cb3f2")
DEFAULT = ColorSchemeItem("black", "white")
@staticmethod
def has_dark_background(widget):
brightness = sum(widget.palette().color(QPalette.Background).getRgb()[0:3])
return brightness < (255*3/2)
@staticmethod
def update_from_widget(widget):
if ColorScheme.has_dark_background(widget):
ColorScheme.dark_scheme = True
if __name__ == "__main__":
app = QApplication([])
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))
t.start()
app.exec_()
================================================
FILE: gui/qt/utxo_list.py
================================================
#!/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 .util import *
from electrum.i18n import _
class UTXOList(MyTreeWidget):
filter_columns = [0, 2] # Address, Label
def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
def get_name(self, x):
return x.get('prevout_hash') + ":%d"%x.get('prevout_n')
def on_update(self):
self.wallet = self.parent.wallet
item = self.currentItem()
self.clear()
self.utxos = self.wallet.get_utxos()
for x in self.utxos:
address = x.get('address')
height = x.get('height')
name = self.get_name(x)
label = self.wallet.get_label(x.get('prevout_hash'))
amount = self.parent.format_amount(x['value'])
utxo_item = QTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]])
utxo_item.setFont(0, QFont(MONOSPACE_FONT))
utxo_item.setFont(4, QFont(MONOSPACE_FONT))
utxo_item.setData(0, Qt.UserRole, name)
if self.wallet.is_frozen(address):
utxo_item.setBackground(0, ColorScheme.BLUE.as_color(True))
self.addChild(utxo_item)
def create_menu(self, position):
selected = [x.data(0, Qt.UserRole) for x in self.selectedItems()]
if not selected:
return
menu = QMenu()
coins = filter(lambda x: self.get_name(x) in selected, self.utxos)
menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins))
if len(selected) == 1:
txid = selected[0].split(':')[0]
tx = self.wallet.transactions.get(txid)
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
menu.exec_(self.viewport().mapToGlobal(position))
def on_permit_edit(self, item, column):
# disable editing fields in this tab (labels)
return False
================================================
FILE: gui/stdio.py
================================================
from decimal import Decimal
_ = lambda x:x
#from i18n import _
from electrum import WalletStorage, Wallet
from electrum.util import format_satoshis, set_verbosity
from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
import getpass, datetime
# minimal fdisk like gui for console usage
# written by rofl0r, with some bits stolen from the text gui (ncurses)
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.done = 0
self.last_balance = ""
set_verbosity(False)
self.str_recipient = ""
self.str_description = ""
self.str_amount = ""
self.str_fee = ""
self.wallet = Wallet(storage)
self.wallet.start_threads(self.network)
self.contacts = self.wallet.contacts
self.network.register_callback(self.on_network, ['updated', 'banner'])
self.commands = [_("[h] - displays this help text"), \
_("[i] - display transaction history"), \
_("[o] - enter payment order"), \
_("[p] - print stored payment order"), \
_("[s] - send stored payment order"), \
_("[r] - show own receipt addresses"), \
_("[c] - display contacts"), \
_("[b] - print server banner"), \
_("[q] - quit") ]
self.num_commands = len(self.commands)
def on_network(self, event, *args):
if event == 'updated':
self.updated()
elif event == 'banner':
self.print_banner()
def main_command(self):
self.print_balance()
c = input("enter command: ")
if c == "h" : self.print_commands()
elif c == "i" : self.print_history()
elif c == "o" : self.enter_order()
elif c == "p" : self.print_order()
elif c == "s" : self.send_order()
elif c == "r" : self.print_addresses()
elif c == "c" : self.print_contacts()
elif c == "b" : self.print_banner()
elif c == "n" : self.network_dialog()
elif c == "e" : self.settings_dialog()
elif c == "q" : self.done = 1
else: self.print_commands()
def updated(self):
s = self.get_balance()
if s != self.last_balance:
print(s)
self.last_balance = s
return True
def print_commands(self):
self.print_list(self.commands, "Available commands")
def print_history(self):
width = [20, 40, 14, 14]
delta = (80 - 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"
messages = []
for item in self.wallet.get_history():
tx_hash, height, conf, timestamp, delta, balance = item
if conf:
try:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
except Exception:
time_str = "unknown"
else:
time_str = 'unconfirmed'
label = self.wallet.get_label(tx_hash)
messages.append( format_str%( time_str, label, format_satoshis(delta, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
self.print_list(messages[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance")))
def print_balance(self):
print(self.get_balance())
def get_balance(self):
if self.wallet.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" )
return(msg)
def print_contacts(self):
messages = map(lambda x: "%20s %45s "%(x[0], x[1][1]), self.contacts.items())
self.print_list(messages, "%19s %25s "%("Key", "Value"))
def print_addresses(self):
messages = map(lambda addr: "%30s %30s "%(addr, self.wallet.labels.get(addr,"")), self.wallet.get_addresses())
self.print_list(messages, "%19s %25s "%("Address", "Label"))
def print_order(self):
print("send order to " + self.str_recipient + ", amount: " + self.str_amount \
+ "\nfee: " + self.str_fee + ", desc: " + self.str_description)
def enter_order(self):
self.str_recipient = input("Pay to: ")
self.str_description = input("Description : ")
self.str_amount = input("Amount: ")
self.str_fee = input("Fee: ")
def send_order(self):
self.do_send()
def print_banner(self):
for i, x in enumerate( self.wallet.network.banner.split('\n') ):
print( x )
def print_list(self, lst, firstline):
lst = list(lst)
self.maxpos = len(lst)
if not self.maxpos: return
print(firstline)
for i in range(self.maxpos):
msg = lst[i] if i < len(lst) else ""
print(msg)
def main(self):
while self.done == 0: self.main_command()
def do_send(self):
if not is_address(self.str_recipient):
print(_('Invalid BTCP address'))
return
try:
amount = int(Decimal(self.str_amount) * COIN)
except Exception:
print(_('Invalid Amount'))
return
try:
fee = int(Decimal(self.str_fee) * COIN)
except Exception:
print(_('Invalid Fee'))
return
if self.wallet.has_password():
password = self.password_dialog()
if not password:
return
else:
password = None
c = ""
while c != "y":
c = input("ok to send (y/n)?")
if c == "n": return
try:
tx = self.wallet.mktx([(TYPE_ADDRESS, self.str_recipient, amount)], password, self.config, fee)
except Exception as e:
print(str(e))
return
if self.str_description:
self.wallet.labels[tx.txid()] = self.str_description
print(_("Please wait..."))
status, msg = self.network.broadcast(tx)
if status:
print(_('Payment sent.'))
#self.do_clear()
#self.update_contacts_tab()
else:
print(_('Error'))
def network_dialog(self):
print("use 'electrum setconfig server/proxy' to change your network settings")
return True
def settings_dialog(self):
print("use 'electrum setconfig' to change your settings")
return True
def password_dialog(self):
return getpass.getpass()
# XXX unused
def run_receive_tab(self, c):
#if c == 10:
# out = self.run_popup('Address', ["Edit label", "Freeze", "Prioritize"])
return
def run_contacts_tab(self, c):
pass
================================================
FILE: gui/text.py
================================================
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 BTCP 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(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
================================================
FILE: icns-from-vector.sh
================================================
# Tool to generate a .iconset from a png
# (used 400x400 input.png)
mkdir output.iconset
sips -z 16 16 input.png --out output.iconset/icon_16x16.png
sips -z 32 32 input.png --out output.iconset/icon_16x16@2x.png
sips -z 32 32 input.png --out output.iconset/icon_32x32.png
sips -z 64 64 input.png --out output.iconset/icon_32x32@2x.png
sips -z 128 128 input.png --out output.iconset/icon_128x128.png
sips -z 256 256 input.png --out output.iconset/icon_128x128@2x.png
sips -z 256 256 input.png --out output.iconset/icon_256x256.png
#sips -z 512 512 input.png --out output.iconset/icon_256x256@2x.png
#sips -z 512 512 input.png --out output.iconset/icon_512x512.png
#cp input.png output.iconset/icon_512x512@2x.png
iconutil -c icns output.iconset
rm -R output.iconset
================================================
FILE: icons.qrc
================================================
icons/electrum.png
icons/clock1.png
icons/clock2.png
icons/clock3.png
icons/clock4.png
icons/clock5.png
icons/confirmed.png
icons/copy.png
icons/digitalbitbox.png
icons/digitalbitbox_unpaired.png
icons/expired.png
icons/electrum_light_icon.png
icons/electrum_dark_icon.png
icons/file.png
icons/keepkey.png
icons/keepkey_unpaired.png
icons/key.png
icons/ledger.png
icons/ledger_unpaired.png
icons/lock.png
icons/microphone.png
icons/network.png
icons/qrcode.png
icons/qrcode_white.png
icons/preferences.png
icons/seed.png
icons/status_connected.png
icons/status_connected_proxy.png
icons/status_disconnected.png
icons/status_waiting.png
icons/status_lagging.png
icons/seal.png
icons/tab_addresses.png
icons/tab_coins.png
icons/tab_console.png
icons/tab_contacts.png
icons/tab_history.png
icons/tab_receive.png
icons/tab_send.png
icons/tor_logo.png
icons/speaker.png
icons/trezor_unpaired.png
icons/trezor.png
icons/trustedcoin-status.png
icons/trustedcoin-wizard.png
icons/unconfirmed.png
icons/unpaid.png
icons/unlock.png
icons/warning.png
icons/zoom.png
================================================
FILE: lib/__init__.py
================================================
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 . import equihash
from .transaction import Transaction
from .plugins import BasePlugin
from .commands import Commands, known_commands
================================================
FILE: lib/base_wizard.py
================================================
# -*- coding: utf-8 -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 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 os
from . import bitcoin
from . import keystore
from .keystore import bip44_derivation
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types
from .i18n import _
class ScriptTypeNotSupported(Exception): pass
class BaseWizard(object):
def __init__(self, config, storage):
super(BaseWizard, self).__init__()
self.config = config
self.storage = storage
self.wallet = None
self.stack = []
self.plugin = None
self.keystores = []
self.is_kivy = config.get('gui') == 'kivy'
self.seed_type = None
def run(self, *args):
action = args[0]
args = args[1:]
self.stack.append((action, args))
if not action:
return
if type(action) is tuple:
self.plugin, action = action
if self.plugin and hasattr(self.plugin, action):
f = getattr(self.plugin, action)
f(self, *args)
elif hasattr(self, action):
f = getattr(self, action)
f(*args)
else:
raise BaseException("unknown action", action)
def can_go_back(self):
return len(self.stack)>1
def go_back(self):
if not self.can_go_back():
return
self.stack.pop()
action, args = self.stack.pop()
self.run(action, *args)
def new(self):
name = os.path.basename(self.storage.path)
title = _("Create") + ' ' + name
message = '\n'.join([
_("What kind of wallet do you want to create?")
])
wallet_kinds = [
('standard', _("Standard wallet")),
('multisig', _("Multi-signature wallet")),
('imported', _("Import BTCP addresses or private keys")),
]
choices = [pair for pair in wallet_kinds if pair[0] in wallet_types]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
def load_2fa(self):
self.storage.put('wallet_type', '2fa')
self.storage.put('use_trustedcoin', True)
self.plugin = self.plugins.load_plugin('trustedcoin')
def on_wallet_type(self, choice):
self.wallet_type = choice
if choice == 'standard':
action = 'choose_keystore'
elif choice == 'multisig':
action = 'choose_multisig'
elif choice == '2fa':
self.load_2fa()
action = self.storage.get_action()
elif choice == 'imported':
action = 'import_addresses_or_keys'
self.run(action)
def choose_multisig(self):
def on_multisig(m, n):
self.multisig_type = "%dof%d"%(m, n)
self.storage.put('wallet_type', self.multisig_type)
self.n = n
self.run('choose_keystore')
self.multisig_dialog(run_next=on_multisig)
def choose_keystore(self):
assert self.wallet_type in ['standard', 'multisig']
i = len(self.keystores)
title = _('Add cosigner') + ' (%d of %d)'%(i+1, self.n) if self.wallet_type=='multisig' else _('Keystore')
if self.wallet_type =='standard' or i==0:
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
choices = [
('choose_seed_type', _('Create a new seed')),
('restore_from_seed', _('I already have a seed')),
('restore_from_key', _('Use public or private keys')),
]
if not self.is_kivy:
choices.append(('choose_hw_device', _('Use a hardware device')))
else:
message = _('Add a cosigner to your multi-sig wallet')
choices = [
('restore_from_key', _('Enter cosigner key')),
('restore_from_seed', _('Enter cosigner seed')),
]
if not self.is_kivy:
choices.append(('choose_hw_device', _('Cosign with hardware device')))
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
def import_addresses_or_keys(self):
v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x)
title = _("Import BTCP Addresses")
message = _("Enter a list of BTCP addresses (this will create a watching-only wallet), or a list of private keys.")
self.add_xpub_dialog(title=title, message=message, run_next=self.on_import,
is_valid=v, allow_multi=True)
def on_import(self, text):
if keystore.is_address_list(text):
self.wallet = Imported_Wallet(self.storage)
for x in text.split():
self.wallet.import_address(x)
elif keystore.is_private_key_list(text):
k = keystore.Imported_KeyStore({})
self.storage.put('keystore', k.dump())
self.wallet = Imported_Wallet(self.storage)
for x in text.split():
self.wallet.import_private_key(x, None)
self.terminate()
def restore_from_key(self):
if self.wallet_type == 'standard':
v = keystore.is_master_key
title = _("Create keystore from a master key")
message = ' '.join([
_("To create a watching-only wallet, please enter your master public key (xpub/ypub/zpub)."),
_("To create a spending wallet, please enter a master private key (xprv/yprv/zprv).")
])
self.add_xpub_dialog(title=title, message=message, run_next=self.on_restore_from_key, is_valid=v)
else:
i = len(self.keystores) + 1
self.add_cosigner_dialog(index=i, run_next=self.on_restore_from_key, is_valid=keystore.is_bip32_key)
def on_restore_from_key(self, text):
k = keystore.from_master_key(text)
self.on_keystore(k)
def choose_hw_device(self):
title = _('Hardware Keystore')
# check available plugins
support = self.plugins.get_hardware_support()
if not support:
msg = '\n'.join([
_('No hardware wallet support found on your system.'),
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
])
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device())
return
# scan devices
devices = []
devmgr = self.plugins.device_manager
for name, description, plugin in support:
try:
# FIXME: side-effect: unpaired_device_info sets client.handler
u = devmgr.unpaired_device_infos(None, plugin)
except:
devmgr.print_error("error", name)
continue
devices += list(map(lambda x: (name, x), u))
if not devices:
msg = ''.join([
_('No hardware device detected.') + '\n',
_('To trigger a rescan, press \'Next\'.') + '\n\n',
_('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ',
_('On Linux, you might have to add a new permission to your udev rules.'),
])
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device())
return
# select device
self.devices = devices
choices = []
for name, info in devices:
state = _("initialized") if info.initialized else _("wiped")
label = info.label or _("An unnamed %s")%name
descr = "%s [%s, %s]" % (label, name, state)
choices.append(((name, info), descr))
msg = _('Select a device') + ':'
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_device)
def on_device(self, name, device_info):
self.plugin = self.plugins.get_plugin(name)
try:
self.plugin.setup_device(device_info, self)
except BaseException as e:
self.show_error(str(e))
self.choose_hw_device()
return
if self.wallet_type=='multisig':
# There is no general standard for HD multisig.
# This is partially compatible with BIP45; assumes index=0
self.on_hw_derivation(name, device_info, "m/45'/0")
else:
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
self.derivation_dialog(f)
def derivation_dialog(self, f):
default = bip44_derivation(0, bip43_purpose=44)
message = '\n'.join([
_('Enter your wallet derivation here.'),
_('If you are not sure what this is, leave this field unchanged.')
])
presets = (
('legacy BIP44', bip44_derivation(0, bip43_purpose=44)),
#('p2sh-segwit BIP49', bip44_derivation(0, bip43_purpose=49)),
#('native-segwit BIP84', bip44_derivation(0, bip43_purpose=84)),
)
while True:
try:
self.line_dialog(run_next=f, title=_('Derivation'), message=message,
default=default, test=bitcoin.is_bip32_derivation,
presets=presets)
return
except ScriptTypeNotSupported as e:
self.show_error(e)
# let the user choose again
def on_hw_derivation(self, name, device_info, derivation):
from .keystore import hardware_keystore
xtype = keystore.xtype_from_derivation(derivation)
try:
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
except ScriptTypeNotSupported:
raise # this is handled in derivation_dialog
except BaseException as e:
self.show_error(e)
return
d = {
'type': 'hardware',
'hw_type': name,
'derivation': derivation,
'xpub': xpub,
'label': device_info.label,
}
k = hardware_keystore(d)
self.on_keystore(k)
def passphrase_dialog(self, run_next):
title = _('Seed extension')
message = '\n'.join([
_('You may extend your seed with custom words.'),
_('Your seed extension must be saved together with your seed.'),
])
warning = '\n'.join([
_('Note that this is NOT your encryption password.'),
_('If you do not know what this is, leave this field empty.'),
])
self.line_dialog(title=title, message=message, warning=warning, default='', test=lambda x:True, run_next=run_next)
def restore_from_seed(self):
self.opt_bip39 = True
self.opt_ext = True
is_cosigning_seed = lambda x: bitcoin.seed_type(x) in ['standard', 'segwit']
test = bitcoin.is_seed if self.wallet_type == 'standard' else is_cosigning_seed
self.restore_seed_dialog(run_next=self.on_restore_seed, test=test)
def on_restore_seed(self, seed, is_bip39, is_ext):
self.seed_type = 'bip39' if is_bip39 else bitcoin.seed_type(seed)
if self.seed_type == 'bip39':
f = lambda passphrase: self.on_restore_bip39(seed, passphrase)
self.passphrase_dialog(run_next=f) if is_ext else f('')
elif self.seed_type in ['standard', 'segwit']:
f = lambda passphrase: self.run('create_keystore', seed, passphrase)
self.passphrase_dialog(run_next=f) if is_ext else f('')
elif self.seed_type == 'old':
self.run('create_keystore', seed, '')
elif self.seed_type == '2fa':
if self.is_kivy:
self.show_error(_('2FA seeds are not supported in this version'))
self.run('restore_from_seed')
else:
self.load_2fa()
self.run('on_restore_seed', seed, is_ext)
else:
raise BaseException('Unknown seed type', self.seed_type)
def on_restore_bip39(self, seed, passphrase):
f = lambda x: self.run('on_bip43', seed, passphrase, str(x))
self.derivation_dialog(f)
def create_keystore(self, seed, passphrase):
k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig')
self.on_keystore(k)
def on_bip43(self, seed, passphrase, derivation):
k = keystore.from_bip39_seed(seed, passphrase, derivation)
self.on_keystore(k)
def on_keystore(self, k):
has_xpub = isinstance(k, keystore.Xpub)
if has_xpub:
from .bitcoin import xpub_type
t1 = xpub_type(k.xpub)
if self.wallet_type == 'standard':
if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
self.show_error(_('Wrong key type') + ' %s'%t1)
self.run('choose_keystore')
return
self.keystores.append(k)
self.run('create_wallet')
elif self.wallet_type == 'multisig':
assert has_xpub
if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']:
self.show_error(_('Wrong key type') + ' %s'%t1)
self.run('choose_keystore')
return
if k.xpub in map(lambda x: x.xpub, self.keystores):
self.show_error(_('Error: duplicate master public key'))
self.run('choose_keystore')
return
if len(self.keystores)>0:
t2 = xpub_type(self.keystores[0].xpub)
if t1 != t2:
self.show_error(_('Cannot add this cosigner:') + '\n' + "Their key type is '%s', we are '%s'"%(t1, t2))
self.run('choose_keystore')
return
self.keystores.append(k)
if len(self.keystores) == 1:
xpub = k.get_master_public_key()
self.stack = []
self.run('show_xpub_and_add_cosigners', xpub)
elif len(self.keystores) < self.n:
self.run('choose_keystore')
else:
self.run('create_wallet')
def create_wallet(self):
if any(k.may_have_password() for k in self.keystores):
self.request_password(run_next=self.on_password)
else:
self.on_password(None, False)
def on_password(self, password, encrypt):
self.storage.set_password(password, encrypt)
for k in self.keystores:
if k.may_have_password():
k.update_password(None, password)
if self.wallet_type == 'standard':
self.storage.put('seed_type', self.seed_type)
keys = self.keystores[0].dump()
self.storage.put('keystore', keys)
self.wallet = Standard_Wallet(self.storage)
self.run('create_addresses')
elif self.wallet_type == 'multisig':
for i, k in enumerate(self.keystores):
self.storage.put('x%d/'%(i+1), k.dump())
self.storage.write()
self.wallet = Multisig_Wallet(self.storage)
self.run('create_addresses')
def show_xpub_and_add_cosigners(self, xpub):
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
def on_cosigner(self, text, password, i):
k = keystore.from_master_key(text, password)
self.on_keystore(k)
def choose_seed_type(self):
title = _('Choose Seed type')
message = ' '.join([
_("The type of addresses used by your wallet will depend on your seed."),
])
choices = [
('create_standard_seed', _('Standard')),
#('create_segwit_seed', _('Segwit')),
]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
def create_segwit_seed(self): self.create_seed('segwit')
def create_standard_seed(self): self.create_seed('standard')
def create_seed(self, seed_type):
from . import mnemonic
self.seed_type = seed_type
seed = mnemonic.Mnemonic('en').make_seed(self.seed_type)
self.opt_bip39 = False
f = lambda x: self.request_passphrase(seed, x)
self.show_seed_dialog(run_next=f, seed_text=seed)
def request_passphrase(self, seed, opt_passphrase):
if opt_passphrase:
f = lambda x: self.confirm_seed(seed, x)
self.passphrase_dialog(run_next=f)
else:
self.run('confirm_seed', seed, '')
def confirm_seed(self, seed, passphrase):
f = lambda x: self.confirm_passphrase(seed, passphrase)
self.confirm_seed_dialog(run_next=f, test=lambda x: x==seed)
def confirm_passphrase(self, seed, passphrase):
f = lambda x: self.run('create_keystore', seed, x)
if passphrase:
title = _('Confirm Seed Extension')
message = '\n'.join([
_('Your seed extension must be saved together with your seed.'),
_('Please type it here.'),
])
self.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x: x==passphrase)
else:
f('')
def create_addresses(self):
def task():
self.wallet.synchronize()
self.wallet.storage.write()
self.terminate()
msg = _("Electrum is generating your addresses, please wait.")
self.waiting_dialog(task, msg)
================================================
FILE: lib/bitcoin.py
================================================
# -*- coding: utf-8 -*-
#
# 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 hashlib
import base64
import hmac
import os
import json
import struct
import ecdsa
import pyaes
from .util import bfh, bh2u, to_string
from . import version
from .util import print_error, InvalidPassword, assert_bytes, to_bytes, inv_dict
from . import segwit_addr
def read_json(filename, default):
path = os.path.join(os.path.dirname(__file__), filename)
try:
with open(path, 'r') as f:
r = json.loads(f.read())
except:
r = default
return r
# Version numbers for BIP32 extended keys
# standard: xprv, xpub
# segwit in p2sh: yprv, ypub
# native segwit: zprv, zpub
XPRV_HEADERS = {
'standard': 0x0488ade4,
'p2wpkh-p2sh': 0x049d7878,
'p2wsh-p2sh': 0x295b005,
'p2wpkh': 0x4b2430c,
'p2wsh': 0x2aa7a99
}
XPUB_HEADERS = {
'standard': 0x0488b21e,
'p2wpkh-p2sh': 0x049d7cb2,
'p2wsh-p2sh': 0x295b43f,
'p2wpkh': 0x4b24746,
'p2wsh': 0x2aa7ed3
}
class NetworkConstants:
@classmethod
def set_mainnet(cls):
cls.TESTNET = False
cls.WIF_PREFIX = 0x80
cls.ADDRTYPE_P2PKH = [0x13, 0x25]
cls.ADDRTYPE_P2SH = [0x13, 0xAF]
cls.ADDRTYPE_SHIELDED = [0x16, 0xA8]
cls.SEGWIT_HRP = "bc" #TODO btcp has no segwit
cls.GENESIS = "0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602"
cls.DEFAULT_PORTS = {'t': '50001', 's': '50002'}
cls.DEFAULT_SERVERS = read_json('servers.json', {})
cls.CHECKPOINTS = read_json('checkpoints.json', [])
cls.EQUIHASH_N = 200
cls.EQUIHASH_K = 9
cls.HEADERS_URL = "http://headers.btcprivate.org/blockchain_headers"
cls.CHUNK_SIZE = 200
@classmethod
def set_testnet(cls):
cls.TESTNET = True
cls.WIF_PREFIX = 0xef
cls.ADDRTYPE_P2PKH = [0x19, 0x58]
cls.ADDRTYPE_P2SH = [0x19, 0xE0]
cls.ADDRTYPE_SHIELDED = [0x16, 0xC0]
cls.SEGWIT_HRP = "tb" #TODO btcp has no segwit
cls.GENESIS = "03e1c4bb705c871bf9bfda3e74b7f8f86bff267993c215a89d5795e3708e5e1f"
cls.DEFAULT_PORTS = {'t': '51001', 's': '51002'}
cls.DEFAULT_SERVERS = read_json('servers_testnet.json', {})
cls.CHECKPOINTS = read_json('checkpoints_testnet.json', [])
cls.EQUIHASH_N = 200
cls.EQUIHASH_K = 9
#cls.HEADERS_URL = "http://35.224.186.7/blockchain_headers"
cls.CHUNK_SIZE = 200
NetworkConstants.set_mainnet()
################################## transactions
FEE_STEP = 10000
DEFAULT_FEE_RATE = 10000
MAX_FEE_RATE = 300000
FEE_TARGETS = [25, 10, 5, 2]
COINBASE_MATURITY = 100
COIN = 100000000
# supported types of transaction outputs
TYPE_ADDRESS = 0
TYPE_PUBKEY = 1
TYPE_SCRIPT = 2
# AES encryption
try:
from Cryptodome.Cipher import AES
except:
AES = None
class InvalidPadding(Exception):
pass
def append_PKCS7_padding(data):
assert_bytes(data)
padlen = 16 - (len(data) % 16)
return data + bytes([padlen]) * padlen
def strip_PKCS7_padding(data):
assert_bytes(data)
if len(data) % 16 != 0 or len(data) == 0:
raise InvalidPadding("invalid length")
padlen = data[-1]
if padlen > 16:
raise InvalidPadding("invalid padding byte (large)")
for i in data[-padlen:]:
if i != padlen:
raise InvalidPadding("invalid padding byte (inconsistent)")
return data[0:-padlen]
def aes_encrypt_with_iv(key, iv, data):
assert_bytes(key, iv, data)
data = append_PKCS7_padding(data)
if AES:
e = AES.new(key, AES.MODE_CBC, iv).encrypt(data)
else:
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
aes = pyaes.Encrypter(aes_cbc, padding=pyaes.PADDING_NONE)
e = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer
return e
def aes_decrypt_with_iv(key, iv, data):
assert_bytes(key, iv, data)
if AES:
cipher = AES.new(key, AES.MODE_CBC, iv)
data = cipher.decrypt(data)
else:
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
aes = pyaes.Decrypter(aes_cbc, padding=pyaes.PADDING_NONE)
data = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer
try:
return strip_PKCS7_padding(data)
except InvalidPadding:
raise InvalidPassword()
def EncodeAES(secret, s):
assert_bytes(s)
iv = bytes(os.urandom(16))
ct = aes_encrypt_with_iv(secret, iv, s)
e = iv + ct
return base64.b64encode(e)
def DecodeAES(secret, e):
e = bytes(base64.b64decode(e))
iv, e = e[:16], e[16:]
s = aes_decrypt_with_iv(secret, iv, e)
return s
def pw_encode(s, password):
if password:
secret = Hash(password)
return EncodeAES(secret, to_bytes(s, "utf8")).decode('utf8')
else:
return s
def pw_decode(s, password):
if password is not None:
secret = Hash(password)
try:
d = to_string(DecodeAES(secret, s), "utf8")
except Exception:
raise InvalidPassword()
return d
else:
return s
def rev_hex(s):
return bh2u(bfh(s)[::-1])
def int_to_hex(i, length=1):
assert isinstance(i, int)
s = hex(i)[2:].rstrip('L')
s = "0"*(2*length - len(s)) + s
return rev_hex(s)
def var_int(i):
# https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer
if i<0xfd:
return int_to_hex(i)
elif i<=0xffff:
return "fd"+int_to_hex(i,2)
elif i<=0xffffffff:
return "fe"+int_to_hex(i,4)
else:
return "ff"+int_to_hex(i,8)
def op_push(i):
if i<0x4c:
return int_to_hex(i)
elif i<0xff:
return '4c' + int_to_hex(i)
elif i<0xffff:
return '4d' + int_to_hex(i,2)
else:
return '4e' + int_to_hex(i,4)
def push_script(x):
return op_push(len(x)//2) + x
# ZCASH specific utils methods
# https://github.com/zcash/zcash/blob/master/qa/rpc-tests/test_framework/mininode.py
HEADER_SIZE = 1487
hash_to_str = lambda x: bytes(reversed(x)).hex()
str_to_hash = lambda x: bytes(reversed(bytes.fromhex(x)))
def read_vector_size(f):
nit = struct.unpack(">= 32
return rs
def sha256(x):
if isinstance(x, str):
x = x.encode('utf8')
return bytes(hashlib.sha256(x).digest())
def Hash(x):
out = bytes(sha256(sha256(x)))
return out
hash_encode = lambda x: bh2u(x[::-1])
hash_decode = lambda x: bfh(x)[::-1]
hmac_sha_512 = lambda x, y: hmac.new(x, y, hashlib.sha512).digest()
def is_new_seed(x, prefix=version.SEED_PREFIX):
from . import mnemonic
x = mnemonic.normalize_text(x)
s = bh2u(hmac_sha_512(b"Seed version", x.encode('utf8')))
return s.startswith(prefix)
def is_old_seed(seed):
from . import old_mnemonic, mnemonic
seed = mnemonic.normalize_text(seed)
words = seed.split()
try:
# checks here are deliberately left weak for legacy reasons, see #3149
old_mnemonic.mn_decode(words)
uses_electrum_words = True
except Exception:
uses_electrum_words = False
try:
seed = bfh(seed)
is_hex = (len(seed) == 16 or len(seed) == 32)
except Exception:
is_hex = False
return is_hex or (uses_electrum_words and (len(words) == 12 or len(words) == 24))
def seed_type(x):
if is_old_seed(x):
return 'old'
elif is_new_seed(x):
return 'standard'
elif is_new_seed(x, version.SEED_PREFIX_SW):
return 'segwit'
elif is_new_seed(x, version.SEED_PREFIX_2FA):
return '2fa'
return ''
is_seed = lambda x: bool(seed_type(x))
# pywallet openssl private key implementation
def i2o_ECPublicKey(pubkey, compressed=False):
# public keys are 65 bytes long (520 bits)
# 0x04 + 32-byte X-coordinate + 32-byte Y-coordinate
# 0x00 = point at infinity, 0x02 and 0x03 = compressed, 0x04 = uncompressed
# compressed keys: where is 0x02 if y is even and 0x03 if y is odd
if compressed:
if pubkey.point.y() & 1:
key = '03' + '%064x' % pubkey.point.x()
else:
key = '02' + '%064x' % pubkey.point.x()
else:
key = '04' + \
'%064x' % pubkey.point.x() + \
'%064x' % pubkey.point.y()
return bfh(key)
# end pywallet openssl private key implementation
############ functions from pywallet #####################
def hash_160(public_key):
try:
md = hashlib.new('ripemd160')
md.update(sha256(public_key))
return md.digest()
except BaseException:
from . import ripemd
md = ripemd.new(sha256(public_key))
return md.digest()
def hash160_to_b58_address(h160, addrtype, witness_program_version=1):
s = bytes([addrtype[0]])
s += bytes([addrtype[1]])
s += h160
return base_encode(s+Hash(s)[0:4], base=58)
def b58_address_to_hash160(addr):
addr = to_bytes(addr, 'ascii')
_bytes = base_decode(addr, 26, base=58)
return [_bytes[0], _bytes[1]], _bytes[2:22]
def hash160_to_p2pkh(h160):
return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2PKH)
def hash160_to_p2sh(h160):
return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2SH)
def public_key_to_p2pkh(public_key):
return hash160_to_p2pkh(hash_160(public_key))
def hash_to_segwit_addr(h):
return segwit_addr.encode(NetworkConstants.SEGWIT_HRP, 0, h)
def public_key_to_p2wpkh(public_key):
return hash_to_segwit_addr(hash_160(public_key))
def script_to_p2wsh(script):
return hash_to_segwit_addr(sha256(bfh(script)))
def p2wpkh_nested_script(pubkey):
pkh = bh2u(hash_160(bfh(pubkey)))
return '00' + push_script(pkh)
def p2wsh_nested_script(witness_script):
wsh = bh2u(sha256(bfh(witness_script)))
return '00' + push_script(wsh)
def pubkey_to_address(txin_type, pubkey):
if txin_type == 'p2pkh':
return public_key_to_p2pkh(bfh(pubkey))
elif txin_type == 'p2wpkh':
return hash_to_segwit_addr(hash_160(bfh(pubkey)))
elif txin_type == 'p2wpkh-p2sh':
scriptSig = p2wpkh_nested_script(pubkey)
return hash160_to_p2sh(hash_160(bfh(scriptSig)))
else:
raise NotImplementedError(txin_type)
def redeem_script_to_address(txin_type, redeem_script):
if txin_type == 'p2sh':
return hash160_to_p2sh(hash_160(bfh(redeem_script)))
elif txin_type == 'p2wsh':
return script_to_p2wsh(redeem_script)
elif txin_type == 'p2wsh-p2sh':
scriptSig = p2wsh_nested_script(redeem_script)
return hash160_to_p2sh(hash_160(bfh(scriptSig)))
else:
raise NotImplementedError(txin_type)
def script_to_address(script):
from .transaction import get_address_from_output_script
t, addr = get_address_from_output_script(bfh(script))
assert t == TYPE_ADDRESS
return addr
def address_to_script(addr):
witver, witprog = segwit_addr.decode(NetworkConstants.SEGWIT_HRP, addr)
if witprog is not None:
assert (0 <= witver <= 16)
OP_n = witver + 0x50 if witver > 0 else 0
script = bh2u(bytes([OP_n]))
script += push_script(bh2u(bytes(witprog)))
return script
addrtype, hash_160 = b58_address_to_hash160(addr)
if addrtype == NetworkConstants.ADDRTYPE_P2PKH:
script = '76a9' # op_dup, op_hash_160
script += push_script(bh2u(hash_160))
script += '88ac' # op_equalverify, op_checksig
elif addrtype == NetworkConstants.ADDRTYPE_P2SH:
script = 'a9' # op_hash_160
script += push_script(bh2u(hash_160))
script += '87' # op_equal
else:
raise BaseException('unknown address type')
return script
def address_to_scripthash(addr):
script = address_to_script(addr)
return script_to_scripthash(script)
def script_to_scripthash(script):
h = sha256(bytes.fromhex(script))[0:32]
return bh2u(bytes(reversed(h)))
def public_key_to_p2pk_script(pubkey):
script = push_script(pubkey)
script += 'ac' # op_checksig
return script
__b58chars = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
assert len(__b58chars) == 58
__b43chars = b'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:'
assert len(__b43chars) == 43
def base_encode(v, base):
""" encode v, which is a string of bytes, to base58."""
assert_bytes(v)
assert base in (58, 43)
chars = __b58chars
if base == 43:
chars = __b43chars
long_value = 0
for (i, c) in enumerate(v[::-1]):
long_value += (256**i) * c
result = bytearray()
while long_value >= base:
div, mod = divmod(long_value, base)
result.append(chars[mod])
long_value = div
result.append(chars[long_value])
# Bitcoin does a little leading-zero-compression:
# leading 0-bytes in the input become leading-1s
nPad = 0
for c in v:
if c == 0x00:
nPad += 1
else:
break
result.extend([chars[0]] * nPad)
result.reverse()
return result.decode('ascii')
def base_decode(v, length, base):
""" decode v into a string of len bytes."""
# assert_bytes(v)
v = to_bytes(v, 'ascii')
assert base in (58, 43)
chars = __b58chars
if base == 43:
chars = __b43chars
long_value = 0
for (i, c) in enumerate(v[::-1]):
long_value += chars.find(bytes([c])) * (base**i)
result = bytearray()
while long_value >= 256:
div, mod = divmod(long_value, 256)
result.append(mod)
long_value = div
result.append(long_value)
nPad = 0
for c in v:
if c == chars[0]:
nPad += 1
else:
break
result.extend(b'\x00' * nPad)
if length is not None and len(result) != length:
return None
result.reverse()
return bytes(result)
def EncodeBase58Check(vchIn):
hash = Hash(vchIn)
return base_encode(vchIn + hash[0:4], base=58)
def DecodeBase58Check(psz):
vchRet = base_decode(psz, None, base=58)
key = vchRet[0:-4]
csum = vchRet[-4:]
hash = Hash(key)
cs32 = hash[0:4]
if cs32 != csum:
return None
else:
return key
# extended key export format for segwit
SCRIPT_TYPES = {
'p2pkh':0,
'p2wpkh':1,
'p2wpkh-p2sh':2,
'p2sh':5,
'p2wsh':6,
'p2wsh-p2sh':7
}
def serialize_privkey(secret, compressed, txin_type):
prefix = bytes([(SCRIPT_TYPES[txin_type]+NetworkConstants.WIF_PREFIX)&255])
suffix = b'\01' if compressed else b''
vchIn = prefix + secret + suffix
return EncodeBase58Check(vchIn)
def deserialize_privkey(key):
# whether the pubkey is compressed should be visible from the keystore
vch = DecodeBase58Check(key)
if is_minikey(key):
return 'p2pkh', minikey_to_private_key(key), True
elif vch:
txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - NetworkConstants.WIF_PREFIX]
assert len(vch) in [33, 34]
compressed = len(vch) == 34
return txin_type, vch[1:33], compressed
else:
raise BaseException("cannot deserialize", key)
def regenerate_key(pk):
assert len(pk) == 32
return EC_KEY(pk)
def GetPubKey(pubkey, compressed=False):
return i2o_ECPublicKey(pubkey, compressed)
def GetSecret(pkey):
return bfh('%064x' % pkey.secret)
def is_compressed(sec):
return deserialize_privkey(sec)[2]
def public_key_from_private_key(pk, compressed):
pkey = regenerate_key(pk)
public_key = GetPubKey(pkey.pubkey, compressed)
return bh2u(public_key)
def address_from_private_key(sec):
txin_type, privkey, compressed = deserialize_privkey(sec)
public_key = public_key_from_private_key(privkey, compressed)
return pubkey_to_address(txin_type, public_key)
def is_segwit_address(addr):
try:
witver, witprog = segwit_addr.decode(NetworkConstants.SEGWIT_HRP, addr)
except Exception as e:
return False
return witprog is not None
def is_b58_address(addr):
try:
addrtype, h = b58_address_to_hash160(addr)
except Exception as e:
return False
if addrtype not in [NetworkConstants.ADDRTYPE_P2PKH, NetworkConstants.ADDRTYPE_P2SH]:
return False
return addr == hash160_to_b58_address(h, addrtype)
def is_address(addr):
return is_segwit_address(addr) or is_b58_address(addr)
def is_private_key(key):
try:
k = deserialize_privkey(key)
return k is not False
except:
return False
########### end pywallet functions #######################
def is_minikey(text):
# Minikeys are typically 22 or 30 characters, but this routine
# permits any length of 20 or more provided the minikey is valid.
# A valid minikey must begin with an 'S', be in base58, and when
# suffixed with '?' have its SHA256 hash begin with a zero byte.
# They are widely used in Casascius physical bitcoins.
return (len(text) >= 20 and text[0] == 'S'
and all(ord(c) in __b58chars for c in text)
and sha256(text + '?')[0] == 0x00)
def minikey_to_private_key(text):
return sha256(text)
from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1
from ecdsa.curves import SECP256k1
from ecdsa.ellipticcurve import Point
from ecdsa.util import string_to_number, number_to_string
def msg_magic(message):
length = bfh(var_int(len(message)))
return b"\x19BitcoinPrivate Signed Message:\n" + length + message
def verify_message(address, sig, message):
assert_bytes(sig, message)
try:
h = Hash(msg_magic(message))
public_key, compressed = pubkey_from_signature(sig, h)
# check public key using the address
pubkey = point_to_ser(public_key.pubkey.point, compressed)
for txin_type in ['p2pkh','p2wpkh','p2wpkh-p2sh']:
addr = pubkey_to_address(txin_type, bh2u(pubkey))
if address == addr:
break
else:
raise Exception("Bad signature")
# check message
public_key.verify_digest(sig[1:], h, sigdecode = ecdsa.util.sigdecode_string)
return True
except Exception as e:
print_error("Verification error: {0}".format(e))
return False
def encrypt_message(message, pubkey):
return EC_KEY.encrypt_message(message, bfh(pubkey))
def chunks(l, n):
return [l[i:i+n] for i in range(0, len(l), n)]
def ECC_YfromX(x,curved=curve_secp256k1, odd=True):
_p = curved.p()
_a = curved.a()
_b = curved.b()
for offset in range(128):
Mx = x + offset
My2 = pow(Mx, 3, _p) + _a * pow(Mx, 2, _p) + _b % _p
My = pow(My2, (_p+1)//4, _p )
if curved.contains_point(Mx,My):
if odd == bool(My&1):
return [My,offset]
return [_p-My,offset]
raise Exception('ECC_YfromX: No Y found')
def negative_point(P):
return Point( P.curve(), P.x(), -P.y(), P.order() )
def point_to_ser(P, comp=True ):
if comp:
return bfh( ('%02x'%(2+(P.y()&1)))+('%064x'%P.x()) )
return bfh( '04'+('%064x'%P.x())+('%064x'%P.y()) )
def ser_to_point(Aser):
curve = curve_secp256k1
generator = generator_secp256k1
_r = generator.order()
assert Aser[0] in [0x02, 0x03, 0x04]
if Aser[0] == 0x04:
return Point( curve, string_to_number(Aser[1:33]), string_to_number(Aser[33:]), _r )
Mx = string_to_number(Aser[1:])
return Point( curve, Mx, ECC_YfromX(Mx, curve, Aser[0] == 0x03)[0], _r )
class MyVerifyingKey(ecdsa.VerifyingKey):
@classmethod
def from_signature(klass, sig, recid, h, curve):
""" See http://www.secg.org/download/aid-780/sec1-v2.pdf, chapter 4.1.6 """
from ecdsa import util, numbertheory
from . import msqr
curveFp = curve.curve
G = curve.generator
order = G.order()
# extract r,s from signature
r, s = util.sigdecode_string(sig, order)
# 1.1
x = r + (recid//2) * order
# 1.3
alpha = ( x * x * x + curveFp.a() * x + curveFp.b() ) % curveFp.p()
beta = msqr.modular_sqrt(alpha, curveFp.p())
y = beta if (beta - recid) % 2 == 0 else curveFp.p() - beta
# 1.4 the constructor checks that nR is at infinity
R = Point(curveFp, x, y, order)
# 1.5 compute e from message:
e = string_to_number(h)
minus_e = -e % order
# 1.6 compute Q = r^-1 (sR - eG)
inv_r = numbertheory.inverse_mod(r,order)
Q = inv_r * ( s * R + minus_e * G )
return klass.from_public_point( Q, curve )
def pubkey_from_signature(sig, h):
if len(sig) != 65:
raise Exception("Wrong encoding")
nV = sig[0]
if nV < 27 or nV >= 35:
raise Exception("Bad encoding")
if nV >= 31:
compressed = True
nV -= 4
else:
compressed = False
recid = nV - 27
return MyVerifyingKey.from_signature(sig[1:], recid, h, curve = SECP256k1), compressed
class MySigningKey(ecdsa.SigningKey):
"""Enforce low S values in signatures"""
def sign_number(self, number, entropy=None, k=None):
curve = SECP256k1
G = curve.generator
order = G.order()
r, s = ecdsa.SigningKey.sign_number(self, number, entropy, k)
if s > order//2:
s = order - s
return r, s
class EC_KEY(object):
def __init__( self, k ):
secret = string_to_number(k)
self.pubkey = ecdsa.ecdsa.Public_key( generator_secp256k1, generator_secp256k1 * secret )
self.privkey = ecdsa.ecdsa.Private_key( self.pubkey, secret )
self.secret = secret
def get_public_key(self, compressed=True):
return bh2u(point_to_ser(self.pubkey.point, compressed))
def sign(self, msg_hash):
private_key = MySigningKey.from_secret_exponent(self.secret, curve = SECP256k1)
public_key = private_key.get_verifying_key()
signature = private_key.sign_digest_deterministic(msg_hash, hashfunc=hashlib.sha256, sigencode = ecdsa.util.sigencode_string)
assert public_key.verify_digest(signature, msg_hash, sigdecode = ecdsa.util.sigdecode_string)
return signature
def sign_message(self, message, is_compressed):
message = to_bytes(message, 'utf8')
signature = self.sign(Hash(msg_magic(message)))
for i in range(4):
sig = bytes([27 + i + (4 if is_compressed else 0)]) + signature
try:
self.verify_message(sig, message)
return sig
except Exception as e:
continue
else:
raise Exception("error: cannot sign message")
def verify_message(self, sig, message):
assert_bytes(message)
h = Hash(msg_magic(message))
public_key, compressed = pubkey_from_signature(sig, h)
# check public key
if point_to_ser(public_key.pubkey.point, compressed) != point_to_ser(self.pubkey.point, compressed):
raise Exception("Bad signature")
# check message
public_key.verify_digest(sig[1:], h, sigdecode = ecdsa.util.sigdecode_string)
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac
@classmethod
def encrypt_message(self, message, pubkey):
assert_bytes(message)
pk = ser_to_point(pubkey)
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, pk.x(), pk.y()):
raise Exception('invalid pubkey')
ephemeral_exponent = number_to_string(ecdsa.util.randrange(pow(2,256)), generator_secp256k1.order())
ephemeral = EC_KEY(ephemeral_exponent)
ecdh_key = point_to_ser(pk * ephemeral.privkey.secret_multiplier)
key = hashlib.sha512(ecdh_key).digest()
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True))
encrypted = b'BIE1' + ephemeral_pubkey + ciphertext
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
return base64.b64encode(encrypted + mac)
def decrypt_message(self, encrypted):
encrypted = base64.b64decode(encrypted)
if len(encrypted) < 85:
raise Exception('invalid ciphertext: length')
magic = encrypted[:4]
ephemeral_pubkey = encrypted[4:37]
ciphertext = encrypted[37:-32]
mac = encrypted[-32:]
if magic != b'BIE1':
raise Exception('invalid ciphertext: invalid magic bytes')
try:
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
except AssertionError as e:
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, ephemeral_pubkey.x(), ephemeral_pubkey.y()):
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
ecdh_key = point_to_ser(ephemeral_pubkey * self.privkey.secret_multiplier)
key = hashlib.sha512(ecdh_key).digest()
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
if mac != hmac.new(key_m, encrypted[:-32], hashlib.sha256).digest():
raise InvalidPassword()
return aes_decrypt_with_iv(key_e, iv, ciphertext)
###################################### BIP32 ##############################
random_seed = lambda n: "%032x"%ecdsa.util.randrange( pow(2,n) )
BIP32_PRIME = 0x80000000
def get_pubkeys_from_secret(secret):
# public key
private_key = ecdsa.SigningKey.from_string( secret, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
K = public_key.to_string()
K_compressed = GetPubKey(public_key.pubkey,True)
return K, K_compressed
# Child private key derivation function (from master private key)
# k = master private key (32 bytes)
# c = master chain code (extra entropy for key derivation) (32 bytes)
# n = the index of the key we want to derive. (only 32 bits will be used)
# If n is negative (i.e. the 32nd bit is set), the resulting private key's
# corresponding public key can NOT be determined without the master private key.
# However, if n is positive, the resulting private key's corresponding
# public key can be determined without the master private key.
def CKD_priv(k, c, n):
is_prime = n & BIP32_PRIME
return _CKD_priv(k, c, bfh(rev_hex(int_to_hex(n,4))), is_prime)
def _CKD_priv(k, c, s, is_prime):
order = generator_secp256k1.order()
keypair = EC_KEY(k)
cK = GetPubKey(keypair.pubkey,True)
data = bytes([0]) + k + s if is_prime else cK + s
I = hmac.new(c, data, hashlib.sha512).digest()
k_n = number_to_string( (string_to_number(I[0:32]) + string_to_number(k)) % order , order )
c_n = I[32:]
return k_n, c_n
# Child public key derivation function (from public key only)
# K = master public key
# c = master chain code
# n = index of key we want to derive
# This function allows us to find the nth public key, as long as n is
# non-negative. If n is negative, we need the master private key to find it.
def CKD_pub(cK, c, n):
if n & BIP32_PRIME: raise
return _CKD_pub(cK, c, bfh(rev_hex(int_to_hex(n,4))))
# helper function, callable with arbitrary string
def _CKD_pub(cK, c, s):
order = generator_secp256k1.order()
I = hmac.new(c, cK + s, hashlib.sha512).digest()
curve = SECP256k1
pubkey_point = string_to_number(I[0:32])*curve.generator + ser_to_point(cK)
public_key = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 )
c_n = I[32:]
cK_n = GetPubKey(public_key.pubkey,True)
return cK_n, c_n
def xprv_header(xtype):
return bfh("%08x" % XPRV_HEADERS[xtype])
def xpub_header(xtype):
return bfh("%08x" % XPUB_HEADERS[xtype])
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4):
xprv = xprv_header(xtype) + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
return EncodeBase58Check(xprv)
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4):
xpub = xpub_header(xtype) + bytes([depth]) + fingerprint + child_number + c + cK
return EncodeBase58Check(xpub)
def deserialize_xkey(xkey, prv):
xkey = DecodeBase58Check(xkey)
if len(xkey) != 78:
raise BaseException('Invalid length')
depth = xkey[4]
fingerprint = xkey[5:9]
child_number = xkey[9:13]
c = xkey[13:13+32]
header = int('0x' + bh2u(xkey[0:4]), 16)
headers = XPRV_HEADERS if prv else XPUB_HEADERS
if header not in headers.values():
raise BaseException('Invalid xpub format', hex(header))
xtype = list(headers.keys())[list(headers.values()).index(header)]
n = 33 if prv else 32
K_or_k = xkey[13+n:]
return xtype, depth, fingerprint, child_number, c, K_or_k
def deserialize_xpub(xkey):
return deserialize_xkey(xkey, False)
def deserialize_xprv(xkey):
return deserialize_xkey(xkey, True)
def xpub_type(x):
return deserialize_xpub(x)[0]
def is_xpub(text):
try:
deserialize_xpub(text)
return True
except:
return False
def is_xprv(text):
try:
deserialize_xprv(text)
return True
except:
return False
def xpub_from_xprv(xprv):
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
K, cK = get_pubkeys_from_secret(k)
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
def bip32_root(seed, xtype):
I = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest()
master_k = I[0:32]
master_c = I[32:]
K, cK = get_pubkeys_from_secret(master_k)
xprv = serialize_xprv(xtype, master_c, master_k)
xpub = serialize_xpub(xtype, master_c, cK)
return xprv, xpub
def xpub_from_pubkey(xtype, cK):
assert cK[0] in [0x02, 0x03]
return serialize_xpub(xtype, b'\x00'*32, cK)
def bip32_derivation(s):
assert s.startswith('m/')
s = s[2:]
for n in s.split('/'):
if n == '': continue
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
yield i
def is_bip32_derivation(x):
try:
[ i for i in bip32_derivation(x)]
return True
except :
return False
def bip32_private_derivation(xprv, branch, sequence):
assert sequence.startswith(branch)
if branch == sequence:
return xprv, xpub_from_xprv(xprv)
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
sequence = sequence[len(branch):]
for n in sequence.split('/'):
if n == '': continue
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
parent_k = k
k, c = CKD_priv(k, c, i)
depth += 1
_, parent_cK = get_pubkeys_from_secret(parent_k)
fingerprint = hash_160(parent_cK)[0:4]
child_number = bfh("%08X"%i)
K, cK = get_pubkeys_from_secret(k)
xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
xprv = serialize_xprv(xtype, c, k, depth, fingerprint, child_number)
return xprv, xpub
def bip32_public_derivation(xpub, branch, sequence):
xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
assert sequence.startswith(branch)
sequence = sequence[len(branch):]
for n in sequence.split('/'):
if n == '': continue
i = int(n)
parent_cK = cK
cK, c = CKD_pub(cK, c, i)
depth += 1
fingerprint = hash_160(parent_cK)[0:4]
child_number = bfh("%08X"%i)
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
def bip32_private_key(sequence, k, chain):
for i in sequence:
k, chain = CKD_priv(k, chain, i)
return k
================================================
FILE: lib/blockchain.py
================================================
# Electrum - lightweight Bitcoin client
# Copyright (C) 2012 thomasv@ecdsa.org
#
# 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 threading
import struct
from io import BytesIO
from . import util
from . import bitcoin
from .bitcoin import *
import base64
from .equihash import is_gbp_valid
import logging
logging.basicConfig(level=logging.INFO)
# https://en.bitcoin.it/wiki/Target
MAX_TARGET = 0x0007FFFFFFFF0000000000000000000000000000000000000000000000000000
def serialize_header(res):
r = b''
r += struct.pack(" target:
raise BaseException("insufficient proof of work: %s vs target %s" % (int('0x' + _powhash, 16), target))
nonce = uint256_from_bytes(str_to_hash(header.get('nonce')))
n_solution = vector_from_bytes(base64.b64decode(header.get('n_solution').encode('utf8')))
if not is_gbp_valid(serialize_header(header), nonce, n_solution,
NetworkConstants.EQUIHASH_N, NetworkConstants.EQUIHASH_K):
raise BaseException("Equihash invalid")
def verify_chunk(self, index, data):
num = len(data) // bitcoin.HEADER_SIZE
prev_header = None
if index != 0:
prev_header = self.read_header(index * NetworkConstants.CHUNK_SIZE - 1)
for i in range(num):
raw_header = data[i*bitcoin.HEADER_SIZE:(i+1) * bitcoin.HEADER_SIZE]
header = deserialize_header(raw_header, index*NetworkConstants.CHUNK_SIZE + i)
self.verify_header(header, prev_header)
prev_header = header
def path(self):
d = util.get_headers_dir(self.config)
filename = 'blockchain_headers' if self.parent_id is None else os.path.join('forks', 'fork_%d_%d'%(self.parent_id, self.checkpoint))
return os.path.join(d, filename)
def save_chunk(self, index, chunk):
filename = self.path()
d = (index * NetworkConstants.CHUNK_SIZE - self.checkpoint) * bitcoin.HEADER_SIZE
if d < 0:
chunk = chunk[-d:]
d = 0
self.write(chunk, d)
self.swap_with_parent()
def swap_with_parent(self):
if self.parent_id is None:
return
parent_branch_size = self.parent().height() - self.checkpoint + 1
if parent_branch_size >= self.size():
return
self.print_error("swap", self.checkpoint, self.parent_id)
parent_id = self.parent_id
checkpoint = self.checkpoint
parent = self.parent()
with open(self.path(), 'rb') as f:
my_data = f.read()
with open(parent.path(), 'rb') as f:
f.seek((checkpoint - parent.checkpoint)*bitcoin.HEADER_SIZE)
parent_data = f.read(parent_branch_size*bitcoin.HEADER_SIZE)
self.write(parent_data, 0)
parent.write(my_data, (checkpoint - parent.checkpoint)*bitcoin.HEADER_SIZE)
# store file path
for b in blockchains.values():
b.old_path = b.path()
# swap parameters
self.parent_id = parent.parent_id; parent.parent_id = parent_id
self.checkpoint = parent.checkpoint; parent.checkpoint = checkpoint
self._size = parent._size; parent._size = parent_branch_size
# move files
for b in blockchains.values():
if b in [self, parent]: continue
if b.old_path != b.path():
self.print_error("renaming", b.old_path, b.path())
os.rename(b.old_path, b.path())
# update pointers
blockchains[self.checkpoint] = self
blockchains[parent.checkpoint] = parent
def write(self, data, offset):
filename = self.path()
with self.lock:
with open(filename, 'rb+') as f:
if offset != self._size*bitcoin.HEADER_SIZE:
f.seek(offset)
f.truncate()
f.seek(offset)
f.write(data)
f.flush()
os.fsync(f.fileno())
self.update_size()
def save_header(self, header):
delta = header.get('block_height') - self.checkpoint
data = serialize_header(header)
assert delta == self.size()
assert len(data) == bitcoin.HEADER_SIZE
self.write(data, delta*bitcoin.HEADER_SIZE)
self.swap_with_parent()
def read_header(self, height):
assert self.parent_id != self.checkpoint
if height < 0:
return
if height < self.checkpoint:
return self.parent().read_header(height)
if height > self.height():
return
delta = height - self.checkpoint
name = self.path()
if os.path.exists(name):
with open(name, 'rb') as f:
f.seek(delta * bitcoin.HEADER_SIZE)
h = f.read(bitcoin.HEADER_SIZE)
return deserialize_header(h, height)
def get_hash(self, height):
return self.hash_header(self.read_header(height))
def hash_header(self, header):
return hash_header(header)
def bits_to_target(self, bits):
bitsN = (bits >> 24) & 0xff
# if not (bitsN >= 0x03 and bitsN <= 0x1d):
# raise BaseException("First part of bits should be in [0x03, 0x1d]")
bitsBase = bits & 0xffffff
# if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff):
# raise BaseException("Second part of bits should be in [0x8000, 0x7fffff]")
if bitsN <= 3:
return bitsBase >> (8 * (3 - bitsN))
else:
return bitsBase << (8 * (bitsN - 3))
def target_to_bits(self, target):
c = ("%064x" % target)[2:]
while c[:2] == '00' and len(c) > 6:
c = c[2:]
bitsN, bitsBase = len(c) // 2, int('0x' + c[:6], 16)
if bitsBase >= 0x800000:
bitsN += 1
bitsBase >>= 8
return bitsN << 24 | bitsBase
def can_connect(self, header, check_height=True):
# import pdb; pdb.set_trace()
height = header['block_height']
if check_height and self.height() != height - 1:
self.print_error("cannot connect at height", height)
return False
if height == 0:
return hash_header(header) == NetworkConstants.GENESIS
try:
prev_header = self.read_header(height - 1)
prev_hash = self.hash_header(prev_header)
except:
return False
if prev_hash != header.get('prev_block_hash'):
return False
try:
self.verify_header(header, prev_header)
except BaseException as e:
import traceback
traceback.print_exc()
self.print_error('verify_header failed', str(e))
return False
return True
def connect_chunk(self, idx, hexdata):
try:
data = bytes.fromhex(hexdata)
self.verify_chunk(idx, data)
self.print_error("validated chunk %d" % idx)
self.save_chunk(idx, data)
return True
except BaseException as e:
import traceback
traceback.print_exc()
self.print_error('verify_chunk failed', str(e))
return False
================================================
FILE: lib/checkpoints.json
================================================
[
[
"000000065093005a1a46ee95d6d66c2b07008220ca64dd3b3a93bbd1945480c0",
14134776517815698497336078495404605830980533548759267698564454644503805952
]
]
================================================
FILE: lib/checkpoints_testnet.json
================================================
[
[
"00000000864b744c5025331036aa4a16e9ed1cbb362908c625272150fa059b29",
[
0,
0
]
],
[
"000000002e9ccffc999166ccf8d72129e1b2e9c754f6c90ad2f77cab0d9fb4c7",
[
0,
0
]
],
[
"0000000009b9f0436a9c733e2c9a9d9c8fe3475d383bdc1beb7bfa995f90be70",
[
0,
0
]
],
[
"000000000a9c9c79f246042b9e2819822287f2be7cd6487aecf7afab6a88bed5",
[
0,
0
]
],
[
"000000003a7002e1247b0008cba36cd46f57cd7ce56ac9d9dc5644265064df09",
[
0,
0
]
],
[
"00000000061e01e82afff6e7aaea4eb841b78cc0eed3af11f6706b14471fa9c8",
[
0,
0
]
],
[
"000000003911e011ae2459e44d4581ac69ba703fb26e1421529bd326c538f12d",
[
0,
0
]
],
[
"000000000a5984d6c73396fe40de392935f5fc2a8e48eedf38034ce0a3178a60",
[
0,
0
]
],
[
"000000000786bdc642fa54c0a791d58b732ed5676516fffaeca04492be97c243",
[
0,
0
]
],
[
"000000001359c49f9618f3ee69afbd1b3196f1832acc47557d42256fcc6b7f48",
[
0,
0
]
],
[
"00000000270dde98d582af35dff5aed02087dad8529dc5c808c67573d6dabaf4",
[
0,
0
]
],
[
"00000000425c160908c215c4adf998771a2d1c472051bc58320696f3a5eb0644",
[
0,
0
]
],
[
"0000000006a5976471986377805d4a148d8822bb7f458138c83f167d197817c9",
[
0,
0
]
],
[
"000000000318394ea17038ef369f3cccc79b3d7dfda957af6c8cd4a471ffa814",
[
0,
0
]
],
[
"000000000ad4f9d0b8e86871478cc849f7bc42fb108ebec50e4a795afc284926",
[
0,
0
]
],
[
"000000000207e63e68f2a7a4c067135883d726fd65e3620142fb9bdf50cce1f6",
[
0,
0
]
],
[
"00000000003b426d2c12ee66b2eedb4dcc05d5e158685b222240d31e43687762",
[
0,
0
]
],
[
"00000000017cf6ee86e3d483f9a978ded72be1fa5af37d287a71c5dfb87cdd83",
[
0,
0
]
],
[
"00000000004b1d9fe16fc0c72cfa0395c98a3e460cd2affb8640e28bca295a4a",
[
0,
0
]
],
[
"0000000046d191b09f7726e4f8bfaffed6c30734afbf1f95e6bddbe0b07d9e88",
[
0,
0
]
],
[
"0000000082cec8200e9ea055c2991bf74560eb7e7140691ea53e7828dbdc9553",
[
0,
0
]
],
[
"000000003775b96d6b362d4804afe2d9c3cf3cbb46a45c3ccc377c94e83edd23",
[
0,
0
]
],
[
"00000000037835a92404acb2f18768a49d4f93685ead30aad6bb3b073f411e02",
[
0,
0
]
],
[
"0000000006cf75d17706d1f62e6b08e6ba5facfde38a8920b7d808a6b6781ff2",
[
0,
0
]
],
[
"0000000003dff257cdae43703fcd0ca91fda0970f5fc04258b4608fb1942a6f6",
[
0,
0
]
],
[
"0000000000532d97d18867658e08c789f627535652382147e33bf8626d4131bc",
[
0,
0
]
],
[
"000000000266dfb79bb11dedd0ae748505863ab3ab731269cd71a2c2fbd159b3",
[
0,
0
]
],
[
"00000000349ff0119d5c0dd8ffad8bf41cd6126a88416148b81fa4dcaebc42e1",
[
0,
0
]
],
[
"000000003c61939b4799eeea4335218d30de9b1071605126d719dce0f0d14810",
[
0,
0
]
],
[
"000000003d9284570ed648d2b12ad24046ac8b9abcf05c4e9813ea110490cf73",
[
0,
0
]
],
[
"0000000001360b66e6dc0ccfbd75356034e721ae55c3d5c71a58be5d281c252b",
[
0,
0
]
],
[
"000000000c114f42504916bfb2ee26ed8307b3f7f74226c1cfe1f5302ec23d26",
[
0,
0
]
],
[
"0000000007acac3fcf97b4ca81821263b704364adaa2736fce0a0722bfed4f8d",
[
0,
0
]
],
[
"00000000059768ef7731d27f9c2be48c6e16d7cb56680625f08ff25ead504280",
[
0,
0
]
],
[
"000000000351c8908f1f52518ce4bd251b896ca3fbccb69a2607db6624bafcfc",
[
0,
0
]
],
[
"0000000068d7ccae048e212e9e2ecb4d944f583b4490df4fbf654b4915597052",
[
0,
0
]
],
[
"000000000e2aaa36417187233ff55325473bd5b7a164b358da60c96d1920fd77",
[
0,
0
]
],
[
"000000001eb11ef6dbe0647bc87a8d218f6e59c2b9690f17edcf0dbd39cd0308",
[
0,
0
]
],
[
"00000000022e7855e24cc3fff67ce093242434a8ffa45882333a0f08a40aad9c",
[
0,
0
]
],
[
"000000000210130ff4e3186258c09a8463c1e196f5c5432b4c7b6954e907bf63",
[
0,
0
]
],
[
"0000000000e01372ede322bf88ee5ed8a46dd4fd8df832eca16180263fc8b1ef",
[
0,
0
]
],
[
"00000000a0701896e26d5d884834b267512e0af52c92edc4bccf1c5c803d3c4f",
[
0,
0
]
],
[
"00000000869fc8d9ac1588f3e5bdfd60253e9824083800b7794010e0e9c6b6fe",
[
0,
0
]
],
[
"000000001d43b3165ec30736f28f0761600b092686f861db23ec38f2d92b0ec6",
[
0,
0
]
],
[
"000000000ef4092da8c2056e5933de0e1530194c3ad941a9b393fbb26f98862e",
[
0,
0
]
],
[
"0000000001e3fed39f70023909f962bea146b03bc8e94e5d19d7da93123f4f64",
[
0,
0
]
],
[
"0000000000b4b8c877bbe3cde97649845290bb78999ecff4621b9bf2ab16aa2e",
[
0,
0
]
],
[
"00000000006095ba3b4742883a0ec427a3fd685ffb65b987ea77ebfedea7da82",
[
0,
0
]
],
[
"000000000168f0a76a6068a34fc042553aff4aa63b906028f28c2a4c327328e1",
[
0,
0
]
],
[
"0000000000af10f3079b4989ac4ff0baaecab38220510cdae9672d6922e93919",
[
0,
0
]
],
[
"0000000000312791ada0f6a4c5eaf2a1cd57cd06f5970a8ab49923817b862c35",
[
0,
0
]
],
[
"000000000055f3d4f45c4d199d9c230cb2cfeb68c8e934cfd061bd616358655a",
[
0,
0
]
],
[
"000000000036b6129bb5a786bfdd75cb4b932f7dcae9da469d3ba35096f1e821",
[
0,
0
]
],
[
"00000000002fbccf271c13e486673251ecd7951ecc12ee73c4390e0ff09e9b59",
[
0,
0
]
],
[
"0000000000314e297a81bf002fc40eb391d8883ea45ee4e782385aa0fdba6452",
[
0,
0
]
],
[
"00000000d3c473819ec3b3c268f7b555df22772e407bc8f246a47cfc579ec61f",
[
0,
0
]
],
[
"0000000075a438fda6bdb391263d0a2a6e8e68edd9dd8f70fe5734eab9351eb8",
[
0,
0
]
],
[
"0000000017ebae0a2bec50008b4a4ea8839798cbd9ff228e76aba087d0ff1736",
[
0,
0
]
],
[
"000000000800466ba31c0bbc12b125f16d05ed27788de045e25d6f093817d29c",
[
0,
0
]
],
[
"00000000002163c41f2264f202e611aeb9ba6c0a3ee95cd8e5e7e571edc64edf",
[
0,
0
]
],
[
"0000000000de9882d417786fce8c755cfaad17f40cda744d4badedfe5e414e31",
[
0,
0
]
],
[
"00000000002af352cf41f60a5ebf033bf7e4967c0597cee706ba877b795aefb4",
[
0,
0
]
],
[
"0000000000009ca0030f1dd0b09cc628f2d4d278c87b20781a1b136dc395debf",
[
0,
0
]
],
[
"00000000ffd27370a76d06a0da0e3805f47e35e2cf584d73d2c5ecaa2e525642",
[
0,
0
]
],
[
"00000000720da6910aa75099baa020cb8db37e1dc19cdff66152225b7609c23a",
[
0,
0
]
],
[
"000000000a5c2cc704bce5e8527ce91bac7430c659624ecd86e6a1bb9b697962",
[
0,
0
]
],
[
"00000000084273545134e9a06483c8fab00c2b0628056bb1967f310c74a971bc",
[
0,
0
]
],
[
"0000000002f66f4da52804647b1c3e1f89d17bdb05e9cd4ebbd922007c773f21",
[
0,
0
]
],
[
"00000000c46146c9d0a67a354b3f82947e52670a3bded6d8513ab34a68ae18bd",
[
0,
0
]
],
[
"000000002f61c429d7dbe7bde75796086efe574998766806138710a2d6001eba",
[
0,
0
]
],
[
"0000000001daf3e3e78a57df2c2d2ddd14093d10515925e75c818bec3bbd30c2",
[
0,
0
]
],
[
"0000000002e133a7427a9aac6ceca969b27507c14111a45512cdf8f52a436de0",
[
0,
0
]
],
[
"0000000000f7c4374d458666740de1d0e8c55229a209ced7c38e38708781487c",
[
0,
0
]
],
[
"000000000035bb9ea329ba30b83eeb4ea6f57c2fe703b97f9b879f21e22643e0",
[
0,
0
]
],
[
"00000000001220503e0aaee266bca85de09ce97b0091f24972d1ad1c8afe8609",
[
0,
0
]
],
[
"000000000010a614c60457f8d2ae2bb826d037f52113252888fadda8ed773c9c",
[
0,
0
]
],
[
"00000000585a8b882ecff8aa8434feeac4ef199ca669bd81ed473e37f0bb4528",
[
0,
0
]
],
[
"000000009504ffdb5fe82ad88218fb5e75a8bc185247e30e22d23b9fd9b7f282",
[
0,
0
]
],
[
"000000000ddec7d73bcd653168d82e34cf5746e006bccda8a9c031c3289b9568",
[
0,
0
]
],
[
"000000000cb6620ee4e8cb8b6b4d51251e5961f7ae2e83538ab3a4fef3bcc773",
[
0,
0
]
],
[
"000000000239224a0841738513c1eda712b73266ea958aa75f44a3985ebfab82",
[
0,
0
]
],
[
"00000000002630c7c3586fcc19079300403c54dc293bcfdf8a9981f85a5c31bc",
[
0,
0
]
],
[
"000000000028d8c34f44e51fd71f5401094a983f6566e6d08ce86ec5d1bd639c",
[
0,
0
]
],
[
"00000000000dca95f1828adc3c37b4625f60aeb35a6614a4358322b7a6bc2f7d",
[
0,
0
]
],
[
"00000000d72ec84fda18959ddc474d1a31a3a13b1d94695136c4810af8c01a0b",
[
0,
0
]
],
[
"00000000327c29604996eb7f0a208160969ee4408a1cad277a956334f94e0f35",
[
0,
0
]
],
[
"000000000e1bd41d009c1910fcfee7bf1cc1adb04b0b7a632ac36c1092f01bb7",
[
0,
0
]
],
[
"000000000201a5afed48b9d095b949229e9882ef8bc96767be3097c87264dfb6",
[
0,
0
]
],
[
"00000000003f28e8f3f9c80b1269bb0aa3b57501c12458550ef04fd43aca6a33",
[
0,
0
]
],
[
"000000000029e09fc14e38a6a0103c8c67383f41af7d76998055682525f4ca89",
[
0,
0
]
],
[
"00000000285ce297602995582ba5d32d583d618a6a92643566e25dd36cf2b7ab",
[
0,
0
]
],
[
"00000000657045fa54fac52b8480dc84bd4c418940ba63679f4bd6add6a39962",
[
0,
0
]
],
[
"0000000017b7bb58be05a47ff7c4ead27db750813d6bcf3f99cbcc35324cf445",
[
0,
0
]
],
[
"00000000003a310e39b6df17f17450496b4f5c1593399bfa1ab8b4d39bac9b25",
[
0,
0
]
],
[
"00000000000bfbc5294f003548a9636ebbcea3ba42577821266317676fbc363c",
[
0,
0
]
],
[
"000000002329351dd70c24da2eea5ac19f65b6053c4611aa4eb93bcc2783c57e",
[
0,
0
]
],
[
"000000004ce02f1005aa6fa4d158c6e4fce95ab053d88ae74881dd080c24e057",
[
0,
0
]
],
[
"0000000000fdaaa54cdaade8cfb75245de0747c60c0307ad11be9fe154535565",
[
0,
0
]
],
[
"0000000003dc49f7472f960eedb4fb2d1ccc8b0530ca6c75ed2bba9718b6f297",
[
0,
0
]
],
[
"00000000014ca604d769d4b99fff03ae3ac84d1e8eb991c5dac7c3cd4d9e68ee",
[
0,
0
]
],
[
"0000000000190ab8ecef3a3d5583563851672d81a4d4d952b8cf3bd503c655e5",
[
0,
0
]
],
[
"00000000001204d263b607987fab11e1c19c94b7e3e674cc73cc2fb7b05fbf07",
[
0,
0
]
],
[
"0000000000141e8d7f7ac359a8ae58e35ce6010c25ddd6f1881f41c0b939332e",
[
0,
0
]
],
[
"00000000946344dd06ef5ddd13fb74f20c475daf911ff4e3f1dcdf64c330e274",
[
0,
0
]
],
[
"00000000ec77a7892e48b85bcbaf404d16d7fc93747d7e9e3ba6195a9b6f1525",
[
0,
0
]
],
[
"0000000018a305c04dea8e93e423ce9569872e0ec5af49d23a0e3872b0ad6297",
[
0,
0
]
],
[
"00000000055e32c5f8a86c9a712eeb6440bbf9810ae6da12d0cea2493138a885",
[
0,
0
]
],
[
"0000000001913fcbe67badbce4234e86e35a1ea867ecd69814b5f5ab039b7d4b",
[
0,
0
]
],
[
"00000000002c71fe4403aee704720ceafd21f9f8c9c97a8bfbd25bb46223aa40",
[
0,
0
]
],
[
"0000000000343a42da0c811836d0785c272591facd816f0e7fdcfb1109d8f9a8",
[
0,
0
]
],
[
"00000000000309b182608b3eea7fafd0d72e3c79a0a3a9cda03cde3947e332e1",
[
0,
0
]
],
[
"00000000000204cc04e421c3958a64d7bc024a474ce792d42ab5b48a5a6f3927",
[
0,
0
]
],
[
"000000005eaa010e7255bd37e0b00780575074a74d889e17c4dbc578f917348d",
[
0,
0
]
],
[
"00000000a0d425f62d9196c069286dc6635ded9d027de40070d397e45bd63e0e",
[
0,
0
]
],
[
"000000003355fd37068ce2d5d2a94ef964eeb9b687f21f4a00850a3e6cc4a71f",
[
0,
0
]
],
[
"000000000ca9148dabe9424cd8c96860c90d836ab25970a3e91856764e2e640c",
[
0,
0
]
],
[
"0000000000bde23f829dde8edef35436be4b8978da21fd2c3a8100ef5334e3cc",
[
0,
0
]
],
[
"000000000028bb26f1427fbfabeae65d55a9e59e18230713e40f0f7c9c2dee12",
[
0,
0
]
],
[
"00000000002ac05422d254e597ee6b5e0f8be9b3e2f887486442d720c7766919",
[
0,
0
]
],
[
"00000000000e36d0b6f187dd9601b1d1dcd987c3e0f6a081ffd039c7c5e32462",
[
0,
0
]
],
[
"0000000000048d7b1f2a2a11fda34a5cfeea067ab03e482931e5a0f463f438ba",
[
0,
0
]
],
[
"00000000f780ab88c8a4f4247573a749fbb087a4e3fb6a7d29926de8a9ab3462",
[
0,
0
]
],
[
"000000000313bbe6a940e6a8c40ba091aa1ebbaad135bbbff3ed8ae07cf574d2",
[
0,
0
]
],
[
"000000001d4ab29721aa2722482562670a0d71dc1eb73231c5dafb64756b04e8",
[
0,
0
]
],
[
"0000000006588bcbdec38d19962b96cf0352cbf1b90f3379cc6787d018cdb96d",
[
0,
0
]
],
[
"000000000022e79539a21ac24f9daa2cbddf2bb4a3125f88a5efc20d13ea856b",
[
0,
0
]
],
[
"0000000000dd284b7fee584cc578a10fbe57e8efe6bf6ebacb23c0ac5d46cdf7",
[
0,
0
]
],
[
"00000000001451143787f411c93d5506065c3fb597966f2fd7a4a5c078ee6aa2",
[
0,
0
]
],
[
"00000000000ca977394af1e414dc1f9d83efa007f7226e11d3a00f59a1fdfad1",
[
0,
0
]
],
[
"0000000000011f8caa80580e7a796bbce5b84e60731bf48e03c6ff5c6bba868e",
[
0,
0
]
],
[
"000000000001705beb1376af1af08b437acef6befbe7d3b60c5fbaf6bb7f38c9",
[
0,
0
]
],
[
"000000000000c838f1f45422d93ca9b5838368a37423efa8439ee24b2bf247a2",
[
0,
0
]
],
[
"00000000000111ad857d31d07fdc8b32d17af2522c18bdaccfef449b29d17362",
[
0,
0
]
],
[
"000000000000312a7718fc616b0ecfdbf6066f71ec1a4a8c43f50f02f61cc398",
[
0,
0
]
],
[
"0000000000007d232b217a59b804ef67091c5720a5460c2c16bf97b97a24801e",
[
0,
0
]
],
[
"000000000000177235c33695aced585685b4c500eb76e72caad02e17503900eb",
[
0,
0
]
],
[
"00000000000037f5c5890da7a8e2acd2b0669ad7db648ac43140c637a1c81637",
[
0,
0
]
],
[
"0000000000002123904063f223bc35135c426a4f9a0b74c1907e837b810f0321",
[
0,
0
]
],
[
"0000000000000961db809da357d91a9341170fafef9f24896d8730bd05cf3f96",
[
0,
0
]
],
[
"000000000d2e8fcd05eb874e98cfc3a6e239f6974950e6f50b0487513ecab760",
[
0,
0
]
],
[
"00000000017e362508c8db23fae0431eaed708d9db13e48fd5d318066bf6733f",
[
0,
0
]
],
[
"000000000011b2bc4fe36f90b7ba5a62f974db250bfdc285b70c71148023c7e3",
[
0,
0
]
],
[
"000000000001be28570b378dd5dd2eb3aa495c229913b6757fe8900dfa3cce99",
[
0,
0
]
],
[
"0000000000242bd0bb16d0a5324e0b4b5a83697dabb3b4a059084557478e50b9",
[
0,
0
]
],
[
"0000000000d8ce69d18da32ed52e503d6b5ad48d970b90545f956b2d2af2edf6",
[
0,
0
]
],
[
"0000000000366655bf0cb3dd0cd7801e0adbd26b5b441b77a9e3642597effb00",
[
0,
0
]
],
[
"00000000000dc7aa00d4607ca8374d40d1187f1c084b620edb45fc39bc8d2db8",
[
0,
0
]
],
[
"000000000003baf60d9c6e70a765cf517f66a124509191188e9547ad09edf68b",
[
0,
0
]
],
[
"000000000000e0f476893b8fb4d37e855353075fde73dbc1fe181cc956349f19",
[
0,
0
]
],
[
"00000000000032ed16b7de758abadf4a4fb2df7a101ff275c51f29e1555a89a5",
[
0,
0
]
],
[
"0000000000000a564d03f0f2fe20f6fb5f038d931f732d817641cd7fff3b0acd",
[
0,
0
]
],
[
"000000000000011aa4d0fdcea8d4ca85cd5d548e322e2b6abd17f8444be855c5",
[
0,
0
]
],
[
"0000000000000610588540267a0eb544531047d4c8af0f21fca7cd3d96205cfc",
[
0,
0
]
],
[
"00000000000002770dab5e14843149df8f76b8dc8458ed3ed2ed8a14a6e2e564",
[
0,
0
]
],
[
"00000000000006b70ebc9f75bd32f466602cbd4b86c3c2d2379059542bb8bec6",
[
0,
0
]
],
[
"00000000000000ef579af389fa7674f98a2371063fa8b218c5ca0ad94e21b896",
[
0,
0
]
],
[
"000000000000021b6108dc988f9153383f9501ab9001109aa87902ddd4c8a4d1",
[
0,
0
]
],
[
"000000000000022c02ff22bc0af5201f0e1a14a75879c494731e4fbf999218c8",
[
0,
0
]
],
[
"000000000000032651c988edc1ccd08e82b888cbb8135e24a958ac0c0b640d5d",
[
0,
0
]
],
[
"000000000000015aefdfa0790bed326c38c358c07aac0674f5b2e771258b8df3",
[
0,
0
]
],
[
"00000000000000822e1534c86afef911b67d3fa20cf2b12d93d20d64005f54d7",
[
0,
0
]
],
[
"00000000000000338b871276768c923b1c603fd6150bd054c2287e532e61de7f",
[
0,
0
]
],
[
"00000000000002d0af52c0cae894bf836b61137ace2bd7500abd13a584c02741",
[
0,
0
]
],
[
"000000006f8443a458f38d8731821c07a2fda0ecdbb1cf797f541844d468ce0c",
[
0,
0
]
],
[
"0000000000b6fbd8b4e227f5514979a61d8b0b918d2adc154e585ca926386704",
[
0,
0
]
],
[
"000000000f4f5e49b10278e27d9dee15b92f9d4a257138a206831e0c00188767",
[
0,
0
]
],
[
"0000000002c7e9769bd8ae9906fc5682e937b5c31ab5b5b86e4d70af2c15a95c",
[
0,
0
]
],
[
"0000000000f68a1db8cd387e0a2f93f45149fe1ee4a230bb386313bdd42058e8",
[
0,
0
]
],
[
"0000000000f0f65c360c8f0f9853ad1142f16675dc1175d61afdbef977776b25",
[
0,
0
]
],
[
"000000000004f734e634156511cbef7dfefebdf317e7488aa6c2562572d7ecb7",
[
0,
0
]
],
[
"0000000000002a46a7a16787e8317dc567ae26816324c2035be0186ba54d5cb8",
[
0,
0
]
],
[
"000000000001a593e6f01875b77e270163538d88452779bb557df7c2607c28e0",
[
0,
0
]
],
[
"0000000000004f24cfafa10bd50a452535f64be577a6161e51c7c71542f654c4",
[
0,
0
]
],
[
"00000000597cce73e84b63f08cfcb9b01f5e7621752d8c8e08fabbd6ab5c0dd5",
[
0,
0
]
],
[
"000000007cad379df01247771fff471bc99faea1b86218602f45ab13efc5e9f6",
[
0,
0
]
],
[
"000000000d6085aab25892be49c49d6c0a3949befdc3ddce2faa46b104e1e804",
[
0,
0
]
],
[
"0000000002be5996786b42d6a229093896aea9966b1854ea261e01e84da1f420",
[
0,
0
]
],
[
"00000000002684b72056e270b115d80b12b2f68eac7412355287226aecd9b5e0",
[
0,
0
]
],
[
"0000000079ea27efb24366c87856a9e371c56fcbd59d09d3164a5c2fc15fcbca",
[
0,
0
]
],
[
"000000001694120525dba4548ca54087544da1fbefa51c38f0208d683418825d",
[
0,
0
]
],
[
"000000000693e80d372938f3553151ab9d0a9a6922182591c701df739dc9a502",
[
0,
0
]
],
[
"0000000002950d9cb23c8511937811910b712f73d448e6fdc2e39e029b86848b",
[
0,
0
]
],
[
"000000000091c40056c6a48f33db17764af89c01f62ae653aa5e494146164cee",
[
0,
0
]
],
[
"00000000001f373c47e1a39af4e1ebcd8c88411ec49d6bd520c2781564070971",
[
0,
0
]
],
[
"00000000000809ca4b2170c57958709b867095b1972d80a2ee55359fbd0940fe",
[
0,
0
]
],
[
"0000000000038e7bd66fc3308447b1370dbdd0661c427c512bdbc641ff360fb2",
[
0,
0
]
],
[
"000000009a3325df76e2de1fc1970cc2f241fa8a41da9ad745a0d9666d9ff51d",
[
0,
0
]
],
[
"000000003176e92ff837bf43a48a995c1a321b166475f586ffb4b962e0254a4a",
[
0,
0
]
],
[
"0000000001ae3292e81ca3859b75bccd5bff825cd9f496efd085160c716ed05e",
[
0,
0
]
],
[
"00000000033bdac4f0d36bb912fba28bb5caa54d1b611759a10f79ff3c969cf2",
[
0,
0
]
],
[
"00000000004c6db7fa0e2c9f08693abfeb128c5827b511a5c46c623a103b416b",
[
0,
0
]
],
[
"00000000003d87f48bb95e9431760d0c5f4f93c77d02fce9dd1673e9f5b01029",
[
0,
0
]
],
[
"00000000000e214fc3d8b97571eb75d248ca29f8e25a584c33de8488ceee72b0",
[
0,
0
]
],
[
"00000000000133269b7159b828700d02de770a8cbd91f3d166e6bbc95d8e0dfc",
[
0,
0
]
],
[
"000000000000cc92e2dd933a08f7fd87f84451627982fb66583587858217c059",
[
0,
0
]
],
[
"00000000000030708136c20c4c8216314005b3cb5c551ded33b26cf64d2ff47d",
[
0,
0
]
],
[
"00000000c472a1341d479ed02f31b699e448c035049a7092670b38f4ec6121f0",
[
0,
0
]
],
[
"000000000a358834d6eed41b9b7161a338aba53828111414cdea7552ed15548a",
[
0,
0
]
],
[
"000000000e13e77372daea775c8358916e57ed11835899c14e5140ed9be11089",
[
0,
0
]
],
[
"00000000008252cd0931f94b2465bd4f93e4bfeec6697962c5b034cf3d12cf7c",
[
0,
0
]
],
[
"00000000019812cd6cde3a43831234be71e68118be24a80161349b8b327acb5b",
[
0,
0
]
],
[
"00000000005865499f301adfb59f8380743e4c3b3ab220ca4eb97dc6628df626",
[
0,
0
]
],
[
"000000000015f77e1e61329560a4378eb401fa5bf0ef90b0a014a4d7857ca7a8",
[
0,
0
]
],
[
"00000000e9cbcbb625e8a463ba8e7f14be46ba9538ffe93338784ccad3d992e8",
[
0,
0
]
],
[
"000000000fb27169efcc2873cfaac223ebb91cc5e1e5ad7e9a312d42bedf7c42",
[
0,
0
]
],
[
"000000000c9c96d62ebfbf3fa4003f1d46d175140ab084dee17e8125fa40f24a",
[
0,
0
]
],
[
"000000000311e3a766b1ab2064b68a344a561eb496d595126808ffb166c71cc1",
[
0,
0
]
],
[
"00000000677568c82262ac3a4ca3f909bdfb0b35145ad490fa3fbdc719d06b91",
[
0,
0
]
],
[
"000000000ee77ba9ab657e51fd9140f5c9b46731d9341e98188f929c97d04746",
[
0,
0
]
],
[
"0000000008a67eb9c91a6d74168f3f385270fa942ea00bdd31924d1b6ea11148",
[
0,
0
]
],
[
"00000000017f93c9e0026e90d579e18c83b4a8557f0c00e9b85ab164cf4466c5",
[
0,
0
]
],
[
"0000000000994efa379235c03711a8e6b29895d928b5fde96cb01c02374c0602",
[
0,
0
]
],
[
"00000000b3be9f23c943d71d7c7dbdf6dd672d77a712f6c83e9796a85e4379f2",
[
0,
0
]
],
[
"000000000713e1089b0b2bdcba462b740c9396f822f1c73e090713978a7f1314",
[
0,
0
]
],
[
"0000000002fc44d358401a7ac9ce4ddcb17f3cbac08e40242e755e60ab2292ed",
[
0,
0
]
],
[
"00000000021ef2c04fd30be7049f73b9a8353ac96a467dd5f0b9c1457be1bc5e",
[
0,
0
]
],
[
"000000000023b95b440ccbbdcb914172cf675cd15d6111bd7f5a436a4925d36e",
[
0,
0
]
],
[
"00000000001983521dbffd1b742a6d4b5dfda3f46579fbbdd83a2ebf9a039bec",
[
0,
0
]
],
[
"0000000000044d53dbea312432e68fa90dc2148946f613216dbdeec86f6a67c1",
[
0,
0
]
],
[
"00000000000107667692f12d21a55a72ff1dce828f96872e36c35bfbae475a8d",
[
0,
0
]
],
[
"000000000000252d1d0c01744ec25af801ef7c57e2581c95295070b6a8a85bd5",
[
0,
0
]
],
[
"000000001c1da54e16dc06158677024d9e74bff39bfaec83434ac33673fcc251",
[
0,
0
]
],
[
"00000000b4d0c6ae86bfdf7ba4c205fc3e6b3b6d63836b85e30e9d8bac922301",
[
0,
0
]
],
[
"000000002b16179cb022bf678bd847dd6fc1908d0df04abf0c7874981eb33ee7",
[
0,
0
]
],
[
"000000000e6783554aae41856424d184dc4fa061f40676efd107e6f933a25641",
[
0,
0
]
],
[
"00000000005ae4acbab519895b4b523d97a09e381c9e4b044e642f73b8c0f1b0",
[
0,
0
]
],
[
"000000000010372b59c9595d947064804b75ab21868dd075a3842ab7d2df6181",
[
0,
0
]
],
[
"00000000002f9f587ea304093be049d3142ac0c92f9c68928a4f82d12b929b69",
[
0,
0
]
],
[
"000000000005d4cae51b3c76dc3c61bed0c265c4f228c0c4d1d3d147146c34eb",
[
0,
0
]
],
[
"000000000001a5b6c0e0a0b485a490cb52ccdf9b22596656039b51545bb07be5",
[
0,
0
]
],
[
"000000000000d723d0976338edf55d08edab995dd6283cbb688855f0dca6e8f5",
[
0,
0
]
],
[
"00000000bfebfae90208a82c7fa06c0f61674dbf1e4f9162e370656c38d611bb",
[
0,
0
]
],
[
"000000000c91cd144b2a92ab5024c87f70cc1d76a4a7f26a82a98c5aaad62850",
[
0,
0
]
],
[
"00000000077c8114eb5cfb69c3924c699d0c70334360dd1daa95db0db4816953",
[
0,
0
]
],
[
"000000000348a6443e091db8f68e88a10afad7c6e3e5392247902c4b4feade43",
[
0,
0
]
],
[
"0000000000d63b70351e05829ad8a56336521b361b0d50eb7ea1f5b46c25b00a",
[
0,
0
]
],
[
"00000000004658603163f0ede572120a1bbfce8d313aa282ae54d2ffd9fe9079",
[
0,
0
]
],
[
"0000000000048063b410c793db34856f23acfb19a0ce72f5997fa572773378c8",
[
0,
0
]
],
[
"00000000000228fb6e587fa593ff8b4764064bba8bfc2f43ba5b1f12af33d04a",
[
0,
0
]
],
[
"00000000000082e3ddb75c0ea2a98922b1556ce10346f9bb0cedd97ccb3fdf62",
[
0,
0
]
],
[
"00000000000005571b54d4886b44b81c21dfbefa554cd7c23430e5aeff6b5ae2",
[
0,
0
]
],
[
"00000000306a603ca1a0d961e08e103a9f13f3615163c3373d1bd2a67cadc2a7",
[
0,
0
]
],
[
"00000000195d93ba7ae19832b622de86ebdadf3c78f1751ef2b2e9b0e3a530d8",
[
0,
0
]
],
[
"0000000000476d0d00cbc68bb20b4893f0e608b02a1e029b8c6c73e169c49e69",
[
0,
0
]
],
[
"000000000051348044bc10fc05960c244c3ccd3b3b6c145ffd9958a1c8bc0215",
[
0,
0
]
],
[
"0000000001e4df369203badca9aedc28c240d592b12d284ce0b0463fc7537c09",
[
0,
0
]
],
[
"000000000091cc1ccd448b0ec9185618a84dea96f52477cfb9b9ca2b60cebe83",
[
0,
0
]
],
[
"000000000024a50299c0ef0c6dec9c64336b6cf5c1a1b0013e22fd4fcee1d7d1",
[
0,
0
]
],
[
"00000000000349248c1df06c3783d1270cd97ce7f605b9036fca0fdc2f0fbb96",
[
0,
0
]
],
[
"000000000001afe6793e7427a3d780876d26eb7f2ded92563f991bf7302aea69",
[
0,
0
]
],
[
"0000000000007148006e139e24d9fccc307661c9a0cbcd1af983487c2f0780c9",
[
0,
0
]
],
[
"0000000000002734722a341984738177a3f6f264291424e4984f2128d921bf29",
[
0,
0
]
],
[
"000000000109b02caaa95e49a477757a41a42daed40e92f54fa09e63f5538cd2",
[
0,
0
]
],
[
"000000009a11c7ff8b8fa7fbff5a04c25906f701ab5bd67195736f9ccc839ab9",
[
0,
0
]
],
[
"000000002b1d77f8e0cd60af1c62ef6d381e8905665b15a7fbc546d0c1a45e18",
[
0,
0
]
],
[
"0000000002588cb017de9e2f23cea7edc5082f1b3faec890f9252d556efeac40",
[
0,
0
]
],
[
"00000000008b07f177adc24a4b1a64d2dbcfbcc903ba861d493e11d6b33af7dc",
[
0,
0
]
],
[
"0000000000bab8db5020aa8e052165275e8eb3e7c843533246bf6e4c8374757e",
[
0,
0
]
],
[
"0000000000138488fdca8bfc327e6dbd6c72c5f1dc5868d9c0ea886665b9b56b",
[
0,
0
]
],
[
"0000000000094021fc954efbf08be667fef1b817e8715d4093a561fc30264aa7",
[
0,
0
]
],
[
"000000000000e8183e64072db79adfc6c09b650c4178001be3fade4050b06005",
[
0,
0
]
],
[
"0000000000004c93e8661c75974cd191c68dd66999da4f70d039c0ba4a12b970",
[
0,
0
]
],
[
"00000000000021c675b3ec404bb996f5e68f9eeceeac6946e5a6822987824d33",
[
0,
0
]
],
[
"0000000000000ad85684d30f25d1ec34638f099df2f33b418a07307c68fe3c2d",
[
0,
0
]
],
[
"000000000009c6add76ac42a1942c4ce74d25d1b8975d4e3ac8932185e785a44",
[
0,
0
]
],
[
"000000001e7d828d354716881683eb6fb5caec5d91afce298e4e3bcee9574924",
[
0,
0
]
],
[
"000000000a0e438ab203d8fd3e56100f2f14759f704bff6c699df0bb4e9aad64",
[
0,
0
]
],
[
"000000000b7d5c2895df8bc1fdf5d31e0f663564cb5cff3b18642c44a71b6248",
[
0,
0
]
],
[
"000000000193209ecd92fce00a75975446423d94a325ed525c15d5ab921da273",
[
0,
0
]
],
[
"000000000020835bdc30ac67efdbc785d15186914bc14e86387f97450df46418",
[
0,
0
]
],
[
"00000000000c9078321f0030214c75e170b01ec664d39bab1b1e48460a54eb63",
[
0,
0
]
],
[
"00000000000ac68b63d486ade190dc9108eb3730d25e7537649fe21c30e0121f",
[
0,
0
]
],
[
"000000000002a94dfc5f4b677b251a7a7647dbb99c0803df8658222227fe3e3f",
[
0,
0
]
],
[
"000000000000b076bbef0e50593b1595ffb3d571e7ad95dbdf06dca8824ef7f3",
[
0,
0
]
],
[
"000000000000167075c8bcd24233d25cd268271c0e8fcb6f301ee1b6f6ff0341",
[
0,
0
]
],
[
"00000000013107aa587bcf12ac445330ff0325d73c5253f7e6a49ed8c50257bb",
[
0,
0
]
],
[
"00000000090ff53d49c9ffd51511af8d5cba2038a8e25e3b17186b1bc941f43d",
[
0,
0
]
],
[
"000000000d9e704d5607f77f8983cc56069571a3761d5bd5da55f05ec5d8e844",
[
0,
0
]
],
[
"0000000002b2b4c0950fb6390f0ae860840e84eb0a82e5e8a9bc37c14bbf43b0",
[
0,
0
]
],
[
"0000000000be10137a2434dce1d97850b768ce878c1c80ec905f6e9f21e65fa7",
[
0,
0
]
],
[
"00000000005cd966f80183d4c048e63a5c14f649298dfd261d989d9e3c026bf4",
[
0,
0
]
],
[
"00000000000e8f30e55006a4082380c4b1a372b7ad919d3a9b0a52fe5ee881d3",
[
0,
0
]
],
[
"0000000000018c70a4c27bdba237ad19ebae5d3ca23f1394ccc746d73669a1c4",
[
0,
0
]
],
[
"0000000000022acc8432c883953227786f7a6560aeaf0176d232c8affa5b25b4",
[
0,
0
]
],
[
"0000000000001854e95b28b4efcb2cfeb08c76d8cf1fb03f2055b3fb758f3a1c",
[
0,
0
]
],
[
"000000000000187080c2c39f5a3ea8be72ac4d3ec0d16b21cd34f1541bef23be",
[
0,
0
]
],
[
"0000000000001593766a3c63b524f658ec7690df467cc7bbcebbdb56385500d4",
[
0,
0
]
],
[
"00000000000012d6966dc51a41f2c617192169ec8418405e164ba83b9f7ecdfe",
[
0,
0
]
],
[
"0000000000001d0c7d0a2605e127b00448b71e756ad96625116ab8ca18f74900",
[
0,
0
]
],
[
"000000000009cb439ea49282d257595ad1f7602856c16cc26fff423f7783c792",
[
0,
0
]
],
[
"0000000000889282b98336c994d7420a639221e0484b511227fd616d78dbd028",
[
0,
0
]
],
[
"000000000071a4a2ad6767864bd21239c74c9912a40ca9fd3b209e21b66460d9",
[
0,
0
]
],
[
"0000000000f3ed2c3c9a7c3a7291e859cecba8cf9243d23a4892e6be8ea9b70f",
[
0,
0
]
],
[
"00000000006a4258ffdff8b7f6f4f685ce18c6eb1d7a1cf501ca9e02fcb7620a",
[
0,
0
]
],
[
"00000000004af78f1a109d1267a9c24d69c6a4b30fea49f0efa6c8834cf394f9",
[
0,
0
]
],
[
"0000000000193bf3efbb145747198470a81b2cd33c991057676742d5c22a64b2",
[
0,
0
]
],
[
"000000000006b436798c7e4a8c3bdbf054a66707feee5a18ce9ca57eb95bb48a",
[
0,
0
]
],
[
"0000000000001db50c7caa3a02ea4f173343f958f334a8bf3f8638add9e69b34",
[
0,
0
]
],
[
"0000000000003c621629cc0bcec5968d61d2e42c6673de4d46555118ad5001d8",
[
0,
0
]
],
[
"0000000000001262bef2918265f6dd4534013a4650444054fb4f5e490c5ed57b",
[
0,
0
]
],
[
"0000000000000120ceee972d70cc84430006645997c7337976c673bd75cbef2b",
[
0,
0
]
],
[
"00000000ba16134dc0c418a116b97ad5deccd6bf6e3daa028a8a6a80d7823faf",
[
0,
0
]
],
[
"00000000a1a00d6d6fe0660e63402a5a7c7248589211594d37fd800456ce84b6",
[
0,
0
]
],
[
"00000000394766cec78f962c29aaa715b66e3ad34e1f2323dba45e087cb3b395",
[
0,
0
]
],
[
"0000000008b15a3020676f5e084210ecc05f646885eca1cf6a10e9ae9e3995cc",
[
0,
0
]
],
[
"0000000002cf7eb98abe784f6e516670a88b9028a6faabfd099a364c2dc5c42b",
[
0,
0
]
],
[
"000000000054015fec337a9ee43eea501d2292f031f5bc1f09758d20f5cd3135",
[
0,
0
]
],
[
"0000000000068d24d31a9f1192d848155a2f90939627bc456c9a337135a923fa",
[
0,
0
]
],
[
"000000000006262bd09358258edcc455f9ba46b7f9d6e69d0f6b9da89488a4a5",
[
0,
0
]
],
[
"000000000002327bf77ae67961463ea98a78dab06c24ac7d58b1727c5f856626",
[
0,
0
]
],
[
"0000000000006672235c1606fbacd7861b16b267d203b4d687708eeb1fc25e6d",
[
0,
0
]
],
[
"000000000000ac0c9a39a47313a8715f125c46d6ea8be8741b99b1db4a8aae47",
[
0,
0
]
],
[
"0000000000007e93f6578e7856aae0ecf6341e1312664d9e1d812ff254c37ae6",
[
0,
0
]
],
[
"0000000000002a980acdb1443926875e7d4a57859b2b45ce3fa92c7716319f62",
[
0,
0
]
],
[
"0000000000683bfd82c63514bc58a80daf699a6bcd040bb2a499540baf52463d",
[
0,
0
]
],
[
"00000000373e6262928d7a6cac965b294aef35f90b72c85100ef91501775e06a",
[
0,
0
]
],
[
"0000000000f7bc44061b65c62d4d7747138df127dd2a30f583c3ebb66a25c7a4",
[
0,
0
]
],
[
"000000000212a71c38d0e13ab7c5646c949d4b7ca23afedbe351a43b7607043b",
[
0,
0
]
],
[
"0000000000a836e88f76ee5dcca1e884572f32f4460a3b024280738d76e98ced",
[
0,
0
]
],
[
"0000000000413f6c1b1c9841961636bb3290f2410ba0731f3522c4ff3faa2e0e",
[
0,
0
]
],
[
"0000000000082336107412226110ab2a53016d4faad4deec048828507a300248",
[
0,
0
]
],
[
"000000000000a91e7a3f35a23f01621dd051e314da617714991467131808d3bf",
[
0,
0
]
],
[
"000000000000cd6576950f6f238227c3ba7f62405ed1bf3af4878c6dc1b04635",
[
0,
0
]
],
[
"0000000000674099e9741e44da03e9531402a2607a19a65660b57470340828db",
[
0,
0
]
],
[
"0000000030c4744001ae85f9e6b46ed0664449927b86b8fbf25b22b851d23671",
[
0,
0
]
],
[
"00000000002f5095ad1a12eb9eedf88ce1e7268368461b6b4e10051148f436cb",
[
0,
0
]
],
[
"000000000057d3e2a77eadb8b9613cb839ab02a96094dd5d0a6d1f09026c3936",
[
0,
0
]
],
[
"00000000004e0a28be887d6ed037cd9102cbbda7d6c9e584ba51f2c2dce96232",
[
0,
0
]
],
[
"0000000000211346d8099f7ecea72481c4cd45591f5e0d7e347725ac2162f142",
[
0,
0
]
],
[
"0000000000199ae9fc06c5acee766db6033b86f76c266cadefe1461c611c2198",
[
0,
0
]
],
[
"00000000004c9e5748558d4f5a75bc824171e3b958152dfd6844330f1e907f8c",
[
0,
0
]
],
[
"0000000000137addf1521361dad1ee007eb9e6dd4eb8441492ebfaa3c240d556",
[
0,
0
]
],
[
"000000000054d4c77bb7964e5327c35760d87b890ea336aec5ecdeb783350738",
[
0,
0
]
],
[
"00000000006b7b06d04818e97a4df66164b471912f88d9cd02de4af6c8bbe74f",
[
0,
0
]
],
[
"0000000000380fa9858e3e90335c061a3776a26bee1e8b6851de33ec63670782",
[
0,
0
]
],
[
"00000000000842598b03fb79ce7386e9f9181a02dcf1effc8f70d3ff7368ccd5",
[
0,
0
]
],
[
"000000000003d3475edecd733fc7b82432882d9c9f1350a98ef8921b87db4dec",
[
0,
0
]
],
[
"00000000000000e330a8d57a38dbcc0b0a5dc7a4210f231b8082b9be5f9e4bce",
[
0,
0
]
],
[
"000000000000218ff87fd50cfba2fd04203a78d2600cb2c4dcb039d803426e19",
[
0,
0
]
],
[
"00000000007c96e6e3ed3146260348ac79ea7dc2ec2ae6bf8dc203400a37721d",
[
0,
0
]
],
[
"000000005abaa10bf7260470c28ba32f1755b4cfd3734aad580681e39a9605a5",
[
0,
0
]
],
[
"00000000005e77c226e6fffccafa56055e68f0ea0a30101e6a243ab9b3e07db0",
[
0,
0
]
],
[
"0000000000e989fe27f85b89c1e852d7bc94b09033cc6c8b32fbbbd9383a9ae1",
[
0,
0
]
],
[
"000000000091a1e962438583146293ef34156962445ffc5e81e4d0fe327d37ac",
[
0,
0
]
],
[
"0000000000477978a6903217e2817d10e99bdfedb4f8bc396b96fd5b0b93b522",
[
0,
0
]
],
[
"00000000000bfd9e5f13a9c03c48e8b58a937cf1ae2849160f1ca11f8fcced3c",
[
0,
0
]
],
[
"00000000000158dd3c31b6379887b4353ef2898c03b7ce55458fcd57cb6f0639",
[
0,
0
]
],
[
"00000000000029d7009eb56b9d38366005576b82a9b59fc845522a34ad36a38a",
[
0,
0
]
],
[
"0000000000e6e207a82b8ad7136352204bb8e9ccfcd25885a715d3c65cbee997",
[
0,
0
]
],
[
"0000000000fadc4429f50fc534ccac4db5e51a313df25034d6c5c25f7e83448c",
[
0,
0
]
],
[
"000000000019c58defcfdab6c6ab9497685e61118effda4c2613bf44be19fcbd",
[
0,
0
]
],
[
"000000000006cf444d846093c5045d42ddc0986ca805f261476d0fd2eb474c39",
[
0,
0
]
],
[
"0000000000d0856a3d6a1e5b1ac7e388cc029bd8410b3b1489598974fe470568",
[
0,
0
]
],
[
"00000000003d9aae63ed532b78082ca5386211e22410fd24ebd5318d1a4cd1da",
[
0,
0
]
],
[
"00000000000345003879f86021a6d5e3fe93813246818c145947b7e225691177",
[
0,
0
]
],
[
"00000000000175393730cde3e49de7af2b81ae736eee005a9f9c4a1e878c52ec",
[
0,
0
]
],
[
"00000000000087a8c621c879aec2a897258632d6aa631b9a38ba4d564e08682a",
[
0,
0
]
],
[
"0000000000002ea641b2975935bd9caf337b51ac9f9bb90a54f6ea6ee5d3112b",
[
0,
0
]
],
[
"0000000000000c544f9b6a8cbab6d25caf949875622bf75139234850b10affe1",
[
0,
0
]
],
[
"0000000000000f66fc4e37232a29f3389c493863a980d58a1d570eddd5268999",
[
0,
0
]
],
[
"00000000001213fe2bbb8aacb1fc14983586e09db964151cb507956a81b35f25",
[
0,
0
]
],
[
"0000000000ba82c2160602ddc1913bc4c133ad0af8848e014367c84110d00e05",
[
0,
0
]
],
[
"0000000000b7a98b364b1cf9521275a915c7a1b3a0f0c052c7d8efb620ec0870",
[
0,
0
]
],
[
"000000000047dc62db23540ab4aee43e54812aedb623a2a158aa3244fc784722",
[
0,
0
]
],
[
"00000000005291002da10e53c3855882251a6e5a425b5e639ef9be3bd05767ca",
[
0,
0
]
],
[
"00000000005ffbcbc0d9b380584bdc78050a6f0c3582b4c9c5103a150cbc71f5",
[
0,
0
]
],
[
"00000000000a7a69cc06b0a68b27a8fa5d29727ec3b6db8d32d61cf7489b5ff3",
[
0,
0
]
],
[
"000000000007212eb8c49758d98cefaa6098da2b877a6055be341f5f7c0ad301",
[
0,
0
]
],
[
"000000000068d1099d8cf3f43f6d164f2925b1d52ede75640cc65ca020e1de1c",
[
0,
0
]
],
[
"0000000008d5ddef4468a4414bd08184c2eba0ec536b85a743b1091828a6a884",
[
0,
0
]
],
[
"000000000acae40db93b589783b0cde70b98552955cb3c12f08de1b417d9008d",
[
0,
0
]
],
[
"000000000066a51eaa3a54036f338719da3d5779180c0bc3787b533410de90e5",
[
0,
0
]
],
[
"00000000008b521677a6e897950aac69640e52efb01b7af10bba3820ecd09a89",
[
0,
0
]
],
[
"00000000001823f0e399311cab0fcf57403e094feebf99b22030bafd2004da87",
[
0,
0
]
],
[
"00000000000bf821c2abf5bcd00ca96439ddf5b0b593be5601145fda5338efdc",
[
0,
0
]
],
[
"000000000003f4fd19b2af0141289177014ecc6dce6ea8fb50bab93d4a291095",
[
0,
0
]
],
[
"00000000000011842d892a02e55ca594caddc9f3cea1979ddffefc070eda8498",
[
0,
0
]
],
[
"000000000000208aa0259d20f51c0e7b8895e18a93aea79af9b3832e710ef134",
[
0,
0
]
],
[
"00000000000007218f849e72dee1f7fb6fcf36f3b6745c6468187ed2ed13287f",
[
0,
0
]
],
[
"00000000000f79f656cae641c2b74554c6ecd673c0c7550671c4c2af940661b3",
[
0,
0
]
],
[
"0000000000199b4d178c05fd1c3154c9a4632eadc7bfc734c4522176c977ce8a",
[
0,
0
]
],
[
"00000000085d0682d481635cb2e6de2e4d9884589455a86194f0b222f9acb3c6",
[
0,
0
]
],
[
"00000000015972a5a6786a14b009bf582c4bbf7b9854591dd8d26f82b43ddaef",
[
0,
0
]
],
[
"000000000064bf72b7bdbfcbe96dbbd0efcaf7aa94c0f92cb4e6662819468fe4",
[
0,
0
]
],
[
"00000000003df36b7962bb4ad62266c462382eddc93f4bfeac464b95f7a89ee9",
[
0,
0
]
],
[
"000000000006516d3a9f424eb61db5dfb85aeee29708b78c65d24827bd926263",
[
0,
0
]
],
[
"000000000001c1709fe1b294712638db356e89155650f6fbecde79ec47a92af7",
[
0,
0
]
],
[
"000000000000dfc23251344b593c16c28cd195abcb337519d7bc82175721a033",
[
0,
0
]
],
[
"0000000000000aae2dd2bf0b8581d137fcfa3d9c4cadbe3ef3834d7cae4268c0",
[
0,
0
]
],
[
"000000000000092a5baff3d9a5ae87689b2afe668e71bac3b342c7d383f0060f",
[
0,
0
]
],
[
"00000000000fa906eeff7d2e126698d88b8cda01d32ea2c039c26984daaa17a3",
[
0,
0
]
],
[
"00000000002d4315e5bdc2bcfdb245b914130764a50943a2b2e02ea3acf5c47b",
[
0,
0
]
],
[
"0000000000fc2bc9bb83e04cbe922d64719295bfef6320027725402306bcf1a0",
[
0,
0
]
],
[
"000000000142690e7c334b97612746d6db208e6153bdfa8479d86d1b575feacd",
[
0,
0
]
],
[
"0000000000629a7820e8cdbbed18dcfe16c992152badc745ca73b9b34e53fb0d",
[
0,
0
]
],
[
"000000000023c2e9dbf3fe03248e40f4ec3fb2dc81ac573d5a6a4f490c701877",
[
0,
0
]
],
[
"000000000013658a43b6d1c4be95fa36e32d3edf80716de3a8f7e98858016adb",
[
0,
0
]
],
[
"000000000007c847295d8c4b6da9d8a64b57c3a2307e64387bf8882b9d35d6de",
[
0,
0
]
],
[
"0000000000032bf90b823332af80bd2ea18f411f081c7dca8f2fe79d9215526b",
[
0,
0
]
],
[
"000000000000001bc0655da6f24c6952e811006897a0c6dd8b6bd94f178636c8",
[
0,
0
]
],
[
"0000000000001e1d09b15393190cf686e25488db7fcbc2f1ebacc8165fe6e3a0",
[
0,
0
]
],
[
"00000000000cc79ae066badb4157def4067057cefd705bf87f1d832845a7ab36",
[
0,
0
]
],
[
"000000000014408398244b94b4eff6b54875802ede6df2d1d21915333a195719",
[
0,
0
]
],
[
"0000000000114135a1bc757110c05162fa649b694db9569be117e34832c87257",
[
0,
0
]
],
[
"00000000009b15fb2bcee1af904989ba0761e4cddc6b3ee214c0bb07dac6211f",
[
0,
0
]
],
[
"000000000012be506dde2c54adf355bdb41a457b0abec436202a3be73f0b052c",
[
0,
0
]
],
[
"00000000000963760ceb5fc65570650d494805e05c9d753f3ea6d44247ad3d08",
[
0,
0
]
],
[
"00000000000bfec54977673f68b6fe5f088398e697d778fa7987f8bab6a70825",
[
0,
0
]
],
[
"000000000000e7f428bb413c17032c0031af0d26133ba93f744a5a0c16cf7e1a",
[
0,
0
]
],
[
"00000000000036bc80378323c6eaff8ab350b6d89955f602960cb7c93d2feb4c",
[
0,
0
]
],
[
"00000000000f0d5edcaeba823db17f366be49a80d91d15b77747c2e017b8c20a",
[
0,
0
]
],
[
"00000000001ff8fd57798082ab5a7452ada211e1c3be38745155505601498829",
[
0,
0
]
],
[
"000000000020f960b535eac585e5810ad64f158c1142f0eecd925c8058172933",
[
0,
0
]
],
[
"0000000000067bd89409368d221507a160e5c45972eeb01efe210054fe8e7d85",
[
0,
0
]
],
[
"00000000003521f2d5ea3232d4835ca6c6bae083ba90458f67d4cd765ce93b09",
[
0,
0
]
],
[
"000000000005ab3ff3a0c484eff7b571fb78ce27d93f77a480074232e5ce0c1d",
[
0,
0
]
],
[
"00000000001048c9eca7cc1cbb86946c04498052071f7e7c775bba565ada337c",
[
0,
0
]
],
[
"00000000000154caacde41be616f924d7d478812148242fba85605eefec9ac61",
[
0,
0
]
],
[
"000000000000c34f75bd6f338c0206a31a8d5021cc2ded51e88a6ef4fe686d10",
[
0,
0
]
],
[
"0000000000001e0581d86c49a6ca14ba88639ef908abb09210b57989e06b1a1f",
[
0,
0
]
],
[
"0000000000d0e6dc0bf830b50bde3e400e16ec4f772f92a55390e62d4aa73af3",
[
0,
0
]
],
[
"00000000069c2501a2f32cc69af72a602ff674438ae04dd05516f72a71b9ab26",
[
0,
0
]
],
[
"0000000000c926b38954550c9b8d363ff058c2eb135eebdb3e640cfa67df803d",
[
0,
0
]
],
[
"000000000011e9ad9c18e9e2095c3662af5be1e918dff653758583aa45dc8197",
[
0,
0
]
],
[
"0000000000f311624ff4dcdf07400d0d2fec8b16b14c1c16babc377a2d85ad21",
[
0,
0
]
],
[
"00000000002e455cabfdc2a8955e8ddfe717b12efe5b80937b0c0ad6ac977fc5",
[
0,
0
]
],
[
"00000000000fed8889a22339b340f599ac7908e790bfc3cfca9b78078a52d228",
[
0,
0
]
],
[
"0000000000012ca4492956b3f859b00e5db14b54d422cd95c68c7150743db365",
[
0,
0
]
],
[
"0000000000004c58e8f7bac59eb4a036764a4d8e0da51c0290858ab14fb72481",
[
0,
0
]
],
[
"0000000000002f60bc99563ff5b4b800c176fe8bde95e8f968fd6b53d74c9cef",
[
0,
0
]
],
[
"0000000000000bffd10a3fb0b5b86d8b2561f39d07f8a4c41dfa08e3e49b7db5",
[
0,
0
]
],
[
"00000000000006a296be9cd8fd4e3145c146863adbe08b71831abb8a869d032c",
[
0,
0
]
],
[
"0000000000000c557f496e82891039ff22e277bd604be6e2e8b95e519bee91f9",
[
0,
0
]
],
[
"0000000000399b30d2111c4bf3051c1f7f2f35bba7ff290d92393341ae47df55",
[
0,
0
]
],
[
"000000001f88733439e4e8d3c474504aed62037faa16f3845b4c671f69732e26",
[
0,
0
]
],
[
"0000000018aa2f93d2ab76a7e2f1bf5b565b4a1b0ececb6ee46490984f6c0d4b",
[
0,
0
]
],
[
"0000000005e22674fcf65ce7be896a0557205ab26d1f76d73a717f5f14a6d6ad",
[
0,
0
]
],
[
"0000000000223d866b324c097973210f8fc715c9535908359d61d8e1ab2f0100",
[
0,
0
]
],
[
"00000000002b321fd6452ab43849bd7a781953ec4485554e0fdc579f2a52c90a",
[
0,
0
]
],
[
"0000000000173132748c51b5754b0341232325bd118455bf3c8d25164d3eb92a",
[
0,
0
]
],
[
"00000000000143158cdea5fbb9453bbe1a7a900e6feba1e2193e4f5c106d9fba",
[
0,
0
]
],
[
"0000000000014677751456af5630025b3d9921a4eafb4d36a06498f0c6a84c56",
[
0,
0
]
],
[
"000000000000243976cf2d30ecd3cb1fd0b805fba4da92d2758f78e1c6f8ae92",
[
0,
0
]
],
[
"0000000000001323db1ab3f247bcb1e92592004b43e4bed0966ed09f675cf269",
[
0,
0
]
],
[
"000000000000017a410c22c4b6caf710f5ccf005d644caf276ea8626a538798d",
[
0,
0
]
],
[
"0000000000170b2b1374e3a0dfdce2fbc5e302e1e0e9fb419dc057c9959902d1",
[
0,
0
]
],
[
"000000000015b4fad4d929630487680cda2d3aada138c58cc08241ef6dd4ab09",
[
0,
0
]
],
[
"00000000000abebab869f1620843d413a3d9e06dc7d9f5201a414d547ace1f99",
[
0,
0
]
],
[
"00000000000b0bdaf05c2fe8b12ebd2372f49d8eabcfbccdadd68b5e5b7c9565",
[
0,
0
]
],
[
"00000000000ca1af42ee1be2c8895d94f39dab5fcdbe0b4b4065f4be534e7294",
[
0,
0
]
],
[
"000000000069d0cc8c0452bf86cff87db05232f801a162acab2d080d6e4e9ea9",
[
0,
0
]
],
[
"000000000019c7f7685f5bdc3afbb5e978cb3f4f70fea7b2b410139741303b53",
[
0,
0
]
],
[
"00000000000d3874ce21db78f4d1883ad9ae8b26c1d7c13f3d723ff85629d595",
[
0,
0
]
],
[
"0000000000033f87c25275ff72b58630d8da90221f2c84bcbd77c8e615709f8b",
[
0,
0
]
],
[
"000000000000dc72adaaae6483eb6737de7d21b3a24b2426330e80b078ceaed1",
[
0,
0
]
],
[
"00000000000002fb1337228db02ac464565271f22f045c1b6ee5e449f057a829",
[
0,
0
]
],
[
"00000000000001902376ff640d3088899af0819dbd15f602156a13ac2fc8e94e",
[
0,
0
]
],
[
"000000000000007ee49761a1c8284a3b8acefa39e37e455be4773d648e2db794",
[
0,
0
]
],
[
"00000000000005b4d495a77f57018dbc72bf47993d494349329a3c653f04ab93",
[
0,
0
]
],
[
"000000000000009dcb3ae6d68828e2f5ccfd58780abb260354e74484106f81ce",
[
0,
0
]
],
[
"00000000a3ceb118021fb42d39be52db951c6f852bb9a241046e972706f7329a",
[
0,
0
]
],
[
"00000000574e8e1c27fa54c77b4e7cd1b79de070f0d3ad5b383206ab9777d983",
[
0,
0
]
],
[
"0000000039d562f640c1743421d53e7e04c3e8ba222c339fff6f3d25b1d4a7fe",
[
0,
0
]
],
[
"000000000001cb1559d55c697871e18d5c26800f77fb11587241bfbec3b15e26",
[
0,
0
]
],
[
"000000000006e01a93090319756c7ca826ef655feb0cc2ef9abcc59d67de5e5b",
[
0,
0
]
],
[
"000000000000a81aaf5a4c013032638a077af6aad8bc449d74daef8ad3a74419",
[
0,
0
]
],
[
"00000000000087d0574963c1582f2161298e2de5e48f74566291ef9afc2be24a",
[
0,
0
]
],
[
"0000000000033251e71c347cd663945fb68efe82a8c6666c0b41e93f1c46658d",
[
0,
0
]
],
[
"000000000000f592857e6f0e4711b5b93fdf95f2b21a5963bde15be750a07908",
[
0,
0
]
],
[
"0000000000004353c8426e18b942a5012934ddac8322b86d6ab98ed7c0ee86ed",
[
0,
0
]
],
[
"00000000004f027845b699f42e7d0d30c530e99524c5f97186ce6a250a5fac42",
[
0,
0
]
],
[
"000000002fc6407edc060df90785082834867331e6746a43ed34a26fbdc5df64",
[
0,
0
]
],
[
"0000000000048733007c91ea3665bd4e1653b10799e3f43abee0fe830ffbb3ad",
[
0,
0
]
],
[
"0000000000025a9b1c5afceba0c78c4b0320797acdc1ad50b4e040f148fbff7f",
[
0,
0
]
],
[
"00000000007ca6d026d27387edc1c5570de41c61bacbcb1dad2c0f300b49e637",
[
0,
0
]
],
[
"00000000000258f683a77ad509da82a4fab24188fdb4b4690e212c50794a9abb",
[
0,
0
]
],
[
"0000000000015111bce7b6ac13c930484e14e31e13e43355cb4d63c8f1782440",
[
0,
0
]
],
[
"000000000001ca074fdecac7749d95f28f10c83a7e13787fd865bfbe505382bc",
[
0,
0
]
],
[
"0000000000001c11a6505dd44ab405fdc07ddfc015f3c1166a5d9352ab58b52c",
[
0,
0
]
],
[
"0000000000000c83f7f8e1cab4efa08d6c68c4555fb6ab542e01b87edd8f56ac",
[
0,
0
]
],
[
"00000000000009561d0ceba15388573d2a994aff24512ec3ed7d7881aa0997dd",
[
0,
0
]
],
[
"00000000007dc7cfbbb94db1fbc076a70a1252fd595686b4d75b2ea77ed6ee9e",
[
0,
0
]
],
[
"00000000000251feb68a8c90852f73aeb29ebda191038737b7edd37c9475f4ac",
[
0,
0
]
],
[
"0000000000013f9a97045ea9047654e514951288911b2c3986787c27bab49106",
[
0,
0
]
],
[
"0000000006e8c37735c61f22bec69f4cb7eba03172349e7012b7704652f3e83a",
[
0,
0
]
],
[
"0000000001f341add5657043d8e50e53ba079fe24966a2668f904be5579c84b9",
[
0,
0
]
],
[
"000000000029a6275cd477d77939424bd183c2f1308a9912f45aa7cc9ed13b56",
[
0,
0
]
],
[
"00000000000a0336239e5e1faedf5bd2eedf38c9a5ba34a832356aea70aeb102",
[
0,
0
]
],
[
"000000000003c1a2b25093a64eb624055f6a3a26e18b8e7ea2d9382ec7a3609a",
[
0,
0
]
],
[
"000000000001bd89bf7e8740ce22adfa6e8793bd1716a647e558ed1742ee8329",
[
0,
0
]
],
[
"0000000000001320421f1bb2c94000e11a621f581fc277c0e2911c3b89f680bd",
[
0,
0
]
],
[
"000000000054ce90a949f5ae2d43c4ace599668c6ccbc50620f6d5705922ea7c",
[
0,
0
]
],
[
"00000000200d16fea4857e6b73169cc593421a57971acdbcaf87a31d7d8d72c8",
[
0,
0
]
],
[
"0000000000e75602181c88f713b91c49de291ed878be305d25b75c0ec5fbe942",
[
0,
0
]
],
[
"000000000081f8169c3c3665f20351dc0fe499612ae232ec0b55858a8e5dc6e9",
[
0,
0
]
],
[
"0000000000d7ad232e7593fb435d125343b8113bbdb3705ab58ac0e18c26cc79",
[
0,
0
]
],
[
"0000000000076df615d887e33193ca2dc0f2fc0e70744512c95da6242e9b1a81",
[
0,
0
]
],
[
"0000000000084a62093d1929843e74456686429b698a7ea9b1901c1565779f58",
[
0,
0
]
],
[
"00000000000251d1da01e9de9fcaf3ca3a64bff78a5faf51a8e697dfab6b5e4b",
[
0,
0
]
],
[
"000000000000609a8798996b1f1fe0b66060a628eadc380d0d369a2318c2d0ec",
[
0,
0
]
],
[
"00000000000014770aeab044a022e86d888a6ede75b6474022c71aead3a1db74",
[
0,
0
]
],
[
"00000000000004101d04ebc90ade5d4b911aa13c038ecf25e9887d877203ddb8",
[
0,
0
]
],
[
"000000007c700410b61eb7ff1aaccbfc3a79e4e4484ad7a2b0eda4d91dc4b613",
[
0,
0
]
],
[
"00000000055ff438a031413ee042fd3c0a2b69be98690542806ff123b7988024",
[
0,
0
]
],
[
"000000002eca5f9f2c3b656d2550662fdee4c95da133eade51a5cae653bc69fe",
[
0,
0
]
],
[
"000000000c679b76ccf0c5b943095fdee8fa466311edbea2c4a05f9430ffef3f",
[
0,
0
]
],
[
"00000000007c6f494e32d5d9de58fa008a770fdc0a7b4a141be5b7c2de3ab970",
[
0,
0
]
],
[
"0000000000d5dcd5a26c8ad29c1293e70401e2f90d8288469df3816b8cc6d4aa",
[
0,
0
]
],
[
"00000000000d754d94f36cacbfb620710672afb1558499cabe17ca62c54a7d3a",
[
0,
0
]
],
[
"000000000004096bb78fba714b130f7f1f929e2803c75a7a85619f7a2b86567f",
[
0,
0
]
],
[
"0000000000020e686c38d44c35896df35f9f1b7723a82a826a5e2393c25ef68c",
[
0,
0
]
],
[
"000000000000504f9af6885c0cb6484109ea205a956c8efae9557a1f5b9233da",
[
0,
0
]
],
[
"0000000000000e8746e52e4320ec17e66434a3936a3825f7046fe874e92275fb",
[
0,
0
]
],
[
"0000000000000f48d818a9a026270c9f733f629959bea25192596d59874b1ce2",
[
0,
0
]
],
[
"00000000eaa9214cb05b241828a1cfb0c4209fb7ea64429815d61f7c1d98939e",
[
0,
0
]
],
[
"000000001f7f915a6002cce4edd5cba392307f3a199a520ee8937327a9135162",
[
0,
0
]
],
[
"0000000009674ee0c606d687bdcddf8e023462927e2902b3381bc4bb862a7397",
[
0,
0
]
],
[
"0000000001f3f3528c083a4b11eb2f04d8bbeca92b57f05d8282909bde78bc77",
[
0,
0
]
],
[
"000000000131917ac459aefb91774dbb42caeca497afc0cfd1766e0338cc7f88",
[
0,
0
]
],
[
"000000000027634444081e1289354cb50034a506bb306a2ac1d8280683771c5c",
[
0,
0
]
],
[
"000000000017a852acff78fbee573329d45bb8b121e9f6fc1e4f687bb3778ada",
[
0,
0
]
],
[
"000000000006789e1a00eca982fb2827f680b254c4a0ecb005af4464f3585a02",
[
0,
0
]
],
[
"0000000000015d2e9f54b1e9419d6b32ce68ae626cdd7f2a1954f22ca39ae0fa",
[
0,
0
]
],
[
"0000000000002f7893bc169165ed9fefb434b6201103f23cc84a747a68ff8797",
[
0,
0
]
],
[
"00000000000008471ccf356a18dd48aa12506ef0b6162cb8f98a8d8bb0465902",
[
0,
0
]
],
[
"0000000000000596f00b9db53c4111bcde16f3781471c5307af1a996e34ec20a",
[
0,
0
]
],
[
"000000000000007b5d2406f64f5f5833c063a6906552e815e603140c00bca951",
[
0,
0
]
],
[
"0000000093ca5d935740a1b25f10ce092fd777c2bb521f3156619389ae78931e",
[
0,
0
]
],
[
"00000000292f3a48559527341f72400a0f8a783aebcaae5bfa0e390dfaa5286b",
[
0,
0
]
],
[
"000000001e852ed7ddf0108d1fce0f4f686f43c8c1b85bcb12c43e564dc7630e",
[
0,
0
]
],
[
"000000000c4bea8fb1e7f3a1f3e6c6b3f71388c0ec7eef3de381853767e89f87",
[
0,
0
]
],
[
"00000000029ef31a21711b55c4300efa38ace0b706091e373f48285286f2c578",
[
0,
0
]
],
[
"0000000000979060786bb008f193d3917e28667bb1b28329f3adadc172e4cce7",
[
0,
0
]
],
[
"000000000019030ceb98013b1627517b45b04ee055ef445813bbebaa25fa1ed3",
[
0,
0
]
],
[
"00000000000adf202247bb794fc9a3c82cd8767143f1e6ed5f60940ee18b09a8",
[
0,
0
]
],
[
"000000000000b19061e2481d8be6183b3d881b0d58601072d2a32729435f6af3",
[
0,
0
]
],
[
"0000000000007a6d34f59b29e8d4da53e51e3414acd18527466d064945fe19fc",
[
0,
0
]
],
[
"0000000000002e66ca213a2c3e9eb5fa62de29feb83880a0bd29f90fca8ad199",
[
0,
0
]
],
[
"0000000000000b4ca10aa100728d0928f37db5296303db1b74ffe29e4a17b6cd",
[
0,
0
]
],
[
"0000000000000143309f6b19567955743775f61f8dc6932c0b46cf5fb11c6c72",
[
0,
0
]
],
[
"00000000000000b04d5409b3ac60cc18c0b9a3d58b303594635a8f75a9d2abd5",
[
0,
0
]
],
[
"000000000000040a2699f62a552703a278608248c2ce823f4cd8845376e9a371",
[
0,
0
]
],
[
"00000000000005cfcb850db7e83d4963994f958bae9b1de1483f5aeb3d449925",
[
0,
0
]
],
[
"00000000000190f80220e70c1481153671a7c90fd856988c183ab0e3d9313df8",
[
0,
0
]
],
[
"000000009374563a06178641d06776f66554c2a094b5319f0801fe35cef72ccf",
[
0,
0
]
],
[
"00000000003e4e6e5e8e4a89e7de50eed104d4a49d2992ff101b6740beec7cb5",
[
0,
0
]
],
[
"0000000000618cd377d14aaa441cbdb92527894f98da316eca81664f8ab5488d",
[
0,
0
]
],
[
"00000000000d977ab2897885fee712f58612fce8c10ffbe9400326fe3429b77b",
[
0,
0
]
],
[
"00000000000c3575b487dd0f938c5bc744fa65ca4ca3a9c981b8bda903ec110b",
[
0,
0
]
],
[
"0000000000247ac689595ed8d62678bfe53e5af13c0f5455e558f5e6bb375c16",
[
0,
0
]
],
[
"0000000000093d175376aa621176511f335a48f824b66d998e8082f85134a48b",
[
0,
0
]
],
[
"000000000000c0c0448fe922f2c737946297d35f2c25ad7cc223e11bbe58e1f8",
[
0,
0
]
],
[
"00000000000016abe4e7c10ddb658bb089b2ef3b1de3f3329097cf679eedf2b5",
[
0,
0
]
],
[
"000000000000242757cea5b68c52b83dd8c2eb9257492074fc69dfa30bd4cbf4",
[
0,
0
]
],
[
"00000000000006813f3dd7726a509fbe3101835db155dfd35d44aeae6aedb316",
[
0,
0
]
],
[
"000000000000053cc4f39cff1c8cee1aff7e289a85dee84164d2d981afc8f17a",
[
0,
0
]
],
[
"00000000000000789724805cf1d37ef689acf52c47a460507f540d5e5ca79bfa",
[
0,
0
]
],
[
"00000000000003d71618bb8952887f65540270a5e54d6246b9419e08831b5e4e",
[
0,
0
]
],
[
"0000000000000251a513a33eadfad67c015f6e3b291dfd0ae1cc4bb3a43006dc",
[
0,
0
]
],
[
"00000000968009e3f8d6e6071e7def68298307717a9af6c2d44986deaae297d5",
[
0,
0
]
],
[
"0000000062bcacb734df83bbfa3e1b9a8dfa570ecffb6c29eaaf8e9498cccd30",
[
0,
0
]
],
[
"000000001d4618c0931bd3c25ee592c35341f30ff3b549a671f637b3c26ef414",
[
0,
0
]
],
[
"000000000418b329df96a004f1b652ad06a7ca295f9f2e711c412d00493f5a86",
[
0,
0
]
],
[
"000000000302bfb88e9027237d023c4b969e106c9a7a23a84103776de7880836",
[
0,
0
]
],
[
"000000000069b9f7d9134fd93c8b7e3af8b26bbcbb5553af02fb6ed644d7fca5",
[
0,
0
]
],
[
"00000000000411ec444240ee91e2777ad18b80dee854e3e838e32209e84774fa",
[
0,
0
]
],
[
"0000000000007c73f322eba4dee5463305227c7e1a8139f1b7b296444f265052",
[
0,
0
]
],
[
"00000000000129adf0f9c0242aedbb9d87935d67ee4ddea758c00344d4b6a29e",
[
0,
0
]
],
[
"000000000000343594e671158b6e1b4b6499f6ad66e2aeabf1f6d295d3dba850",
[
0,
0
]
],
[
"000000000000320f0d5c22ba22b588b97a0e02273034bcd53669b1c8c4eeda1b",
[
0,
0
]
],
[
"0000000000001e8cdb2d98471a5c60bdbddbe644b9ad08e17a97b3a7dce1e332",
[
0,
0
]
],
[
"0000000000000026c9994ccdd027e86f51a2e36812f754bd855a7f9b1ca56511",
[
0,
0
]
],
[
"00000000000002746a820a2c08b35b8d0493c4b5d468fcc971b9c88003e84849",
[
0,
0
]
],
[
"000000000002949f844e92645df73ce9c093e5aac0d962a0fa13eb076eec835c",
[
0,
0
]
],
[
"00000000000156fbda67468ae2863993b98a41227c420246e4bc4e68c84df0e8",
[
0,
0
]
],
[
"000000000003b43c6c807122c8dd10e2a0cffbf72946f41c97c1ab82d416f74d",
[
0,
0
]
],
[
"000000000004e0635c2438b1b649007e5d424b3de846299a8db53049ebf4da0c",
[
0,
0
]
],
[
"00000000000258e4b79e3cca2ab7d12b35ba77fc491572f2e794f0a10f5236d9",
[
0,
0
]
],
[
"0000000000f5816875d9fece105e499b0467b8fb23ea973c48d828a235acdebd",
[
0,
0
]
],
[
"000000000001353bbaec810af7a4c74b4964ae072361c0889ed6d59cf16db6fd",
[
0,
0
]
],
[
"00000000000b354d8c389473670ca6bed7e3dffa069f270d35ec9dad810af141",
[
0,
0
]
],
[
"000000000002fa1f39e7cd8730fa08085ba2b532146ad1ef3b400a13e835ca36",
[
0,
0
]
],
[
"000000000000d2c7943eee59652a9783bff27e474a92ec206c5c6e3cdd58d0d7",
[
0,
0
]
],
[
"00000000000036034181b4d9a84a97490b49fbee4262b9cfb25a7bfc9c0eec9f",
[
0,
0
]
],
[
"00000000000007deb59381cce692f152fc902732d96a7e7d463bc83915b37c0a",
[
0,
0
]
],
[
"00000000ea7d32833462c0f72ade0cae4766e6065caa4e510331929c56d16632",
[
0,
0
]
],
[
"000000000068fce0ddd370d4c8f9129a7bc7843e75fc57666202d3b90239e269",
[
0,
0
]
],
[
"0000000026b4a2212c9c9493f8bd9d5331cab6d8eda8ee017410e58a783ca069",
[
0,
0
]
],
[
"0000000009535ea2dc7e83c31cd17f1db1bb78b0a678fc0610844273de143bf5",
[
0,
0
]
],
[
"00000000008607cbd5baca91d5b8b82ee965aace335744a3e21578af22bee8ba",
[
0,
0
]
],
[
"000000000030dcedae0f5e98c4e176f9569ce76c4d4135bb028fc3144ef381d9",
[
0,
0
]
],
[
"0000000000297c3f0e3fa85731222ba934a955bf513247a72a33c74c498cadbe",
[
0,
0
]
],
[
"0000000000020a0d4a1e8120cbdb486e758b58919c9df12e0edc8ca1f2795e94",
[
0,
0
]
],
[
"000000000000078773afc9023182bfb6534a60158672e6bc6e8aa5052854da80",
[
0,
0
]
],
[
"00000000000102ecdd67800807d9e137357805b9bbf8a439ed86bde5b19fbeb7",
[
0,
0
]
],
[
"0000000000005c3d2e3c7ee737c67ab465533acb233e0df902c1525fc11c3a55",
[
0,
0
]
],
[
"0000000000001a77771650cdbbceff87caa4461391ba6a4ddc9815b5b0ab47b0",
[
0,
0
]
],
[
"000000000000071ec390bbd28fa2a84e52ab5b32901d0723d22646b04ae01dc3",
[
0,
0
]
],
[
"00000000000005c3ec3194f710c6f26ee736d59cc935ddfa574440f39846433a",
[
0,
0
]
],
[
"00000000000001cc3df6924591939269d61ead563b9eb68402a2ca01d7ff99e2",
[
0,
0
]
],
[
"000000008c778b3554ceaf3a13a856acbfe46b5750fb86fd92ba30651c2852f4",
[
0,
0
]
],
[
"00000000107ca31f75f8ea76073dda3c33330b2706c1ec20c3ec240e853b65c5",
[
0,
0
]
],
[
"0000000006ba99b08e7f2869ce113e2ad7464891de7b4cfa96f330d706a2da46",
[
0,
0
]
],
[
"000000000f31036bd51b2818f6dfb90ada9be5019abf55fb15694b181e269865",
[
0,
0
]
],
[
"00000000004fcc101bc47eb7a379b9f608d5c00ac04d2d0ea165ae2937070796",
[
0,
0
]
],
[
"000000000044d5ca3eda838edef0df7e69e1934047f8482822ce58ff7a18466d",
[
0,
0
]
],
[
"000000000029bdfb157be6d400c4dd3370d98afdd8cd3db6f1ada8c19bbf4650",
[
0,
0
]
],
[
"000000000005e9699ad8035caa4f73af781ac2040c87b8aa77459b3607209aa8",
[
0,
0
]
],
[
"000000000001c0ba033f7d85beeaa167c9bde0e192240653a7ff6d9b81679842",
[
0,
0
]
],
[
"0000000000000e0176111f29e800b49c7b8c7226dbbf4df715f1a4f06bcaaa49",
[
0,
0
]
],
[
"00000000ac3bb2cf42192e9053f5384355228a2b3d70b4ece4d45773a5d5ddd2",
[
0,
0
]
],
[
"000000000f29f7b60842b1044b2db7998e9bcbd92f8ec6fe8d159c6d582f1f1a",
[
0,
0
]
],
[
"00000000352f86bc5f9760961a25de009940508bb2cd0b37f378fbc87dc97eef",
[
0,
0
]
],
[
"000000000e9b3086008679ed57f59857f64c3954368ba1088117dbf88d5839cd",
[
0,
0
]
],
[
"000000000015324bd8fed0e61b62bd1d6c663b862cb98ea03c494a92e4a8d0af",
[
0,
0
]
],
[
"000000000020475a181b7a084b341860a72fc0c1fdfcc13a85adeb0471444b0f",
[
0,
0
]
],
[
"0000000000031905c508a975707b74f24e733880382775ee0e6250666473e1d8",
[
0,
0
]
],
[
"000000000000ca38b15d2ea33a6eef505a9c661540a18882f79ba9a3c575a9bd",
[
0,
0
]
],
[
"000000000002739979a7a89fa279303b6606885e750b19e91ed637d7f222b392",
[
0,
0
]
],
[
"00000000000091e935fc266facc2c92759d5468a39aee5be6b76b519a9bc7567",
[
0,
0
]
],
[
"00000000000006e339938254208203b67c3c400f703fc29535fc646699e36e58",
[
0,
0
]
],
[
"00000000000008f6f1d1150d77f93a7f1baa24b65ceb471b1825b2e92ca6997c",
[
0,
0
]
]
]
================================================
FILE: lib/coinchooser.py
================================================
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2015 kyuupichan@gmail
#
# 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 defaultdict, namedtuple
from math import floor, log10
from .bitcoin import sha256, COIN, TYPE_ADDRESS
from .transaction import Transaction
from .util import NotEnoughFunds, PrintError
# A simple deterministic PRNG. Used to deterministically shuffle a
# set of coins - the same set of coins should produce the same output.
# Although choosing UTXOs "randomly" we want it to be deterministic,
# so if sending twice from the same UTXO set we choose the same UTXOs
# to spend. This prevents attacks on users by malicious or stale
# servers.
class PRNG:
def __init__(self, seed):
self.sha = sha256(seed)
self.pool = bytearray()
def get_bytes(self, n):
while len(self.pool) < n:
self.pool.extend(self.sha)
self.sha = sha256(self.sha)
result, self.pool = self.pool[:n], self.pool[n:]
return result
def randint(self, start, end):
# Returns random integer in [start, end)
n = end - start
r = 0
p = 1
while p < n:
r = self.get_bytes(1)[0] + (r << 8)
p = p << 8
return start + (r % n)
def choice(self, seq):
return seq[self.randint(0, len(seq))]
def shuffle(self, x):
for i in reversed(range(1, len(x))):
# pick an element in x[:i+1] with which to exchange x[i]
j = self.randint(0, i+1)
x[i], x[j] = x[j], x[i]
Bucket = namedtuple('Bucket',
['desc',
'weight', # as in BIP-141
'value', # in satoshis
'coins', # UTXOs
'min_height', # min block height where a coin was confirmed
'witness']) # whether any coin uses segwit
def strip_unneeded(bkts, sufficient_funds):
'''Remove buckets that are unnecessary in achieving the spend amount'''
bkts = sorted(bkts, key = lambda bkt: bkt.value)
for i in range(len(bkts)):
if not sufficient_funds(bkts[i + 1:]):
return bkts[i:]
# Shouldn't get here
return bkts
class CoinChooserBase(PrintError):
def keys(self, coins):
raise NotImplementedError
def bucketize_coins(self, coins):
keys = self.keys(coins)
buckets = defaultdict(list)
for key, coin in zip(keys, coins):
buckets[key].append(coin)
def make_Bucket(desc, coins):
witness = any(Transaction.is_segwit_input(coin) for coin in coins)
# note that we're guessing whether the tx uses segwit based
# on this single bucket
weight = sum(Transaction.estimated_input_weight(coin, witness)
for coin in coins)
value = sum(coin['value'] for coin in coins)
min_height = min(coin['height'] for coin in coins)
return Bucket(desc, weight, value, coins, min_height, witness)
return list(map(make_Bucket, buckets.keys(), buckets.values()))
def penalty_func(self, tx):
def penalty(candidate):
return 0
return penalty
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
# Break change up if bigger than max_change
output_amounts = [o[2] for o in tx.outputs()]
# Don't split change of less than 0.02 BTC
max_change = max(max(output_amounts) * 1.25, 0.02 * COIN)
# Use N change outputs
for n in range(1, count + 1):
# How much is left if we add this many change outputs?
change_amount = max(0, tx.get_fee() - fee_estimator(n))
if change_amount // n <= max_change:
break
# Get a handle on the precision of the output amounts; round our
# change to look similar
def trailing_zeroes(val):
s = str(val)
return len(s) - len(s.rstrip('0'))
zeroes = [trailing_zeroes(i) for i in output_amounts]
min_zeroes = min(zeroes)
max_zeroes = max(zeroes)
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
# Calculate change; randomize it a bit if using more than 1 output
remaining = change_amount
amounts = []
while n > 1:
average = remaining / n
amount = self.p.randint(int(average * 0.7), int(average * 1.3))
precision = min(self.p.choice(zeroes), int(floor(log10(amount))))
amount = int(round(amount, -precision))
amounts.append(amount)
remaining -= amount
n -= 1
# Last change output. Round down to maximum precision but lose
# no more than 100 satoshis to fees (2dp)
N = pow(10, min(2, zeroes[0]))
amount = (remaining // N) * N
amounts.append(amount)
assert sum(amounts) <= change_amount
return amounts
def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold):
amounts = self.change_amounts(tx, len(change_addrs), fee_estimator,
dust_threshold)
assert min(amounts) >= 0
assert len(change_addrs) >= len(amounts)
# If change is above dust threshold after accounting for the
# size of the change output, add it to the transaction.
dust = sum(amount for amount in amounts if amount < dust_threshold)
amounts = [amount for amount in amounts if amount >= dust_threshold]
change = [(TYPE_ADDRESS, addr, amount)
for addr, amount in zip(change_addrs, amounts)]
self.print_error('change:', change)
if dust:
self.print_error('not keeping dust', dust)
return change
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
dust_threshold):
"""Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.
Note: fee_estimator expects virtual bytes
"""
# Deterministic randomness from coins
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
self.p = PRNG(''.join(sorted(utxos)))
# Copy the ouputs so when adding change we don't modify "outputs"
tx = Transaction.from_io([], outputs[:])
# Weight of the transaction with no inputs and no change
# Note: this will use legacy tx serialization as the need for "segwit"
# would be detected from inputs. The only side effect should be that the
# marker and flag are excluded, which is compensated in get_tx_weight()
base_weight = tx.estimated_weight()
spent_amount = tx.output_value()
def fee_estimator_w(weight):
return fee_estimator(Transaction.virtual_size_from_weight(weight))
def get_tx_weight(buckets):
total_weight = base_weight + sum(bucket.weight for bucket in buckets)
is_segwit_tx = any(bucket.witness for bucket in buckets)
if is_segwit_tx:
total_weight += 2 # marker and flag
# non-segwit inputs were previously assumed to have
# a witness of '' instead of '00' (hex)
# note that mixed legacy/segwit buckets are already ok
num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins)
for bucket in buckets)
total_weight += num_legacy_inputs
return total_weight
def sufficient_funds(buckets):
'''Given a list of buckets, return True if it has enough
value to pay for the transaction'''
total_input = sum(bucket.value for bucket in buckets)
total_weight = get_tx_weight(buckets)
return total_input >= spent_amount + fee_estimator_w(total_weight)
# Collect the coins into buckets, choose a subset of the buckets
buckets = self.bucketize_coins(coins)
buckets = self.choose_buckets(buckets, sufficient_funds,
self.penalty_func(tx))
tx.add_inputs([coin for b in buckets for coin in b.coins])
tx_weight = get_tx_weight(buckets)
# This takes a count of change outputs and returns a tx fee
output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
tx.add_outputs(change)
self.print_error("using %d inputs" % len(tx.inputs()))
self.print_error("using buckets:", [bucket.desc for bucket in buckets])
return tx
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
raise NotImplemented('To be subclassed')
class CoinChooserRandom(CoinChooserBase):
def bucket_candidates_any(self, buckets, sufficient_funds):
'''Returns a list of bucket sets.'''
if not buckets:
raise NotEnoughFunds()
candidates = set()
# Add all singletons
for n, bucket in enumerate(buckets):
if sufficient_funds([bucket]):
candidates.add((n, ))
# And now some random ones
attempts = min(100, (len(buckets) - 1) * 10 + 1)
permutation = list(range(len(buckets)))
for i in range(attempts):
# Get a random permutation of the buckets, and
# incrementally combine buckets until sufficient
self.p.shuffle(permutation)
bkts = []
for count, index in enumerate(permutation):
bkts.append(buckets[index])
if sufficient_funds(bkts):
candidates.add(tuple(sorted(permutation[:count + 1])))
break
else:
raise NotEnoughFunds()
candidates = [[buckets[n] for n in c] for c in candidates]
return [strip_unneeded(c, sufficient_funds) for c in candidates]
def bucket_candidates_prefer_confirmed(self, buckets, sufficient_funds):
"""Returns a list of bucket sets preferring confirmed coins.
Any bucket can be:
1. "confirmed" if it only contains confirmed coins; else
2. "unconfirmed" if it does not contain coins with unconfirmed parents
3. "unconfirmed parent" otherwise
This method tries to only use buckets of type 1, and if the coins there
are not enough, tries to use the next type but while also selecting
all buckets of all previous types.
"""
conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0]
unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0]
unconf_par_buckets = [bkt for bkt in buckets if bkt.min_height == -1]
bucket_sets = [conf_buckets, unconf_buckets, unconf_par_buckets]
already_selected_buckets = []
for bkts_choose_from in bucket_sets:
try:
def sfunds(bkts):
return sufficient_funds(already_selected_buckets + bkts)
candidates = self.bucket_candidates_any(bkts_choose_from, sfunds)
break
except NotEnoughFunds:
already_selected_buckets += bkts_choose_from
else:
raise NotEnoughFunds()
candidates = [(already_selected_buckets + c) for c in candidates]
return [strip_unneeded(c, sufficient_funds) for c in candidates]
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
candidates = self.bucket_candidates_prefer_confirmed(buckets, sufficient_funds)
penalties = [penalty_func(cand) for cand in candidates]
winner = candidates[penalties.index(min(penalties))]
self.print_error("Bucket sets:", len(buckets))
self.print_error("Winning penalty:", min(penalties))
return winner
class CoinChooserPrivacy(CoinChooserRandom):
"""Attempts to better preserve user privacy.
First, if any coin is spent from a user address, all coins are.
Compared to spending from other addresses to make up an amount, this reduces
information leakage about sender holdings. It also helps to
reduce blockchain UTXO bloat, and reduce future privacy loss that
would come from reusing that address' remaining UTXOs.
Second, it penalizes change that is quite different to the sent amount.
Third, it penalizes change that is too big.
"""
def keys(self, coins):
return [coin['address'] for coin in coins]
def penalty_func(self, tx):
min_change = min(o[2] for o in tx.outputs()) * 0.75
max_change = max(o[2] for o in tx.outputs()) * 1.33
spent_amount = sum(o[2] for o in tx.outputs())
def penalty(buckets):
badness = len(buckets) - 1
total_input = sum(bucket.value for bucket in buckets)
# FIXME "change" here also includes fees
change = float(total_input - spent_amount)
# Penalize change not roughly in output range
if change < min_change:
badness += (min_change - change) / (min_change + 10000)
elif change > max_change:
badness += (change - max_change) / (max_change + 10000)
# Penalize large change; 5 BTC excess ~= using 1 more input
badness += change / (COIN * 5)
return badness
return penalty
COIN_CHOOSERS = {
'Privacy': CoinChooserPrivacy,
}
def get_name(config):
kind = config.get('coin_chooser')
if not kind in COIN_CHOOSERS:
kind = 'Privacy'
return kind
def get_coin_chooser(config):
klass = COIN_CHOOSERS[get_name(config)]
return klass()
================================================
FILE: lib/commands.py
================================================
#!/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
from .util import bfh, bh2u, format_satoshis
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 BaseException("wallet not loaded. Use 'electrum daemon load_wallet'")
if c.requires_password and password is None and wallet.storage.get('use_encryption'):
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 BaseException('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 BaseException('Not a JSON-RPC command')
@command('wp')
def password(self, password=None, new_password=None):
"""Change wallet password. """
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)
@command('')
def setconfig(self, key, value):
"""Set a configuration variable. 'value' may be a string or a Python expression."""
try:
value = ast.literal_eval(value)
except:
pass
self.config.set_key(key, value)
return True
@command('')
def make_seed(self, nbits=132, entropy=1, language=None, segwit=False):
"""Create a seed"""
from .mnemonic import Mnemonic
t = 'segwit' if segwit else 'standard'
s = Mnemonic(language).make_seed(t, nbits, custom_entropy=entropy)
return s
@command('')
def check_seed(self, seed, entropy=1, language=None):
"""Check that a seed was generated with given entropy"""
from .mnemonic import Mnemonic
return Mnemonic(language).check_seed(seed, entropy)
@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.
"""
return self.network.synchronous_get(('blockchain.address.get_history', [address]))
@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.
"""
return self.network.synchronous_get(('blockchain.address.listunspent', [address]))
@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 = bitcoin.public_key_from_private_key(privkey, 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 = bitcoin.public_key_from_private_key(privkey2, compressed)
h160 = bitcoin.hash_160(bfh(pubkey))
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, timeout=30):
"""Broadcast a transaction to the network. """
tx = Transaction(tx)
return self.network.broadcast(tx, timeout)
@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.
"""
out = self.network.synchronous_get(('blockchain.address.get_balance', [address]))
out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
return out
@command('n')
def getproof(self, address):
"""Get Merkle branch of an address in the UTXO set"""
p = self.network.synchronous_get(('blockchain.address.get_proof', [address]))
out = []
for i,s in p:
out.append(i)
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.synchronous_get(('blockchain.transaction.get_merkle', [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 BaseException('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 bitcoin.verify_message(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:
tx.set_rbf(True)
if not unsigned:
run_hook('sign_tx', self.wallet, tx)
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=False, 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=False, 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):
"""Wallet history. Returns the transaction history of your wallet."""
balance = 0
out = []
for item in self.wallet.get_history():
tx_hash, height, conf, timestamp, value, balance = item
if timestamp:
date = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
else:
date = "----"
label = self.wallet.get_label(tx_hash)
tx = self.wallet.transactions.get(tx_hash)
tx.deserialize()
input_addresses = []
output_addresses = []
for x in tx.inputs():
if x['type'] == 'coinbase': continue
addr = x.get('address')
if addr == None: continue
if addr == "(pubkey)":
prevout_hash = x.get('prevout_hash')
prevout_n = x.get('prevout_n')
_addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n)
if _addr:
addr = _addr
input_addresses.append(addr)
for addr, v in tx.get_outputs():
output_addresses.append(addr)
out.append({
'txid': tx_hash,
'timestamp': timestamp,
'date': date,
'input_addresses': input_addresses,
'output_addresses': output_addresses,
'label': label,
'value': str(Decimal(value)/COIN) if value is not None else None,
'height': height,
'confirmations': conf
})
return out
@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.synchronous_get(('blockchain.transaction.get', [txid]))
if raw:
tx = Transaction(raw)
else:
raise BaseException("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."""
return bitcoin.encrypt_message(message, pubkey)
@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 (BTCP)'] = 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 BaseException("Request not found")
return self._format_request(r)
#@command('w')
#def ackrequest(self, serialized):
# """"""
# 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 condidered 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('wp')
def signrequest(self, address, password=None):
"Sign payment request with an OpenAlias"
alias = self.config.get('alias')
if not alias:
raise BaseException('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. Everytime 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')}
try:
req = urllib.request.Request(URL, json.dumps(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.send([('blockchain.address.subscribe', [address])], callback)
return True
@command('wn')
def is_synchronized(self):
""" return wallet synchronization status """
return self.wallet.is_up_to_date()
@command('')
def help(self):
# for the python console
return sorted(known_commands.keys())
param_descriptions = {
'privkey': 'Private key. Type \'?\' to get a prompt.',
'destination': 'BTCP address, contact or alias',
'address': 'BTCP 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 BTCP). Type \'!\' to send the maximum available.',
'requested_amount': 'Requested amount (in BTCP).',
'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 BTCP)"),
'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"),
'entropy': (None, "Custom 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."),
}
# 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,
'entropy': 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,
}
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: %s)') % 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=False, 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("--nossl", action="store_true", dest="nossl", default=False, help="Disable SSL")
def get_parser():
# create main parser
parser = argparse.ArgumentParser(
epilog="Run 'electrum help ' to see the help for a command")
add_global_options(parser)
subparsers = parser.add_subparsers(dest='cmd', metavar='')
# 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
================================================
FILE: lib/contacts.py
================================================
# 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 re
import dns
import json
from . import bitcoin
from . import dnssec
class Contacts(dict):
def __init__(self, storage):
self.storage = storage
d = self.storage.get('contacts', {})
try:
self.update(d)
except:
return
# backward compatibility
for k, v in self.items():
_type, n = v
if _type == 'address' and bitcoin.is_address(n):
self.pop(k)
self[n] = ('address', k)
def save(self):
self.storage.put('contacts', dict(self))
def import_file(self, path):
try:
with open(path, 'r') as f:
d = self._validate(json.loads(f.read()))
except:
return
self.update(d)
self.save()
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
self.save()
def pop(self, key):
if key in self.keys():
dict.pop(self, key)
self.save()
def resolve(self, k):
if bitcoin.is_address(k):
return {
'address': k,
'type': 'address'
}
if k in self.keys():
_type, addr = self[k]
if _type == 'address':
return {
'address': addr,
'type': 'contact'
}
out = self.resolve_openalias(k)
if out:
address, name, validated = out
return {
'address': address,
'name': name,
'type': 'openalias',
'validated': validated
}
raise Exception("Invalid BTCP address or alias", k)
def resolve_openalias(self, url):
# support email-style addresses, per the OA standard
url = url.replace('@', '.')
records, validated = dnssec.query(url, dns.rdatatype.TXT)
prefix = 'btc'
for record in records:
string = record.strings[0]
if string.startswith('oa1:' + prefix):
address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)')
name = self.find_regex(string, r'recipient_name=([^;]+)')
if not name:
name = address
if not address:
continue
return address, name, validated
def find_regex(self, haystack, needle):
regex = re.compile(needle)
try:
return regex.search(haystack).groups()[0]
except AttributeError:
return None
def _validate(self, data):
for k,v in list(data.items()):
if k == 'contacts':
return self._validate(v)
if not bitcoin.is_address(k):
data.pop(k)
else:
_type,_ = v
if _type != 'address':
data.pop(k)
return data
================================================
FILE: lib/currencies.json
================================================
{
"CoinMarketCap": [
"USD",
"EUR",
"CNY"
]
}
================================================
FILE: lib/daemon.py
================================================
#!/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
# 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
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
self.fx = None
else:
self.network = Network(config)
self.network.start()
self.fx = FxThread(config, 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'))
self.cmd_runner.wallet = wallet
response = True
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()
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()},
'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.requires_upgrade():
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)
config.fee_estimates = self.network.config.fee_estimates.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)
self.gui.main()
================================================
FILE: lib/dnssec.py
================================================
#!/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.
# Check DNSSEC trust chain.
# Todo: verify expiration dates
#
# Based on
# http://backreference.org/2010/11/17/dnssec-verification-with-dig/
# https://github.com/rthalley/dnspython/blob/master/tests/test_dnssec.py
# import traceback
# import sys
import time
import struct
import dns.name
import dns.query
import dns.dnssec
import dns.message
import dns.resolver
import dns.rdatatype
import dns.rdtypes.ANY.NS
import dns.rdtypes.ANY.CNAME
import dns.rdtypes.ANY.DLV
import dns.rdtypes.ANY.DNSKEY
import dns.rdtypes.ANY.DS
import dns.rdtypes.ANY.NSEC
import dns.rdtypes.ANY.NSEC3
import dns.rdtypes.ANY.NSEC3PARAM
import dns.rdtypes.ANY.RRSIG
import dns.rdtypes.ANY.SOA
import dns.rdtypes.ANY.TXT
import dns.rdtypes.IN.A
import dns.rdtypes.IN.AAAA
# Pure-Python version of dns.dnssec._validate_rsig
import ecdsa
from . import rsakey
def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
from dns.dnssec import ValidationFailure, ECDSAP256SHA256, ECDSAP384SHA384
from dns.dnssec import _find_candidate_keys, _make_hash, _is_ecdsa, _is_rsa, _to_rdata, _make_algorithm_id
if isinstance(origin, str):
origin = dns.name.from_text(origin, dns.name.root)
for candidate_key in _find_candidate_keys(keys, rrsig):
if not candidate_key:
raise ValidationFailure('unknown key')
# For convenience, allow the rrset to be specified as a (name, rdataset)
# tuple as well as a proper rrset
if isinstance(rrset, tuple):
rrname = rrset[0]
rdataset = rrset[1]
else:
rrname = rrset.name
rdataset = rrset
if now is None:
now = time.time()
if rrsig.expiration < now:
raise ValidationFailure('expired')
if rrsig.inception > now:
raise ValidationFailure('not yet valid')
hash = _make_hash(rrsig.algorithm)
if _is_rsa(rrsig.algorithm):
keyptr = candidate_key.key
(bytes,) = struct.unpack('!B', keyptr[0:1])
keyptr = keyptr[1:]
if bytes == 0:
(bytes,) = struct.unpack('!H', keyptr[0:2])
keyptr = keyptr[2:]
rsa_e = keyptr[0:bytes]
rsa_n = keyptr[bytes:]
n = ecdsa.util.string_to_number(rsa_n)
e = ecdsa.util.string_to_number(rsa_e)
pubkey = rsakey.RSAKey(n, e)
sig = rrsig.signature
elif _is_ecdsa(rrsig.algorithm):
if rrsig.algorithm == ECDSAP256SHA256:
curve = ecdsa.curves.NIST256p
key_len = 32
digest_len = 32
elif rrsig.algorithm == ECDSAP384SHA384:
curve = ecdsa.curves.NIST384p
key_len = 48
digest_len = 48
else:
# shouldn't happen
raise ValidationFailure('unknown ECDSA curve')
keyptr = candidate_key.key
x = ecdsa.util.string_to_number(keyptr[0:key_len])
y = ecdsa.util.string_to_number(keyptr[key_len:key_len * 2])
assert ecdsa.ecdsa.point_is_valid(curve.generator, x, y)
point = ecdsa.ellipticcurve.Point(curve.curve, x, y, curve.order)
verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point, curve)
r = rrsig.signature[:key_len]
s = rrsig.signature[key_len:]
sig = ecdsa.ecdsa.Signature(ecdsa.util.string_to_number(r),
ecdsa.util.string_to_number(s))
else:
raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm)
hash.update(_to_rdata(rrsig, origin)[:18])
hash.update(rrsig.signer.to_digestable(origin))
if rrsig.labels < len(rrname) - 1:
suffix = rrname.split(rrsig.labels + 1)[1]
rrname = dns.name.from_text('*', suffix)
rrnamebuf = rrname.to_digestable(origin)
rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
rrsig.original_ttl)
rrlist = sorted(rdataset);
for rr in rrlist:
hash.update(rrnamebuf)
hash.update(rrfixed)
rrdata = rr.to_digestable(origin)
rrlen = struct.pack('!H', len(rrdata))
hash.update(rrlen)
hash.update(rrdata)
digest = hash.digest()
if _is_rsa(rrsig.algorithm):
digest = _make_algorithm_id(rrsig.algorithm) + digest
if pubkey.verify(bytearray(sig), bytearray(digest)):
return
elif _is_ecdsa(rrsig.algorithm):
diglong = ecdsa.util.string_to_number(digest)
if verifying_key.pubkey.verifies(diglong, sig):
return
else:
raise ValidationFailure('unknown algorithm %s' % rrsig.algorithm)
raise ValidationFailure('verify failure')
# replace validate_rrsig
dns.dnssec._validate_rrsig = python_validate_rrsig
dns.dnssec.validate_rrsig = python_validate_rrsig
dns.dnssec.validate = dns.dnssec._validate
from .util import print_error
# hard-coded trust anchors (root KSKs)
trust_anchors = [
# KSK-2017:
dns.rrset.from_text('.', 1 , 'IN', 'DNSKEY', '257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU='),
# KSK-2010:
dns.rrset.from_text('.', 15202, 'IN', 'DNSKEY', '257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0='),
]
def check_query(ns, sub, _type, keys):
q = dns.message.make_query(sub, _type, want_dnssec=True)
response = dns.query.tcp(q, ns, timeout=5)
assert response.rcode() == 0, 'No answer'
answer = response.answer
assert len(answer) != 0, ('No DNS record found', sub, _type)
assert len(answer) != 1, ('No DNSSEC record found', sub, _type)
if answer[0].rdtype == dns.rdatatype.RRSIG:
rrsig, rrset = answer
elif answer[1].rdtype == dns.rdatatype.RRSIG:
rrset, rrsig = answer
else:
raise BaseException('No signature set in record')
if keys is None:
keys = {dns.name.from_text(sub):rrset}
dns.dnssec.validate(rrset, rrsig, keys)
return rrset
def get_and_validate(ns, url, _type):
# get trusted root key
root_rrset = None
for dnskey_rr in trust_anchors:
try:
# Check if there is a valid signature for the root dnskey
root_rrset = check_query(ns, '', dns.rdatatype.DNSKEY, {dns.name.root: dnskey_rr})
break
except dns.dnssec.ValidationFailure:
# It's OK as long as one key validates
continue
if not root_rrset:
raise dns.dnssec.ValidationFailure('None of the trust anchors found in DNS')
keys = {dns.name.root: root_rrset}
# top-down verification
parts = url.split('.')
for i in range(len(parts), 0, -1):
sub = '.'.join(parts[i-1:])
name = dns.name.from_text(sub)
# If server is authoritative, don't fetch DNSKEY
query = dns.message.make_query(sub, dns.rdatatype.NS)
response = dns.query.udp(query, ns, 3)
assert response.rcode() == dns.rcode.NOERROR, "query error"
rrset = response.authority[0] if len(response.authority) > 0 else response.answer[0]
rr = rrset[0]
if rr.rdtype == dns.rdatatype.SOA:
continue
# get DNSKEY (self-signed)
rrset = check_query(ns, sub, dns.rdatatype.DNSKEY, None)
# get DS (signed by parent)
ds_rrset = check_query(ns, sub, dns.rdatatype.DS, keys)
# verify that a signed DS validates DNSKEY
for ds in ds_rrset:
for dnskey in rrset:
htype = 'SHA256' if ds.digest_type == 2 else 'SHA1'
good_ds = dns.dnssec.make_ds(name, dnskey, htype)
if ds == good_ds:
break
else:
continue
break
else:
raise BaseException("DS does not match DNSKEY")
# set key for next iteration
keys = {name: rrset}
# get TXT record (signed by zone)
rrset = check_query(ns, url, _type, keys)
return rrset
def query(url, rtype):
# 8.8.8.8 is Google's public DNS server
nameservers = ['8.8.8.8']
ns = nameservers[0]
try:
out = get_and_validate(ns, url, rtype)
validated = True
except BaseException as e:
#traceback.print_exc(file=sys.stderr)
print_error("DNSSEC error:", str(e))
resolver = dns.resolver.get_default_resolver()
out = resolver.query(url, rtype)
validated = False
return out, validated
================================================
FILE: lib/equihash.py
================================================
# ZCASH implementation: https://github.com/zcash/zcash/blob/master/qa/rpc-tests/test_framework/equihash.py
from pyblake2 import blake2b
from operator import itemgetter
import struct
from functools import reduce
DEBUG = False
VERBOSE = False
word_size = 32
word_mask = (1<= 8 and word_size >= 7+bit_len
bit_len_mask = (1<= bit_len:
acc_bits -= bit_len
for x in range(byte_pad, out_width):
out[j+x] = (
# Big-endian
acc_value >> (acc_bits+(8*(out_width-x-1)))
) & (
# Apply bit_len_mask across byte boundaries
(bit_len_mask >> (8*(out_width-x-1))) & 0xFF
)
j += out_width
return out
def compress_array(inp, out_len, bit_len, byte_pad=0):
assert bit_len >= 8 and word_size >= 7+bit_len
in_width = (bit_len+7)//8 + byte_pad
assert out_len == bit_len*len(inp)//(8*in_width)
out = bytearray(out_len)
bit_len_mask = (1 << bit_len) - 1
# The acc_bits least-significant bits of acc_value represent a bit sequence
# in big-endian order.
acc_bits = 0
acc_value = 0
j = 0
for i in range(out_len):
# When we have fewer than 8 bits left in the accumulator, read the next
# input element.
if acc_bits < 8:
acc_value = ((acc_value << bit_len) & word_mask) | inp[j]
for x in range(byte_pad, in_width):
acc_value = acc_value | (
(
# Apply bit_len_mask across byte boundaries
inp[j+x] & ((bit_len_mask >> (8*(in_width-x-1))) & 0xFF)
) << (8*(in_width-x-1))); # Big-endian
j += in_width
acc_bits += bit_len
acc_bits -= 8
out[i] = (acc_value >> acc_bits) & 0xFF
return out
def get_indices_from_minimal(minimal, bit_len):
eh_index_size = 4
assert (bit_len+7)//8 <= eh_index_size
len_indices = 8*eh_index_size*len(minimal)//bit_len
byte_pad = eh_index_size - (bit_len+7)//8
expanded = expand_array(minimal, len_indices, bit_len, byte_pad)
return [struct.unpack('>I', expanded[i:i+4])[0] for i in range(0, len_indices, eh_index_size)]
def get_minimal_from_indices(indices, bit_len):
eh_index_size = 4
assert (bit_len+7)//8 <= eh_index_size
len_indices = len(indices)*eh_index_size
min_len = bit_len*len_indices//(8*eh_index_size)
byte_pad = eh_index_size - (bit_len+7)//8
byte_indices = bytearray(b''.join([struct.pack('>I', i) for i in indices]))
return compress_array(byte_indices, min_len, bit_len, byte_pad)
def hash_nonce(digest, nonce):
for i in range(8):
digest.update(struct.pack('> (32*i) & 0xffffffff))
def hash_xi(digest, xi):
digest.update(struct.pack(' 0:
# 2b) Find next set of unordered pairs with collisions on first n/(k+1) bits
j = 1
while j < len(X):
if not has_collision(X[-1][0], X[-1-j][0], i, collision_length):
break
j += 1
# 2c) Store tuples (X_i ^ X_j, (i, j)) on the table
for l in range(0, j-1):
for m in range(l+1, j):
# Check that there are no duplicate indices in tuples i and j
if distinct_indices(X[-1-l][1], X[-1-m][1]):
if X[-1-l][1][0] < X[-1-m][1][0]:
concat = X[-1-l][1] + X[-1-m][1]
else:
concat = X[-1-m][1] + X[-1-l][1]
Xc.append((xor(X[-1-l][0], X[-1-m][0]), concat))
# 2d) Drop this set
while j > 0:
X.pop(-1)
j -= 1
# 2e) Replace previous list with new list
X = Xc
# k+1) Find a collision on last 2n(k+1) bits
if DEBUG:
print('Final round:')
print('- Sorting list')
X.sort(key=itemgetter(0))
if DEBUG and VERBOSE:
for Xi in X[-32:]:
print('%s %s' % (print_hash(Xi[0]), Xi[1]))
if DEBUG: print('- Finding collisions')
solns = []
while len(X) > 0:
j = 1
while j < len(X):
if not (has_collision(X[-1][0], X[-1-j][0], k, collision_length) and
has_collision(X[-1][0], X[-1-j][0], k+1, collision_length)):
break
j += 1
for l in range(0, j-1):
for m in range(l+1, j):
res = xor(X[-1-l][0], X[-1-m][0])
if count_zeroes(res) == 8*hash_length and distinct_indices(X[-1-l][1], X[-1-m][1]):
if DEBUG and VERBOSE:
print('Found solution:')
print('- %s %s' % (print_hash(X[-1-l][0]), X[-1-l][1]))
print('- %s %s' % (print_hash(X[-1-m][0]), X[-1-m][1]))
if X[-1-l][1][0] < X[-1-m][1][0]:
solns.append(list(X[-1-l][1] + X[-1-m][1]))
else:
solns.append(list(X[-1-m][1] + X[-1-l][1]))
# 2d) Drop this set
while j > 0:
X.pop(-1)
j -= 1
return [get_minimal_from_indices(soln, collision_length+1) for soln in solns]
def gbp_validate(digest, minimal, n, k):
validate_params(n, k)
collision_length = n//(k+1)
hash_length = (k+1)*((collision_length+7)//8)
indices_per_hash_output = 512//n
solution_width = (1 << k)*(collision_length+1)//8
if len(minimal) != solution_width:
print('Invalid solution length: %d (expected %d)' % \
(len(minimal), solution_width))
return False
X = []
for i in get_indices_from_minimal(minimal, collision_length+1):
r = i % indices_per_hash_output
# X_i = H(I||V||x_i)
curr_digest = digest.copy()
hash_xi(curr_digest, i//indices_per_hash_output)
tmp_hash = curr_digest.digest()
X.append((
expand_array(bytearray(tmp_hash[r*n//8:(r+1)*n//8]),
hash_length, collision_length),
(i,)
))
for r in range(1, k+1):
Xc = []
for i in range(0, len(X), 2):
if not has_collision(X[i][0], X[i+1][0], r, collision_length):
print('Invalid solution: invalid collision length between StepRows')
return False
if X[i+1][1][0] < X[i][1][0]:
print('Invalid solution: Index tree incorrectly ordered')
return False
if not distinct_indices(X[i][1], X[i+1][1]):
print('Invalid solution: duplicate indices')
return False
Xc.append((xor(X[i][0], X[i+1][0]), X[i][1] + X[i+1][1]))
X = Xc
if len(X) != 1:
print('Invalid solution: incorrect length after end of rounds: %d' % len(X))
return False
if count_zeroes(X[0][0]) != 8*hash_length:
print('Invalid solution: incorrect number of zeroes: %d' % count_zeroes(X[0][0]))
return False
return True
def zcash_person(n, k):
return b'ZcashPoW' + struct.pack('= n):
raise ValueError('n must be larger than k')
if (((n/(k+1))+1) >= 32):
raise ValueError('Parameters must satisfy n/(k+1)+1 < 32')
# a bit different from https://github.com/zcash/zcash/blob/master/qa/rpc-tests/test_framework/mininode.py#L747
# since electrum is a SPV oriented and not a node
def is_gbp_valid(header, nNonce, nSolution, n=48, k=5):
# H(I||...
digest = blake2b(digest_size=(512//n)*n//8, person=zcash_person(n, k))
digest.update(header[:108])
hash_nonce(digest, nNonce)
return gbp_validate(digest, nSolution, n, k)
================================================
FILE: lib/exchange_rate.py
================================================
from datetime import datetime
import inspect
import requests
import sys
from threading import Thread
import time
import csv
from decimal import Decimal
from .bitcoin import COIN
from .i18n import _
from .util import PrintError, ThreadJob
# 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'})
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 get_historical_rates_safe(self, ccy):
try:
self.print_error("requesting fx history for", ccy)
self.history[ccy] = self.historical_rates(ccy)
self.print_error("received fx history for", ccy)
self.on_history()
except BaseException as e:
self.print_error("failed fx history:", e)
def get_historical_rates(self, ccy):
result = self.history.get(ccy)
if not result and ccy in self.history_ccys():
t = Thread(target=self.get_historical_rates_safe, args=(ccy,))
t.setDaemon(True)
t.start()
return result
def history_ccys(self):
return []
def historical_rate(self, ccy, d_t):
return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'))
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) in [3,4]])
class CoinMarketCap(ExchangeBase):
def get_rates(self, ccy):
json = self.get_json('api.coinmarketcap.com',
"/v1/ticker/bitcoin-private?convert=%s")
return {'USD': Decimal(json[0]['price_usd'])}
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') 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()
except:
continue
with open(path, 'w') 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.set_exchange(self.config_exchange())
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))
return fmt_str.format(round(amount, prec))
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)
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_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', 'USD')
def config_exchange(self):
return self.config.get('use_exchange', 'CoinMarketCap')
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, CoinMarketCap)
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
def on_quotes(self):
self.network.trigger_callback('on_quotes')
def on_history(self):
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:
return Decimal(rate)
def format_amount_and_units(self, btc_balance):
rate = self.exchange_rate()
return '' if rate is None 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 exchange rate available)") if rate is None else " 1 %s=%s %s" % (base_unit,
self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
def value_str(self, satoshis, rate):
if satoshis is None: # Can happen with incomplete history
return _("Unknown")
if rate:
value = Decimal(satoshis) / COIN * Decimal(rate)
return "%s" % (self.ccy_amount_str(value, True))
return _("No data")
def history_rate(self, d_t):
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 is None and (datetime.today().date() - d_t.date()).days <= 2:
rate = self.exchange.quotes.get(self.ccy)
self.history_used_spot = True
return rate
def historical_value_str(self, satoshis, d_t):
rate = self.history_rate(d_t)
return self.value_str(satoshis, rate)
================================================
FILE: lib/i18n.py
================================================
#!/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 gettext, os
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'),
'cs_CZ':_('Czech'),
'da_DK':_('Danish'),
'de_DE':_('German'),
'eo_UY':_('Esperanto'),
'el_GR':_('Greek'),
'en_UK':_('English'),
'es_ES':_('Spanish'),
'fr_FR':_('French'),
'hu_HU':_('Hungarian'),
'hy_AM':_('Armenian'),
'id_ID':_('Indonesian'),
'it_IT':_('Italian'),
'ja_JP':_('Japanese'),
'ky_KG':_('Kyrgyz'),
'lv_LV':_('Latvian'),
'nl_NL':_('Dutch'),
'no_NO':_('Norwegian'),
'pl_PL':_('Polish'),
'pt_BR':_('Brasilian'),
'pt_PT':_('Portuguese'),
'ro_RO':_('Romanian'),
'ru_RU':_('Russian'),
'sk_SK':_('Slovak'),
'sl_SI':_('Slovenian'),
'ta_IN':_('Tamil'),
'th_TH':_('Thai'),
'vi_VN':_('Vietnamese'),
'zh_CN':_('Chinese')
}
================================================
FILE: lib/interface.py
================================================
#!/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
from .util import print_error, get_cert_path
ca_path = get_cert_path()
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:
print_error(e)
s = None
except:
return
if s and self.check_host_name(s.getpeercert(), 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
dercert = s.getpeercert(True)
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'
with open(temporary_path,"w") as f:
f.write(cert)
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:
with open(cert_path) 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. It's 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 = {}
# Set last ping to zero to ensure immediate ping
self.last_request = time.time()
self.last_ping = 0
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.'''
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 socket.error as e:
self.print_error("socket 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):
'''Maintains time since last ping. Returns True if a ping should
be sent.
'''
now = time.time()
if now - self.last_ping > 60:
self.last_ping = now
return True
return False
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) as f:
cert = f.read()
check_cert(c, cert)
if __name__ == "__main__":
test_certificates()
================================================
FILE: lib/jsonrpc.py
================================================
#!/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):
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()
================================================
FILE: lib/keystore.py
================================================
#!/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
from .bitcoin import *
from .util import PrintError, InvalidPassword, hfu
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 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))
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 = regenerate_key(privkey)
return key.sign_message(message, compressed)
def decrypt_message(self, sequence, message, password):
privkey, compressed = self.get_private_key(sequence, password)
ec = regenerate_key(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 can_change_password(self):
return True
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 = public_key_from_private_key(privkey, compressed)
self.keypairs[pubkey] = pw_encode(sec, 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 != public_key_from_private_key(privkey, 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 can_change_password(self):
return not self.is_watching_only()
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)
master_private_key = ecdsa.SigningKey.from_secret_exponent(secexp, curve = SECP256k1)
master_public_key = master_private_key.get_verifying_key().to_string()
return bh2u(master_public_key)
@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 = ecdsa.VerifyingKey.from_string(bfh(mpk), curve = SECP256k1)
pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator
public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1)
return '04' + bh2u(public_key2.to_string())
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):
order = generator_secp256k1.order()
secexp = (secexp + self.get_sequence(self.mpk, for_change, n)) % order
pk = number_to_string(secexp, generator_secp256k1.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 = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
master_public_key = master_private_key.get_verifying_key().to_string()
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 diconnected. 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 can_change_password(self):
return False
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):
k = BIP32_KeyStore({})
bip32_seed = bip39_to_seed(seed, passphrase)
xtype = xtype_from_derivation(derivation)
k.add_xprv_from_seed(bip32_seed, xtype, derivation)
return k
def xtype_from_derivation(derivation):
"""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'
else:
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 BaseException("Cannot parse pubkey")
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 BaseException('unknown hardware type', hw_type)
def load_keystore(storage, name):
w = storage.get('wallet_type', 'standard')
d = storage.get(name, {})
t = d.get('type')
if not t:
raise BaseException('wallet format requires update')
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 BaseException('unknown wallet type', t)
return k
def is_old_mpk(mpk):
try:
int(mpk, 16)
except:
return False
return len(mpk) == 128
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 = 1 if bitcoin.NetworkConstants.TESTNET else 183 # BTCP - SLIP0044
return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
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 BaseException(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 BaseException('Invalid key')
return k
================================================
FILE: lib/mnemonic.py
================================================
#!/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') 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 check_seed(self, seed, custom_entropy):
assert is_new_seed(seed)
i = self.mnemonic_decode(seed)
return i % custom_entropy == 0
def make_seed(self, seed_type='standard', num_bits=132, custom_entropy=1):
prefix = version.seed_prefix(seed_type)
# increase num_bits in order to obtain a uniform distibution for the last word
bpw = math.log(len(self.wordlist), 2)
num_bits = int(math.ceil(num_bits/bpw) * bpw)
# handle custom entropy; make sure we add at least 16 bits
n_custom = int(math.ceil(math.log(custom_entropy, 2)))
n = max(16, num_bits - n_custom)
print_error("make_seed", prefix, "adding %d bits"%n)
my_entropy = 1
while my_entropy < pow(2, n - bpw):
# try again if seed would not contain enough words
my_entropy = ecdsa.util.randrange(pow(2, n))
nonce = 0
while True:
nonce += 1
i = custom_entropy * (my_entropy + nonce)
seed = self.mnemonic_encode(i)
assert i == self.mnemonic_decode(seed)
if is_old_seed(seed):
continue
if is_new_seed(seed, prefix):
break
print_error('%d words'%len(seed.split()))
return seed
================================================
FILE: lib/msqr.py
================================================
# 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
================================================
FILE: lib/network.py
================================================
# 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 stat
import errno
import random
import re
import select
from collections import defaultdict
import threading
import socket
import json
import socks
from . import util
from . import bitcoin
from .bitcoin import *
from .interface import Connection, Interface
from . import blockchain
from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
NODES_RETRY_INTERVAL = 60
SERVER_RETRY_INTERVAL = 10
def parse_servers(result):
""" parse servers list into dict format"""
from .version import PROTOCOL_VERSION
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 = NetworkConstants.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 = NetworkConstants.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).split(':')
assert protocol in 'st'
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)
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
self.protocol = 't' if self.config.get('nossl') else 's'
# Server for addresses and transactions
self.default_server = self.config.get('server')
# Sanitize default server
try:
host, port, protocol = deserialize_server(self.default_server)
assert protocol == self.protocol
except:
self.default_server = None
if not self.default_server:
self.default_server = pick_random_server(protocol=self.protocol)
self.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()
self.banner = ''
self.donation_address = ''
self.relay_fee = None
# callbacks passed with subscriptions
self.subscriptions = defaultdict(list)
self.sub_cache = {}
# callbacks set by the GUI
self.callbacks = defaultdict(list)
dir_path = os.path.join( self.config.path, 'certs')
if not os.path.exists(dir_path):
os.mkdir(dir_path)
os.chmod(dir_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
# subscriptions and requests
self.subscribed_addresses = set()
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
self.interfaces = {}
self.auto_connect = self.config.get('auto_connect', True)
self.connecting = set()
self.requested_chunks = set()
self.socket_queue = queue.Queue()
self.start_network(self.protocol, deserialize_proxy(self.config.get('proxy')))
def register_callback(self, callback, events):
with self.lock:
for event in events:
self.callbacks[event].append(callback)
def unregister_callback(self, callback):
with self.lock:
for callbacks in self.callbacks.values():
if callback in callbacks:
callbacks.remove(callback)
def trigger_callback(self, event, *args):
with self.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") as f:
data = f.read()
return json.loads(data)
except:
return []
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") as f:
f.write(s)
except:
pass
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'
def is_up_to_date(self):
return self.unanswered_requests == {}
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
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
def send_subscriptions(self):
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', [])
if self.interface.ping_required():
params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
self.queue_request('server.version', params, self.interface)
for h in self.subscribed_addresses:
self.queue_request('blockchain.scripthash.subscribe', [h])
def request_fee_estimates(self):
self.config.requested_fee_estimates()
for i in bitcoin.FEE_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 == '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
def get_interfaces(self):
'''The interfaces that are in connected state'''
return list(self.interfaces.keys())
def get_servers(self):
out = NetworkConstants.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
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)
c = Connection(server, self.socket_queue, self.config.path)
def start_random_interface(self):
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
socket.getaddrinfo = socket._getaddrinfo
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([])
self.protocol = protocol
self.set_proxy(proxy)
self.start_interfaces()
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
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))
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)
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')
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()
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("<--", 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)
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 == '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)
self.print_error("relayfee", self.relay_fee)
elif method == 'blockchain.block.get_chunk':
self.on_get_chunk(interface, response)
elif method == 'blockchain.block.get_header':
self.on_get_header(interface, response)
for callback in callbacks:
callback(response)
def get_index(self, 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:
assert interface == self.interface
callbacks = [client_req[2]]
else:
# fixme: will only work for subscriptions
k = self.get_index(method, params)
callbacks = 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':
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 = self.subscriptions.get(k, [])
# update cache if it's a subscription
if method.endswith('.subscribe'):
self.sub_cache[k] = response
# Response is now in canonical form
self.process_response(interface, response, callbacks)
def addr_to_scripthash(self, addr):
h = bitcoin.address_to_scripthash(addr)
if h not in self.h2addr:
self.h2addr[h] = addr
return h
def overload_cb(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):
hashes = [self.addr_to_scripthash(addr) for addr in addresses]
msgs = [('blockchain.scripthash.subscribe', [x]) for x in hashes]
self.send(msgs, self.overload_cb(callback))
def request_address_history(self, address, callback):
h = self.addr_to_scripthash(address)
self.send([('blockchain.scripthash.get_history', [h])], self.overload_cb(callback))
def send(self, messages, callback):
'''Messages is a list of (method, params) tuples'''
messages = list(messages)
with self.lock:
self.pending_sends.append((messages, callback))
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.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 = self.subscriptions.get(k, [])
if callback not in l:
l.append(callback)
self.subscriptions[k] = l
# check cached response for subscriptions
r = self.sub_cache.get(k)
if r is not None:
util.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.lock:
for v in self.subscriptions.values():
if callback in v:
v.remove(callback)
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')
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
self.interfaces[server] = interface
self.queue_request('blockchain.headers.subscribe', [], 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
for interface in list(self.interfaces.values()):
if interface.has_timed_out():
self.connection_down(interface.server)
elif interface.ping_required():
params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
self.queue_request('server.version', params, interface)
now = time.time()
# nodes
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
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)
self.queue_request('blockchain.block.get_chunk', [index], interface)
def on_get_chunk(self, interface, response):
'''Handle receiving a chunk of block headers'''
error = response.get('error')
result = response.get('result')
params = response.get('params')
if result is None or params is None or error is not None:
interface.print_error(error or 'bad response')
return
index = params[0]
# Ignore unsolicited chunks
if index not in self.requested_chunks:
return
self.requested_chunks.remove(index)
connect = interface.blockchain.connect_chunk(index, result)
if not connect:
self.connection_down(interface.server)
return
# If not finished, get the next chunk
if interface.blockchain.height() < interface.tip:
self.request_chunk(interface, index+1)
else:
interface.mode = 'default'
interface.print_error('catch up done', interface.blockchain.height())
interface.blockchain.catch_up = None
self.notify('updated')
def request_header(self, interface, height):
interface.print_error("requesting header %d" % height)
self.queue_request('blockchain.block.get_header', [height], interface)
interface.request = height
interface.req_time = time.time()
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 int(interface.request) != height:
interface.print_error("unsolicited header",interface.request, height)
self.connection_down(interface.server)
return
interface.print_error("interface.mode %s" % interface.mode)
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
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(0, interface.tip - 2 * delta)
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
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('', 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)
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 BaseException(interface.mode)
# If not finished, get the next header
if next_height:
if interface.mode == 'catch_up' and interface.tip > next_height + 50:
self.request_chunk(interface, next_height // NetworkConstants.CHUNK_SIZE)
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):
for interface in list(self.interfaces.values()):
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
rin = [i for i in self.interfaces.values()]
win = [i for i in self.interfaces.values() if i.num_requests()]
try:
rout, wout, xout = select.select(rin, win, [], 0.1)
except socket.error as e:
# TODO: py3, get code from e
code = None
if code == 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]
print(b.get_hash(0), NetworkConstants.GENESIS)
if b.get_hash(0) == NetworkConstants.GENESIS:
self.downloading_headers = False
return
filename = b.path()
def download_thread():
try:
import urllib, socket
socket.setdefaulttimeout(30)
self.print_error("downloading ", NetworkConstants.HEADERS_URL)
urllib.request.urlretrieve(NetworkConstants.HEADERS_URL, filename)
self.print_error("done.")
except Exception:
import traceback
traceback.print_exc()
self.print_error("download failed. creating file", filename)
open(filename, 'wb+').close()
b = self.blockchains[0]
with b.lock: b.update_size()
self.downloading_headers = False
self.downloading_headers = True
t = threading.Thread(target = download_thread)
t.daemon = True
t.start()
def run(self):
self.init_headers_file()
while self.is_running() and self.downloading_headers:
time.sleep(1)
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):
height = header.get('block_height')
if not height:
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
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
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)
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]
def get_blockchains(self):
out = {}
for k, b in self.blockchains.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)
for i in self.interfaces.values():
if i.blockchain == blockchain:
self.switch_to_interface(i.server)
break
else:
raise BaseException('blockchain not found', index)
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()
def synchronous_get(self, request, timeout=30):
q = queue.Queue()
self.send([request], q.put)
try:
r = q.get(True, timeout)
except queue.Empty:
raise BaseException('Server did not answer')
if r.get('error'):
raise BaseException(r.get('error'))
return r.get('result')
def broadcast(self, tx, timeout=30):
tx_hash = tx.txid()
try:
out = self.synchronous_get(('blockchain.transaction.broadcast', [str(tx)]), timeout)
except BaseException as e:
return False, "error: " + str(e)
if out != tx_hash:
return False, "error: " + out
return True, out
================================================
FILE: lib/old_mnemonic.py
================================================
#!/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:]))
================================================
FILE: lib/paymentrequest.proto
================================================
//
// 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
}
================================================
FILE: lib/paymentrequest.py
================================================
#!/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 util
from .util import print_error, bh2u, bfh, get_cert_path
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 = get_cert_path()
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') as f:
data = f.read()
except IOError:
data = None
error = "payment URL not pointing to a valid file"
else:
raise BaseException("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 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 dispayed 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 = ''
message = pr.SerializeToString()
if bitcoin.verify_message(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 = bitcoin.regenerate_key(alias_privkey)
address = bitcoin.address_from_private_key(alias_privkey)
compressed = bitcoin.is_compressed(alias_privkey)
pr.signature = ec_key.sign_message(message, compressed, address)
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 BaseException("ERROR: Supplied CA Certificate Error")
if not cert_num > 1:
raise BaseException("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 BaseException("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 BaseException("Algorithm not supported")
util.print_error(self.error, algo.getComponentByName('algorithm'))
if not verify:
raise BaseException("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') as f:
params = pem.parse_private_key(f.read())
with open(cert_path, 'r') 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') as f:
params = pem.parse_private_key(f.read())
privkey = rsakey.RSAKey(*params)
with open(cert_path, 'r') 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
self.paid[txid] = pr.get_id()
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):
try:
with open(path, 'r') as f:
d = json.loads(f.read())
self.load(d)
except:
traceback.print_exc(file=sys.stderr)
return
self.save()
def save(self):
l = {}
for k, pr in self.invoices.items():
l[k] = {
'hex': bh2u(pr.raw),
'requestor': pr.requestor,
'txid': pr.tx
}
self.storage.put('invoices', l)
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())]
================================================
FILE: lib/paymentrequest_pb2.py
================================================
# 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)
================================================
FILE: lib/pem.py
================================================
#!/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 ."""
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]))
================================================
FILE: lib/plot.py
================================================
from PyQt5.QtGui import *
from electrum.i18n import _
import datetime
from collections import defaultdict
from electrum.bitcoin import COIN
import matplotlib
matplotlib.use('Qt5Agg')
import matplotlib.pyplot as plt
import matplotlib.dates as md
from matplotlib.patches import Ellipse
from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker
def plot_history(wallet, history):
hist_in = defaultdict(int)
hist_out = defaultdict(int)
for item in history:
tx_hash, height, confirmations, timestamp, value, balance = item
if not confirmations:
continue
if timestamp is None:
continue
value = value*1./COIN
date = datetime.datetime.fromtimestamp(timestamp)
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
dates, values = zip(*sorted(hist_in.items()))
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')
return plt
================================================
FILE: lib/plugins.py
================================================
#!/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:
plugins = __import__('electrum_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()
# 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 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)
client = self.client_lookup(_id)
self.clients.pop(client, None)
if client:
client.close()
def unpair_id(self, id_):
xpub = self.xpub_by_id(id_)
if xpub:
self.unpair_xpub(xpub)
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 BaseException(_("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 %s.\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.') % 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 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 not device.product_key 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 %s. Verify the cable is '
'connected and that no other application is using it.\n\n'
'Try to connect again?') % plugin.device
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 %s device to use:") % plugin.device
descriptions = [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)
handler.win.wallet.save_keystore()
return info
def scan_devices(self):
# All currently supported hardware libraries use hid, so we
# assume it here. This can be easily abstracted if necessary.
# Note this import must be local so those without hardware
# wallet libraries are not affected.
import hid
self.print_error("scanning devices...")
with self.hid_lock:
hid_list = hid.enumerate(0, 0)
# First see what's connected that we know about
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))
# Now 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:
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
================================================
FILE: lib/qrscanner.py
================================================
#!/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 == 'windows':
name = 'libzbar.dll'
else:
name = 'libzbar.so.0'
try:
libzbar = ctypes.cdll.LoadLibrary(name)
except OSError:
libzbar = None
def scan_barcode(device='', timeout=-1, display=True, threaded=False):
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:
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())
================================================
FILE: lib/ripemd.py
================================================
## ripemd.py - pure Python implementation of the RIPEMD-160 algorithm.
## Bjorn Edstrom 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(" 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)
================================================
FILE: lib/segwit_addr.py
================================================
# 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
================================================
FILE: lib/servers-orig.json
================================================
{
"E-X.not.fyi": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"ELECTRUMX.not.fyi": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"ELEX01.blackpole.online": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"VPS.hsmiths.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"bitcoin.freedomnode.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"btc.smsys.me": {
"pruning": "-",
"s": "995",
"version": "1.1"
},
"currentlane.lovebitco.in": {
"pruning": "-",
"t": "50001",
"version": "1.1"
},
"daedalus.bauerj.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"de01.hamster.science": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"ecdsa.net": {
"pruning": "-",
"s": "110",
"t": "50001",
"version": "1.1"
},
"elec.luggs.co": {
"pruning": "-",
"s": "443",
"version": "1.1"
},
"electrum.akinbo.org": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.antumbra.se": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.be": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.coinucopia.io": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.cutie.ga": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.festivaldelhumor.org": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.hsmiths.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.qtornado.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.vom-stausee.de": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum3.hachre.de": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrumx.bot.nu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrumx.westeurope.cloudapp.azure.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"elx01.knas.systems": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"ex-btc.server-on.net": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"helicarrier.bauerj.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"mooo.not.fyi": {
"pruning": "-",
"s": "50012",
"t": "50011",
"version": "1.1"
},
"ndnd.selfhost.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"node.arihanc.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"node.xbt.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"node1.volatilevictory.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"noserver4u.de": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"qmebr.spdns.org": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"raspi.hsmiths.com": {
"pruning": "-",
"s": "51002",
"t": "51001",
"version": "1.1"
},
"s2.noip.pl": {
"pruning": "-",
"s": "50102",
"version": "1.1"
},
"s5.noip.pl": {
"pruning": "-",
"s": "50105",
"version": "1.1"
},
"songbird.bauerj.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"us.electrum.be": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"us01.hamster.science": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
}
}
================================================
FILE: lib/servers.json
================================================
{
"electrum.btcprivate.org": {"s":"5222"}
}
================================================
FILE: lib/servers_testnet.json
================================================
{
"35.190.188.161": {"t":"50001"}
}
================================================
FILE: lib/simple_config.py
================================================
import json
import threading
import time
import os
import stat
from copy import deepcopy
from .util import user_dir, print_error, print_stderr, PrintError
from .bitcoin import DEFAULT_FEE_RATE, MAX_FEE_RATE, FEE_TARGETS
SYSTEM_CONFIG_PATH = "/etc/electrum.conf"
config = None
def get_config():
global config
return config
def set_config(c):
global config
config = c
class SimpleConfig(PrintError):
"""
The SimpleConfig class is responsible for handling operations involving
configuration files.
There are 3 different sources of possible configuration values:
1. Command line options.
2. User configuration (in the user's config directory)
3. System configuration (in /etc/)
They are taken in order (1. overrides config options set in 2., that
override config set in 3.)
"""
fee_rates = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
def __init__(self, options={}, read_system_config_function=None,
read_user_config_function=None, read_user_dir_function=None):
# This lock needs to be acquired for updating and reading the config in
# a thread-safe way.
self.lock = threading.RLock()
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_system_config_function is None:
read_system_config_function = read_system_config
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)
# Portable wallets don't use a system config
if self.cmdline_options.get('portable', False):
self.system_config = {}
else:
self.system_config = read_system_config_function()
# 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)
# Upgrade obsolete keys
self.fixup_keys({'auto_cycle': 'auto_connect'})
# Make a singleton instance of 'self'
set_config(self)
def electrum_path(self):
# Read electrum_path from command line / system configuration
# Otherwise use the user's default data directory.
path = self.get('electrum_path')
if path is None:
path = self.user_dir()
def make_dir(path):
# Make directory if it does not yet exist.
if not os.path.exists(path):
if os.path.islink(path):
raise BaseException('Dangling link: ' + path)
os.mkdir(path)
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
make_dir(path)
if self.get('testnet'):
path = os.path.join(path, 'testnet')
make_dir(path)
self.print_error("electrum directory", path)
return path
def fixup_config_keys(self, config, keypairs):
updated = False
for old_key, new_key in keypairs.items():
if old_key in config:
if not new_key in config:
config[new_key] = config[old_key]
del config[old_key]
updated = True
return updated
def fixup_keys(self, keypairs):
'''Migrate old key names to new ones'''
self.fixup_config_keys(self.cmdline_options, keypairs)
self.fixup_config_keys(self.system_config, keypairs)
if self.fixup_config_keys(self.user_config, keypairs):
self.save_user_config()
def set_key(self, key, value, save = True):
if not self.is_modifiable(key):
print_stderr("Warning: not changing config key '%s' set on the command line" % key)
return
with self.lock:
self.user_config[key] = value
if save:
self.save_user_config()
return
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)
if out is None:
out = self.system_config.get(key, default)
return out
def is_modifiable(self, key):
return not key 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)
with open(path, "w") as f:
f.write(s)
os.chmod(path, stat.S_IREAD | stat.S_IWRITE)
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
dirpath = os.path.join(self.path, "wallets")
if not os.path.exists(dirpath):
if os.path.islink(dirpath):
raise BaseException('Dangling link: ' + dirpath)
os.mkdir(dirpath)
os.chmod(dirpath, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
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 default_fee_rate(self):
f = self.get('default_fee_rate', DEFAULT_FEE_RATE)
if f==0:
f = DEFAULT_FEE_RATE
return f
def max_fee_rate(self):
f = self.get('max_fee_rate', MAX_FEE_RATE)
if f==0:
f = MAX_FEE_RATE
return f
def dynfee(self, i):
if i < 4:
j = FEE_TARGETS[i]
fee = self.fee_estimates.get(j)
else:
assert i == 4
fee = self.fee_estimates.get(2)
if fee is not None:
fee += fee/2
if fee is not None:
fee = min(5*MAX_FEE_RATE, fee)
return fee
def reverse_dynfee(self, fee_per_kb):
import operator
l = list(self.fee_estimates.items()) + [(1, self.dynfee(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 static_fee(self, i):
return self.fee_rates[i]
def static_fee_index(self, value):
dist = list(map(lambda x: abs(x - value), self.fee_rates))
return min(range(len(dist)), key=dist.__getitem__)
def has_fee_estimates(self):
return len(self.fee_estimates)==4
# 'dynamic' fees are disabled - we use 'static' (but adjustable) fees
def is_dynfee(self):
return self.get('dynamic_fees', False)
def fee_per_kb(self):
dyn = self.is_dynfee()
if dyn:
fee_rate = self.dynfee(self.get('fee_level', 2))
else:
fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
return fee_rate
def estimate_fee(self, size):
return self.estimate_fee_for_feerate(self.fee_per_kb(), size)
@classmethod
def estimate_fee_for_feerate(cls, fee_per_kb, size):
return int(fee_per_kb * size / 1000.)
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()
prev_updates = self.fee_estimates_last_updated.values()
oldest_fee_time = min(prev_updates) if prev_updates else 0
stale_fees = now - oldest_fee_time > 7200
old_request = now - self.last_time_fee_estimates_requested > 60
return stale_fees and old_request
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_system_config(path=SYSTEM_CONFIG_PATH):
"""Parse and return the system config settings in /etc/electrum.conf."""
result = {}
if os.path.exists(path):
import configparser
p = configparser.ConfigParser()
try:
p.read(path)
for k, v in p.items('client'):
result[k] = v
except (configparser.NoSectionError, configparser.MissingSectionHeaderError):
pass
return result
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") 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
================================================
FILE: lib/storage.py
================================================
#!/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 .util import PrintError, profiler
from .plugins import run_hook, plugin_loaders
from .keystore import bip44_derivation
from . import bitcoin
# 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 = 16 # 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.'''
match = re.match('(\d+)of(\d+)', wallet_type)
if match:
match = [int(x) for x in match.group(1, 2)]
return match
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") as f:
self.raw = f.read()
if not self.is_encrypted():
self.load_data(self.raw)
else:
# 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 BaseException("This wallet has multiple accounts and must be split")
if self.requires_upgrade():
self.upgrade()
def is_encrypted(self):
try:
return base64.b64decode(self.raw)[0:4] == b'BIE1'
except:
return False
def file_exists(self):
return self.path and os.path.exists(self.path)
def get_key(self, password):
secret = pbkdf2.PBKDF2(password, '', iterations = 1024, macmodule = hmac, digestmodule = hashlib.sha512).read(64)
ec_key = bitcoin.EC_KEY(secret)
return ec_key
def decrypt(self, password):
ec_key = self.get_key(password)
s = zlib.decompress(ec_key.decrypt_message(self.raw)) if self.raw else None
self.pubkey = ec_key.get_public_key()
s = s.decode('utf8')
self.load_data(s)
def set_password(self, password, encrypt):
self.put('use_encryption', bool(password))
if encrypt and password:
ec_key = self.get_key(password)
self.pubkey = ec_key.get_public_key()
else:
self.pubkey = None
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)
json.dumps(value)
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)
if self.pubkey:
s = bytes(s, 'utf8')
c = zlib.compress(s)
s = bitcoin.encrypt_message(c, self.pubkey)
s = s.decode('utf8')
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
with open(temp_path, "w") 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 BaseException("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
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.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self.write()
def convert_wallet_type(self):
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
# 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
assert self.get('seed_type') != 'segwit' # unsupported derivation
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_imported(self):
# '/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 BaseException('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 BaseException('no addresses or privkeys')
def convert_account(self):
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 BaseException(
('storage upgrade: unexpected version %d (should be %d-%d)'
% (cur_version, min_version, max_version)))
else:
return True
def get_action(self):
action = run_hook('get_action', self)
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 BaseException('This version of Electrum is too old to open this wallet')
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 BaseException(msg)
================================================
FILE: lib/synchronizer.py
================================================
#!/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.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):
params, result = self.parse_response(response)
if not params:
return
addr = params[0]
history = self.wallet.get_address_history(addr)
if self.get_status(history) != result:
if self.requested_histories.get(addr) is None:
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):
params, result = self.parse_response(response)
if not params:
return
addr = params[0]
self.print_error("receiving history", addr, len(result))
server_status = self.requested_histories[addr]
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))
# Note if the server hasn't been patched to sort the items properly
if hist != sorted(hist, key=lambda x:x[1]):
self.network.interface.print_error("serving improperly sorted address histories")
# 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 tx_response(self, response):
params, result = self.parse_response(response)
if not params:
return
tx_hash = params[0]
#assert tx_hash == hash_encode(Hash(bytes.fromhex(result)))
tx = Transaction(result)
try:
tx.deserialize()
except Exception:
self.print_msg("cannot deserialize transaction, skipping", tx_hash)
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
requests = []
for tx_hash, tx_height in hist:
if tx_hash in self.requested_tx:
continue
if tx_hash in self.wallet.transactions:
continue
requests.append(('blockchain.transaction.get', [tx_hash]))
self.requested_tx[tx_hash] = tx_height
self.network.send(requests, self.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()))
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')
================================================
FILE: lib/tests/__init__.py
================================================
================================================
FILE: lib/tests/test_bitcoin.py
================================================
import base64
import unittest
import sys
from ecdsa.util import number_to_string
from lib.bitcoin import (
generator_secp256k1, point_to_ser, public_key_to_p2pkh, EC_KEY,
bip32_root, bip32_public_derivation, bip32_private_derivation, pw_encode,
pw_decode, Hash, public_key_from_private_key, 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, regenerate_key,
verify_message, 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, NetworkConstants)
from lib.util import bfh
try:
import ecdsa
except ImportError:
sys.exit("Error: python-ecdsa does not seem to be installed. Try 'sudo pip install ecdsa'")
class Test_bitcoin(unittest.TestCase):
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 = generator_secp256k1
_r = G.order()
pvk = ecdsa.util.randrange( pow(2,256) ) %_r
Pub = pvk*G
pubkey_c = point_to_ser(Pub,True)
#pubkey_u = point_to_ser(Pub,False)
addr_c = public_key_to_p2pkh(pubkey_c)
#print "Private key ", '%064x'%pvk
eck = EC_KEY(number_to_string(pvk,_r))
#print "Compressed public key ", pubkey_c.encode('hex')
enc = EC_KEY.encrypt_message(message, pubkey_c)
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
EC_KEY.verify_message(eck, signature, message)
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 = regenerate_key(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(verify_message(addr1, sig1, msg1))
self.assertTrue(verify_message(addr2, sig2, msg2))
self.assertFalse(verify_message(addr1, b'wrong', msg1))
self.assertFalse(verify_message(addr1, sig2, msg1))
def test_aes_homomorphic(self):
"""Make sure AES is homomorphic."""
payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0'
password = u'secret'
enc = pw_encode(payload, password)
dec = pw_decode(enc, password)
self.assertEqual(dec, payload)
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 = pw_encode(payload, None)
self.assertEqual(payload, enc)
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 = pw_decode(payload, None)
self.assertEqual(payload, enc)
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 = pw_encode(payload, password)
self.assertRaises(Exception, 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_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), '4dff00')
self.assertEqual(op_push(0x100), '4d0001')
self.assertEqual(op_push(0x1234), '4d3412')
self.assertEqual(op_push(0xfffe), '4dfeff')
self.assertEqual(op_push(0xffff), '4effff0000')
self.assertEqual(op_push(0x10000), '4e00000100')
self.assertEqual(op_push(0x12345678), '4e78563412')
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(unittest.TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
NetworkConstants.set_testnet()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
NetworkConstants.set_mainnet()
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(unittest.TestCase):
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
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)
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'])
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'))
def test_xpub_type(self):
for xprv_details in self.xprv_xpub:
xpub = xprv_details['xpub']
self.assertEqual(xprv_details['xtype'], xpub_type(xpub))
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"))
class Test_keyImport(unittest.TestCase):
priv_pub_addr = (
{'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997',
'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR',
'minikey' : False,
'txin_type': 'p2pkh',
'compressed': True,
'addr_encoding': 'base58',
'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'},
{'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f',
'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6',
'minikey': False,
'txin_type': 'p2pkh',
'compressed': False,
'addr_encoding': 'base58',
'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'},
{'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz',
'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81',
'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7',
'minikey': False,
'txin_type': 'p2wpkh-p2sh',
'compressed': True,
'addr_encoding': 'base58',
'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'},
{'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj',
'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b',
'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue',
'minikey': False,
'txin_type': 'p2wpkh',
'compressed': True,
'addr_encoding': 'bech32',
'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'},
# from http://bitscan.com/articles/security/spotlight-on-mini-private-keys
{'priv': 'SzavMBLoXU6kDrqtUVmffv',
'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9',
'address': '19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR',
'minikey': True,
'txin_type': 'p2pkh',
'compressed': True, # this is actually ambiguous... issue #2748
'addr_encoding': 'base58',
'scripthash': '60ad5a8b922f758cd7884403e90ee7e6f093f8d21a0ff24c9a865e695ccefdf1'},
)
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 = public_key_from_private_key(privkey, compressed)
self.assertEqual(priv_details['pub'], result)
self.assertEqual(priv_details['txin_type'], txin_type)
self.assertEqual(priv_details['compressed'], compressed)
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)
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"))
def test_is_private_key(self):
for priv_details in self.priv_pub_addr:
self.assertTrue(is_private_key(priv_details['priv']))
self.assertFalse(is_private_key(priv_details['pub']))
self.assertFalse(is_private_key(priv_details['address']))
self.assertFalse(is_private_key("not a privkey"))
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)
if not priv_details['minikey']:
self.assertEqual(priv_details['priv'], priv2)
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)
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))
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(unittest.TestCase):
""" 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)
================================================
FILE: lib/tests/test_interface.py
================================================
import unittest
from lib import interface
class TestInterface(unittest.TestCase):
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'))
================================================
FILE: lib/tests/test_mnemonic.py
================================================
import unittest
from lib import keystore
from lib import mnemonic
from lib import old_mnemonic
from lib.util import bh2u
class Test_NewMnemonic(unittest.TestCase):
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(unittest.TestCase):
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(unittest.TestCase):
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)
================================================
FILE: lib/tests/test_simple_config.py
================================================
import ast
import sys
import os
import unittest
import tempfile
import shutil
from io import StringIO
from lib.simple_config import (SimpleConfig, read_system_config,
read_user_config)
class Test_SimpleConfig(unittest.TestCase):
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_system = lambda : {}
fake_read_user = lambda _: {"auto_cycle": True}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
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_system_config_function=fake_read_system,
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_system = lambda : {"electrum_path": "a"}
fake_read_user = lambda _: {"electrum_path": "b"}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
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_overrides_system_config(self):
"""Options passed in user config override system config."""
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
fake_read_user = lambda _: {"electrum_path": "b"}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir)
self.assertEqual("b", config.get("electrum_path"))
def test_simple_config_system_config_ignored_if_portable(self):
"""If electrum is started with the "portable" flag, system
configuration is completely ignored."""
fake_read_system = lambda : {"some_key": "some_value"}
fake_read_user = lambda _: {}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={"portable": True},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir)
self.assertEqual(config.get("some_key"), None)
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_system = lambda : {}
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={},
read_system_config_function=fake_read_system,
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_system = lambda : {}
fake_read_user = lambda _: {"electrum_path": "b"}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
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_from_system_config(self):
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
fake_read_user = lambda _: {}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir)
config.set_key("electrum_path", "c")
self.assertEqual("c", config.get("electrum_path"))
def test_can_set_options_set_in_user_config(self):
another_path = tempfile.mkdtemp()
fake_read_system = lambda : {}
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={},
read_system_config_function=fake_read_system,
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_can_set_options_from_system_config_if_portable(self):
"""If the "portable" flag is set, the user can overwrite system
configuration options."""
another_path = tempfile.mkdtemp()
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
fake_read_user = lambda _: {}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={"portable": True},
read_system_config_function=fake_read_system,
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 or system
options when saved."""
fake_read_system = lambda : {"something": "b"}
fake_read_user = lambda _: {"something": "a"}
read_user_dir = lambda : self.user_dir
self.options.update({"something": "c"})
config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
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)
self.assertEqual({"something": "a"}, result)
class TestSystemConfig(unittest.TestCase):
sample_conf = """
[client]
gap_limit = 5
[something_else]
everything = 42
"""
def setUp(self):
super(TestSystemConfig, self).setUp()
self.thefile = tempfile.mkstemp(suffix=".electrum.test.conf")[1]
def tearDown(self):
super(TestSystemConfig, self).tearDown()
os.remove(self.thefile)
def test_read_system_config_file_does_not_exist(self):
somefile = "/foo/I/do/not/exist/electrum.conf"
result = read_system_config(somefile)
self.assertEqual({}, result)
def test_read_system_config_file_returns_file_options(self):
with open(self.thefile, "w") as f:
f.write(self.sample_conf)
result = read_system_config(self.thefile)
self.assertEqual({"gap_limit": "5"}, result)
def test_read_system_config_file_no_sections(self):
with open(self.thefile, "w") as f:
f.write("gap_limit = 5") # The file has no sections at all
result = read_system_config(self.thefile)
self.assertEqual({}, result)
class TestUserConfig(unittest.TestCase):
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)
================================================
FILE: lib/tests/test_storage_upgrade.py
================================================
import shutil
import tempfile
from lib.storage import WalletStorage
from lib.wallet import Wallet
from lib.tests.test_wallet import WalletTestCase
# 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
================================================
FILE: lib/tests/test_transaction.py
================================================
import unittest
from lib import transaction
from lib.bitcoin import TYPE_ADDRESS
from lib.keystore import xpubkey_to_address
from lib.util import bh2u
unsigned_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000005701ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700"
signed_segwit_blob = "01000000000101b66d722484f2db63e827ebf41d02684fed0c6550e85015a6c9d41ef216a8a6f00000000000fdffffff0280c3c90100000000160014b65ce60857f7e7892b983851c2a8e3526d09e4ab64bac30400000000160014c478ebbc0ab2097706a98e10db7cf101839931c4024730440220789c7d47f876638c58d98733c30ae9821c8fa82b470285dcdf6db5994210bf9f02204163418bbc44af701212ad42d884cc613f3d3d831d2d0cc886f767cca6e0235e012103083a6dc250816d771faa60737bfe78b23ad619f6b458e0a1f1688e3a0605e79c00000000"
class TestBCDataStream(unittest.TestCase):
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(unittest.TestCase):
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}],
'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)
self.assertEqual(tx.raw, signed_blob)
tx.update(unsigned_blob)
tx.raw = None
blob = str(tx)
self.assertEqual(transaction.deserialize(blob), expected)
def test_tx_signed(self):
expected = {
'inputs': [{
'type': 'p2pkh',
'address': '1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD',
'num_sig': 1,
'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a',
'prevout_n': 0,
'pubkeys': ['02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6'],
'scriptSig': '493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6',
'sequence': 4294967295,
'signatures': ['3046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501'],
'x_pubkeys': ['02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6']}],
'lockTime': 0,
'outputs': [{
'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs',
'prevout_n': 0,
'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac',
'type': TYPE_ADDRESS,
'value': 1000000}],
'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)
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_txid_coinbase_to_p2pk(self):
tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4103400d0302ef02062f503253482f522cfabe6d6dd90d39663d10f8fd25ec88338295d4c6ce1c90d4aeb368d8bdbadcc1da3b635801000000000000000474073e03ffffffff013c25cf2d01000000434104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac00000000')
self.assertEqual('dbaf14e1c476e76ea05a8b71921a46d6b06f0a950f17c5f9f1a03b8fae467f10', tx.txid())
def test_txid_coinbase_to_p2pkh(self):
tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff25033ca0030400001256124d696e656420627920425443204775696c640800000d41000007daffffffff01c00d1298000000001976a91427a1f12771de5cc3b73941664b2537c15316be4388ac00000000')
self.assertEqual('4328f9311c6defd9ae1bd7f4516b62acf64b361eb39dfcf09d9925c5fd5c61e8', tx.txid())
def test_txid_segwit_coinbase_to_p2pk(self):
tx = transaction.Transaction('020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502cd010101ffffffff0240be402500000000232103f4e686cdfc96f375e7c338c40c9b85f4011bb843a3e62e46a1de424ef87e9385ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000')
self.assertEqual('fb5a57c24e640a6d8d831eb6e41505f3d54363c507da3733b098d820e3803301', tx.txid())
def test_txid_segwit_coinbase_to_p2pkh(self):
tx = transaction.Transaction('020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502c3010101ffffffff0240be4025000000001976a9141ea896d897483e0eb33dd6423f4a07970d0a0a2788ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000')
self.assertEqual('ed3d100577477d799107eba97e76770b3efa253c7200e9abfb43da5d2b33513e', tx.txid())
def test_txid_p2pk_to_p2pkh(self):
tx = transaction.Transaction('010000000118231a31d2df84f884ced6af11dc24306319577d4d7c340124a7e2dd9c314077000000004847304402200b6c45891aed48937241907bc3e3868ee4c792819821fcde33311e5a3da4789a02205021b59692b652a01f5f009bd481acac2f647a7d9c076d71d85869763337882e01fdffffff016c95052a010000001976a9149c4891e7791da9e622532c97f43863768264faaf88ac00000000')
self.assertEqual('90ba90a5b115106d26663fce6c6215b8699c5d4b2672dd30756115f3337dddf9', tx.txid())
def test_txid_p2pk_to_p2sh(self):
tx = transaction.Transaction('0100000001e4643183d6497823576d17ac2439fb97eba24be8137f312e10fcc16483bb2d070000000048473044022032bbf0394dfe3b004075e3cbb3ea7071b9184547e27f8f73f967c4b3f6a21fa4022073edd5ae8b7b638f25872a7a308bb53a848baa9b9cc70af45fcf3c683d36a55301fdffffff011821814a0000000017a9143c640bc28a346749c09615b50211cb051faff00f8700000000')
self.assertEqual('172bdf5a690b874385b98d7ab6f6af807356f03a26033c6a65ab79b4ac2085b5', tx.txid())
def test_txid_p2pk_to_p2wpkh(self):
tx = transaction.Transaction('01000000015e5e2bf15f5793fdfd01e0ccd380033797ed2d4dba9498426ca84904176c26610000000049483045022100c77aff69f7ab4bb148f9bccffc5a87ee893c4f7f7f96c97ba98d2887a0f632b9022046367bdb683d58fa5b2e43cfc8a9c6d57724a27e03583942d8e7b9afbfeea5ab01fdffffff017289824a00000000160014460fc70f208bffa9abf3ae4abbd2f629d9cdcf5900000000')
self.assertEqual('ca554b1014952f900aa8cf6e7ab02137a6fdcf933ad6a218de3891a2ef0c350d', tx.txid())
def test_txid_p2pkh_to_p2pkh(self):
tx = transaction.Transaction('0100000001f9dd7d33f315617530dd72264b5d9c69b815626cce3f66266d1015b1a590ba90000000006a4730440220699bfee3d280a499daf4af5593e8750b54fef0557f3c9f717bfa909493a84f60022057718eec7985b7796bb8630bf6ea2e9bf2892ac21bd6ab8f741a008537139ffe012103b4289890b40590447b57f773b5843bf0400e9cead08be225fac587b3c2a8e973fdffffff01ec24052a010000001976a914ce9ff3d15ed5f3a3d94b583b12796d063879b11588ac00000000')
self.assertEqual('24737c68f53d4b519939119ed83b2a8d44d716d7f3ca98bcecc0fbb92c2085ce', tx.txid())
def test_txid_p2pkh_to_p2sh(self):
tx = transaction.Transaction('010000000195232c30f6611b9f2f82ec63f5b443b132219c425e1824584411f3d16a7a54bc000000006b4830450221009f39ac457dc8ff316e5cc03161c9eff6212d8694ccb88d801dbb32e85d8ed100022074230bb05e99b85a6a50d2b71e7bf04d80be3f1d014ea038f93943abd79421d101210317be0f7e5478e087453b9b5111bdad586038720f16ac9658fd16217ffd7e5785fdffffff0200e40b540200000017a914d81df3751b9e7dca920678cc19cac8d7ec9010b08718dfd63c2c0000001976a914303c42b63569ff5b390a2016ff44651cd84c7c8988acc7010000')
self.assertEqual('155e4740fa59f374abb4e133b87247dccc3afc233cb97c2bf2b46bba3094aedc', tx.txid())
def test_txid_p2pkh_to_p2wpkh(self):
tx = transaction.Transaction('0100000001ce85202cb9fbc0ecbc98caf3d716d7448d2a3bd89e113999514b3df5687c7324000000006b483045022100adab7b6cb1179079c9dfc0021f4db0346730b7c16555fcc4363059dcdd95f653022028bcb816f4fb98615fb8f4b18af3ad3708e2d72f94a6466cc2736055860422cf012102a16a25148dd692462a691796db0a4a5531bcca970a04107bf184a2c9f7fd8b12fdffffff012eb6042a010000001600147d0170de18eecbe84648979d52b666dddee0b47400000000')
self.assertEqual('ed29e100499e2a3a64a2b0cb3a68655b9acd690d29690fa541be530462bf3d3c', tx.txid())
def test_txid_p2sh_to_p2pkh(self):
tx = transaction.Transaction('01000000000101f9823f87af35d158e7dc81a67011f4e511e3f6cab07ac108e524b0ff8b950b39000000002322002041f0237866eb72e4a75cd6faf5ccd738703193907d883aa7b3a8169c636706a9fdffffff020065cd1d000000001976a9148150cd6cf729e7e262699875fec1f760b0aab3cc88acc46f9a3b0000000017a91433ccd0f95a7b9d8eef68be40bb59c64d6e14d87287040047304402205ca97126a5956c2deaa956a2006d79a348775d727074a04b71d9c18eb5e5525402207b9353497af15881100a2786adab56c8930c02d46cc1a8b55496c06e22d3459b01483045022100b4fa898057927c2d920ae79bca752dda58202ea8617d3e6ed96cbd5d1c0eb2fc02200824c0e742d1b4d643cec439444f5d8779c18d4f42c2c87cce24044a3babf2df0147522102db78786b3c214826bd27010e3c663b02d67144499611ee3f2461c633eb8f1247210377082028c124098b59a5a1e0ea7fd3ebca72d59c793aecfeedd004304bac15cd52aec9010000')
self.assertEqual('17e1d498ba82503e3bfa81ac4897a57e33f3d36b41bcf4765ba604466c478986', tx.txid())
def test_txid_p2sh_to_p2sh(self):
tx = transaction.Transaction('01000000000101b58520acb479ab656a3c03263af0567380aff6b67a8db98543870b695adf2b170000000017160014cfd2b9f7ed9d4d4429ed6946dbb3315f75e85f14fdffffff020065cd1d0000000017a91485f5681bec38f9f07ae9790d7f27c2bb90b5b63c87106ab32c0000000017a914ff402e164dfce874435641ae9ac41fc6fb14c4e18702483045022100b3d1c89c7c92151ed1df78815924569446782776b6a2c170ca5d74c5dd1ad9b102201d7bab1974fd2aa66546dd15c1f1e276d787453cec31b55a2bd97b050abf20140121024a1742ece86df3dbce4717c228cf51e625030cef7f5e6dde33a4fffdd17569eac7010000')
self.assertEqual('ead0e7abfb24ddbcd6b89d704d7a6091e43804a458baa930adf6f1cb5b6b42f7', tx.txid())
def test_txid_p2sh_to_p2wpkh(self):
tx = transaction.Transaction('010000000001018689476c4604a65b76f4bc416bd3f3337ea59748ac81fa3b3e5082ba98d4e1170100000023220020ae40340707f9726c0f453c3d47c96e7f3b7b4b85608eb3668b69bbef9c7ab374fdffffff0218b2cc1d0000000017a914f2fdd81e606ff2ab804d7bb46bf8838a711c277b870065cd1d0000000016001496ad8959c1f0382984ecc4da61c118b4c8751e5104004730440220387b9e7d402fbcada9ba55a27a8d0563eafa9904ebd2f8f7e3d86e4b45bc0ec202205f37fa0e2bf8cbd384f804562651d7c6f69adce5db4c1a5b9103250a47f73e6b01473044022074903f4dd4fd6b32289be909eb5109924740daa55e79be6dbd728687683f9afa02205d934d981ca12cbec450611ca81dc4127f8da5e07dd63d41049380502de3f15401475221025c3810b37147105106cef970f9b91d3735819dee4882d515c1187dbd0b8f0c792103e007c492323084f1c103beff255836408af89bb9ae7f2fcf60502c28ff4b0c9152aeca010000')
self.assertEqual('6f294c84cbd0241650931b4c1be3dfb2f175d682c7a9538b30b173e1083deed3', tx.txid())
def test_txid_p2wpkh_to_p2pkh(self):
tx = transaction.Transaction('0100000000010197e6bf4a70bc118e3a8d9842ed80422e335679dfc29b5ba0f9123f6a5863b8470000000000fdffffff02402bca7f130000001600146f579c953d9e7e7719f2baa20bde22eb5f24119200e87648170000001976a9140cd8fa5fd81c3acf33f93efd179b388de8dd693388ac0247304402204ff33b3ea8fb270f62409bfc257457ca5eb1fec5e4d3a7c11aa487207e131d4d022032726b998e338e5245746716e5cd0b40d32b69d1535c3d841f049d98a5d819b1012102dc3ce3220363aff579eb2c45c973e8b186a829c987c3caea77c61975666e7d1bc8010000')
self.assertEqual('c721ed35767a3a209b688e68e3bb136a72d2b631fe81c56be8bdbb948c343dbc', tx.txid())
def test_txid_p2wpkh_to_p2sh(self):
tx = transaction.Transaction('010000000001013c3dbf620453be41a50f69290d69cd9a5b65683acbb0a2643a2a9e4900e129ed0000000000fdffffff02002f68590000000017a914c7c4dcd0ddf70f15c6df13b4a4d56e9f13c49b2787a0429cd000000000160014e514e3ecf89731e7853e4f3a20983484c569d3910247304402205368cc548209303db5a8f2ebc282bd0f7af0d080ce0f7637758587f94d3971fb0220098cec5752554758bc5fa4de332b980d5e0054a807541581dc5e4de3ed29647501210233717cd73d95acfdf6bd72c4fb5df27cd6bd69ce947daa3f4a442183a97877efc8010000')
self.assertEqual('390b958bffb024e508c17ab0caf6e311e5f41170a681dce758d135af873f82f9', tx.txid())
def test_txid_p2wpkh_to_p2wpkh(self):
tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000')
self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid())
class NetworkMock(object):
def __init__(self, unspent):
self.unspent = unspent
def synchronous_get(self, arg):
return self.unspent
================================================
FILE: lib/tests/test_util.py
================================================
import unittest
from lib.util import format_satoshis, parse_URI
class TestUtil(unittest.TestCase):
def test_format_satoshis(self):
result = format_satoshis(1234)
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')
================================================
FILE: lib/tests/test_wallet.py
================================================
import shutil
import tempfile
import sys
import unittest
import os
import json
from io import StringIO
from lib.storage import WalletStorage, FINAL_SEED_VERSION
class FakeSynchronizer(object):
def __init__(self):
self.store = []
def add(self, address):
self.store.append(address)
class WalletTestCase(unittest.TestCase):
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))
================================================
FILE: lib/tests/test_wallet_vertical.py
================================================
import unittest
from unittest import mock
import lib.bitcoin as bitcoin
import lib.keystore as keystore
import lib.storage as storage
import lib.wallet as wallet
from plugins.trustedcoin import trustedcoin
# TODO passphrase/seed_extension
class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
gap_limit = 1 # make tests run faster
def _check_seeded_keystore_sanity(self, ks):
self.assertTrue (ks.is_deterministic())
self.assertFalse(ks.is_watching_only())
self.assertFalse(ks.can_import())
self.assertTrue (ks.has_seed())
def _check_xpub_keystore_sanity(self, ks):
self.assertTrue (ks.is_deterministic())
self.assertTrue (ks.is_watching_only())
self.assertFalse(ks.can_import())
self.assertFalse(ks.has_seed())
def _create_standard_wallet(self, ks):
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893')
store.put('keystore', ks.dump())
store.put('gap_limit', self.gap_limit)
w = wallet.Standard_Wallet(store)
w.synchronize()
return w
def _create_multisig_wallet(self, ks1, ks2, ks3=None):
"""Creates a 2-of-2 or 2-of-3 multisig wallet."""
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893')
store.put('x%d/' % 1, ks1.dump())
store.put('x%d/' % 2, ks2.dump())
if ks3 is None:
multisig_type = "%dof%d" % (2, 2)
else:
multisig_type = "%dof%d" % (2, 3)
store.put('x%d/' % 3, ks3.dump())
store.put('wallet_type', multisig_type)
store.put('gap_limit', self.gap_limit)
w = wallet.Multisig_Wallet(store)
w.synchronize()
return w
@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)
self._check_seeded_keystore_sanity(ks)
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K32jECVM729vWgGq4mUDJCk1ozqAStTphzQtCTuoFmFafNoG1g55iCnBTXUzz3zWnDb5CVLGiFvmaZjuazHDL8a81cPQ8KL6')
self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U')
w = self._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')
@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)
self._check_seeded_keystore_sanity(ks)
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
self.assertEqual(ks.xprv, 'zprvAZswDvNeJeha8qZ8g7efN3FXYVJLaEUsE9TW6qXDEbVe74AZ75c2sZFZXPNFzxnhChDQ89oC8C5AjWwHmH1HeRKE1c4kKBQAmjUDdKDUZw2')
self.assertEqual(ks.xpub, 'zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ')
w = self._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')
@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)
self._check_seeded_keystore_sanity(ks)
self.assertTrue(isinstance(ks, keystore.Old_KeyStore))
self.assertEqual(ks.mpk, 'e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3')
w = self._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')
@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.signing_xpub, long_user_id)
ks3 = keystore.from_xpub(xpub3)
self._check_xpub_keystore_sanity(ks3)
self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))
w = self._create_multisig_wallet(ks1, ks2, ks3)
self.assertEqual(w.txin_type, 'p2sh')
self.assertEqual(w.get_receiving_addresses()[0], '35L8XmCDoEBKeaWRjvmZvoZvhp8BXMMMPV')
self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy')
@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 = self._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')
@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 = self._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')
@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 = self._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')
@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)
self._check_seeded_keystore_sanity(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')
self._check_xpub_keystore_sanity(ks2)
self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))
w = self._create_multisig_wallet(ks1, ks2)
self.assertEqual(w.txin_type, 'p2sh')
self.assertEqual(w.get_receiving_addresses()[0], '32ji3QkAgXNz6oFoRfakyD3ys1XXiERQYN')
self.assertEqual(w.get_change_addresses()[0], '36XWwEHrrVCLnhjK5MrVVGmUHghr9oWTN1')
@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)
self._check_seeded_keystore_sanity(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')
self._check_xpub_keystore_sanity(ks2)
self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))
w = self._create_multisig_wallet(ks1, ks2)
self.assertEqual(w.txin_type, 'p2wsh')
self.assertEqual(w.get_receiving_addresses()[0], 'bc1qvzezdcv6vs5h45ugkavp896e0nde5c5lg5h0fwe2xyfhnpkxq6gq7pnwlc')
self.assertEqual(w.get_change_addresses()[0], 'bc1qxqf840dqswcmu7a8v82fj6ej0msx08flvuy6kngr7axstjcaq6us9hrehd')
@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')
self._check_xpub_keystore_sanity(ks2)
self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))
w = self._create_multisig_wallet(ks1, ks2)
self.assertEqual(w.txin_type, 'p2sh')
self.assertEqual(w.get_receiving_addresses()[0], '3JPTQ2nitVxXBJ1yhMeDwH6q417UifE3bN')
self.assertEqual(w.get_change_addresses()[0], '3FGyDuxgUDn2pSZe5xAJH1yUwSdhzDMyEE')
@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')
self._check_xpub_keystore_sanity(ks2)
self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))
w = self._create_multisig_wallet(ks1, ks2)
self.assertEqual(w.txin_type, 'p2wsh-p2sh')
self.assertEqual(w.get_receiving_addresses()[0], '35LeC45QgCVeRor1tJD6LiDgPbybBXisns')
self.assertEqual(w.get_change_addresses()[0], '39RhtDchc6igmx5tyoimhojFL1ZbQBrXa6')
================================================
FILE: lib/transaction.py
================================================
#!/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 .util import print_error, profiler
from . import bitcoin
from .bitcoin import *
import struct
#
# Workalike python implementation of Bitcoin's CDataStream class.
#
from .keystore import xpubkey_to_address, xpubkey_to_pubkey
NO_SIGNATURE = 'ff'
class SerializationError(Exception):
""" Thrown when there's a problem deserializing or serializing """
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")
return ''
def read_boolean(self): return self.read_bytes(1)[0] != chr(0)
def read_int16(self): return self._read_num('= opcodes.OP_SINGLEBYTE_END:
opcode <<= 8
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(' 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("cannot find address in input script", bh2u(_bytes))
return
match = [ opcodes.OP_PUSHDATA4 ]
if match_decoded(decoded, match):
item = decoded[0][1]
if item[0] == 0:
d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item))
d['type'] = 'p2wpkh-p2sh' if len(item) == 22 else 'p2wsh-p2sh'
else:
# payto_pubkey
d['type'] = 'p2pk'
d['address'] = "(pubkey)"
d['signatures'] = [bh2u(item)]
d['num_sig'] = 1
d['x_pubkeys'] = ["(pubkey)"]
d['pubkeys'] = ["(pubkey)"]
return
# non-generated TxIn transactions push a signature
# (seventy-something bytes) and then their public key
# (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("cannot find address in input script", 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 not match_decoded(decoded, match):
print_error("cannot find address in input script", bh2u(_bytes))
return
x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1])
# 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['redeemScript'] = redeemScript
d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript)))
def parse_redeemScript(s):
dec2 = [ x for x in script_GetOp(s) ]
m = dec2[0][0] - opcodes.OP_1 + 1
n = dec2[-2][0] - opcodes.OP_1 + 1
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):
print_error("cannot find address in input script", bh2u(s))
return
x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
redeemScript = multisig_script(pubkeys, m)
return m, n, x_pubkeys, pubkeys, redeemScript
def get_address_from_output_script(_bytes):
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])
# 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])
# segwit address
match = [ opcodes.OP_0, opcodes.OP_PUSHDATA4 ]
if match_decoded(decoded, match):
return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1])
return TYPE_SCRIPT, bh2u(_bytes)
def parse_input(vds):
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['sequence'] = sequence
if prevout_hash == '00'*32:
d['type'] = 'coinbase'
d['scriptSig'] = bh2u(scriptSig)
else:
d['x_pubkeys'] = []
d['pubkeys'] = []
d['signatures'] = {}
d['address'] = None
d['type'] = 'unknown'
d['num_sig'] = 0
if scriptSig:
d['scriptSig'] = bh2u(scriptSig)
parse_scriptSig(d, scriptSig)
else:
d['scriptSig'] = ''
return d
def parse_witness(vds, txin):
n = vds.read_compact_size()
if n == 0:
return
if n == 0xffffffff:
txin['value'] = vds.read_uint64()
n = vds.read_compact_size()
w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n))
if txin['type'] == 'coinbase':
pass
elif n > 2:
txin['signatures'] = parse_sig(w[1:-1])
m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1]))
txin['num_sig'] = m
txin['x_pubkeys'] = x_pubkeys
txin['pubkeys'] = pubkeys
txin['witnessScript'] = witnessScript
else:
txin['num_sig'] = 1
txin['x_pubkeys'] = [w[1]]
txin['pubkeys'] = [safe_parse_pubkey(w[1])]
txin['signatures'] = parse_sig([w[0]])
def parse_output(vds, i):
d = {}
d['value'] = vds.read_int64()
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):
vds = BCDataStream()
vds.write(bfh(raw))
d = {}
start = vds.read_cursor
d['version'] = vds.read_int32()
n_vin = vds.read_compact_size()
d['inputs'] = [parse_input(vds) for i in range(n_vin)]
n_vout = vds.read_compact_size()
d['outputs'] = [parse_output(vds, i) for i in range(n_vout)]
d['lockTime'] = vds.read_uint32()
return d
# pay & redeem scripts
def multisig_script(public_keys, m):
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 BaseException("cannot initialize transaction", raw)
self._inputs = None
self._outputs = None
self.locktime = 0
self.version = 1
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
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, raw):
"""Add new signatures to a transaction"""
d = deserialize(raw)
for i, txin in enumerate(self.inputs()):
pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
sigs1 = txin.get('signatures')
sigs2 = d['inputs'][i].get('signatures')
for sig in sigs2:
if sig in sigs1:
continue
pre_hash = Hash(bfh(self.serialize_preimage(i)))
# der to string
order = ecdsa.ecdsa.generator_secp256k1.order()
r, s = ecdsa.util.sigdecode_der(bfh(sig[:-2]), order)
sig_string = ecdsa.util.sigencode_string(r, s, order)
compressed = True
for recid in range(4):
public_key = MyVerifyingKey.from_signature(sig_string, recid, pre_hash, curve = SECP256k1)
pubkey = bh2u(point_to_ser(public_key.pubkey.point, compressed))
if pubkey in pubkeys:
public_key.verify_digest(sig_string, pre_hash, sigdecode = ecdsa.util.sigdecode_string)
j = pubkeys.index(pubkey)
print_error("adding sig", i, j, pubkey, sig)
self._inputs[i]['signatures'][j] = sig
#self._inputs[i]['x_pubkeys'][j] = pubkey
break
# redo raw
self.raw = self.serialize()
def deserialize(self):
if self.raw is None:
return
#self.raw = self.serialize()
if self._inputs is not None:
return
d = deserialize(self.raw)
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']
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)
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):
add_w = lambda x: var_int(len(x)//2) + x
if not self.is_segwit_input(txin):
return '00'
pubkeys, sig_list = self.get_siglist(txin, estimate_size)
if txin['type'] in ['p2wpkh', 'p2wpkh-p2sh']:
witness = var_int(2) + add_w(sig_list[0]) + add_w(pubkeys[0])
elif txin['type'] in ['p2wsh', 'p2wsh-p2sh']:
n = len(sig_list) + 2
witness_script = multisig_script(pubkeys, txin['num_sig'])
witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script)
else:
raise BaseException('wrong txin type')
if self.is_txin_complete(txin) or estimate_size:
value_field = ''
else:
value_field = var_int(0xffffffff) + int_to_hex(txin['value'], 8)
return value_field + witness
@classmethod
def is_segwit_input(cls, txin):
return cls.is_segwit_inputtype(txin['type'])
@classmethod
def is_segwit_inputtype(cls, txin_type):
return txin_type in ('p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh')
@classmethod
def input_script(self, txin, estimate_size=False):
_type = txin['type']
if _type == 'coinbase':
return txin['scriptSig']
pubkeys, sig_list = self.get_siglist(txin, estimate_size)
script = ''.join(push_script(x) for x in sig_list)
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':
witness_script = self.get_preimage_script(txin)
scriptSig = bitcoin.p2wsh_nested_script(witness_script)
return push_script(scriptSig)
elif _type == 'address':
script += push_script(pubkeys[0])
elif _type == 'unknown':
return txin['scriptSig']
return script
@classmethod
def is_txin_complete(self, txin):
num_sig = txin.get('num_sig', 1)
x_signatures = txin['signatures']
signatures = list(filter(None, x_signatures))
return len(signatures) == num_sig
@classmethod
def get_preimage_script(self, txin):
# only for non-segwit
if txin['type'] == 'p2pkh':
return bitcoin.address_to_script(txin['address'])
elif txin['type'] in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
return multisig_script(pubkeys, txin['num_sig'])
elif txin['type'] in ['p2wpkh', 'p2wpkh-p2sh']:
pubkey = txin['pubkeys'][0]
pkh = bh2u(bitcoin.hash_160(bfh(pubkey)))
return '76a9' + push_script(pkh) + '88ac'
elif txin['type'] == 'p2pk':
pubkey = txin['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 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 = 1 | 0x40
nHashType = nHashType | (42 << 8)
nHashType = int_to_hex(nHashType, 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):
return any(self.is_segwit_input(x) for x in self.inputs())
def serialize(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)
if witness and self.is_segwit():
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 hash(self):
print("warning: deprecated tx.hash()")
return self.txid()
def txid(self):
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(witness=False)
return bh2u(Hash(bfh(ser))[::-1])
def wtxid(self):
ser = self.serialize(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):
assert is_segwit_tx
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."""
if not self.is_segwit():
return 0
inputs = self.inputs()
estimate = not self.is_complete()
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):
s, r = self.signature_count()
return r == s
def sign(self, keypairs):
for i, txin in enumerate(self.inputs()):
num = txin['num_sig']
pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
for j, x_pubkey in enumerate(x_pubkeys):
signatures = list(filter(None, txin['signatures']))
if len(signatures) == num:
# txin is complete
break
if x_pubkey in keypairs.keys():
print_error("adding signature for", x_pubkey)
sec, compressed = keypairs.get(x_pubkey)
pubkey = public_key_from_private_key(sec, compressed)
# add signature
pre_hash = Hash(bfh(self.serialize_preimage(i)))
pkey = regenerate_key(sec)
secexp = pkey.secret
private_key = bitcoin.MySigningKey.from_secret_exponent(secexp, curve = SECP256k1)
public_key = private_key.get_verifying_key()
sig = private_key.sign_digest_deterministic(pre_hash, hashfunc=hashlib.sha256, sigencode = ecdsa.util.sigencode_der)
assert public_key.verify_digest(sig, pre_hash, sigdecode = ecdsa.util.sigdecode_der)
txin['signatures'][j] = bh2u(sig) + '41'
#txin['x_pubkeys'][j] = pubkey
txin['pubkeys'][j] = pubkey # needed for fd keys
self._inputs[i] = txin
print_error("is_complete", self.is_complete())
self.raw = self.serialize()
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:
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"]
================================================
FILE: lib/util.py
================================================
# 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
from decimal import Decimal
import traceback
import urllib
import threading
import hmac
import requests
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()}
is_bundle = getattr(sys, 'frozen', False)
is_macOS = sys.platform == 'darwin'
base_units = {'BTCP':8, 'mBTCP':5, 'uBTCP':2}
fee_levels = [_('Within 25 blocks'), _('Within 10 blocks'), _('Within 5 blocks'), _('Within 2 blocks'), _('In the next block')]
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")
# 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 MyEncoder(json.JSONEncoder):
def default(self, obj):
from .transaction import Transaction
if isinstance(obj, Transaction):
return obj.as_dict()
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):
print_error("[%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):
os.mkdir(d)
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_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-btcp")
elif "APPDATA" in os.environ:
return os.path.join(os.environ["APPDATA"], "Electrum-btcp")
elif "LOCALAPPDATA" in os.environ:
return os.path.join(os.environ["LOCALAPPDATA"], "Electrum-btcp")
else:
#raise Exception("No home directory found in environment variables.")
return
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, is_diff=False, num_zeros = 0, decimal_point = 8, whitespaces=False):
from locale import localeconv
if x is None:
return 'Unknown'
x = int(x) # Some callers pass Decimal
scale_factor = pow (10, decimal_point)
integer_part = "{:n}".format(int(abs(x) / scale_factor))
if x < 0:
integer_part = '-' + integer_part
elif is_diff:
integer_part = '+' + integer_part
dp = localeconv()['decimal_point']
fract_part = ("{:0" + str(decimal_point) + "}").format(abs(x) % scale_factor)
fract_part = fract_part.rstrip('0')
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
def timestamp_to_datetime(timestamp):
try:
return datetime.fromtimestamp(timestamp)
except:
return None
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))
# For raw json, append /insight-api-zcash
mainnet_block_explorers = {
'explorer.btcprivate.org': ('https://explorer.btcprivate.org',
{'tx': 'tx', 'addr': 'address'}),
'system default': ('blockchain:',
{'tx': 'tx', 'addr': 'address'})
}
testnet_block_explorers = {
#'testnet.btcprivate.org': ('https://testnet.btcprivate.org',
# {'tx': 'tx', 'addr': 'address'}),
'system default': ('blockchain:',
{'tx': 'tx', 'addr': 'address'})
}
def block_explorer_info():
from . import bitcoin
return testnet_block_explorers if bitcoin.NetworkConstants.TESTNET else mainnet_block_explorers
def block_explorer(config):
return config.get('block_explorer', 'explorer.btcprivate.org')
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 BaseException("Not a BTCP address")
return {'address': uri}
u = urllib.parse.urlparse(uri)
if u.scheme != 'bitcoin':
raise BaseException("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 BaseException("Invalid BTCP 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
except OSError as e:
print_error("OSError", 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 get_cert_path():
if is_bundle and is_macOS:
# set in ./electrum
return requests.utils.DEFAULT_CA_BUNDLE_PATH
return requests.certs.where()
================================================
FILE: lib/verifier.py
================================================
# 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
from .bitcoin import *
class SPV(ThreadJob):
""" Simple Payment Verification """
def __init__(self, network, wallet):
self.wallet = wallet
self.network = network
self.blockchain = network.blockchain()
# Keyed by tx hash. Value is None if the merkle branch was
# requested, and the merkle root once it has been verified
self.merkle_roots = {}
def run(self):
lh = 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) and (tx_height <= lh):
header = self.network.blockchain().read_header(tx_height)
if header is None and self.network.interface:
index = tx_height // NetworkConstants.CHUNK_SIZE
self.network.request_chunk(self.network.interface, index)
else:
if tx_hash not in self.merkle_roots:
request = ('blockchain.transaction.get_merkle',
[tx_hash, tx_height])
self.network.send([request], self.verify_merkle)
self.print_error('requested merkle', tx_hash)
self.merkle_roots[tx_hash] = None
if self.network.blockchain() != self.blockchain:
self.blockchain = self.network.blockchain()
self.undo_verifications()
def verify_merkle(self, r):
if r.get('error'):
self.print_error('received an error:', r)
return
params = r['params']
merkle = r['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')
merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos)
header = self.network.blockchain().read_header(tx_height)
if not header or header.get('merkle_root') != merkle_root:
# FIXME: we should make a fresh connection to a server to
# recover from this, as this TX will now never verify
self.print_error("merkle verification failed for", tx_hash)
return
# we passed all the tests
self.merkle_roots[tx_hash] = merkle_root
self.print_error("verified %s" % tx_hash)
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
def hash_merkle_root(self, 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))
return hash_encode(h)
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.merkle_roots.pop(tx_hash, None)
================================================
FILE: lib/version.py
================================================
# version of the client package
ELECTRUM_VERSION = 'P!1.0.0'
# protocol version requested
PROTOCOL_VERSION = '1.1'
# 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
================================================
FILE: lib/wallet.py
================================================
# 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
import sys
from .i18n import _
from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler,
format_satoshis, NoDynamicFeeEstimates)
from .bitcoin import *
from .version import *
from .keystore import load_keystore, Hardware_KeyStore
from .storage import multisig_type
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 = [
_('Replaceable'),
_('Unconfirmed parent'),
_('Low fee'),
_('Unconfirmed'),
_('Not Verified'),
]
def relayfee(network):
RELAY_FEE = 1000
MAX_RELAY_FEE = 50000
f = network.relay_fee if network and network.relay_fee else RELAY_FEE
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)
sh = bitcoin.address_to_scripthash(address)
else:
script = bitcoin.public_key_to_p2pk_script(pubkey)
sh = bitcoin.script_to_scripthash(script)
address = '(pubkey)'
u = network.synchronous_get(('blockchain.scripthash.listunspent', [sh]))
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'] = 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 = bitcoin.public_key_from_private_key(privkey, 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 BaseException(_('No inputs found. (Note that inputs need to be confirmed)'))
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 BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
if total - fee < dust_threshold(network):
raise BaseException(_('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 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
# 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.load_keystore()
self.load_addresses()
self.load_transactions()
self.build_reverse_history()
# load requests
self.receive_requests = self.storage.get('payment_requests', {})
# Transactions pending verification. A map from tx hash to transaction
# height. Access is not contended so no lock is needed.
self.unverified_tx = defaultdict(int)
# Verified transactions. Each value is a (height, timestamp, block_pos) tuple. Access with self.lock.
self.verified_tx = storage.get('verified_tx3', {})
# there is a difference between wallet.up_to_date and interface.is_up_to_date()
# interface.is_up_to_date() returns true when all requests have been answered and processed
# wallet.up_to_date is true when the wallet is synchronized (stronger requirement)
self.up_to_date = False
self.lock = threading.Lock()
self.transaction_lock = threading.Lock()
self.check_history()
# 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)
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):
self.txi = self.storage.get('txi', {})
self.txo = self.storage.get('txo', {})
self.tx_fees = self.storage.get('tx_fees', {})
self.pruned_txo = self.storage.get('pruned_txo', {})
tx_list = self.storage.get('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 and (tx_hash not in self.pruned_txo.values()):
self.print_error("removing unreferenced tx", tx_hash)
self.transactions.pop(tx_hash)
@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('pruned_txo', self.pruned_txo)
self.storage.put('addr_history', self.history)
if write:
self.storage.write()
def clear_history(self):
with self.transaction_lock:
self.txi = {}
self.txo = {}
self.tx_fees = {}
self.pruned_txo = {}
self.save_transactions()
with self.lock:
self.history = {}
self.tx_addr_hist = {}
@profiler
def build_reverse_history(self):
self.tx_addr_hist = {}
for addr, hist in self.history.items():
for tx_hash, h in hist:
s = self.tx_addr_hist.get(tx_hash, set())
s.add(addr)
self.tx_addr_hist[tx_hash] = s
@profiler
def check_history(self):
save = False
mine_addrs = list(filter(lambda k: self.is_mine(self.history[k]), self.history.keys()))
if len(mine_addrs) != len(self.history.keys()):
save = True
for addr in mine_addrs:
hist = self.history[addr]
for tx_hash, tx_height in hist:
if tx_hash in self.pruned_txo.values() or 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)
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 synchronize(self):
pass
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)
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 is_mine(self, address):
return address in self.get_addresses()
def is_change(self, address):
if not self.is_mine(address):
return False
return address in self.change_addresses
def get_address_index(self, address):
if address in self.receiving_addresses:
return False, self.receiving_addresses.index(address)
if address in self.change_addresses:
return True, self.change_addresses.index(address)
raise Exception("Address not found", address)
def export_private_key(self, address, password):
""" extended WIF format """
if self.is_watching_only():
return []
index = self.get_address_index(address)
pk, compressed = self.keystore.get_private_key(index, password)
if self.txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
pubkeys = self.get_public_keys(address)
redeem_script = self.pubkeys_to_redeem_script(pubkeys)
else:
redeem_script = None
return bitcoin.serialize_privkey(pk, compressed, self.txin_type), redeem_script
def get_public_keys(self, address):
sequence = self.get_address_index(address)
return self.get_pubkeys(*sequence)
def add_unverified_tx(self, tx_hash, tx_height):
if tx_height == 0 and tx_hash in self.verified_tx:
self.verified_tx.pop(tx_hash)
self.verifier.merkle_roots.pop(tx_hash, None)
# tx will be verified only if height > 0
if tx_hash not in self.verified_tx:
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 and
self.unverified_tx.pop(tx_hash, None)
with self.lock:
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'''
return self.unverified_tx
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):
""" return the height and timestamp of a verified transaction. """
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
else:
height = self.unverified_tx[tx_hash]
return height, 0, False
def get_txpos(self, tx_hash):
"return position, even if the tx is unverified"
with self.lock:
x = self.verified_tx.get(tx_hash)
y = self.unverified_tx.get(tx_hash)
if x:
height, timestamp, pos = x
return height, pos
elif y > 0:
return y, 0
else:
return 1e12 - y, 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"
# pruned
if tx_hash in self.pruned_txo.values():
return None
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_wallet_delta(self, tx):
""" effect of tx on wallet """
addresses = self.get_addresses()
is_relevant = False
is_mine = False
is_pruned = False
is_partial = False
v_in = v_out = v_out_mine = 0
for item in tx.inputs():
addr = item.get('address')
if addr in addresses:
is_mine = True
is_relevant = True
d = self.txo.get(item['prevout_hash'], {}).get(addr, [])
for n, v, cb in d:
if n == item['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 addr in addresses:
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 = _("%d confirmations") % conf
else:
status = _('Not Verified')
else:
status = _('Unconfirmed')
if fee is None:
fee = self.tx_fees.get(tx_hash)
if fee and self.network.config.has_fee_estimates():
size = tx.estimated_size()
fee_per_kb = fee * 1000 / size
exp_n = self.network.config.reverse_dynfee(fee_per_kb)
can_bump = is_mine and not tx.is_final()
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.history.get(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
for txo, (tx_height, v, is_cb) in received.items():
if is_cb and tx_height + COINBASE_MATURITY > self.get_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()
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()
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, address):
with self.lock:
return self.history.get(address, [])
def find_pay_to_pubkey_address(self, prevout_hash, 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:
self.print_error("found pay-to-pubkey address:", addr)
return addr
def add_transaction(self, tx_hash, tx):
is_shielded_input = len(tx.inputs()) == 0
is_coinbase = not is_shielded_input and tx.inputs()[0]['type'] == 'coinbase'
with self.transaction_lock:
# add inputs
self.txi[tx_hash] = d = {}
for txi in tx.inputs():
addr = txi.get('address')
if txi['type'] != 'coinbase':
prevout_hash = txi['prevout_hash']
prevout_n = txi['prevout_n']
ser = prevout_hash + ':%d'%prevout_n
if addr == "(pubkey)":
addr = self.find_pay_to_pubkey_address(prevout_hash, prevout_n)
# find value from prev output
if addr and self.is_mine(addr):
dd = self.txo.get(prevout_hash, {})
for n, v, is_cb in dd.get(addr, []):
if n == prevout_n:
if d.get(addr) is None:
d[addr] = []
d[addr].append((ser, v))
break
else:
self.pruned_txo[ser] = tx_hash
# add outputs
self.txo[tx_hash] = d = {}
for n, txo in enumerate(tx.outputs()):
ser = tx_hash + ':%d'%n
_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
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.pruned_txo.get(ser)
if next_tx is not None:
self.pruned_txo.pop(ser)
dd = self.txi.get(next_tx, {})
if dd.get(addr) is None:
dd[addr] = []
dd[addr].append((ser, v))
# save
self.transactions[tx_hash] = tx
def remove_transaction(self, tx_hash):
with self.transaction_lock:
self.print_error("removing tx from history", tx_hash)
#tx = self.transactions.pop(tx_hash)
for ser, hh in list(self.pruned_txo.items()):
if hh == tx_hash:
self.pruned_txo.pop(ser)
# add tx to pruned_txo, and undo the txi addition
for next_tx, dd in self.txi.items():
for addr, l in list(dd.items()):
ll = l[:]
for item in ll:
ser, v = item
prev_hash, prev_n = ser.split(':')
if prev_hash == tx_hash:
l.remove(item)
self.pruned_txo[ser] = next_tx
if l == []:
dd.pop(addr)
else:
dd[addr] = l
try:
self.txi.pop(tx_hash)
self.txo.pop(tx_hash)
except KeyError:
self.print_error("tx was not in history", tx_hash)
def receive_tx_callback(self, tx_hash, tx, tx_height):
self.add_transaction(tx_hash, tx)
self.add_unverified_tx(tx_hash, tx_height)
def receive_history_callback(self, addr, hist, tx_fees):
with self.lock:
old_hist = self.history.get(addr, [])
for tx_hash, height in old_hist:
if (tx_hash, height) not in hist:
# remove tx if it's not referenced in histories
self.tx_addr_hist[tx_hash].remove(addr)
if not self.tx_addr_hist[tx_hash]:
self.remove_transaction(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)
# add reference in tx_addr_hist
s = self.tx_addr_hist.get(tx_hash, set())
s.add(addr)
self.tx_addr_hist[tx_hash] = s
# if addr is new, we have to recompute txi and txo
tx = self.transactions.get(tx_hash)
if tx is not None and self.txi.get(tx_hash, {}).get(addr) is None and self.txo.get(tx_hash, {}).get(addr) is None:
self.add_transaction(tx_hash, tx)
# Store fees
self.tx_fees.update(tx_fees)
def get_history(self, domain=None):
# get domain
if domain is None:
domain = self.get_addresses()
# 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 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
if conf == 0:
tx = self.transactions.get(tx_hash)
if not tx:
return 3, 'Unknown'
is_final = tx and tx.is_final()
fee = self.tx_fees.get(tx_hash)
if fee and self.network and self.network.config.has_fee_estimates():
size = len(tx.raw)/2
low_fee = int(self.network.config.dynfee(0)*size/1000)
is_lowfee = fee < low_fee * 0.5
else:
is_lowfee = False
if height==0 and not is_final:
status = 0
elif height < 0:
status = 1
elif height == 0 and is_lowfee:
status = 2
elif height == 0:
status = 3
else:
status = 4
else:
status = 4 + min(conf, 6)
time_str = format_time(timestamp) if timestamp else _("Unknown")
status_str = TX_STATUS[status] if status < 5 else time_str
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 BaseException("Invalid bitcoin address:" + data)
if value == '!':
if i_max is not None:
raise BaseException("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:
change_addrs = [inputs[0]['address']]
# 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 BaseException('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:
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 = max(0, sendable - tx.output_value() - fee)
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 prepare_for_verifier(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)
# if we are on a pruning server, remove unverified transactions
with self.lock:
vr = list(self.verified_tx.keys()) + list(self.unverified_tx.keys())
for tx_hash in list(self.transactions):
if tx_hash not in vr:
self.print_error("removing transaction", tx_hash)
self.transactions.pop(tx_hash)
def start_threads(self, network):
self.network = network
if self.network is not None:
self.prepare_for_verifier()
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 syncronizer or verifier
# remain so they will be GC-ed
self.storage.put('stored_height', self.get_local_height())
self.save_transactions()
self.storage.put('verified_tx3', self.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,[])
c, u, x = self.get_addr_balance(address)
return len(h) > 0 and 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 BaseException(_("Cannot bump fee: transaction is final"))
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 BaseException(_('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_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_segwit_input(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 can_sign(self, tx):
if tx.is_complete():
return False
for k in self.get_keystores():
if k.can_sign(tx):
return True
return False
def get_input_tx(self, tx_hash):
# 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)
if not tx and self.network:
request = ('blockchain.transaction.get', [tx_hash])
tx = Transaction(self.network.synchronous_get(request))
return tx
def add_hw_info(self, tx):
# add previous tx for hw wallets
for txin in tx.inputs():
tx_hash = txin['prevout_hash']
txin['prev_tx'] = self.get_input_tx(tx_hash)
# 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_change(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
# 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
for k in self.get_keystores():
try:
if k.can_sign(tx):
k.sign_transaction(tx, password)
except UserCancelled:
continue
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:
baseurl = baseurl.replace(*rewrite)
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']
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') 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(x):
try:
addr = x.get('address')
return self.get_address_index(addr) or addr
except:
return addr
return sorted(map(lambda x: self.get_payment_request(x, config), self.receive_requests.keys()), key=f)
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.storage.get('use_encryption', False)
def check_password(self, password):
self.keystore.check_password(password)
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)
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 can_change_password(self):
return self.keystore.can_change_password()
def update_password(self, old_pw, new_pw, encrypt=False):
if old_pw is None and self.has_password():
raise InvalidPassword()
self.keystore.update_password(old_pw, new_pw)
self.save_keystore()
self.storage.set_password(new_pw, encrypt)
self.storage.write()
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_change_password(self):
return not self.is_watching_only()
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, is_change):
return False
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)
# FIXME: what about pruned_txo?
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:
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:
raise BaseException('Invalid private key', sec)
if txin_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
if redeem_script is not None:
raise BaseException('Cannot use redeem script with', txin_type, sec)
addr = bitcoin.pubkey_to_address(txin_type, pubkey)
elif txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
if redeem_script is None:
raise BaseException('Redeem script required for', txin_type, sec)
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 export_private_key(self, address, password):
d = self.addresses[address]
pubkey = d['pubkey']
redeem_script = d['redeem_script']
sec = pw_decode(self.keystore.keypairs[pubkey], password)
return sec, 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:
redeem_script = self.addresses[address]['redeem_script']
num_sig = 2
num_keys = 3
txin['num_sig'] = num_sig
txin['redeem_script'] = redeem_script
txin['signatures'] = [None] * num_keys
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 is_deterministic(self):
return self.keystore.is_deterministic()
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 create_new_address(self, for_change=False):
assert type(for_change) is bool
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.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:
if self.is_deterministic():
self.synchronize_sequence(False)
self.synchronize_sequence(True)
else:
if len(self.receiving_addresses) != len(self.keystore.keypairs):
pubkeys = self.keystore.keypairs.keys()
self.receiving_addresses = [self.pubkeys_to_address(i) for i in pubkeys]
self.save_addresses()
for addr in self.receiving_addresses:
self.add_address(addr)
def is_beyond_limit(self, address, is_change):
addr_list = self.get_change_addresses() if is_change else self.get_receiving_addresses()
i = addr_list.index(address)
prev_addresses = addr_list[:max(0, i)]
limit = self.gap_limit_for_change if is_change else self.gap_limit
if len(prev_addresses) < limit:
return False
prev_addresses = prev_addresses[max(0, i - limit):]
for addr in prev_addresses:
if self.history.get(addr):
return False
return True
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 get_public_keys(self, address):
return [self.get_public_key(address)]
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 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 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 update_password(self, old_pw, new_pw, encrypt=False):
if old_pw is None and self.has_password():
raise InvalidPassword()
for name, keystore in self.keystores.items():
if keystore.can_change_password():
keystore.update_password(old_pw, new_pw)
self.storage.put(name, keystore.dump())
self.storage.set_password(new_pw, encrypt)
self.storage.write()
def has_seed(self):
return self.keystore.has_seed()
def can_change_password(self):
return self.keystore.can_change_password()
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)
txin['x_pubkeys'] = [k.get_xpubkey(*derivation) for k in self.get_keystores()]
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: " + wallet_type)
================================================
FILE: lib/websockets.py
================================================
#!/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
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) 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.send([('blockchain.address.subscribe', [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')
params = r.get('params')
result = r.get('result')
if result is None:
continue
if method == 'blockchain.address.subscribe':
self.network.send([('blockchain.address.get_balance', params)], self.response_queue.put)
elif method == 'blockchain.address.get_balance':
addr = params[0]
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()
================================================
FILE: lib/wordlist/chinese_simplified.txt
================================================
的
一
是
在
不
了
有
和
人
这
中
大
为
上
个
国
我
以
要
他
时
来
用
们
生
到
作
地
于
出
就
分
对
成
会
可
主
发
年
动
同
工
也
能
下
过
子
说
产
种
面
而
方
后
多
定
行
学
法
所
民
得
经
十
三
之
进
着
等
部
度
家
电
力
里
如
水
化
高
自
二
理
起
小
物
现
实
加
量
都
两
体
制
机
当
使
点
从
业
本
去
把
性
好
应
开
它
合
还
因
由
其
些
然
前
外
天
政
四
日
那
社
义
事
平
形
相
全
表
间
样
与
关
各
重
新
线
内
数
正
心
反
你
明
看
原
又
么
利
比
或
但
质
气
第
向
道
命
此
变
条
只
没
结
解
问
意
建
月
公
无
系
军
很
情
者
最
立
代
想
已
通
并
提
直
题
党
程
展
五
果
料
象
员
革
位
入
常
文
总
次
品
式
活
设
及
管
特
件
长
求
老
头
基
资
边
流
路
级
少
图
山
统
接
知
较
将
组
见
计
别
她
手
角
期
根
论
运
农
指
几
九
区
强
放
决
西
被
干
做
必
战
先
回
则
任
取
据
处
队
南
给
色
光
门
即
保
治
北
造
百
规
热
领
七
海
口
东
导
器
压
志
世
金
增
争
济
阶
油
思
术
极
交
受
联
什
认
六
共
权
收
证
改
清
美
再
采
转
更
单
风
切
打
白
教
速
花
带
安
场
身
车
例
真
务
具
万
每
目
至
达
走
积
示
议
声
报
斗
完
类
八
离
华
名
确
才
科
张
信
马
节
话
米
整
空
元
况
今
集
温
传
土
许
步
群
广
石
记
需
段
研
界
拉
林
律
叫
且
究
观
越
织
装
影
算
低
持
音
众
书
布
复
容
儿
须
际
商
非
验
连
断
深
难
近
矿
千
周
委
素
技
备
半
办
青
省
列
习
响
约
支
般
史
感
劳
便
团
往
酸
历
市
克
何
除
消
构
府
称
太
准
精
值
号
率
族
维
划
选
标
写
存
候
毛
亲
快
效
斯
院
查
江
型
眼
王
按
格
养
易
置
派
层
片
始
却
专
状
育
厂
京
识
适
属
圆
包
火
住
调
满
县
局
照
参
红
细
引
听
该
铁
价
严
首
底
液
官
德
随
病
苏
失
尔
死
讲
配
女
黄
推
显
谈
罪
神
艺
呢
席
含
企
望
密
批
营
项
防
举
球
英
氧
势
告
李
台
落
木
帮
轮
破
亚
师
围
注
远
字
材
排
供
河
态
封
另
施
减
树
溶
怎
止
案
言
士
均
武
固
叶
鱼
波
视
仅
费
紧
爱
左
章
早
朝
害
续
轻
服
试
食
充
兵
源
判
护
司
足
某
练
差
致
板
田
降
黑
犯
负
击
范
继
兴
似
余
坚
曲
输
修
故
城
夫
够
送
笔
船
占
右
财
吃
富
春
职
觉
汉
画
功
巴
跟
虽
杂
飞
检
吸
助
升
阳
互
初
创
抗
考
投
坏
策
古
径
换
未
跑
留
钢
曾
端
责
站
简
述
钱
副
尽
帝
射
草
冲
承
独
令
限
阿
宣
环
双
请
超
微
让
控
州
良
轴
找
否
纪
益
依
优
顶
础
载
倒
房
突
坐
粉
敌
略
客
袁
冷
胜
绝
析
块
剂
测
丝
协
诉
念
陈
仍
罗
盐
友
洋
错
苦
夜
刑
移
频
逐
靠
混
母
短
皮
终
聚
汽
村
云
哪
既
距
卫
停
烈
央
察
烧
迅
境
若
印
洲
刻
括
激
孔
搞
甚
室
待
核
校
散
侵
吧
甲
游
久
菜
味
旧
模
湖
货
损
预
阻
毫
普
稳
乙
妈
植
息
扩
银
语
挥
酒
守
拿
序
纸
医
缺
雨
吗
针
刘
啊
急
唱
误
训
愿
审
附
获
茶
鲜
粮
斤
孩
脱
硫
肥
善
龙
演
父
渐
血
欢
械
掌
歌
沙
刚
攻
谓
盾
讨
晚
粒
乱
燃
矛
乎
杀
药
宁
鲁
贵
钟
煤
读
班
伯
香
介
迫
句
丰
培
握
兰
担
弦
蛋
沉
假
穿
执
答
乐
谁
顺
烟
缩
征
脸
喜
松
脚
困
异
免
背
星
福
买
染
井
概
慢
怕
磁
倍
祖
皇
促
静
补
评
翻
肉
践
尼
衣
宽
扬
棉
希
伤
操
垂
秋
宜
氢
套
督
振
架
亮
末
宪
庆
编
牛
触
映
雷
销
诗
座
居
抓
裂
胞
呼
娘
景
威
绿
晶
厚
盟
衡
鸡
孙
延
危
胶
屋
乡
临
陆
顾
掉
呀
灯
岁
措
束
耐
剧
玉
赵
跳
哥
季
课
凯
胡
额
款
绍
卷
齐
伟
蒸
殖
永
宗
苗
川
炉
岩
弱
零
杨
奏
沿
露
杆
探
滑
镇
饭
浓
航
怀
赶
库
夺
伊
灵
税
途
灭
赛
归
召
鼓
播
盘
裁
险
康
唯
录
菌
纯
借
糖
盖
横
符
私
努
堂
域
枪
润
幅
哈
竟
熟
虫
泽
脑
壤
碳
欧
遍
侧
寨
敢
彻
虑
斜
薄
庭
纳
弹
饲
伸
折
麦
湿
暗
荷
瓦
塞
床
筑
恶
户
访
塔
奇
透
梁
刀
旋
迹
卡
氯
遇
份
毒
泥
退
洗
摆
灰
彩
卖
耗
夏
择
忙
铜
献
硬
予
繁
圈
雪
函
亦
抽
篇
阵
阴
丁
尺
追
堆
雄
迎
泛
爸
楼
避
谋
吨
野
猪
旗
累
偏
典
馆
索
秦
脂
潮
爷
豆
忽
托
惊
塑
遗
愈
朱
替
纤
粗
倾
尚
痛
楚
谢
奋
购
磨
君
池
旁
碎
骨
监
捕
弟
暴
割
贯
殊
释
词
亡
壁
顿
宝
午
尘
闻
揭
炮
残
冬
桥
妇
警
综
招
吴
付
浮
遭
徐
您
摇
谷
赞
箱
隔
订
男
吹
园
纷
唐
败
宋
玻
巨
耕
坦
荣
闭
湾
键
凡
驻
锅
救
恩
剥
凝
碱
齿
截
炼
麻
纺
禁
废
盛
版
缓
净
睛
昌
婚
涉
筒
嘴
插
岸
朗
庄
街
藏
姑
贸
腐
奴
啦
惯
乘
伙
恢
匀
纱
扎
辩
耳
彪
臣
亿
璃
抵
脉
秀
萨
俄
网
舞
店
喷
纵
寸
汗
挂
洪
贺
闪
柬
爆
烯
津
稻
墙
软
勇
像
滚
厘
蒙
芳
肯
坡
柱
荡
腿
仪
旅
尾
轧
冰
贡
登
黎
削
钻
勒
逃
障
氨
郭
峰
币
港
伏
轨
亩
毕
擦
莫
刺
浪
秘
援
株
健
售
股
岛
甘
泡
睡
童
铸
汤
阀
休
汇
舍
牧
绕
炸
哲
磷
绩
朋
淡
尖
启
陷
柴
呈
徒
颜
泪
稍
忘
泵
蓝
拖
洞
授
镜
辛
壮
锋
贫
虚
弯
摩
泰
幼
廷
尊
窗
纲
弄
隶
疑
氏
宫
姐
震
瑞
怪
尤
琴
循
描
膜
违
夹
腰
缘
珠
穷
森
枝
竹
沟
催
绳
忆
邦
剩
幸
浆
栏
拥
牙
贮
礼
滤
钠
纹
罢
拍
咱
喊
袖
埃
勤
罚
焦
潜
伍
墨
欲
缝
姓
刊
饱
仿
奖
铝
鬼
丽
跨
默
挖
链
扫
喝
袋
炭
污
幕
诸
弧
励
梅
奶
洁
灾
舟
鉴
苯
讼
抱
毁
懂
寒
智
埔
寄
届
跃
渡
挑
丹
艰
贝
碰
拔
爹
戴
码
梦
芽
熔
赤
渔
哭
敬
颗
奔
铅
仲
虎
稀
妹
乏
珍
申
桌
遵
允
隆
螺
仓
魏
锐
晓
氮
兼
隐
碍
赫
拨
忠
肃
缸
牵
抢
博
巧
壳
兄
杜
讯
诚
碧
祥
柯
页
巡
矩
悲
灌
龄
伦
票
寻
桂
铺
圣
恐
恰
郑
趣
抬
荒
腾
贴
柔
滴
猛
阔
辆
妻
填
撤
储
签
闹
扰
紫
砂
递
戏
吊
陶
伐
喂
疗
瓶
婆
抚
臂
摸
忍
虾
蜡
邻
胸
巩
挤
偶
弃
槽
劲
乳
邓
吉
仁
烂
砖
租
乌
舰
伴
瓜
浅
丙
暂
燥
橡
柳
迷
暖
牌
秧
胆
详
簧
踏
瓷
谱
呆
宾
糊
洛
辉
愤
竞
隙
怒
粘
乃
绪
肩
籍
敏
涂
熙
皆
侦
悬
掘
享
纠
醒
狂
锁
淀
恨
牲
霸
爬
赏
逆
玩
陵
祝
秒
浙
貌
役
彼
悉
鸭
趋
凤
晨
畜
辈
秩
卵
署
梯
炎
滩
棋
驱
筛
峡
冒
啥
寿
译
浸
泉
帽
迟
硅
疆
贷
漏
稿
冠
嫩
胁
芯
牢
叛
蚀
奥
鸣
岭
羊
凭
串
塘
绘
酵
融
盆
锡
庙
筹
冻
辅
摄
袭
筋
拒
僚
旱
钾
鸟
漆
沈
眉
疏
添
棒
穗
硝
韩
逼
扭
侨
凉
挺
碗
栽
炒
杯
患
馏
劝
豪
辽
勃
鸿
旦
吏
拜
狗
埋
辊
掩
饮
搬
骂
辞
勾
扣
估
蒋
绒
雾
丈
朵
姆
拟
宇
辑
陕
雕
偿
蓄
崇
剪
倡
厅
咬
驶
薯
刷
斥
番
赋
奉
佛
浇
漫
曼
扇
钙
桃
扶
仔
返
俗
亏
腔
鞋
棱
覆
框
悄
叔
撞
骗
勘
旺
沸
孤
吐
孟
渠
屈
疾
妙
惜
仰
狠
胀
谐
抛
霉
桑
岗
嘛
衰
盗
渗
脏
赖
涌
甜
曹
阅
肌
哩
厉
烃
纬
毅
昨
伪
症
煮
叹
钉
搭
茎
笼
酷
偷
弓
锥
恒
杰
坑
鼻
翼
纶
叙
狱
逮
罐
络
棚
抑
膨
蔬
寺
骤
穆
冶
枯
册
尸
凸
绅
坯
牺
焰
轰
欣
晋
瘦
御
锭
锦
丧
旬
锻
垄
搜
扑
邀
亭
酯
迈
舒
脆
酶
闲
忧
酚
顽
羽
涨
卸
仗
陪
辟
惩
杭
姚
肚
捉
飘
漂
昆
欺
吾
郎
烷
汁
呵
饰
萧
雅
邮
迁
燕
撒
姻
赴
宴
烦
债
帐
斑
铃
旨
醇
董
饼
雏
姿
拌
傅
腹
妥
揉
贤
拆
歪
葡
胺
丢
浩
徽
昂
垫
挡
览
贪
慰
缴
汪
慌
冯
诺
姜
谊
凶
劣
诬
耀
昏
躺
盈
骑
乔
溪
丛
卢
抹
闷
咨
刮
驾
缆
悟
摘
铒
掷
颇
幻
柄
惠
惨
佳
仇
腊
窝
涤
剑
瞧
堡
泼
葱
罩
霍
捞
胎
苍
滨
俩
捅
湘
砍
霞
邵
萄
疯
淮
遂
熊
粪
烘
宿
档
戈
驳
嫂
裕
徙
箭
捐
肠
撑
晒
辨
殿
莲
摊
搅
酱
屏
疫
哀
蔡
堵
沫
皱
畅
叠
阁
莱
敲
辖
钩
痕
坝
巷
饿
祸
丘
玄
溜
曰
逻
彭
尝
卿
妨
艇
吞
韦
怨
矮
歇
================================================
FILE: lib/wordlist/english.txt
================================================
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
================================================
FILE: lib/wordlist/japanese.txt
================================================
あいこくしん
あいさつ
あいだ
あおぞら
あかちゃん
あきる
あけがた
あける
あこがれる
あさい
あさひ
あしあと
あじわう
あずかる
あずき
あそぶ
あたえる
あたためる
あたりまえ
あたる
あつい
あつかう
あっしゅく
あつまり
あつめる
あてな
あてはまる
あひる
あぶら
あぶる
あふれる
あまい
あまど
あまやかす
あまり
あみもの
あめりか
あやまる
あゆむ
あらいぐま
あらし
あらすじ
あらためる
あらゆる
あらわす
ありがとう
あわせる
あわてる
あんい
あんがい
あんこ
あんぜん
あんてい
あんない
あんまり
いいだす
いおん
いがい
いがく
いきおい
いきなり
いきもの
いきる
いくじ
いくぶん
いけばな
いけん
いこう
いこく
いこつ
いさましい
いさん
いしき
いじゅう
いじょう
いじわる
いずみ
いずれ
いせい
いせえび
いせかい
いせき
いぜん
いそうろう
いそがしい
いだい
いだく
いたずら
いたみ
いたりあ
いちおう
いちじ
いちど
いちば
いちぶ
いちりゅう
いつか
いっしゅん
いっせい
いっそう
いったん
いっち
いってい
いっぽう
いてざ
いてん
いどう
いとこ
いない
いなか
いねむり
いのち
いのる
いはつ
いばる
いはん
いびき
いひん
いふく
いへん
いほう
いみん
いもうと
いもたれ
いもり
いやがる
いやす
いよかん
いよく
いらい
いらすと
いりぐち
いりょう
いれい
いれもの
いれる
いろえんぴつ
いわい
いわう
いわかん
いわば
いわゆる
いんげんまめ
いんさつ
いんしょう
いんよう
うえき
うえる
うおざ
うがい
うかぶ
うかべる
うきわ
うくらいな
うくれれ
うけたまわる
うけつけ
うけとる
うけもつ
うける
うごかす
うごく
うこん
うさぎ
うしなう
うしろがみ
うすい
うすぎ
うすぐらい
うすめる
うせつ
うちあわせ
うちがわ
うちき
うちゅう
うっかり
うつくしい
うったえる
うつる
うどん
うなぎ
うなじ
うなずく
うなる
うねる
うのう
うぶげ
うぶごえ
うまれる
うめる
うもう
うやまう
うよく
うらがえす
うらぐち
うらない
うりあげ
うりきれ
うるさい
うれしい
うれゆき
うれる
うろこ
うわき
うわさ
うんこう
うんちん
うんてん
うんどう
えいえん
えいが
えいきょう
えいご
えいせい
えいぶん
えいよう
えいわ
えおり
えがお
えがく
えきたい
えくせる
えしゃく
えすて
えつらん
えのぐ
えほうまき
えほん
えまき
えもじ
えもの
えらい
えらぶ
えりあ
えんえん
えんかい
えんぎ
えんげき
えんしゅう
えんぜつ
えんそく
えんちょう
えんとつ
おいかける
おいこす
おいしい
おいつく
おうえん
おうさま
おうじ
おうせつ
おうたい
おうふく
おうべい
おうよう
おえる
おおい
おおう
おおどおり
おおや
おおよそ
おかえり
おかず
おがむ
おかわり
おぎなう
おきる
おくさま
おくじょう
おくりがな
おくる
おくれる
おこす
おこなう
おこる
おさえる
おさない
おさめる
おしいれ
おしえる
おじぎ
おじさん
おしゃれ
おそらく
おそわる
おたがい
おたく
おだやか
おちつく
おっと
おつり
おでかけ
おとしもの
おとなしい
おどり
おどろかす
おばさん
おまいり
おめでとう
おもいで
おもう
おもたい
おもちゃ
おやつ
おやゆび
およぼす
おらんだ
おろす
おんがく
おんけい
おんしゃ
おんせん
おんだん
おんちゅう
おんどけい
かあつ
かいが
がいき
がいけん
がいこう
かいさつ
かいしゃ
かいすいよく
かいぜん
かいぞうど
かいつう
かいてん
かいとう
かいふく
がいへき
かいほう
かいよう
がいらい
かいわ
かえる
かおり
かかえる
かがく
かがし
かがみ
かくご
かくとく
かざる
がぞう
かたい
かたち
がちょう
がっきゅう
がっこう
がっさん
がっしょう
かなざわし
かのう
がはく
かぶか
かほう
かほご
かまう
かまぼこ
かめれおん
かゆい
かようび
からい
かるい
かろう
かわく
かわら
がんか
かんけい
かんこう
かんしゃ
かんそう
かんたん
かんち
がんばる
きあい
きあつ
きいろ
ぎいん
きうい
きうん
きえる
きおう
きおく
きおち
きおん
きかい
きかく
きかんしゃ
ききて
きくばり
きくらげ
きけんせい
きこう
きこえる
きこく
きさい
きさく
きさま
きさらぎ
ぎじかがく
ぎしき
ぎじたいけん
ぎじにってい
ぎじゅつしゃ
きすう
きせい
きせき
きせつ
きそう
きぞく
きぞん
きたえる
きちょう
きつえん
ぎっちり
きつつき
きつね
きてい
きどう
きどく
きない
きなが
きなこ
きぬごし
きねん
きのう
きのした
きはく
きびしい
きひん
きふく
きぶん
きぼう
きほん
きまる
きみつ
きむずかしい
きめる
きもだめし
きもち
きもの
きゃく
きやく
ぎゅうにく
きよう
きょうりゅう
きらい
きらく
きりん
きれい
きれつ
きろく
ぎろん
きわめる
ぎんいろ
きんかくじ
きんじょ
きんようび
ぐあい
くいず
くうかん
くうき
くうぐん
くうこう
ぐうせい
くうそう
ぐうたら
くうふく
くうぼ
くかん
くきょう
くげん
ぐこう
くさい
くさき
くさばな
くさる
くしゃみ
くしょう
くすのき
くすりゆび
くせげ
くせん
ぐたいてき
くださる
くたびれる
くちこみ
くちさき
くつした
ぐっすり
くつろぐ
くとうてん
くどく
くなん
くねくね
くのう
くふう
くみあわせ
くみたてる
くめる
くやくしょ
くらす
くらべる
くるま
くれる
くろう
くわしい
ぐんかん
ぐんしょく
ぐんたい
ぐんて
けあな
けいかく
けいけん
けいこ
けいさつ
げいじゅつ
けいたい
げいのうじん
けいれき
けいろ
けおとす
けおりもの
げきか
げきげん
げきだん
げきちん
げきとつ
げきは
げきやく
げこう
げこくじょう
げざい
けさき
げざん
けしき
けしごむ
けしょう
げすと
けたば
けちゃっぷ
けちらす
けつあつ
けつい
けつえき
けっこん
けつじょ
けっせき
けってい
けつまつ
げつようび
げつれい
けつろん
げどく
けとばす
けとる
けなげ
けなす
けなみ
けぬき
げねつ
けねん
けはい
げひん
けぶかい
げぼく
けまり
けみかる
けむし
けむり
けもの
けらい
けろけろ
けわしい
けんい
けんえつ
けんお
けんか
げんき
けんげん
けんこう
けんさく
けんしゅう
けんすう
げんそう
けんちく
けんてい
けんとう
けんない
けんにん
げんぶつ
けんま
けんみん
けんめい
けんらん
けんり
こあくま
こいぬ
こいびと
ごうい
こうえん
こうおん
こうかん
ごうきゅう
ごうけい
こうこう
こうさい
こうじ
こうすい
ごうせい
こうそく
こうたい
こうちゃ
こうつう
こうてい
こうどう
こうない
こうはい
ごうほう
ごうまん
こうもく
こうりつ
こえる
こおり
ごかい
ごがつ
ごかん
こくご
こくさい
こくとう
こくない
こくはく
こぐま
こけい
こける
ここのか
こころ
こさめ
こしつ
こすう
こせい
こせき
こぜん
こそだて
こたい
こたえる
こたつ
こちょう
こっか
こつこつ
こつばん
こつぶ
こてい
こてん
ことがら
ことし
ことば
ことり
こなごな
こねこね
このまま
このみ
このよ
ごはん
こひつじ
こふう
こふん
こぼれる
ごまあぶら
こまかい
ごますり
こまつな
こまる
こむぎこ
こもじ
こもち
こもの
こもん
こやく
こやま
こゆう
こゆび
こよい
こよう
こりる
これくしょん
ころっけ
こわもて
こわれる
こんいん
こんかい
こんき
こんしゅう
こんすい
こんだて
こんとん
こんなん
こんびに
こんぽん
こんまけ
こんや
こんれい
こんわく
ざいえき
さいかい
さいきん
ざいげん
ざいこ
さいしょ
さいせい
ざいたく
ざいちゅう
さいてき
ざいりょう
さうな
さかいし
さがす
さかな
さかみち
さがる
さぎょう
さくし
さくひん
さくら
さこく
さこつ
さずかる
ざせき
さたん
さつえい
ざつおん
ざっか
ざつがく
さっきょく
ざっし
さつじん
ざっそう
さつたば
さつまいも
さてい
さといも
さとう
さとおや
さとし
さとる
さのう
さばく
さびしい
さべつ
さほう
さほど
さます
さみしい
さみだれ
さむけ
さめる
さやえんどう
さゆう
さよう
さよく
さらだ
ざるそば
さわやか
さわる
さんいん
さんか
さんきゃく
さんこう
さんさい
ざんしょ
さんすう
さんせい
さんそ
さんち
さんま
さんみ
さんらん
しあい
しあげ
しあさって
しあわせ
しいく
しいん
しうち
しえい
しおけ
しかい
しかく
じかん
しごと
しすう
じだい
したうけ
したぎ
したて
したみ
しちょう
しちりん
しっかり
しつじ
しつもん
してい
してき
してつ
じてん
じどう
しなぎれ
しなもの
しなん
しねま
しねん
しのぐ
しのぶ
しはい
しばかり
しはつ
しはらい
しはん
しひょう
しふく
じぶん
しへい
しほう
しほん
しまう
しまる
しみん
しむける
じむしょ
しめい
しめる
しもん
しゃいん
しゃうん
しゃおん
じゃがいも
しやくしょ
しゃくほう
しゃけん
しゃこ
しゃざい
しゃしん
しゃせん
しゃそう
しゃたい
しゃちょう
しゃっきん
じゃま
しゃりん
しゃれい
じゆう
じゅうしょ
しゅくはく
じゅしん
しゅっせき
しゅみ
しゅらば
じゅんばん
しょうかい
しょくたく
しょっけん
しょどう
しょもつ
しらせる
しらべる
しんか
しんこう
じんじゃ
しんせいじ
しんちく
しんりん
すあげ
すあし
すあな
ずあん
すいえい
すいか
すいとう
ずいぶん
すいようび
すうがく
すうじつ
すうせん
すおどり
すきま
すくう
すくない
すける
すごい
すこし
ずさん
すずしい
すすむ
すすめる
すっかり
ずっしり
ずっと
すてき
すてる
すねる
すのこ
すはだ
すばらしい
ずひょう
ずぶぬれ
すぶり
すふれ
すべて
すべる
ずほう
すぼん
すまい
すめし
すもう
すやき
すらすら
するめ
すれちがう
すろっと
すわる
すんぜん
すんぽう
せあぶら
せいかつ
せいげん
せいじ
せいよう
せおう
せかいかん
せきにん
せきむ
せきゆ
せきらんうん
せけん
せこう
せすじ
せたい
せたけ
せっかく
せっきゃく
ぜっく
せっけん
せっこつ
せっさたくま
せつぞく
せつだん
せつでん
せっぱん
せつび
せつぶん
せつめい
せつりつ
せなか
せのび
せはば
せびろ
せぼね
せまい
せまる
せめる
せもたれ
せりふ
ぜんあく
せんい
せんえい
せんか
せんきょ
せんく
せんげん
ぜんご
せんさい
せんしゅ
せんすい
せんせい
せんぞ
せんたく
せんちょう
せんてい
せんとう
せんぬき
せんねん
せんぱい
ぜんぶ
ぜんぽう
せんむ
せんめんじょ
せんもん
せんやく
せんゆう
せんよう
ぜんら
ぜんりゃく
せんれい
せんろ
そあく
そいとげる
そいね
そうがんきょう
そうき
そうご
そうしん
そうだん
そうなん
そうび
そうめん
そうり
そえもの
そえん
そがい
そげき
そこう
そこそこ
そざい
そしな
そせい
そせん
そそぐ
そだてる
そつう
そつえん
そっかん
そつぎょう
そっけつ
そっこう
そっせん
そっと
そとがわ
そとづら
そなえる
そなた
そふぼ
そぼく
そぼろ
そまつ
そまる
そむく
そむりえ
そめる
そもそも
そよかぜ
そらまめ
そろう
そんかい
そんけい
そんざい
そんしつ
そんぞく
そんちょう
ぞんび
ぞんぶん
そんみん
たあい
たいいん
たいうん
たいえき
たいおう
だいがく
たいき
たいぐう
たいけん
たいこ
たいざい
だいじょうぶ
だいすき
たいせつ
たいそう
だいたい
たいちょう
たいてい
だいどころ
たいない
たいねつ
たいのう
たいはん
だいひょう
たいふう
たいへん
たいほ
たいまつばな
たいみんぐ
たいむ
たいめん
たいやき
たいよう
たいら
たいりょく
たいる
たいわん
たうえ
たえる
たおす
たおる
たおれる
たかい
たかね
たきび
たくさん
たこく
たこやき
たさい
たしざん
だじゃれ
たすける
たずさわる
たそがれ
たたかう
たたく
ただしい
たたみ
たちばな
だっかい
だっきゃく
だっこ
だっしゅつ
だったい
たてる
たとえる
たなばた
たにん
たぬき
たのしみ
たはつ
たぶん
たべる
たぼう
たまご
たまる
だむる
ためいき
ためす
ためる
たもつ
たやすい
たよる
たらす
たりきほんがん
たりょう
たりる
たると
たれる
たれんと
たろっと
たわむれる
だんあつ
たんい
たんおん
たんか
たんき
たんけん
たんご
たんさん
たんじょうび
だんせい
たんそく
たんたい
だんち
たんてい
たんとう
だんな
たんにん
だんねつ
たんのう
たんぴん
だんぼう
たんまつ
たんめい
だんれつ
だんろ
だんわ
ちあい
ちあん
ちいき
ちいさい
ちえん
ちかい
ちから
ちきゅう
ちきん
ちけいず
ちけん
ちこく
ちさい
ちしき
ちしりょう
ちせい
ちそう
ちたい
ちたん
ちちおや
ちつじょ
ちてき
ちてん
ちぬき
ちぬり
ちのう
ちひょう
ちへいせん
ちほう
ちまた
ちみつ
ちみどろ
ちめいど
ちゃんこなべ
ちゅうい
ちゆりょく
ちょうし
ちょさくけん
ちらし
ちらみ
ちりがみ
ちりょう
ちるど
ちわわ
ちんたい
ちんもく
ついか
ついたち
つうか
つうじょう
つうはん
つうわ
つかう
つかれる
つくね
つくる
つけね
つける
つごう
つたえる
つづく
つつじ
つつむ
つとめる
つながる
つなみ
つねづね
つのる
つぶす
つまらない
つまる
つみき
つめたい
つもり
つもる
つよい
つるぼ
つるみく
つわもの
つわり
てあし
てあて
てあみ
ていおん
ていか
ていき
ていけい
ていこく
ていさつ
ていし
ていせい
ていたい
ていど
ていねい
ていひょう
ていへん
ていぼう
てうち
ておくれ
てきとう
てくび
でこぼこ
てさぎょう
てさげ
てすり
てそう
てちがい
てちょう
てつがく
てつづき
でっぱ
てつぼう
てつや
でぬかえ
てぬき
てぬぐい
てのひら
てはい
てぶくろ
てふだ
てほどき
てほん
てまえ
てまきずし
てみじか
てみやげ
てらす
てれび
てわけ
てわたし
でんあつ
てんいん
てんかい
てんき
てんぐ
てんけん
てんごく
てんさい
てんし
てんすう
でんち
てんてき
てんとう
てんない
てんぷら
てんぼうだい
てんめつ
てんらんかい
でんりょく
でんわ
どあい
といれ
どうかん
とうきゅう
どうぐ
とうし
とうむぎ
とおい
とおか
とおく
とおす
とおる
とかい
とかす
ときおり
ときどき
とくい
とくしゅう
とくてん
とくに
とくべつ
とけい
とける
とこや
とさか
としょかん
とそう
とたん
とちゅう
とっきゅう
とっくん
とつぜん
とつにゅう
とどける
ととのえる
とない
となえる
となり
とのさま
とばす
どぶがわ
とほう
とまる
とめる
ともだち
ともる
どようび
とらえる
とんかつ
どんぶり
ないかく
ないこう
ないしょ
ないす
ないせん
ないそう
なおす
ながい
なくす
なげる
なこうど
なさけ
なたでここ
なっとう
なつやすみ
ななおし
なにごと
なにもの
なにわ
なのか
なふだ
なまいき
なまえ
なまみ
なみだ
なめらか
なめる
なやむ
ならう
ならび
ならぶ
なれる
なわとび
なわばり
にあう
にいがた
にうけ
におい
にかい
にがて
にきび
にくしみ
にくまん
にげる
にさんかたんそ
にしき
にせもの
にちじょう
にちようび
にっか
にっき
にっけい
にっこう
にっさん
にっしょく
にっすう
にっせき
にってい
になう
にほん
にまめ
にもつ
にやり
にゅういん
にりんしゃ
にわとり
にんい
にんか
にんき
にんげん
にんしき
にんずう
にんそう
にんたい
にんち
にんてい
にんにく
にんぷ
にんまり
にんむ
にんめい
にんよう
ぬいくぎ
ぬかす
ぬぐいとる
ぬぐう
ぬくもり
ぬすむ
ぬまえび
ぬめり
ぬらす
ぬんちゃく
ねあげ
ねいき
ねいる
ねいろ
ねぐせ
ねくたい
ねくら
ねこぜ
ねこむ
ねさげ
ねすごす
ねそべる
ねだん
ねつい
ねっしん
ねつぞう
ねったいぎょ
ねぶそく
ねふだ
ねぼう
ねほりはほり
ねまき
ねまわし
ねみみ
ねむい
ねむたい
ねもと
ねらう
ねわざ
ねんいり
ねんおし
ねんかん
ねんきん
ねんぐ
ねんざ
ねんし
ねんちゃく
ねんど
ねんぴ
ねんぶつ
ねんまつ
ねんりょう
ねんれい
のいず
のおづま
のがす
のきなみ
のこぎり
のこす
のこる
のせる
のぞく
のぞむ
のたまう
のちほど
のっく
のばす
のはら
のべる
のぼる
のみもの
のやま
のらいぬ
のらねこ
のりもの
のりゆき
のれん
のんき
ばあい
はあく
ばあさん
ばいか
ばいく
はいけん
はいご
はいしん
はいすい
はいせん
はいそう
はいち
ばいばい
はいれつ
はえる
はおる
はかい
ばかり
はかる
はくしゅ
はけん
はこぶ
はさみ
はさん
はしご
ばしょ
はしる
はせる
ぱそこん
はそん
はたん
はちみつ
はつおん
はっかく
はづき
はっきり
はっくつ
はっけん
はっこう
はっさん
はっしん
はったつ
はっちゅう
はってん
はっぴょう
はっぽう
はなす
はなび
はにかむ
はぶらし
はみがき
はむかう
はめつ
はやい
はやし
はらう
はろうぃん
はわい
はんい
はんえい
はんおん
はんかく
はんきょう
ばんぐみ
はんこ
はんしゃ
はんすう
はんだん
ぱんち
ぱんつ
はんてい
はんとし
はんのう
はんぱ
はんぶん
はんぺん
はんぼうき
はんめい
はんらん
はんろん
ひいき
ひうん
ひえる
ひかく
ひかり
ひかる
ひかん
ひくい
ひけつ
ひこうき
ひこく
ひさい
ひさしぶり
ひさん
びじゅつかん
ひしょ
ひそか
ひそむ
ひたむき
ひだり
ひたる
ひつぎ
ひっこし
ひっし
ひつじゅひん
ひっす
ひつぜん
ぴったり
ぴっちり
ひつよう
ひてい
ひとごみ
ひなまつり
ひなん
ひねる
ひはん
ひびく
ひひょう
ひほう
ひまわり
ひまん
ひみつ
ひめい
ひめじし
ひやけ
ひやす
ひよう
びょうき
ひらがな
ひらく
ひりつ
ひりょう
ひるま
ひるやすみ
ひれい
ひろい
ひろう
ひろき
ひろゆき
ひんかく
ひんけつ
ひんこん
ひんしゅ
ひんそう
ぴんち
ひんぱん
びんぼう
ふあん
ふいうち
ふうけい
ふうせん
ぷうたろう
ふうとう
ふうふ
ふえる
ふおん
ふかい
ふきん
ふくざつ
ふくぶくろ
ふこう
ふさい
ふしぎ
ふじみ
ふすま
ふせい
ふせぐ
ふそく
ぶたにく
ふたん
ふちょう
ふつう
ふつか
ふっかつ
ふっき
ふっこく
ぶどう
ふとる
ふとん
ふのう
ふはい
ふひょう
ふへん
ふまん
ふみん
ふめつ
ふめん
ふよう
ふりこ
ふりる
ふるい
ふんいき
ぶんがく
ぶんぐ
ふんしつ
ぶんせき
ふんそう
ぶんぽう
へいあん
へいおん
へいがい
へいき
へいげん
へいこう
へいさ
へいしゃ
へいせつ
へいそ
へいたく
へいてん
へいねつ
へいわ
へきが
へこむ
べにいろ
べにしょうが
へらす
へんかん
べんきょう
べんごし
へんさい
へんたい
べんり
ほあん
ほいく
ぼうぎょ
ほうこく
ほうそう
ほうほう
ほうもん
ほうりつ
ほえる
ほおん
ほかん
ほきょう
ぼきん
ほくろ
ほけつ
ほけん
ほこう
ほこる
ほしい
ほしつ
ほしゅ
ほしょう
ほせい
ほそい
ほそく
ほたて
ほたる
ぽちぶくろ
ほっきょく
ほっさ
ほったん
ほとんど
ほめる
ほんい
ほんき
ほんけ
ほんしつ
ほんやく
まいにち
まかい
まかせる
まがる
まける
まこと
まさつ
まじめ
ますく
まぜる
まつり
まとめ
まなぶ
まぬけ
まねく
まほう
まもる
まゆげ
まよう
まろやか
まわす
まわり
まわる
まんが
まんきつ
まんぞく
まんなか
みいら
みうち
みえる
みがく
みかた
みかん
みけん
みこん
みじかい
みすい
みすえる
みせる
みっか
みつかる
みつける
みてい
みとめる
みなと
みなみかさい
みねらる
みのう
みのがす
みほん
みもと
みやげ
みらい
みりょく
みわく
みんか
みんぞく
むいか
むえき
むえん
むかい
むかう
むかえ
むかし
むぎちゃ
むける
むげん
むさぼる
むしあつい
むしば
むじゅん
むしろ
むすう
むすこ
むすぶ
むすめ
むせる
むせん
むちゅう
むなしい
むのう
むやみ
むよう
むらさき
むりょう
むろん
めいあん
めいうん
めいえん
めいかく
めいきょく
めいさい
めいし
めいそう
めいぶつ
めいれい
めいわく
めぐまれる
めざす
めした
めずらしい
めだつ
めまい
めやす
めんきょ
めんせき
めんどう
もうしあげる
もうどうけん
もえる
もくし
もくてき
もくようび
もちろん
もどる
もらう
もんく
もんだい
やおや
やける
やさい
やさしい
やすい
やすたろう
やすみ
やせる
やそう
やたい
やちん
やっと
やっぱり
やぶる
やめる
ややこしい
やよい
やわらかい
ゆうき
ゆうびんきょく
ゆうべ
ゆうめい
ゆけつ
ゆしゅつ
ゆせん
ゆそう
ゆたか
ゆちゃく
ゆでる
ゆにゅう
ゆびわ
ゆらい
ゆれる
ようい
ようか
ようきゅう
ようじ
ようす
ようちえん
よかぜ
よかん
よきん
よくせい
よくぼう
よけい
よごれる
よさん
よしゅう
よそう
よそく
よっか
よてい
よどがわく
よねつ
よやく
よゆう
よろこぶ
よろしい
らいう
らくがき
らくご
らくさつ
らくだ
らしんばん
らせん
らぞく
らたい
らっか
られつ
りえき
りかい
りきさく
りきせつ
りくぐん
りくつ
りけん
りこう
りせい
りそう
りそく
りてん
りねん
りゆう
りゅうがく
りよう
りょうり
りょかん
りょくちゃ
りょこう
りりく
りれき
りろん
りんご
るいけい
るいさい
るいじ
るいせき
るすばん
るりがわら
れいかん
れいぎ
れいせい
れいぞうこ
れいとう
れいぼう
れきし
れきだい
れんあい
れんけい
れんこん
れんさい
れんしゅう
れんぞく
れんらく
ろうか
ろうご
ろうじん
ろうそく
ろくが
ろこつ
ろじうら
ろしゅつ
ろせん
ろてん
ろめん
ろれつ
ろんぎ
ろんぱ
ろんぶん
ろんり
わかす
わかめ
わかやま
わかれる
わしつ
わじまし
わすれもの
わらう
われる
================================================
FILE: lib/wordlist/portuguese.txt
================================================
# 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
================================================
FILE: lib/wordlist/spanish.txt
================================================
á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
================================================
FILE: lib/x509.py
================================================
#!/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, get_cert_path
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', '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') 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__":
util.set_verbosity(True)
ca_path = get_cert_path()
ca_list, ca_keyID = load_certificates(ca_path)
================================================
FILE: packages.txt
================================================
python3-pip
python3-setuptools
python3-dev
python3-pyqt5
python3-dnspython
pyqt5-dev-tools
protobuf-compiler
python-requests
gettext
wine-development
dirmngr
gnupg2
libudev-dev
libusb-1.0-0-dev
desktop-file-utils
================================================
FILE: plugins/README
================================================
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.
================================================
FILE: plugins/__init__.py
================================================
#!/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.
================================================
FILE: plugins/audio_modem/__init__.py
================================================
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']
================================================
FILE: plugins/audio_modem/qt.py
================================================
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)
================================================
FILE: plugins/cosigner_pool/__init__.py
================================================
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']
================================================
FILE: plugins/cosigner_pool/qt.py
================================================
#!/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
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
PORT = 12344
HOST = 'cosigner.electrum.org'
server = ServerProxy('http://%s:%d'%(HOST,PORT), 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
message = bitcoin.encrypt_message(bfh(tx.raw), bh2u(K)).decode('ascii')
try:
server.put(_hash, message)
except Exception as e:
traceback.print_exc(file=sys.stdout)
window.show_message("Failed to send transaction to cosigning pool.")
return
window.show_message("Your transaction was sent to the cosigning pool.\nOpen 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 wallet.has_password():
password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease 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.\nDo you want to open it now?")):
return
xprv = wallet.keystore.get_master_private_key(password)
if not xprv:
return
try:
k = bh2u(bitcoin.deserialize_xprv(xprv)[-1])
EC = bitcoin.EC_KEY(bfh(k))
message = bh2u(EC.decrypt_message(message))
except Exception as e:
traceback.print_exc(file=sys.stdout)
window.show_message(str(e))
return
self.listener.clear(keyhash)
tx = transaction.Transaction(message)
show_transaction(tx, window, prompt_if_unsaved=True)
================================================
FILE: plugins/digitalbitbox/__init__.py
================================================
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']
================================================
FILE: plugins/digitalbitbox/cmdline.py
================================================
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
================================================
FILE: plugins/digitalbitbox/digitalbitbox.py
================================================
# ----------------------------------------------------------------------------------
# Electrum plugin for the Digital Bitbox hardware wallet by Shift Devices AG
# digitalbitbox.com
#
try:
import electrum
from electrum.bitcoin import TYPE_ADDRESS, push_script, var_int, msg_magic, Hash, verify_message, pubkey_from_signature, point_to_ser, public_key_to_p2pkh, EncodeAES, DecodeAES, MyVerifyingKey
from electrum.bitcoin import serialize_xpub, deserialize_xpub
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
import time
import hid
import json
import math
import binascii
import struct
import hashlib
import requests
import base64
import os
import sys
from ecdsa.ecdsa import generator_secp256k1
from ecdsa.util import sigencode_der
from ecdsa.curves import SECP256k1
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 _get_xpub(self, bip32_path):
if self.check_device_dialog():
return self.hid_send_encrypt(b'{"xpub": "%s"}' % bip32_path.encode('utf8'))
def get_xpub(self, bip32_path, xtype):
assert xtype in ('standard', 'p2wpkh-p2sh')
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 standard type, but it is agnostic
# to the type when signing.
if xtype != 'standard':
_, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
return xpub
else:
raise BaseException('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 binascii.hexlify(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.\r\n\r\nEnter password:")
elif len(password) > 64:
msg = _("Password must have less than 64 characters.\r\n\r\nEnter 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.\r\n\r\nEnter password:")
elif len(password) > 64:
msg = _("Password must have less than 64 characters.\r\n\r\nEnter 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.\r\n\r\n REMEMBER THE PASSWORD!\r\n\r\n" \
"You cannot access your coins or a backup without the password.\r\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):
return False
reply = self.hid_send_encrypt(b'{"led":"blink"}')
if 'error' in reply:
self.password = None
if reply['error']['code'] == 109:
msg = _("Incorrect password entered.\r\n\r\n" \
+ reply['error']['message'] + "\r\n\r\n" \
"Enter your Digital Bitbox password:")
else:
# Should never occur
msg = _("Unexpected error occurred.\r\n\r\n" \
+ reply['error']['message'] + "\r\n\r\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:
with open(os.path.join(dbb_user_dir, "config.dat")) as f:
dbb_config = json.load(f)
except (FileNotFoundError, json.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").encode('utf8')
msg = b'{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, b'Digital Bitbox Electrum Plugin')
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?\r\n\r\n" \
"To continue, touch the Digital Bitbox's light for 3 seconds.\r\n\r\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...\r\n\r\n" \
"To continue, touch the Digital Bitbox's light for 3 seconds.\r\n\r\n" \
"To cancel, briefly touch the light or wait for the timeout."))
msg = b'{"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 %s') % 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 = b'{"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 ...\r\n\r\n" \
"To continue, touch the Digital Bitbox's blinking light for 3 seconds.\r\n\r\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 = bytes([27 + int(reply['sign'][0]['recid'], 16) + 4]) + binascii.unhexlify(reply['sign'][0]['sig'])
pk, compressed = pubkey_from_signature(sig, msg_hash)
pk = point_to_ser(pk.pubkey.point, compressed)
addr = public_key_to_p2pkh(pk)
if verify_message(addr, sig, message) is False:
raise Exception("Could not sign message")
elif 'pubkey' in reply['sign'][0]:
# firmware <= v2.1.1
for i in range(4):
sig = bytes([27 + i + 4]) + binascii.unhexlify(reply['sign'][0]['sig'])
try:
addr = public_key_to_p2pkh(binascii.unhexlify(reply['sign'][0]['pubkey']))
if verify_message(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()
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 ...\r\n\r\n" \
"To continue, touch the Digital Bitbox's blinking light for 3 seconds. " \
"(Touch " + str(step + 1) + " of " + str(int(steps)) + ")\r\n\r\n" \
"To cancel, briefly touch the blinking light or wait for the timeout.\r\n\r\n"))
else:
self.handler.show_message(_("Signing transaction ...\r\n\r\n" \
"To continue, touch the Digital Bitbox's blinking light for 3 seconds.\r\n\r\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 = MyVerifyingKey.from_signature(s, recid, h, curve = SECP256k1)
pk = to_hexstr(point_to_ser(pk.pubkey.point, 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 = sigencode_der(sig_r, sig_s, generator_secp256k1.order())
txin['signatures'][ii] = to_hexstr(sig) + '01'
tx._inputs[i] = txin
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
]
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:
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):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
client.handler = self.create_handler(wizard)
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 ('standard', 'p2wpkh-p2sh'):
raise ScriptTypeNotSupported(_('This type of script is not supported with the Digital Bitbox.'))
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
================================================
FILE: plugins/digitalbitbox/qt.py
================================================
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():
change, index = wallet.get_address_index(addrs[0])
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)
menu.addAction(_("Show on %s") % self.device, show_address)
class DigitalBitbox_Handler(QtHandlerBase):
def __init__(self, win):
super(DigitalBitbox_Handler, self).__init__(win, 'Digital Bitbox')
================================================
FILE: plugins/email_requests/__init__.py
================================================
from electrum.i18n import _
fullname = _('Email')
description = _("Send and receive payment request with an email account")
available_for = ['qt']
================================================
FILE: plugins/email_requests/qt.py
================================================
#!/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 time
import threading
import base64
from functools import partial
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 *
import PyQt5.QtGui as QtGui
from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit)
from electrum.plugins import BasePlugin, hook
from electrum.paymentrequest import PaymentRequest
from electrum.i18n import _
from electrum_gui.qt.util import EnterButton, Buttons, CloseButton
from electrum_gui.qt.util import OkButton, WindowModalDialog
class Processor(threading.Thread):
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
def poll(self):
try:
self.M.select()
except:
return
typ, data = self.M.search(None, 'ALL')
for num in data[0].split():
typ, msg_data = self.M.fetch(num, '(RFC822)')
msg = email.message_from_string(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):
self.M = imaplib.IMAP4_SSL(self.imap_server)
self.M.login(self.username, self.password)
while True:
self.poll()
time.sleep(self.polling_interval)
self.M.close()
self.M.logout()
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)
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()
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)
def on_receive(self, pr_str):
self.print_error('received payment request')
self.pr = PaymentRequest(pr_str)
self.obj.email_new_invoice_signal.emit()
def new_invoice(self):
self.parent.invoices.add(self.pr)
#window.update_invoices_list()
@hook
def receive_list_menu(self, menu, addr):
window = menu.parentWidget()
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 = QtGui.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:
self.processor.send(recipient, message, payload)
except BaseException as e:
window.show_message(str(e))
return
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 acount')))
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)
username = str(username_e.text())
self.config.set_key('email_username', username)
password = str(password_e.text())
self.config.set_key('email_password', password)
================================================
FILE: plugins/greenaddress_instant/__init__.py
================================================
from electrum.i18n import _
fullname = 'GreenAddress instant'
description = _("Allows validating if your transactions have instant confirmations by GreenAddress")
available_for = ['qt']
================================================
FILE: plugins/greenaddress_instant/qt.py
================================================
#!/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
# 1. get the password and sign the verification request
password = None
if wallet.has_password():
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(_('%s is covered by GreenAddress instant confirmation') % (tx.txid()), title=_('Verification successful!'))
else:
d.show_critical(_('%s is not covered by GreenAddress instant confirmation') % (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)
================================================
FILE: plugins/hw_wallet/__init__.py
================================================
from .plugin import HW_PluginBase
from .cmdline import CmdLineHandler
================================================
FILE: plugins/hw_wallet/cmdline.py
================================================
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()
return ''.join(map(lambda x: t[x], o))
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 update_status(self, b):
print_error('trezor status', b)
def finished(self):
pass
================================================
FILE: plugins/hw_wallet/plugin.py
================================================
#!/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 _
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)
================================================
FILE: plugins/hw_wallet/qt.py
================================================
#!/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.
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 MacOSX.
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)
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):
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):
self.error_signal.emit(msg)
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 %s device') % 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):
self.win.show_error(msg, parent=self.top_level_window())
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:
window.show_error(
_("Cannot find python library for") + " '%s'.\n" % self.name \
+ _("Make sure you install it with python3")
)
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)
# 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)
================================================
FILE: plugins/keepkey/__init__.py
================================================
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']
================================================
FILE: plugins/keepkey/client.py
================================================
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)
================================================
FILE: plugins/keepkey/clientbase.py
================================================
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 %s device"),
4: _("Confirm internal entropy on your %s device to begin"),
5: _("Write down the seed word shown on your %s"),
6: _("Confirm on your %s that you want to wipe it clean"),
7: _("Confirm on your %s device the message to sign"),
8: _("Confirm the total amount spent and the transaction fee on your "
"%s device"),
10: _("Confirm wallet address on your %s device"),
'default': _("Check your %s 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 % self.device, self.cancel)
return self.proto.ButtonAck()
def callback_PinMatrixRequest(self, msg):
if msg.type == 2:
msg = _("Enter a new PIN for your %s:")
elif msg.type == 3:
msg = (_("Re-enter the new PIN for your %s.\n\n"
"NOTE: the positions of the numbers have changed!"))
else:
msg = _("Enter your current %s PIN:")
pin = self.handler.get_pin(msg % self.device)
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 %s will prompt you for the "
"passphrase. If you forget the passphrase you cannot "
"access the bitcoins in the wallet.") % 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)
return self.proto.PassphraseAck(passphrase=passphrase)
def callback_WordRequest(self, msg):
self.step += 1
msg = _("Step %d/24. Enter seed word as explained on "
"your %s:") % (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 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 unforunately
# 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 %s device to disable passphrases")
else:
self.msg = _("Confirm on your %s 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 %s device")
self.apply_settings(label=label)
def change_homescreen(self, homescreen):
self.msg = _("Confirm on your %s device to change your home screen")
self.apply_settings(homescreen=homescreen)
def set_pin(self, remove):
if remove:
self.msg = _("Confirm on your %s device to disable PIN protection")
elif self.features.pin_protection:
self.msg = _("Confirm on your %s device to change your PIN")
else:
self.msg = _("Confirm on your %s 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))
pass
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)))
================================================
FILE: plugins/keepkey/cmdline.py
================================================
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
================================================
FILE: plugins/keepkey/keepkey.py
================================================
from .plugin import KeepKeyCompatiblePlugin, KeepKeyCompatibleKeyStore
class KeepKey_KeyStore(KeepKeyCompatibleKeyStore):
hw_type = 'keepkey'
device = 'KeepKey'
class KeepKeyPlugin(KeepKeyCompatiblePlugin):
firmware_URL = 'https://www.keepkey.com'
libraries_URL = 'https://github.com/keepkey/python-keepkey'
minimum_firmware = (1, 0, 0)
keystore_class = KeepKey_KeyStore
def __init__(self, *args):
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.libraries_available = True
except ImportError:
self.libraries_available = False
KeepKeyCompatiblePlugin.__init__(self, *args)
def hid_transport(self, pair):
from keepkeylib.transport_hid import HidTransport
return HidTransport(pair)
def bridge_transport(self, d):
raise NotImplementedError('')
================================================
FILE: plugins/keepkey/plugin.py
================================================
import threading
from binascii import hexlify, unhexlify
from electrum.util import bfh, bh2u
from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey,
TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants,
is_segwit_address)
from electrum.i18n import _
from electrum.plugins import BasePlugin
from electrum.transaction import deserialize
from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase
# TREZOR initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
class KeepKeyCompatibleKeyStore(Hardware_KeyStore):
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 %s') % 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']
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 KeepKeyCompatiblePlugin(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
MAX_LABEL_LEN = 32
def __init__(self, parent, config, name):
HW_PluginBase.__init__(self, parent, config, name)
self.main_thread = threading.current_thread()
# FIXME: move to base class when Ledger is fixed
if self.libraries_available:
self.device_manager().register_devices(self.DEVICE_IDS)
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 _try_bridge(self, device):
self.print_error("Trying to connect over Trezor Bridge...")
try:
return self.bridge_transport({'path': hexlify(device.path)})
except BaseException as e:
self.print_error("cannot connect to bridge", str(e))
return None
def create_client(self, device, handler):
# disable bridge because it seems to never returns if keepkey is plugged
#transport = self._try_bridge(device) or self._try_hid(device)
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 %s firmware for device labelled %s. Please '
'download the updated firmware from %s') %
(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 NetworkConstants.TESTNET else "Bitcoin"
def initialize_device(self, device_id, wizard, handler):
# Initialization method
msg = _("Choose how you want to initialize your %s.\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 %s, and so you should "
"only do those on a computer you know to be trustworthy "
"and free of malware."
) % (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, args=(settings, method, device_id, wizard, handler))
t.setDaemon(True)
t.start()
wizard.loop.exec_()
wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f)
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)
wizard.loop.exit(0)
def setup_device(self, device_info, wizard):
'''Called when creating a new wallet. Select the device to use. If
the device is uninitialized, go through the intialization
process.'''
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
# 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 ('standard',):
raise ScriptTypeNotSupported(_('This type of script is not supported with KeepKey.'))
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())
signed_tx = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[1]
raw = bh2u(signed_tx)
tx.update_signatures(raw)
def show_address(self, wallet, address):
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 'scriptSig' in txin:
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):
outputs = []
has_change = False
for _type, address, amount in tx.outputs():
info = tx.output_info.get(address)
if info is not None and not has_change:
has_change = True # no more than one change address
addrtype, hash_160 = b58_address_to_hash160(address)
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)
else:
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 == NetworkConstants.ADDRTYPE_P2PKH:
txoutputtype.script_type = self.types.PAYTOADDRESS
elif addrtype == NetworkConstants.ADDRTYPE_P2SH:
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
else:
raise BaseException('addrtype: ' + str(addrtype))
txoutputtype.address = 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)
================================================
FILE: plugins/keepkey/qt.py
================================================
from .qt_generic import QtPlugin
from .keepkey import KeepKeyPlugin
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
================================================
FILE: plugins/keepkey/qt_generic.py
================================================
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 .plugin import TIM_NEW, TIM_RECOVER, TIM_MNEMONIC
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from electrum.i18n import _
from electrum.plugins import hook, DeviceMgr
from electrum.util import PrintError, UserCancelled, bh2u
from electrum.wallet import Wallet, Standard_Wallet
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)
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.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.character_dialog.accept()
self.character_dialog = None
return data
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 %s") % 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 or self.device == 'TREZOR':
vbox.addWidget(gb)
gb.setTitle(_("Select your seed length:"))
bg = QButtonGroup()
for i, count in enumerate([12, 18, 24]):
rb = QRadioButton(gb)
rb.setText(_("%d words") % 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,10}')))
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 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 = _("%s Settings") % 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 %s. If you have mutiple devices "
"their labels help distinguish them.")
% 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 %s.") % plugin.device)
pin_msg.setWordWrap(True)
pin_msg.setStyleSheet("color: red")
settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
# Settings tab - Homescreen
if plugin.device != 'KeepKey': # Not yet supported by KK firmware
homescreen_layout = QHBoxLayout()
homescreen_label = QLabel(_("Homescreen"))
homescreen_change_button = QPushButton(_("Change..."))
homescreen_clear_button = QPushButton(_("Reset"))
homescreen_change_button.clicked.connect(change_homescreen)
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 %d x %d monochrome black and "
"white image.") % (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 "
"%s device can spend your bitcoins.") % 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)
================================================
FILE: plugins/labels/__init__.py
================================================
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']
================================================
FILE: plugins/labels/kivy.py
================================================
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()
================================================
FILE: plugins/labels/labels.py
================================================
import hashlib
import requests
import threading
import json
import sys
import traceback
import base64
import electrum
from electrum.plugins import BasePlugin, hook
from electrum.i18n import _
class LabelsPlugin(BasePlugin):
def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name)
self.target_host = 'labels.bauerj.eu'
self.wallets = {}
def encode(self, wallet, msg):
password, iv, wallet_id = self.wallets[wallet]
encrypted = electrum.bitcoin.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 = electrum.bitcoin.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 not wallet 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,
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 BaseException(response.status_code, response.text)
response = response.json()
if "error" in response:
raise BaseException(response["error"])
return response
def push_thread(self, wallet):
wallet_id = self.wallets[wallet][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_id = self.wallets[wallet][2]
nonce = 1 if force else self.get_nonce(wallet) - 1
self.print_error("asking for labels since nonce", nonce)
try:
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)
except Exception 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, args=(wallet, False))
t.setDaemon(True)
t.start()
def stop_wallet(self, wallet):
self.wallets.pop(wallet, None)
================================================
FILE: plugins/labels/qt.py
================================================
from functools import partial
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, d))
download = ThreadedButton("Force download",
partial(self.pull_thread, wallet, True),
partial(self.done_processing, 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(self, dialog, result):
dialog.show_message(_("Your labels have been synchronised."))
@hook
def on_new_window(self, window):
self.obj.labels_changed_signal.connect(window.update_tabs)
self.start_wallet(window.wallet)
@hook
def on_close_window(self, window):
self.stop_wallet(window.wallet)
================================================
FILE: plugins/ledger/__init__.py
================================================
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']
================================================
FILE: plugins/ledger/auth2fa.py
================================================
from binascii import hexlify, unhexlify
from PyQt5.Qt import QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel
import PyQt5.QtCore as QtCore
from PyQt5.QtWidgets import *
from electrum.i18n import _
from electrum_gui.qt.util import *
from electrum.util import print_msg
import os, hashlib, websocket, logging, json, copy
from electrum_gui.qt.qrcodewidget import QRCodeWidget
from btchip.btchip import *
DEBUG = False
helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code. " \
"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. " \
"Verify the transaction summary and type the PIN code here. " \
"Before pressing enter, plug the device back into this computer. " ),
_("Verify the address below. Type the character from your security card corresponding to the BOLD 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 LedgerWallet phone app to pair it with this Ledger device. "
"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(600)
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; }")
self.addrtext.setReadOnly(True)
self.addrtext.setMaximumHeight(120)
card.addWidget(self.addrtext)
def pin_changed(s):
if len(s) < len(self.idxs):
i = self.idxs[len(s)]
addr = self.txdata['address']
addr = addr[:i] + '' + addr[i:i+1] + ' ' + addr[i+1:]
self.addrtext.setHtml(str(addr))
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 - %s") % 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(200)
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)
================================================
FILE: plugins/ledger/cmdline.py
================================================
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
================================================
FILE: plugins/ledger/ledger.py
================================================
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 ..hw_wallet import HW_PluginBase
from electrum.util import print_error, is_verbose, bfh, bh2u
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'
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 versiontuple(self, v):
return tuple(map(int, (v.split("."))))
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
@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 = self.versiontuple(firmware) >= self.versiontuple(MULTI_OUTPUT_SUPPORT)
self.nativeSegwitSupported = self.versiontuple(firmware) >= self.versiontuple(SEGWIT_SUPPORT)
self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and self.versiontuple(firmware) >= self.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")
raise e
def checkDevice(self):
if not self.preflightDone:
try:
self.perform_hw1_preflight()
except BTChipException as e:
if (e.sw == 0x6d00):
raise BaseException("Device not in BTCP mode")
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 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 %s') % self.device)
def sign_message(self, sequence, message, password):
self.signing = True
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.")
else:
self.give_error(e, True)
except UserWarning:
self.handler.show_error(_('Cancelled by user'))
return ''
except Exception as e:
self.give_error(e, True)
finally:
self.handler.finished()
self.signing = False
# 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
def sign_transaction(self, tx, password):
if tx.is_complete():
return
client = self.get_client()
self.signing = True
inputs = []
inputsPaths = []
pubKeys = []
chipInputs = []
redeemScripts = []
signatures = []
preparedTrustedInputs = []
changePath = ""
changeAmount = None
output = None
outputAmount = 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)
inputs.append([txin['prev_tx'].raw, txin['prevout_n'], redeemScript, txin['prevout_hash'], signingPos, txin.get('sequence', 0xffffffff - 1) ])
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
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")
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):
index, xpubs, m = info
changePath = self.get_derivation()[2:] + "/%d/%d"%index
changeAmount = amount
else:
output = address
outputAmount = amount
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:
txtmp = bitcoinTransaction(bfh(utxo[0]))
tmp = bfh(utxo[3])[::-1]
tmp += bfh(int_to_hex(utxo[1], 4))
tmp += txtmp.outputs[utxo[1]].amount
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()
self.get_client().enableAlternate2fa(False)
if segwitTransaction:
self.get_client().startUntrustedTransaction(True, inputIndex,
chipInputs, redeemScripts[inputIndex])
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])
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 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]
txin['signatures'][signingPos] = bh2u(signatures[i])
tx.raw = tx.serialize()
self.signing = False
def show_address(self, sequence, txin_type):
self.signing = True
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)
try:
client.getWalletPublicKey(address_path, showOnScreen=True, segwit=segwit)
except BTChipException as e:
if e.sw == 0x6985: # cancelled by user
pass
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()
self.signing = False
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
(0x2c97, 0x0004), # Nano-X
(0x2c97, 0x0005), # RFU
(0x2c97, 0x0006), # RFU
(0x2c97, 0x0007), # RFU
(0x2c97, 0x0008), # RFU
(0x2c97, 0x0009), # RFU
(0x2c97, 0x000a) # RFU
]
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 btchip_is_connected(self, keystore):
try:
self.get_client(keystore).getFirmwareVersion()
except Exception as e:
return False
return True
def get_btchip_device(self, device):
ledger = False
if (device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c) or (device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c) or (device.product_key[0] == 0x2c97):
ledger = True
dev = hid.device()
dev.open_path(device.path)
dev.set_nonblocking(True)
return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG)
def create_client(self, device, 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):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
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):
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
#assert self.main_thread != threading.current_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):
sequence = wallet.get_address_index(address)
txin_type = wallet.get_txin_type(address)
wallet.get_keystore().show_address(sequence, txin_type)
================================================
FILE: plugins/ledger/qt.py
================================================
import threading
from PyQt5.Qt import QInputDialog, QLineEdit, QVBoxLayout, QLabel
from electrum.i18n import _
from electrum.plugins import hook
from electrum.wallet import Standard_Wallet
from .ledger import LedgerPlugin
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from electrum_gui.qt.util import *
#from btchip.btchipPersoWizard import StartBTChipPersoDialog
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):
dialog = StartBTChipPersoDialog()
dialog.exec_()
================================================
FILE: plugins/trezor/__init__.py
================================================
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']
================================================
FILE: plugins/trezor/client.py
================================================
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)
ProtocolMixin.__init__(self, transport)
TrezorClientBase.__init__(self, handler, plugin, proto)
TrezorClientBase.wrap_methods(TrezorClient)
================================================
FILE: plugins/trezor/clientbase.py
================================================
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 %s device"),
4: _("Confirm internal entropy on your %s device to begin"),
5: _("Write down the seed word shown on your %s"),
6: _("Confirm on your %s that you want to wipe it clean"),
7: _("Confirm on your %s device the message to sign"),
8: _("Confirm the total amount spent and the transaction fee on your "
"%s device"),
10: _("Confirm wallet address on your %s device"),
'default': _("Check your %s 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 % self.device, self.cancel)
return self.proto.ButtonAck()
def callback_PinMatrixRequest(self, msg):
if msg.type == 2:
msg = _("Enter a new PIN for your %s:")
elif msg.type == 3:
msg = (_("Re-enter the new PIN for your %s.\n\n"
"NOTE: the positions of the numbers have changed!"))
else:
msg = _("Enter your current %s PIN:")
pin = self.handler.get_pin(msg % self.device)
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 %s will prompt you for the "
"passphrase. If you forget the passphrase you cannot "
"access the bitcoins in the wallet.") % 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)
return self.proto.PassphraseAck(passphrase=passphrase)
def callback_WordRequest(self, msg):
self.step += 1
msg = _("Step %d/24. Enter seed word as explained on "
"your %s:") % (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 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 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 unforunately
# 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 %s device to disable passphrases")
else:
self.msg = _("Confirm on your %s 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 %s device")
self.apply_settings(label=label)
def change_homescreen(self, homescreen):
self.msg = _("Confirm on your %s device to change your home screen")
self.apply_settings(homescreen=homescreen)
def set_pin(self, remove):
if remove:
self.msg = _("Confirm on your %s device to disable PIN protection")
elif self.features.pin_protection:
self.msg = _("Confirm on your %s device to change your PIN")
else:
self.msg = _("Confirm on your %s 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))
pass
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)
@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)))
================================================
FILE: plugins/trezor/cmdline.py
================================================
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
================================================
FILE: plugins/trezor/plugin.py
================================================
import threading
from binascii import hexlify, unhexlify
from electrum.util import bfh, bh2u
from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey,
TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants)
from electrum.i18n import _
from electrum.plugins import BasePlugin
from electrum.transaction import deserialize
from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
from ..hw_wallet import HW_PluginBase
# TREZOR initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
# script "generation"
SCRIPT_GEN_LEGACY, SCRIPT_GEN_P2SH_SEGWIT, SCRIPT_GEN_NATIVE_SEGWIT = range(0, 3)
class TrezorCompatibleKeyStore(Hardware_KeyStore):
def get_derivation(self):
return self.derivation
def get_script_gen(self):
def is_p2sh_segwit():
return self.derivation.startswith("m/49'/")
def is_native_segwit():
return self.derivation.startswith("m/84'/")
if is_native_segwit():
return SCRIPT_GEN_NATIVE_SEGWIT
elif is_p2sh_segwit():
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 %s') % 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']
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 TrezorCompatiblePlugin(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
MAX_LABEL_LEN = 32
def __init__(self, parent, config, name):
HW_PluginBase.__init__(self, parent, config, name)
self.main_thread = threading.current_thread()
# FIXME: move to base class when Ledger is fixed
if self.libraries_available:
self.device_manager().register_devices(self.DEVICE_IDS)
def _try_hid(self, device):
self.print_error("Trying to connect over USB...")
try:
return self.hid_transport(device)
except BaseException as e:
# see fdb810ba622dc7dbe1259cbafb5b28e19d2ab114
# raise
self.print_error("cannot connect at", device.path, str(e))
return None
def _try_bridge(self, device):
self.print_error("Trying to connect over Trezor Bridge...")
try:
return self.bridge_transport({'path': hexlify(device.path)})
except BaseException as e:
self.print_error("cannot connect to bridge", str(e))
return None
def create_client(self, device, handler):
# disable bridge because it seems to never returns if keepkey is plugged
#transport = self._try_bridge(device) or self._try_hid(device)
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 %s firmware for device labelled %s. Please '
'download the updated firmware from %s') %
(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 NetworkConstants.TESTNET else "Bitcoin"
def initialize_device(self, device_id, wizard, handler):
# Initialization method
msg = _("Choose how you want to initialize your %s.\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 %s, and so you should "
"only do those on a computer you know to be trustworthy "
"and free of malware."
) % (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, args=(settings, method, device_id, wizard, handler))
t.setDaemon(True)
t.start()
wizard.loop.exec_()
wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f)
def _initialize_device(self, settings, method, device_id, wizard, handler):
item, label, pin_protection, passphrase_protection = settings
if method == TIM_RECOVER:
# FIXME the PIN prompt will appear over this message
# which makes this unreadable
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!"))
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
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)
wizard.loop.exit(0)
def setup_device(self, device_info, wizard):
'''Called when creating a new wallet. Select the device to use. If
the device is uninitialized, go through the intialization
process.'''
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
# 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):
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.get_script_gen())
outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.get_script_gen())
signed_tx = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[1]
raw = bh2u(signed_tx)
tx.update_signatures(raw)
def show_address(self, wallet, address):
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)
script_gen = wallet.keystore.get_script_gen()
if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
script_type = self.types.InputScriptType.SPENDWITNESS
elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
script_type = self.types.InputScriptType.SPENDP2SHWITNESS
else:
script_type = self.types.InputScriptType.SPENDADDRESS
client.get_address(self.get_coin_name(), address_n, True, 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)
if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
txinputtype.script_type = self.types.InputScriptType.SPENDWITNESS
elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
txinputtype.script_type = self.types.InputScriptType.SPENDP2SHWITNESS
else:
txinputtype.script_type = self.types.InputScriptType.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 = 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'),
)
if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
script_type = self.types.InputScriptType.SPENDWITNESS
elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
script_type = self.types.InputScriptType.SPENDP2SHWITNESS
else:
script_type = self.types.InputScriptType.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._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 'scriptSig' in txin:
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):
outputs = []
has_change = False
for _type, address, amount in tx.outputs():
info = tx.output_info.get(address)
if info is not None and not has_change:
has_change = True # no more than one change address
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)
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)
else:
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
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._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)
================================================
FILE: plugins/trezor/qt.py
================================================
from ..trezor.qt_generic import QtPlugin
from .trezor import TrezorPlugin
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
================================================
FILE: plugins/trezor/qt_generic.py
================================================
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 .plugin import TIM_NEW, TIM_RECOVER, TIM_MNEMONIC
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from electrum.i18n import _
from electrum.plugins import hook, DeviceMgr
from electrum.util import PrintError, UserCancelled, bh2u
from electrum.wallet import Wallet, Standard_Wallet
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)
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.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.character_dialog.accept()
self.character_dialog = None
return data
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 %s") % 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 or self.device == 'TREZOR':
vbox.addWidget(gb)
gb.setTitle(_("Select your seed length:"))
bg = QButtonGroup()
for i, count in enumerate([12, 18, 24]):
rb = QRadioButton(gb)
rb.setText(_("%d words") % 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,10}')))
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 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 = _("%s Settings") % 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():
dialog = QFileDialog(self, _("Choose Homescreen"))
filename, __ = dialog.getOpenFileName()
if filename.endswith('.toif'):
img = open(filename, 'rb').read()
if img[:8] != b'TOIf\x90\x00\x90\x00':
raise Exception('File is not a TOIF file with size of 144x144')
else:
from PIL import Image # FIXME
im = Image.open(filename)
if im.size != (128, 64):
raise Exception('Image must be 128 x 64 pixels')
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)
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 %s. If you have mutiple devices "
"their labels help distinguish them.")
% 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 %s.") % plugin.device)
pin_msg.setWordWrap(True)
pin_msg.setStyleSheet("color: red")
settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
# Settings tab - Homescreen
if plugin.device != 'KeepKey': # Not yet supported by KK firmware
homescreen_layout = QHBoxLayout()
homescreen_label = QLabel(_("Homescreen"))
homescreen_change_button = QPushButton(_("Change..."))
homescreen_clear_button = QPushButton(_("Reset"))
homescreen_change_button.clicked.connect(change_homescreen)
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 %d x %d monochrome black and "
"white image.") % (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 "
"%s device can spend your bitcoins.") % 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)
================================================
FILE: plugins/trezor/trezor.py
================================================
from .plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore
class TrezorKeyStore(TrezorCompatibleKeyStore):
hw_type = 'trezor'
device = 'TREZOR'
class TrezorPlugin(TrezorCompatiblePlugin):
firmware_URL = 'https://wallet.trezor.io'
libraries_URL = 'https://github.com/trezor/python-trezor'
minimum_firmware = (1, 5, 2)
keystore_class = TrezorKeyStore
def __init__(self, *args):
try:
from . import client
import trezorlib
import trezorlib.ckd_public
import trezorlib.transport_hid
import trezorlib.messages
self.client_class = client.TrezorClient
self.ckd_public = trezorlib.ckd_public
self.types = trezorlib.messages
self.DEVICE_IDS = (trezorlib.transport_hid.DEV_TREZOR1, trezorlib.transport_hid.DEV_TREZOR2)
self.libraries_available = True
except ImportError:
self.libraries_available = False
TrezorCompatiblePlugin.__init__(self, *args)
def hid_transport(self, device):
from trezorlib.transport_hid import HidTransport
return HidTransport.find_by_path(device.path)
def bridge_transport(self, d):
from trezorlib.transport_bridge import BridgeTransport
return BridgeTransport(d)
================================================
FILE: plugins/trustedcoin/__init__.py
================================================
from electrum.i18n import _
fullname = _('Two Factor Authentication')
description = ''.join([
_("This plugin adds two-factor authentication to your wallet."), ' ',
_("For more information, visit"),
" https://api.trustedcoin.com/#/electrum-help "
])
requires_wallet_type = ['2fa']
registers_wallet_type = '2fa'
available_for = ['qt', 'cmdline']
================================================
FILE: plugins/trustedcoin/cmdline.py
================================================
#!/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):
@hook
def sign_tx(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
================================================
FILE: plugins/trustedcoin/qt.py
================================================
#!/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 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 .trustedcoin import TrustedCoinPlugin, server
class TOS(QTextEdit):
tos_signal = pyqtSignal()
error_signal = pyqtSignal(object)
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
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()
@hook
def sign_tx(self, window, tx):
wallet = window.wallet
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):
auth_code = self.auth_dialog(window)
else:
self.print_error("twofactor: xpub3 not needed")
window.wallet.auth_code = auth_code
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:
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.') + ' '\
+ _("For more information, visit") + " https://api.trustedcoin.com/#/electrum-help "
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 everytime you run out of prepaid transactions.') + ' '
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 %d prepaid transactions.")%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 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():
valid_email = re.match(regexp, email_e.text()) is not None
next_button.setEnabled(tos_received and valid_email)
tos_e.tos_signal.connect(on_result)
tos_e.error_signal.connect(on_error)
t = Thread(target=request_TOS)
t.setDaemon(True)
t.start()
regexp = r"[^@]+@[^@]+\.[^@]+"
email_e.textChanged.connect(set_enabled)
email_e.setFocus(True)
window.exec_layout(vbox, next_enabled=False)
next_button.setText(prior_button_text)
return str(email_e.text())
def request_otp_dialog(self, window, _id, otp_secret):
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)
return pw.get_amount(), cb_lost.isChecked()
================================================
FILE: plugins/trustedcoin/trustedcoin.py
================================================
#!/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
from urllib.parse import urljoin
from urllib.parse import quote
import electrum
from electrum import bitcoin
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
# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server
signing_xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL"
billing_xpub = "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."),
]
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 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))
response = requests.request(method, url, **kwargs)
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):
"""
Tranfer 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
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 get_max_amount(self, config, inputs, recipient, fee):
from electrum.transaction import Transaction
sendable = sum(map(lambda x:x['value'], inputs))
for i in inputs:
self.add_input_info(i)
xf = self.extra_fee(config)
_type, addr = recipient
if xf and sendable >= xf:
billing_address = self.billing_info['billing_address']
sendable -= xf
outputs = [(_type, addr, sendable),
(TYPE_ADDRESS, billing_address, xf)]
else:
outputs = [(_type, addr, sendable)]
dummy_tx = Transaction.from_io(inputs, outputs)
if fee is None:
fee = self.estimate_fee(config, dummy_tx.estimated_size())
amount = max(0, sendable - fee)
return amount, fee
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])
assert price <= 100000 * 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 sign_transaction(self, tx, password):
Multisig_Wallet.sign_transaction(self, tx, password)
if tx.is_complete():
return
if not self.auth_code:
self.print_error("sign_transaction: no auth code")
return
long_user_id, short_id = self.get_user_id()
tx_dict = tx.as_dict()
raw_tx = tx_dict["hex"]
r = server.sign(short_id, raw_tx, self.auth_code)
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
# 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(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
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 get_tx_extra_fee(self, wallet, tx):
if type(wallet) != Wallet_2fa:
return
address = wallet.billing_info['billing_address']
for _type, addr, amount in tx.outputs():
if _type == TYPE_ADDRESS and addr == address:
return address, amount
def request_billing_info(self, wallet):
self.print_error("request billing info")
billing_info = server.get(wallet.get_user_id()[1])
billing_address = make_billing_address(wallet, billing_info['billing_index'])
assert billing_address == billing_info['billing_address']
wallet.billing_info = billing_info
wallet.price_per_tx = dict(billing_info['price_per_tx'])
wallet.price_per_tx.pop(1)
self.requesting = False
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(DISCLAIMER), 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 >= 24:
assert 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 BaseException('unrecognized seed length')
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, k1, k2):
k1.update_password(None, password)
wizard.storage.set_password(password, encrypt)
wizard.storage.put('x1/', k1.dump())
wizard.storage.put('x2/', k2.dump())
wizard.storage.write()
msg = [
_("Your wallet file is: %s.")%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 "%s" to close this window, move your '
"wallet file to an online computer, and reopen it with "
"Electrum.") % _('Cancel'),
_('If you are online, click on "%s" to continue.') % _('Next')
]
msg = '\n\n'.join(msg)
wizard.stack = []
wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('create_remote_key'))
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 = 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(signing_xpub, long_user_id)
k3 = keystore.from_xpub(xpub3)
storage.put('x3/', k3.dump())
storage.set_password(password, encrypt)
wizard.wallet = Wallet_2fa(storage)
wizard.create_addresses()
def create_remote_key(self, wizard):
email = self.accept_terms_of_use(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(signing_xpub, long_user_id)
# secret must be sent by the server
try:
r = server.create(xpub1, xpub2, email)
except socket.error:
wizard.show_message('Server not reachable, aborting')
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']
try:
assert _id == short_id, ("user id error", _id, short_id)
assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3)
except Exception as e:
wizard.show_message(str(e))
return
self.check_otp(wizard, short_id, otp_secret, xpub3)
def check_otp(self, wizard, short_id, otp_secret, xpub3):
otp, reset = self.request_otp_dialog(wizard, short_id, otp_secret)
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:
wizard.show_message(_('Incorrect password'))
return
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)
try:
assert xpub1 == wizard.storage.get('x1/')['xpub']
assert xpub2 == wizard.storage.get('x2/')['xpub']
except:
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 = regenerate_key(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.check_otp(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, 'create_remote_key'
================================================
FILE: plugins/virtualkeyboard/__init__.py
================================================
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']
================================================
FILE: plugins/virtualkeyboard/qt.py
================================================
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
================================================
FILE: pubkeys/Animazing.asc
================================================
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: SKS 1.1.4
Comment: Hostname: keys.fedoraproject.org
mQENBFD1gzgBCADGVp8wVMk0viUTR3FBzCfdGGULYcyiKFNdGlKxORyrdWJF6/g5JuYPU0NX
CRJ7hqX4hopOiID+oxE7p/vP/i/Sm6B6xZMq8bJ+PiJ2h8ZqnourgL8tkAqlDV+zLana+XeQ
PsPhJPeARAQDtl5QhQbvWm0idMyd1zuWdt4OVIYnhJ7w7Mw0CdUmBbkTc1P23J8vwqiyyuHq
V3JimkNJLm4vyWGFig6ElgRMbV5YWci41OTH3x8qkWHFdB1h0ODP/28bBwpVVXqnObrp/Lsr
9aQSP5VujGIL/cOdel/7xD2Dc5qS/HXYZgJUk6x2WkLmO47NSpW6rone3W2A82gkKRwXABEB
AAG0H0FuaW1hemluZyA8YW5pbWF6aW5nQGdtYWlsLmNvbT6JATgEEwECACIFAlD1gzgCGwMG
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJECJFMARpVQb95i8IAISo2NvZW8Zln4ivuo5l
6PirIrzSIlec3Xa/xmkgrHTfBVT3WYeGp54NkEcpgarD1qjt5ZFF7wExn9WLErGlsIkPmegO
Zjx2mn3ZyB+Y5y0cJeGPM72aD0Z3jaBi+htv37GMb2mV6yO700dG1x3vwS1YYyRLgR6pVwlR
w+0l+5B/5zHsKdRvpRcuTpmrg8rtgTxZOsRyORn6ck6w7lUbno3+1XMBTFTbVf4/SymMU3Wl
9eru2agYRfkB3puXd8DMYlSzZiiUWTHV6bOsB0N001sJtmFf6KmM1xF8Q/eqMlWevS2sd0XG
OWFQu4RGyAJqN7h1Z3cym278WVCMhYzJn/SJAhwEEAECAAYFAlHaodMACgkQuW8jAK0Ry+6x
ohAAj1Cka1AoIDmPcU5IlPLPwsYoyfLmkAbcpeNKZA6Yf4pm96i3tKXbLgCxSrejSmraVIYZ
oNGFtmAx+rpwdZtDWyFWGlMkDhjRY1rQ6nlkO8CtPU/brbyZJci2J67U7hUsJt+O5gaftclG
2RcC61BQz3Ee0Fl8JKSE5V2KqOD2W1BTCTYrnpY5YdMB8qSalR8txZaSqkA3xpSrIwJ04EHT
1uyQAJiacQ0ynHpcU/xfVBnzj3Hc0mRxBTI5Nznk8NAHleY5OSiZX5YJb3mvycdV34mGXnna
yk9S8HotWs5kLpMQ2TpXOiZK4/lJBlp+q3ksj+01qoqTrRXijI+6z9QRS6y7AkSWO0PaV9Ui
uARH5bBV5qzzT2q9vtYTEkoGHj1IyoW70l2TxyfQLQBVNcYp864+TQJmxiVIVUEMSsJ7Vnoi
fzIvgBz8/d62POQH7zvmnWA7VvYD8Pn3dXVOF7sSOxqJ+YLxcOkjpgqI8ef0Jiypa0w5Tguj
pJ/SVWpeP410SlK4w+ICmxc3shR/pdJfVhgRGS6e8A4GCHnDHg8ksLJ8qoUmPlOWJjdfjakz
QD5vvAV0KwWQ0mu9ca3C3NjivYSWZdiHzeQE5hXqVV8wlghshFKOcm81vdB/hsay5hthilSS
L9trmnNdYNppZHc0bV6PYpYpHWaD1NpWQSbIeiW5AQ0EUPWDOAEIAMzeN8NjFRB53Fid/Iag
QxxQZeD1Y9cn9S6fJ+nEoZWMPQ+iVCictH3gmqm+eZ0qOrqSTxkXgyIh7Uiu2lwjm7nmeBK3
28k2ei1e3bpp0I2oMiShVAehxbXx5Jh29nlPDS8US/xehj6WnUeojR2qXVOSAtK7+V7taSyi
gOpYr/+5fcq+/8Z+d+NaVp97MUmoH5LVNU2jCR+qyIM07qX2G4Vx+hU4tjc79Kl6m1equ6/s
HwnkmMIsGvLHZG+Pq+1wHIZwt1p39MhFvob9B2RFqbFtihTal36D0NBQMdlp9PtKtQZSQR5h
WsmWG+m17/vyux7ZtwnTRbR+p/adFx6fsTcAEQEAAYkBHwQYAQIACQUCUPWDOAIbDAAKCRAi
RTAEaVUG/aARB/9pYfkB4rDe4OulrUXBusTxOGKm1yPjZaXveOqQkD8/2Vwxu7tYFMVMLDKY
tvxpmrUdb6oz9s8XrTkMIW7ZM3FmhNQhxsfEj9g6UGJnCXcf+Zw9wbqmzONNCupfN5wPOtoG
d+KFUR5dVl3+BsUoIEcAcPR5AdP9gbqDSbrDyuk8+j1CfiLVAQXud3u2rkt8GWbTJ/LzKtF1
UM0X3YnE7fxi61DDgX6A1EJfss3aFj2lwmb38Yp041WKik2ZZEQpyQ8rXeYG8Wl0wSEYaMPD
edEHRDoIAYRshRHglZYCD80LF7c31koGLGzVWio3erJgUYUwhy1QUltdVL+5PcVyUlm1
=4eNp
-----END PGP PUBLIC KEY BLOCK-----
================================================
FILE: pubkeys/ThomasV.asc
================================================
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.11 (GNU/Linux)
mQINBE34z9wBEACT31iv9i8Jx/6MhywWmytSGWojS7aJwGiH/wlHQcjeleGnW8HF
Z8R73ICgvpcWM2mfx0R/YIzRIbbT+E2PJ+iTw0BTGU7irRKrdLXReH130K3bDg05
+DaYFf0qY/t/e4WDXRVnr8L28hRQ4/9SnvgNcUBzd0IDOUiicZvhkIm6TikL+xSr
5Gcn/PaJFS1VpbWklXaLfvci9l4fINL3vMyLiV/75b1laSP5LPEvbfd7W9T6HeCX
63epTHmGBmB4ycGqkwOgq6NxxaLHxRWlfylRXRWpI/9B66x8vOUd70jjjyqG+mhQ
+1+qfydeSW3R6Dr2vzDyDrBXbdVMTL2VFXqNG03FYcv191H7zJgPlJGyaO4IZxj+
+O8LaoJuFqAr8/+NX4K4UfWPvcrJ2i+eUkbkDJHo4GQK712/DtSLAA+YGeIF9HAn
zKvaMkZDMwY8z3gBSE/jMV2IcONvpUUOFPQgTmCvlJZAFTPeLTDv+HX8GfhmjAJY
T5rTcvyPEkoq9fWhQiFp5HRpYrD36yLVrpznh2Mx7B1Iy8Rq/7avadwVn87C6scJ
ouPu+0PF3IeVmYfCScbfxtx1FaEczm8wGBlaB/jkDEhx0RR8PYKKTIEM7T2LH2p6
s/+Ei4V7mqkcveF/DPnScMPBprJwuoGNFdx2qKmgCKLycWlSnwec+hdyTwARAQAB
tBlUaG9tYXNWIDx0aG9tYXN2MUBnbXguZGU+iQI4BBMBAgAiBQJN+M/cAhsDBgsJ
CAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAr1YJLf5Rw5hlhD/9T4I/sBCleS9nH
njTJqcOnG28c9C3CRYIizjEui/pKmXz9fB1N9QrCaruPUQx2UacDVCl6dKxac+7s
s3/a6lsjaRn0/2OM/sCVLScyxNPNPQs2b6jkodSNPIM8zv51g+flhwtfrO6h6B4j
IhZgSjFdvqtZd5jaly9rA0uMX045CC4K6HGnq8n4F2p31z0L0LaHBf5EcsCM0MMp
QVkY0aUrNg9uVMGXBHn3osHnOtQaODqcIbpa/OG+Tlt6pVOiDJ7i8TkpQKT7sOaM
VdL//TEoDIOC7qVCN82q2q/gtiBXbziaERVs/eU0O52aX5qUhXu3VIjXTp/riRim
R/f9BPB1dgDZbF2aPZ/rJm26v82ft7gP1Sf52E9MrAaZATTfI0/TUHXeBzN93EA9
xb6/ENAMTX74u+NjlynWPD+hl64eBzJ2ionZF1bJFTgBkMfRYnhllvleCjcq9YfX
md5HKCwtxfygBIujUQSwyUzn0f5DbVCJ7/B19bKdvHGSSBgBEjxqXWQskm2wc0In
ww63goZAGDQliKhIT8xnwOBbLkqSobq4tD9zpQyxvMA2rhy7/gfFRp7TTak7MZHf
lTJ37S5LvcWHm/ccWUZDUN7akoEDc+m6jX3uIEPMD3PQvcHhWv0amco3zDr1qb/+
rXM7TJKd7DPX0E2dRzKu6aYRMTbklbkCDQRN+M/cARAAvktgmJflLg8bz3VMyKF6
OHkpWBzfr9HOBZgmQTborznm35Z0BrqykcpHVGalNOydpNdoL9s3Elmr7S+L0YqX
D3i3AS567S8KeKKenLoAV7J294KL6p2vldDiHHUNjXWSe0YEvbAw62tnigB4Wee0
dhwAxhowFjmnyB3dZVHZ2Ai+k5x7NAEvCGgwec3oD4kRDbtkdyiXAM72Jz63hgC2
XnnYM/XEptxPgPUkPQnpboVtQkSU5dF5KM0YK5ItllOwLMyDQ/pEfBZQq0O9Eqsr
8zc2H8vYSxbmYBCdimzpr6HJWRV32QuyiC0c8TjG/fenNsMHliP7bOp2NhSo784N
+Q/4eM1G/KO4cvpbduNToWe4lBdfiWWySWziGUoM9rBrdy5aex1p+i4Bk7FbAGUb
KxFnsFoc4URM6UB5Q3URh8WtyRx+RjNNfs0MDRL31pfvfTP9z1eueoznY470utIV
Gdhn4rczzPYUW+j39m2qpMMfT+gf5rLQLC2jBKi9lSiWMBDzOaaLwK08MUeDiytp
7xjFbh962ftC0/qs5VHBALI/XYvO0LuhgY55y1Tkb0aRUO5s1bjxW57HT+6k/1E5
GMo3xq0GuhJyaiZHmFKHGaB0kHLgj9MORUVtmr+FE9JMAeCdyIQLrtNiGzPNc/ed
Ofn8CHTAIYJ3cd+I1fobsx0AEQEAAYkCHwQYAQIACQUCTfjP3AIbDAAKCRAr1YJL
f5Rw5olED/90sdEGN4CQ1tGxjkY1NZRnNevdULm82qnCk/2srhBMCamXeJTkX7Cy
AwbvyJLZc5X6KNkDwq+KyGV61Amg0zFLU9TiwDhjfiFhvrWm0ez4+bA2lx2CMBf6
YKH3FDTzXobIhpe3G3FpwklbcrTt+ooiQmMhI87JMtJ37CXUu1ETZ8Drukyakwcb
y35E+rV2n97eqnovuNzpfdT5ufabu7ZyARVu4CdJooagxIyex3vUu1/Vvr8PC9nP
8cG20SByBKY+V4dRXIEPgsOtyNMMrvN0QzDtv9w8Ge7oKZNyiAUSq8Hif1ih2j7i
KPsCd1RPPgmPWny8WWce+FGDzYwN8ZQ8n66MGNthdE6z5YCZoJDxOCNnd0AU0Ws6
oBti7pCVjbF+7u+P6u5zoCt2YGs6WwkWY2zEVEl+B1S6DkkUhQ3vsZuOKweNZ6bx
xjezO7ISR7wfamLzTcIC5W2Rqg2Y0MewQGJ5+3fhmjRX4D+rbCSLHdL2/JGM4qqE
atbfSrGdXpkV7G55a47eo5DRlTn9MgfSR+3afjn9F7F4Gr8innbMTW8S9ORCYEJ8
IZmapO771GjyqyN5zFU3HJU+XCGWKavKJD509olho5J+LAQQyHYzJXlzq3qo32fq
MV2IIs6WaYdkUDzEOh2yh9ASaW/4j/HIRnudrqfFullW48LcHQkW1pkBDQRQ9YM4
AQgAxlafMFTJNL4lE0dxQcwn3RhlC2HMoihTXRpSsTkcq3ViRev4OSbmD1NDVwkS
e4al+IaKToiA/qMRO6f7z/4v0pugesWTKvGyfj4idofGap6Lq4C/LZAKpQ1fsy2p
2vl3kD7D4ST3gEQEA7ZeUIUG71ptInTMndc7lnbeDlSGJ4Se8OzMNAnVJgW5E3NT
9tyfL8Kossrh6ldyYppDSS5uL8lhhYoOhJYETG1eWFnIuNTkx98fKpFhxXQdYdDg
z/9vGwcKVVV6pzm66fy7K/WkEj+VboxiC/3DnXpf+8Q9g3Oakvx12GYCVJOsdlpC
5juOzUqVuq6J3t1tgPNoJCkcFwARAQABtB9BbmltYXppbmcgPGFuaW1hemluZ0Bn
bWFpbC5jb20+iQE4BBMBAgAiBQJQ9YM4AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIe
AQIXgAAKCRAiRTAEaVUG/eYvCACEqNjb2VvGZZ+Ir7qOZej4qyK80iJXnN12v8Zp
IKx03wVU91mHhqeeDZBHKYGqw9ao7eWRRe8BMZ/VixKxpbCJD5noDmY8dpp92cgf
mOctHCXhjzO9mg9Gd42gYvobb9+xjG9plesju9NHRtcd78EtWGMkS4EeqVcJUcPt
JfuQf+cx7CnUb6UXLk6Zq4PK7YE8WTrEcjkZ+nJOsO5VG56N/tVzAUxU21X+P0sp
jFN1pfXq7tmoGEX5Ad6bl3fAzGJUs2YolFkx1emzrAdDdNNbCbZhX+ipjNcRfEP3
qjJVnr0trHdFxjlhULuERsgCaje4dWd3Mptu/FlQjIWMyZ/0iQIcBBABAgAGBQJR
2qHTAAoJELlvIwCtEcvusaIQAI9QpGtQKCA5j3FOSJTyz8LGKMny5pAG3KXjSmQO
mH+KZveot7Sl2y4AsUq3o0pq2lSGGaDRhbZgMfq6cHWbQ1shVhpTJA4Y0WNa0Op5
ZDvArT1P2628mSXItieu1O4VLCbfjuYGn7XJRtkXAutQUM9xHtBZfCSkhOVdiqjg
9ltQUwk2K56WOWHTAfKkmpUfLcWWkqpAN8aUqyMCdOBB09bskACYmnENMpx6XFP8
X1QZ849x3NJkcQUyOTc55PDQB5XmOTkomV+WCW95r8nHVd+Jhl552spPUvB6LVrO
ZC6TENk6VzomSuP5SQZafqt5LI/tNaqKk60V4oyPus/UEUusuwJEljtD2lfVIrgE
R+WwVeas809qvb7WExJKBh49SMqFu9Jdk8cn0C0AVTXGKfOuPk0CZsYlSFVBDErC
e1Z6In8yL4Ac/P3etjzkB+875p1gO1b2A/D593V1The7EjsaifmC8XDpI6YKiPHn
9CYsqWtMOU4Lo6Sf0lVqXj+NdEpSuMPiApsXN7IUf6XSX1YYERkunvAOBgh5wx4P
JLCyfKqFJj5TliY3X42pM0A+b7wFdCsFkNJrvXGtwtzY4r2ElmXYh83kBOYV6lVf
MJYIbIRSjnJvNb3Qf4bGsuYbYYpUki/ba5pzXWDaaWR3NG1ej2KWKR1mg9TaVkEm
yHoluQENBFD1gzgBCADM3jfDYxUQedxYnfyGoEMcUGXg9WPXJ/UunyfpxKGVjD0P
olQonLR94JqpvnmdKjq6kk8ZF4MiIe1IrtpcI5u55ngSt9vJNnotXt26adCNqDIk
oVQHocW18eSYdvZ5Tw0vFEv8XoY+lp1HqI0dql1TkgLSu/le7WksooDqWK//uX3K
vv/GfnfjWlafezFJqB+S1TVNowkfqsiDNO6l9huFcfoVOLY3O/SpeptXqruv7B8J
5JjCLBryx2Rvj6vtcByGcLdad/TIRb6G/QdkRamxbYoU2pd+g9DQUDHZafT7SrUG
UkEeYVrJlhvpte/78rse2bcJ00W0fqf2nRcen7E3ABEBAAGJAR8EGAECAAkFAlD1
gzgCGwwACgkQIkUwBGlVBv2gEQf/aWH5AeKw3uDrpa1FwbrE8Thiptcj42Wl73jq
kJA/P9lcMbu7WBTFTCwymLb8aZq1HW+qM/bPF605DCFu2TNxZoTUIcbHxI/YOlBi
Zwl3H/mcPcG6pszjTQrqXzecDzraBnfihVEeXVZd/gbFKCBHAHD0eQHT/YG6g0m6
w8rpPPo9Qn4i1QEF7nd7tq5LfBlm0yfy8yrRdVDNF92JxO38YutQw4F+gNRCX7LN
2hY9pcJm9/GKdONViopNmWREKckPK13mBvFpdMEhGGjDw3nRB0Q6CAGEbIUR4JWW
Ag/NCxe3N9ZKBixs1VoqN3qyYFGFMIctUFJbXVS/uT3FclJZtQ==
=U0io
-----END PGP PUBLIC KEY BLOCK-----
================================================
FILE: pubkeys/kyuupichan.asc
================================================
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mQGiBD7CrZkRBADjXAk5ONxSGO01JMqUjwQFZAwGynWcEVF093g6SezPXsMN/HEP
PoxndqVhtOOI5M7ZezusYzUEH4RJNEDc/JmrQh9d6Q+CanaLuOn3c84XvgKVAmID
ogMrxeQ7biCn9NyZf6EjqP4QGBwWQdsrb2CvK+NO8h2oLo2Aycv+hciDlwCgoLPl
scZLBvfwNRdcajG45g5NfmMEANqZLvax6e5Wr9lHpfZFQch3ai5hP5dDetA0jeR4
nzeWdbwD4Aph3AXrZt4c64qwkSi3ElS5UKzNRsO6APF3+COR1VXJHyN5ve6jxpuY
69zq9ZM3KwYHztoKfHlXJqTtOx9U0DSK/xwphI+Q1neMBr3RyL8muKQvlESNuN+3
n47+A/9+5pWs8m060jRcs7W1zAYOBCv5fb3Zeot6TgkprDrrRFitcyXbB6k1zyMh
iqUMce5Ht785AWYZuCTjvbxJ2yuT37s26OswyXDoBGwwcPgaFyEO3SBRGaoxvTrW
NQ+gpiW8ILjXCBoJdgPcOlxjj+mhRp3OQ1TJZojMBgio56IWbbQtTmVpbCBCb290
aCAoYWtpaGFiYXJhKSA8bmVpbEBkYWlrb2t1eWEuY28udWs+iFkEExECABkFAj7C
rZkECwcDAgMVAgMDFgIBAh4BAheAAAoJEHV+VfRE0xInLkgAn0c/8n77dUl71F7q
aMolNZ8KmxZcAJ93ESJQ6UWhnyUxHm8l4OFdXXu4RohGBBMRAgAGBQI+0it6AAoJ
EMXAxcchjRjXF7MAoM88e9oNMPdUpeu/hUMmZw6AL+AqAKCPgRmICbdH2fQYR99E
Lyw1wGwii4hGBBMRAgAGBQI+0jp/AAoJEDiaVjzCcqEmYpoAn1kwGm12ovyG4PwS
5rN+Z46FE5wUAJ9tQkDrxF4yhTkCU6KVOje/tufBdohGBBMRAgAGBQI+0la2AAoJ
ELfOmxk3oYfG1yoAoIhyQvVnzA3AUPmpJZP/sfCqzRrNAJ9LmVN+b63BfgyXkJbu
K0w792EDS4hGBBIRAgAGBQI+0pzOAAoJECIYyB6OfAP/1G0An0XtPhXIf+Z8VHrb
u9d+e7tJodQ1AJ4v1rH9v/NopQtHdcnxFvI9alEPH4hGBBMRAgAGBQI+06DGAAoJ
EC4s9nt3lqYLLuoAnRm+RnRj3RJJxE42yTzLP7GslzazAKDO3CHEtgPbj8DseXKS
2jiwi8g9RYhGBBMRAgAGBQI+1BTBAAoJEElFpTfXe0P7LQcAn23wIm2Pg6nO+dBO
8oBrmARV0+ImAKCcO5k/5ByeqUMHy7lKx3HVzOET8YhGBBIRAgAGBQI+1K4rAAoJ
ENGVGa1MfyvuLTIAoITJh0RbLqqsoyKMIPA3DWWs8iUOAJ4g4HO/rL4foavPOyzn
BDaHoDBJ1IhGBBIRAgAGBQI+1Y1MAAoJEFC7KXQtWafSrP8An17bXzC3iyywvnC1
W7RfvjohMzzQAJ9v6BJn9xRORSIHY4ifqfU6BPtVUIhGBBMRAgAGBQI+1M8FAAoJ
EEXlkGj5G7efrxoAoIKXAOjFCrxhlNSIFUpDkgtxaPfyAJ9LYM88b7Jpz/octbEH
6o6lx8B5lIhGBBMRAgAGBQI+1lmrAAoJEFI0hF3yuSD1WDgAn0CjIP3eHGFoNTMF
BTefl5KQWFjlAJ0bt40eHF2hp46HKS6Bbl2p09FFyohGBBMRAgAGBQI+1mfTAAoJ
EG4Dj17go4N34MMAn0GSwAMasmCfBUhFLRxy4uYSy2mhAKDZ2uklcaBauLQyjFoL
M1NMhMCJ/ohGBBMRAgAGBQI+1r14AAoJECTxPj/mjACSpH8AoIxJsUJr3iUoYCzZ
0220/CbBO9kyAJ49dc3iiBArycmnGjbkSLnktpiqwYhGBBIRAgAGBQI+1itWAAoJ
ECn45GVniJZfNQoAoIfjMXEsjsnaHI83sNdrBmN4knQDAJ9NAsEIxzaGFfQmc33E
MhxRpo2cE4hGBBMRAgAGBQI+2BXeAAoJEFlRJ0yBj+NA09QAoLN99g2hp7h7onqW
MIc4T9WYWKgiAKCD/4r1f7vrqR/sSYKHrahI5PbysYhGBBARAgAGBQI+3GFvAAoJ
EGcvIifCwHAobfQAn01wzHEdfIeWy1X44PF3EDA1izBAAJ9oLQ3ES3Tv7R1rPPlo
rSCPocO324hGBBARAgAGBQI+4mQfAAoJEHFzfab4xNFPBgUAoOk8J3Xe3ko2SPjM
d84JfXbIijW+AKD5b7NfeVAkz9mNhYePqU53SpQvQYhGBBMRAgAGBQI+2U6qAAoJ
EFHGMyB5fcdf47MAnAprssI6tke7MQuiqf+xOPO2XxS7AKDOn8YOQDEJtlm8qRW/
SGG0DJ5MBIhGBBMRAgAGBQI+44T5AAoJEN5HUcxjjSIaSpcAn0pWC2xioUmTdjnJ
OVJPV6m3h7TYAKCm3tIRQ/n1e3+SjrZ6B6u9arKRiYhGBBMRAgAGBQI/ASS3AAoJ
EDC3rnBH3fqFUAoAnA5k7w/Y6FCkgSBDqjC8Mz9e+DQ7AJ4npy3wXIW6gk9UD484
6CFTk7rwHIhGBBMRAgAGBQI/ATFzAAoJEF1s/WZ+hdAzbf4AoMjm6YMw4TfxTASb
85EfShS6LdH5AJ4zDq7QAvGkPjTY2JLlwHph3afyHIhGBBMRAgAGBQI/K8oEAAoJ
EHx6uUUZG8DsJYQAn0VwLcYxa3f7Ovk6m+xJUzcpcMMIAJ9F0JRKAS2w//tkMPab
8YZ+sYECv7QhTmVpbCBCb290aCA8a3l1dXBpY2hhbkBnbWFpbC5jb20+iGIEExEC
ACIFAk/485MCGyMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEHV+VfRE0xIn
mTYAnid43IhUSJp4f35yw7V48uWI/PHiAJ9O0+8kcnVukXQOLpKgQekH8GIevbkB
DQQ+wq2lEAQAlZjNXxfrUYxJ8ewd0LIGmig3HSKjREHc7vuN76P56KzH7pdEENaa
rhY7XggGQ826MyFgkpRp7LLNR8LNJb6JR4tPeeUFpS6Xyz2tj3UHIkLTbLeub3em
W3/oinoO3bbzuZ2hVx2GyR2yVlM5hbjUFayne0D5KfSHzCEJ5SLMn0cABAsD/i6c
62u9tAkMRsrWAjcd8z5PLf+Gp5MuviE396gP0CCdcwo3K7RAYcUyiJRObv/zktbN
cE+ZwYclB5zJT7kiJlaDmMfwyHBDYond4bV5VKeveGcr8Xy/cEh+cN7CceWoPC8P
wjcnylxM5UZHMrTBlaoLqKvNq/JZk1sDAau/9g9kiEYEGBECAAYFAj7CraUACgkQ
dX5V9ETTEidutgCdERuNxOlRNFORwpSVJcvOgeItgMsAn3RblJxgwKC8S+z1NpV/
q9K8C944
=MRTd
-----END PGP PUBLIC KEY BLOCK-----
================================================
FILE: pubkeys/wozz.asc
================================================
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: GPGTools - http://gpgtools.org
mQINBFFI4SABEAC7U8hKaLjTBQb05k067LzsT+CTfEVvCA675fxp2413SKCLJUL5
sfBZF+2fsXGqZA3SoA79/y83414YYBogOUxM8OFhIQknSSbukpELcbJ2f6w4GQze
xVDak0FH58mLepu4dx27cP+txvC0JCSXFETe4yvXd0nM0MPj/bbHyhWDZ8qp11qW
p1GtOyFL7Xtkt9Wcbhx2/1pA92H1TYnnfsEqXWgH+Og294Vsjp6novaNerjJsRYD
UQucpG6scNw7DNQgRJE/tdWvj8n9Q/RSy60ZekZlLpFd3reNN67nV0ojI25Y5zOu
H0yz0XSFx4ZZakkP3d8oDsMdWPDycFhQjJGUTj4TWfBKo0cn4DzeoPBlFxPoyO8g
ruemw01fawlOA9YXLe6y3J50l3aZOvME0JzCGh5M32MvWmAywVlc47cjr6YA0uOS
uNuQzO4XGIPwVbfIHUz1+9BBffgpeBA97mm60e+UtuAM50fTWx5ddiGlkERXCdps
NCBfptvsM5b0Efa7v4fhRuAj0lOBE5KbgWaOUGLOAnGJ6ZxkCjzmCcGKPICufxLU
2errH6xk9z1poRsJvVH3l5SsIBbFmuntJwiKCHRy/tPzdnGuFHcQLYihcrjSXaer
siiPcOEHjgAezUvNvAy6MnIW7Erl+zYYUS1k0QqAslul6iPZ61JW2hjwrQARAQAB
tBtNaWNoYWVsIFdvem5pYWsgPG13QGtvaC5tcz6JAkAEEwEKACoCGy8FCQeGH4AF
CwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlFI4tkCGQEACgkQA4wJ9GLCT8c5XxAA
tWNVR1pQhpsBq8J2H0xkCxfeRtuNb5gEq9fmusPW+7Dad/CL+KVIfpyfUSii1+Te
zE9cNCb6I7xf7jGWPUrKohl5IPLHo+bYDWIL27EjoDYIW05v0RYyTHA53fsHNX37
mWd39xyu6DZLV2PDfAqILra3VLH/luDI7kpYfvuHAr/AqFAGByKgMr/ZLLOoDcfV
sI9k9jiyp77VHbPjvTIHan9hhTT7Ee8oCSmxTsZ/ySJcMf1ROKUeCq4hGTXchHSX
FXQNveCKChEL862nVuCQ0betcXYKPolQG3DJVfihMaEpQYP3bkyerSTpE1TRbZTt
doeNvwFSvhXfzhOC8UcuwuD4ET9dS+brq/r2IvM8FrcZt8dEfReiu4NbIXGb+V0A
/fqKatYs5cIyy7hY0U/aILUI7WEaxA1EQ4wOcLudgM6Y4hF/df17RMzofCrXrh/2
eYLVVdVMj9pfj/MHdlwJX7K9OCmzsN2e7PNjwu/wgqY//aG2tyRdhjmeI/ybwLYj
y5sJ8GJkekCrnZbkiSgfBxfO0wNPzq+JGF95qVB4RrhMPiOzLOHyU1RUyP8cJqFr
St1fyHtfNa9XN++r7nRTIMFWQL5nyQ9czJEOSKnBMbADtcSs9kxlFYTAMnfpETGu
XLAONS3wufi9qEN4vIwG/xRqc7I4f6CUPMtSwj7AzQaJASIEEAECAAwFAlF1UFQF
AwASdQAACgkQlxC4m8pXrXyCHggAp8qekPNR3N78WrCiEVqama9iqoeHZxhzoNaB
9ELyH2Y8PP8FAcpFSQK+OmoAOwCPBX9+b+jTXwZYhjoMI7yrSdxcUDvE0PUpO3q2
VV5A2fKQuljSmzYuhnjUgA5ZKbj1lwPjbWpXUR6q8schnu4zDzasuz53bqYdDVlb
qO2IBaPhss5Hc98vTIRMHp9rxbET3Ict9a6XtMYhTKye5jf4O5JUOsNPLZWZeSn1
QGxaxbJ4ko+/NXvHN2ZdNoFyZEm4Am2v2m/tKq2JLMVF5SXUtS1c8MCEOO9byjsH
CE/X7GHQoC3IVxR32wdkwzZ2uZcVG7Zm+RvLLzy14WSPjjRNPIkCHAQSAQIABgUC
UYc4zwAKCRAyidZ6Uw5GxjE2EACNU7LHVUpAQFKjxKN03IM0cd/C6coHObvGmpXY
sMFMta5iyXafmo+MGrR52kEr8xrQ5xz7Ozw2RMC83PyBN3P9nEirQT7gm+gA4m8I
3a/jg/py7iwRh49XXY5Xh7eMAlkvy12DMsmkCHx9yQHVv+Rdt5/N+ELvgeL4aZR9
N8/J+6Lf/MV5ZapxXyxessdOQPF7Ik/EMi8iAPZYkjXlA5qsfGONqS/hs8sJJ4sF
rwndgqMTtsqmYbqwKnZgvSPpMj1CclbXorSVjzBn49/9zsCiv2twsVFr2kS7HnsX
a/Xu+4OLdOCVQ7w4kHkrIIuRQ9Xuz5uFNOTADvpjvwbxeu7q1+SORd+kEbPIoT17
ecQoM6/POXpUv7Xcja67PCL1O8xeLLjsxco68uFCtKGSmnztOGtw95+KrLgzLC2a
ron9D6BcXSOrWwlyrxLP+0CWBnXBDRY39yfXF5RZ1tIlKPRBJEyBPJgIPoS5Fxlq
rN+J3TzvVK8bK5QXTi7rGnSxrTM6teE88jOESBuUfT3KS7Lof3k8n+GWUOdqbf/k
K2fb3YG49w0grQq2cBCs5gaaJLao37xQMPAb9ub0dWQMImEvwJmVn5m9ekcM2KcY
Kb+qpRPZcwJEZOPW7nyl4DbqCKpuiL8BSl5vg2T/+8BdlRIVqj8oH5Fwmbz7OR4f
+LEchokBIgQQAQIADAUCUYeHuQUDABJ1AAAKCRCXELibyletfMv9CACcBOWrLaV7
cXnb3NcBt9EBUchTH8J3JCI2zCE4oFNw3VSVzBA/ocLAm51llvmjdyR1A3/6RmOZ
mF9pLKU0opOXBLjZ9paD3D05xlaOesuTNm++lPgolnMjFXBiPX3t5ulUMW998OHP
guQYaxXIOBIGqqZ3SP8p0OMZcpZCNKwfexELmFknyLW2RcHnax5GcCIB+yDl39JU
EbWx85jGcIwGgwSXtxINnhYYjz5zJMjGZfb4h7MdnV3fQxt3+nNVvA5ePTeS7v9E
sQmnjYd6BFekr8crb6z7GQNrJk8DUjipe+PgA93EskHDMjiYrHAhIS+VV6MVGn5F
MJrKjx9homCliQIcBBIBCAAGBQJRlYDaAAoJEO+dUAE5sB2JlIcP/2KY5EDXlXwo
iSe6OhGZNIyC7qU8SIz06WWAPj9xlhjyjl54sWce1yOrwv1ZeytAEsVyXEQKvNGZ
IcTto/RJqiSag0f7J7mZDcw1s48y2EPYHDk5yV96AT5uSaRUkmMpWPMK8Key0hH6
EMfhZp4H/ziEn9VnKgpIpS9nQ/K5a6JjDcwZiM0TsEtfaDEC1/SFtrUA6Rf+yzK3
xLHXhbPnLRlphM/fUoXqS/Zr0ft2eDwUlhbB6dP1ZfrxCbndNq97dOnxnmgPIxZ2
RVX1pek7XOyi+qgWC+TpJwfzGX5d91do9tPjuC6+l8tR+CGrslHW7Bd3i8AcekdX
L74JbQr5CXUUTEEFPZuKY6iOWGoPHyEy/o8VYztuOeWcZ4dBgrvQMBFGbu0wciPe
Q/d6mZw57KuYfBBs8JpBmzi8MfCCh37VAPBMkYMfWk0FuaelTW0eL7prjEn+4Zlg
+iXDOWFp4OtY06aPMc+h0GXK64HBX3YJsYTS7iZNtQxSJK7Li3toREPy5kkhsYxE
3dRTUdM5px5ekZWEGrRORsxOenQ+TStDGqfQwbCj5noUTy3WLBNwNeC7vxpb2NYI
VkNPv/GS/29NBIkBbwZGIS5j82+SaeEo2/9ZfXiYXV2Va2lbH8OTIyhXLFXS6FQo
BzkCFbsCyLICe5PLtwASnYKa4po+nKcWiQEcBBABAgAGBQJRrggZAAoJENtpSKdF
kwASi9gH/3CIAuxg+oBYUDly4tM+oeFiWknDEUcroY6i2BdHq6n0O2mhvmd0+BuM
OGHi2ilYZ9SeKazgFvGBIlD0wdpApW0EWnS6b4CNa87+ckTpBDqXTXNTyWhKuGS/
bWnUDkAxAT5TxfX0jmTDVsjUT5XjL0acgL7TVxJZXbDFRMS4Qt8z6V2Q8JUY7wwD
ArWri5QaYF9OnMm8rj0a+XnAdLvgTIbAy3w12aq0MEiI45++r47iyL0kXxqAFpRo
YywSthtwvbeMDlGsz0HQfNLUszxWaroAyqARBtHvUlYWXmO9Vhr8ycFuZwrZ/GLm
F56CHTUCQUXN37qq6jN0tkBAzN8Z+EqIRgQQEQIABgUCUbytgAAKCRAD0kzvlgTT
5Mv9AJ0dknWViyDyho0xBvdU8otET/DlIwCfZYMlpofCbtxM3k9NY0FClstdBg2I
RgQTEQIABgUCUbywoQAKCRAoUZ/piO1lEM/sAJwO4ULlzNfIKurfByPELQq5kEzU
uQCeJmBPZJZouZgJIZxWtRKwku+ORKqJARwEEAECAAYFAlG8tIkACgkQS6iSTApa
KQMkSQf8DKe+KKk5H0X4r3c5FSsaU4FchP+5BhiE1zhB//US80kJGOcdxLOgY6Wx
QID0csneIhz/RrPwmHBoamlKlBEwMJKVPHeTlEJ1tBS62DyBsfTMZdPLxts9Z3da
Py/sKSuG1bHuDOlm6quzm3h4gEmjCvMnJmWnBGBq/rONz/r8AY2qsAvVWba9lLVw
9Ih5wVLTtUwG7O7D/TYNRPpVNQpm0VOuNImAiAwj8A3iHtYLN0Zq1c8hrlJQ5eF4
aWaX1F/kSNIL4DL1bQMojVd/wmMp41gzI0WcqodFuqLalWC/hxMxKjJ1hP/CGUA1
9jjJYKbZPryIaQzzlqHXe5EaZmvSP4kCIgQTAQIADAUCUbywVAWDB4YfgAAKCRC5
nwhOJxzwubV0EACgd3vRfbH+QCDeB6y2JS6UldPahx4XGCtH24SAhqGwBNSrLKnu
J/8gl9UXc9xRoa5as3/dVR3wvalmQ9If0q2CzzpvhDjr+xFH3DI7oVr31EjzIrs2
U558QBhKL0SnUre0Tzg0gSYGhBeVcaN3hc/iaYdn+qjDQKIbm1MPqion6zPt0EhP
NRjXR6ttHJmr4os0bCG4Dl/GMKYTdfQRiKlhy1R4d1XvaoK4VrFXggt4gpw+YHxx
YW5Mxk4M8+bxBHbLF63v6x1U/Op9v7xHJvUfysCHaZyi4QYy4H93fbbzeYeYEFS5
rNX+/Y0pIbhvLDdhe+mtMSCpFJrssXSISmxkmPYSKZ6BwF8l9UYWBWF5Xwv+d2m5
gyuaqi3QPQRhZKraMpomUbZjr8TZspDdQG3PCxFuu/H23QxijM7it9LOBzxUXe88
jTOKMtqwAHYZWDfvuGQSlkU0O0yz8OJH+sM1+FyAsjp9RM8nCJJNRFzTyxQmad7U
nclErOWOKeV3wTpPS6iMOSv3wOqFIjSvF7xPvHNtL2Qfr8j3zxCMt62X6k8WJVok
/2CyrbN3dXRuUiZZymIez9R4tF3Y+xf1NhqvEh2sew1WkMjtzhBsHWCMjePgqwod
QtdQvP6c08H9tpXrKagph2WVnPFj4hQfGNI9Hc8bH166hDDrEwo/LWDQo4kCIgQT
AQIADAUCUbywVAWDB4YfgAAKCRC5nwhOJxzwubV0EACgd3vRfbH+QCDeB6y2JS6U
ldPahx4XGCtH24SAhqGwBNSrLKnuJ/8gl9UXc9xRoa5as3/dVR3wvalmQ9If0q2C
zzpvhDjr+xFH3DI7oVr31EjzIrs2U558QBhKL0SnUre0Tzg0gSYGhBeVcaN3hc/i
aYdn+qjDQKIbm1MPqion6zPt0EhPNRjXR6ttHJmr4os0bCG4Dl/GMKYTdfQRiKlh
y1R4d1XvaoK4VrFXggt4gpw+YHxxYW5Mxk4M8+bxBHbLF63v6x1U/Op9v7xHJvUf
ysCHaZz/////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
/////////////////////4kCHAQQAQIABgUCUcM1sAAKCRDofrZNT3C3NcoFD/9O
LaxOr5SjwOEdEWGcfnIYrM9W3JTnPv55Sz5zyKkgDd6p3fONnrSVz6UYMEFnkMPF
jn8KodOnyFvqVrkwko8FP8BctI4ztyNG6i9xv3xu9iOqt2n49ANRZM/tUtwTyb7q
BXVQwcMmhVJ/i8MR1yYMraFA8YM6ovMHUszY3gx/x2Yr97S/xAtuauaRO0sZasA1
UyxaZxWHGrek+/tinuuWp0Y1b/iSyW7tC0WV/tFO0C8Iv5tRcel/4b1EsV8he1EB
8qOLBrAXcrJy3eC8H11h9rkfwL85qUf81oHYYg/vfmR4Y151VglclOZM6Y9kLC/X
8yIhraa6dUG2prLxi2xEnl9eovs8A5xhcxPy2CHcJLRwR5xHA1IsrPK4aZsRC6cw
Rt+oLFxLCvGK8MjW8O2qMjn7u/xeBTnkEEOJpe3NQH+1jql3Tt/3ZkPtANR/Oly0
/7tj/CyR37GUWxM6pIXIElhQ19u+5bGwgxdhD9Htwbe6j2LNqD0e3MUrQphojzIk
pjbmvPU8Xpk6upYFAu04+X/DfANWv61MZdiEHhEiySxIlrg2sthRooOURmmXld0L
gzOCwJzMOjcXkaVDt3nubJGhfetHT4K36cAWBfRURMuC0hUQw+WgCBRzNT5nXBDi
6Ng2l79fXlrY3/AQXDf9bH2EYpHBYXLySqBnxwBWS4hGBBARAgAGBQJRzJUrAAoJ
EHbCDUAgGgwVAHgAn2Lm4q4fsoPwvy56i5NBWItY0ReGAJ4jpm+rsXdK+mW8gp4V
r4RQD4bjO4kCHAQQAQIABgUCUcyVjQAKCRAoOKHZD9LVht9TEACeUgvjTWf/C9j3
8fHj8zd5OrTy9PwXXfGvAZ17THgnzZNy1/U6zIhLqdUUwvyqtRU9EYkZ4YFquCPS
qqZbyNBzpjdFARpoxL41xXSATjaaitHrywYYeWTb7USaUkAjOz/5krsGTuVsM2YG
CLrYn2OVFobLZ0alO7D4mrAwLzTyat+WXuJA8V6wJW11r8E7awf3qdoI/VWGbQch
7d3pYgfp6Q6ZRwt0dZDLkKNUYx41x5saM2wo8d/TgcHWuACnVV5eX9v80GToXUsm
12ruI1ctRqEqsgiTLYpBdrevkzZgHTrY/VLTg5MZxWayUvSLn6abKalYNTvs6HFH
2FuqgY0dSS88ozFbt/WzVG8u/2LPJ5loqYBREawJmdYjZCS7TI0gjq0rpL9Ppwhz
3kkNGYKOlXpSiJVMBL2yiBkqLG7KYnzaJrFpG/aupxnxGPHjzdoLOd+Kpi/K+tnK
FoTngfVBJxPdNG8z2a9TqxeLtXNPkJmwKQUQPibVFzKvvVkbSp7wQ0dfyuxPcHXu
O0Jf3D+/AXl/V0BqrjIsoJBWhONavt3RnyiX1a2UDhTE3HFqblctg0E8wpG649dv
vnZW57/a2PAqV6B7ztZ33tDUtLHnMPOT/kWQRdU+x3WKUi+j1GM4dWksLr2FeTUf
C/5Tekmf0w7TjH32rHU3l4Pty8hhpIhGBBARAgAGBQJRzJW+AAoJEAqchymq1vv2
/hsAoKBSCdynoKY1+RY8WF76WJJO0GxgAJ9p+/nu5kaId8j/mcVQfSULBl5MdYhr
BBARAgArBQJR0eauBYMB4oUAHhpodHRwOi8vd3d3LmNhY2VydC5vcmcvY3BzLnBo
cAAKCRDSuw0BZdD9WIHTAJ972DSGqdJ+MlRGxjhWtteRFdrJcgCeLLzVFbi6C380
Ob0T1kEaPnloUrGJAo0EEwEIAHcFAlIz079wGmh0dHA6Ly91bmRlcmdyaWQubmV0
L2xlZ2FsL2dwZy9wb2xpY3kvMjAxMTEyMjQvNjdkYmUxM2I1YThkNDIwNDFmMGIw
OWUyYjdkMjQ0Zjg0MmZjY2I4Y2EzYmZjNzBiNzkzZWVlM2U2NTI4NmRjMQAKCRAV
0KYu0B4ZDKl1EACgPo7uNemY3DIuOpUfuR1gyxRLOy93/dyHZYyrjCA6qqFEfU7g
i/2i2ZGqualW+ot/AtyHztY32e4KQFJ1msnLk9RKwrQUHJCF7HgNo1Hn3dSrmYxH
vAaMUJ6MRSQF2kKJjgbTa754FzRO5BzMqKcgffn1Dl6JVhjNpVk9RYBx0sRGP8tf
qggsSbEYTtf/s08IhZlSnBriUBFU96eabYzbuGdLTGDQusf4OgtkTcu0mhmNv+hV
FgvB8/sMToZzhLC2LyklA9t3W4S+syFnhI7qutLdzgVZfdXe7ZyYT/SN0PZhZQwR
Ehci/hczbE2mO8k10HvUiOo6TDrOBxWGg4EAAXsRTKLvAwcbLuDDSdfALnPx4agq
dX7pNYFMG11ct218p5AdwO8mUxIIvioN9TAs3ZFw2/OMA/I2L5Wm3etvmPHFWUgV
Z++cxXGV/i0rD5VWUxdqtuw+l3Dx5MddzROlCyggzrTE7G8lkxGv/NoxE257/dJh
oeyCnvkdEfZeQJQweeUP14EHcm322AZpgQIqQMYcsrlVJJVEWXitocbfUuGbhIKo
yNi+7y6zbixcNV+OCxhqeEVXJrC8ZVTaIOPuQiPYJCDWQEuClnLQ46lpBVfwPOu1
VVgg2tBiI12jYlOTL6XaZt+Gkm2JRg8kX9CkwbUhtWh7GmYp9jFhUeZQV4kCjQQT
AQgAdwUCUjPT1XAaaHR0cDovL3VuZGVyZ3JpZC5uZXQvbGVnYWwvZ3BnL3BvbGlj
eS8yMDExMTIyNC82N2RiZTEzYjVhOGQ0MjA0MWYwYjA5ZTJiN2QyNDRmODQyZmNj
YjhjYTNiZmM3MGI3OTNlZWUzZTY1Mjg2ZGMxAAoJEP/OHJpPrfGXAr0P/3fJas1n
EGTYJQDSyvVRREklecwqlEsP5tVru+yW7eswSeFPZnZMab+4Bl+D5pLmp9zZ3oFc
2x6ARb/e9uUNd9SkpcOovvw+TxKRFHeRbIokJz82aLEPK8ascYdbHLG+0LMPFyfA
xFYLQD7i18QX1HGN8FdoVuWUVp7/UO7qMcBc436RZhbRMfmSjsLaB9HDny7DOlIq
ScQYtHbQ/qraBUOrGgI0K/APN/17E/7tt0Amzg944YlfOXi54OwekB/i8g0+fIEU
FQ4b8sjSWbOZwUiE6WC2g6TwYTzSmnKunBjuMXGH5se96VkRPdFi5yHYjt77TzEd
b9xIFztpLwRlOEFD+fzzJ2zqqvd319RBn3IG0rwVOC9lr+pRHEYMpd8R7yAd/v1/
kOQppzgHEqzcfNMQgbUlIyoXqnhFbMH+NIkOTH2Lda3GkFC93p+TOqADbwYOBo8g
YfsUnEU/dASA18D/hshRg/Wjlq3sdJkLQfgrqk0CxVOgaIy6iDz4MerXxiLJG4uH
chgmGJLuhEvle9QB6uGNEa48DOETMUDGwPeSPqgUsk5zxgdCq/qOGH2+76S/Tc1K
DwI8MTf+/04dUbbj1whDuH7tfKdEHrCl3UoPV4gwKbmsoMoZG/sfaHqehkW1XlZy
FtHEVf0XDmOEbruyBMjhRLkQc9apAIqqeVxPiLcEExEIAHcFAlIz099wGmh0dHA6
Ly91bmRlcmdyaWQubmV0L2xlZ2FsL2dwZy9wb2xpY3kvMjAxMTEyMjQvNjdkYmUx
M2I1YThkNDIwNDFmMGIwOWUyYjdkMjQ0Zjg0MmZjY2I4Y2EzYmZjNzBiNzkzZWVl
M2U2NTI4NmRjMQAKCRDVc9WxKatM3fF0AJ9FSrSWq9OUE8LRcA+QxbtoZB+DFQCf
V2NIf0wyR8HWLjwLXiG8beEbj2mItwQTEQgAdwUCUjPT6XAaaHR0cDovL3VuZGVy
Z3JpZC5uZXQvbGVnYWwvZ3BnL3BvbGljeS8yMDExMTIyNC82N2RiZTEzYjVhOGQ0
MjA0MWYwYjA5ZTJiN2QyNDRmODQyZmNjYjhjYTNiZmM3MGI3OTNlZWUzZTY1Mjg2
ZGMxAAoJEFRMSGhi299iPBYAn3F7fsXWvWPXwQFM7/BBxU/PUvayAJ42XPHSSVWf
ZEXDT/gkpYG0xhYJ6bkCDQRRSOEgARAAyWjtpoXnq/G8veD10U0ZSG3OMLsHxLsk
okLKigPxWMHSgFwRrdT+t7OQ+T5f4ETerfPBeripsYqtlcajSVhXEciUJRqXMCXN
l1tyawCOvE4Vs+cQN780XzfxfiwUD/NeC5YzEcUBVMfdqNoVJVYtte/niv24PZaV
dRKRWICX1J9wZj3oA8WS0JdKnvYjzQSzldrJ6iIrY7Hyb/Y4a1hCpEelJ9LombG3
5O1f8bi1RNv5fHHLzJLU2Ngwj9ghb9XGD4n0NJAEqOrLpUm4VZ4UyJOj6kvEsOa1
GlKzzizWblSelFtXvwXwtVgOK3kbpJ3rCYN8omckqk7T23rEAYy+4AO2Yqq+xdYL
1qgloAEFtUCUFunnVAx1j5gqqd7TcjBPDqNOgHkfgeVhZ3GmbB/uUsK2PGocGLd/
YTlXf3EGaJpuwYts4mHOF2Vovhww1+LswX5fKKnJ5CzbfC52y+7HMpO4kLUQYkF9
sTXElNaUSh+3qNwsuUevI4QeFsyt21AEjUj+gcLSrbI7xqsJWG1gyfnfVuOj1KDC
YL6hkq+/XmZSXOHbI/7TeCyEsgl6JGtfV6bhZ3Lgd5nEctaou8byDDL+yL9/aie4
jEtC3tWdHWWZxNqROyQEdCXtgEoRXC95MyrcWA0QADtv7KHBh7lekcLh7uF1gLEN
gOUnLVB1r10AEQEAAYkERAQYAQoADwUCUUjhIAIbLgUJB4YfgAIpCRADjAn0YsJP
x8FdIAQZAQoABgUCUUjhIAAKCRC9XExgS3u4A8pXEACd+Q6zyerRstasztj11Jyi
NbAvaGJ/jIk6CEPttBQOrH4/OgeeaaabaVrQuzYtuYJJ9FwemHiRHKQ7xxg9L67k
a5tHwXuxpA0tjilZABsF0/VuENMH6yf4Z2vPrtmJtuL+ZzeXANijWwuzNfOsUMQS
KK20XeHxUXI5WpboZzRBB9ZjYVgEYg2Yy4RuIV903wy90TFrfkCiNyxEctTbr5PB
uq/Sbvv2cfa2n5K6UUWjWAMAJfeePyrEPjDZusUNonIFyQ+tUtpQCzFDSeLKi9MR
HOplTa77BjWSkdcT58EtOJLYYRkJEHjw/Tv8MDMOjMOHAeo4toaWNNOadYV8Ml0a
O7rULVQuTDeJJqZ5YAtcCl/TxAFv4mIVBKs7919KQobPPHUa68F6MQ/N65l27ly0
Vdc1V+O923NcP37Y59f960FmECKDHwrBwMtCKzLtmXDtP+F63s0Nh1S+Oxe47t7N
g4W8qgILwzSqXyST0RzOafYpJWfvj3bhW7h0Sax7tCeDWltTagaxNKhi7Tzjqph7
JAAauIeRzBSisPTdEJw1ww9mI7LlXz4HGp1rV+ynvraDyhC81tZ+xhIng9/Hj/7v
C4oFOuZABohYXjg5sahFYuR2l+kYRz9hA0fKSFNUo07xqL+rDt0Nah0uhEhJG3MS
8HL5UYRXD6gCqUiwimOF1jj5EACiIL1tjlgvftkcaCyLOFbPegUnbwrgcXNoGo0T
D6D2ir5swV6GL2cLxPTGTwXjQ74zNna3vrnRb0p6cgjk0fVUuGvI2gBWewJ55mDO
tTMZv5u06zN3VfnUVlKp+aiNpEhOwoMBhufOMCRk7U0xzV755BxJs2XjO9Fwv3fq
7jnqNlK/sLPOd6KANJyHLNAtkpkUlF3KiMwHCdNZxzila8x7CBfzlfjJz9tMb1Ug
7MSMZ3A4fOp9Uz7vWEie5gu8gPm4rr+ksAylWeNLd6K+ZlkK5Z+jQ3qRU9YBr3BJ
dXePk5WqGuzav5Tmh2S1avEDfD1V4RzW8bdwDowsfsjqXz1dNeD8ahoZN32PwFiN
7dgHKQtUs4NIbXCqeTaD14oNjmR/lCBjP/Vxdahmh7WuQcnKEx1/ZUa8a2JYWxFW
eol1U/rrXuX/CiTs2U4vYDQzpcDJPZCVs6+731q2qWo4HBG9jkQKjuNyxTGhBoTn
kiBEwGug84/fXHq/+kcdYhsyekU8tR45ILH3FVuRuV89JhPGYorC7ThJOu72dYSv
YDvc+GHc7v/f2cqHGUc/ZBWmuD41lZq3hbZMzypCYTBQglnqGj3ttGM6ZhlIW7KR
t/zwAbMPLlFLnrMb7FkvSvHWH3BNAcuhqr4lC0sUd30jtaVsLt0Mwvi9sv+wZ1Gg
rSxUQQ==
=B9ld
-----END PGP PUBLIC KEY BLOCK-----
================================================
FILE: requirements.txt
================================================
certifi
chardet==3.0.4
dnspython==1.15.0
ecdsa==0.13
idna==2.6
jsonrpclib-pelix==0.3.1
pbkdf2==1.3
protobuf==3.5.0.post1
pyaes==1.6.1
PySocks==1.6.7
qrcode==5.3
requests==2.18.4
six==1.11.0
urllib3==1.22
cython
pyqt5
trezor
keepkey
configparser
btchip-python==0.1.23
pillow
pyblake2
================================================
FILE: requirements_travis.txt
================================================
tox
python-coveralls
tox-travis
================================================
FILE: run-docker.sh
================================================
#!/bin/bash
XSOCK=/tmp/.X11-unix
XAUTH=/tmp/.docker.xauth
DATADIR=.electrum-btcp
xauth nlist :0 | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge -
docker run -ti -v $HOME/$DATADIR:/root/$DATADIR -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH -e XAUTHORITY=$XAUTH electrum-btcp:latest
================================================
FILE: scripts/bip70
================================================
#!/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)
================================================
FILE: scripts/block_headers
================================================
#!/usr/bin/env python
# A simple script that connects to a server and displays block headers
import sys
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([('blockchain.headers.subscribe',[])], callback)
# 3. wait for results
while network.is_connected():
time.sleep(1)
================================================
FILE: scripts/estimate_fee
================================================
#!/usr/bin/env python
import util, json
peers = util.get_peers()
results = util.send_request(peers, 'blockchain.estimatefee', [2])
print(json.dumps(results, indent=4))
================================================
FILE: scripts/get_history
================================================
#!/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 ")
sys.exit(1)
n = Network()
n.start()
_hash = bitcoin.address_to_scripthash(addr)
h = n.synchronous_get(('blockchain.scripthash.get_history',[_hash]))
print_msg(json_encode(h))
================================================
FILE: scripts/peers
================================================
#!/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))
================================================
FILE: scripts/servers
================================================
#!/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))
================================================
FILE: scripts/txradar
================================================
#!/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))))
================================================
FILE: scripts/util.py
================================================
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.
'''
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')
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
================================================
FILE: scripts/watch_address
================================================
#!/usr/bin/env python3
import sys
import time
from electrum import SimpleConfig, Network
from electrum.util import print_msg, json_encode
try:
addr = sys.argv[1]
except Exception:
print("usage: watch_address ")
sys.exit(1)
# 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([('blockchain.address.subscribe',[addr])], callback)
# 3. wait for results
while network.is_connected():
time.sleep(1)
================================================
FILE: setup-mac.sh
================================================
#!/bin/bash
# MacOS build instructions
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
# Optionally (this is bad practice but works if you're stuck)
sudo chown -R "$USER":admin /usr/local
sudo chown -R "$USER":admin /Library/Caches/Homebrew
# Python setuptools
curl https://bootstrap.pypa.io/ez_setup.py -o - | python3
# Setup
python3 setup.py install
================================================
FILE: setup-release.py
================================================
"""
py2app build script for Electrum Bitcoin Private
Usage (Mac OS X):
python setup.py py2app
"""
from setuptools import setup
from plistlib import Plist
import requests
import os
import shutil
from lib.version import ELECTRUM_VERSION as version
CERT_PATH = requests.certs.where()
name = "Electrum BTCP"
mainscript = 'electrum-btcp'
plist = Plist.fromFile('Info.plist')
plist.update(dict(CFBundleIconFile='icons/electrum.icns'))
os.environ["REQUESTS_CA_BUNDLE"] = "cacert.pem"
shutil.copy(mainscript, mainscript + '.py')
mainscript += '.py'
extra_options = dict(
setup_requires=['py2app'],
app=[mainscript],
packages=[
'electrum-btcp',
'electrum-btcp_gui',
'electrum-btcp_gui.qt',
'electrum-btcp_plugins',
'electrum-btcp_plugins.audio_modem',
'electrum-btcp_plugins.cosigner_pool',
'electrum-btcp_plugins.email_requests',
'electrum-btcp_plugins.greenaddress_instant',
'electrum-btcp_plugins.hw_wallet',
'electrum-btcp_plugins.keepkey',
'electrum-btcp_plugins.labels',
'electrum-btcp_plugins.ledger',
'electrum-btcp_plugins.trezor',
'electrum-btcp_plugins.digitalbitbox',
'electrum-btcp_plugins.trustedcoin',
'electrum-btcp_plugins.virtualkeyboard',
],
package_dir={
'electrum-btcp': 'lib',
'electrum-btcp_gui': 'gui',
'electrum-btcp_plugins': 'plugins'
},
data_files=[CERT_PATH],
options=dict(py2app=dict(argv_emulation=False,
includes=['sip'],
packages=['lib', 'gui', 'plugins'],
iconfile='icons/electrum.icns',
plist=plist,
resources=["icons"])),
)
setup(
name=name,
version=version,
**extra_options
)
# Remove the copied py file
os.remove(mainscript)
================================================
FILE: setup.py
================================================
#!/usr/bin/env python3
# python setup.py sdist --format=zip,gztar
from setuptools import setup
import os
import sys
import platform
import imp
import argparse
version = imp.load_source('version', 'lib/version.py')
def readhere(path):
here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, path), 'r') as fd:
return fd.read()
def readreqs(path):
return [req for req in
[line.strip() for line in readhere(path).split('\n')]
if req and not req.startswith(('#', '-r'))]
install_requires = readreqs('requirements.txt')
tests_requires = install_requires + readreqs('requirements_travis.txt')
if sys.version_info[:3] < (3, 4, 0):
sys.exit("Error: Electrum requires Python version >= 3.4.0...")
data_files = []
if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']:
parser = argparse.ArgumentParser()
parser.add_argument('--root=', dest='root_path', metavar='dir', default='/')
opts, _ = parser.parse_known_args(sys.argv[1:])
usr_share = os.path.join(sys.prefix, "share")
if not os.access(opts.root_path + usr_share, os.W_OK) and \
not os.access(opts.root_path, os.W_OK):
if 'XDG_DATA_HOME' in os.environ.keys():
usr_share = os.environ['XDG_DATA_HOME']
else:
usr_share = os.path.expanduser('~/.local/share')
data_files += [
(os.path.join(usr_share, 'applications/'), ['electrum.desktop']),
(os.path.join(usr_share, 'pixmaps/'), ['icons/electrum.png'])
]
setup(
name="Electrum-BTCP",
version=version.ELECTRUM_VERSION,
install_requires=install_requires,
tests_require=tests_requires,
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.trezor',
'electrum_plugins.digitalbitbox',
'electrum_plugins.trustedcoin',
'electrum_plugins.virtualkeyboard',
],
package_dir={
'electrum': 'lib',
'electrum_gui': 'gui',
'electrum_plugins': 'plugins',
},
package_data={
'electrum': [
'servers.json',
'servers_testnet.json',
'currencies.json',
'checkpoints.json',
'checkpoints_testnet.json',
'www/index.html',
'wordlist/*.txt',
'locale/*/LC_MESSAGES/electrum.mo',
]
},
scripts=['electrum-btcp'],
data_files=data_files,
description="Lightweight Bitcoin Private Wallet",
author="BTCP Community",
author_email="csulmone@gmail.com",
license="MIT Licence",
url="https://btcprivate.org",
long_description="""Lightweight Bitcoin Private Wallet"""
)
================================================
FILE: snap/snapcraft.yaml
================================================
name: electrum
version: master
summary: Bitcoin thin client
description: |
Lightweight Bitcoin client
grade: devel # must be 'stable' to release into candidate/stable channels
confinement: strict
apps:
electrum:
command: desktop-launch electrum
plugs: [network, network-bind, x11, unity7]
parts:
electrum:
source: .
plugin: python
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
after: [desktop-qt5]
================================================
FILE: tox.ini
================================================
[tox]
envlist = py35, py36
[testenv]
deps=
pytest
coverage
commands=
coverage run --source=lib -m py.test -v
coverage report