diff --git a/.gitignore b/.gitignore
index 8812c537..f4a8bb74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,17 +6,24 @@ dist/
*.egg/
contrib/pyinstaller/
Electrum.egg-info/
-electrum/gui/qt/icons_rc.py
locale/
.devlocaltmp/
*_trial_temp
packages
env/
-.tox/
.buildozer/
bin/
/app.fil
-.idea/
-# tox files
+
+.idea
+
+# icons
+electrum/gui/qt/icons_rc.py
+electrum/gui/kivy/theming/light-0.png
+electrum/gui/kivy/theming/light.atlas
+
+# tests/tox
+.tox/
.cache/
.coverage
+.pytest_cache
diff --git a/.travis.yml b/.travis.yml
index 63dde547..4690db75 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,9 @@
-sudo: false
+sudo: true
+dist: xenial
language: python
python:
- - 3.5
- 3.6
+ - 3.7
addons:
apt:
sources:
diff --git a/README.rst b/README.rst
index c7665604..42df1790 100644
--- a/README.rst
+++ b/README.rst
@@ -5,7 +5,7 @@ Electrum - Lightweight Bitcoin client
Licence: MIT Licence
Author: Thomas Voegtlin
- Language: Python
+ Language: Python (>= 3.6)
Homepage: https://electrum.org/
diff --git a/RELEASE-NOTES b/RELEASE-NOTES
index 2483455d..74b68774 100644
--- a/RELEASE-NOTES
+++ b/RELEASE-NOTES
@@ -1,3 +1,21 @@
+# Release 3.2.3 - (September 3, 2018)
+
+ * hardware wallet: the Safe-T mini from Archos is now supported.
+ * hardware wallet: the Coldcard from Coinkite is now supported.
+ * BIP39 seeds: if a seed extension (aka passphrase) contained
+ multiple consecutive whitespaces or leading/trailing whitespaces
+ then the derived addresses were not following spec. This has been
+ fixed, and affected should move their coins. The wizard will show a
+ warning in this case. (#4566)
+ * Revealer: the PRNG used has been changed (#4649)
+ * fix Linux distributables: 'typing' was not bundled, needed for python 3.4
+ * fix #4626: fix spending from segwit multisig wallets involving a Trezor
+ cosigner when using a custom derivation path
+ * fix #4491: on Android, if user had set "uBTC" as base unit, app crashed
+ * fix #4497: on Android, paying bip70 invoices from cold start did not work
+ * Several other minor bugfixes and usability improvements.
+
+
# Release 3.2.2 - (July 2nd, 2018)
* Fix DNS resolution on Windows
diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx
index 3c3a1bab..f02f9f5d 100755
--- a/contrib/build-osx/make_osx
+++ b/contrib/build-osx/make_osx
@@ -13,7 +13,7 @@ src_dir=$(dirname "$0")
cd $src_dir/../..
export PYTHONHASHSEED=22
-VERSION=`git describe --tags --dirty`
+VERSION=`git describe --tags --dirty --always`
which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue"
@@ -90,5 +90,11 @@ done
info "Building binary"
pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary"
+info "Adding bitcoin URI types to Info.plist"
+plutil -insert 'CFBundleURLTypes' \
+ -xml ' CFBundleURLName bitcoin CFBundleURLSchemes bitcoin ' \
+ -- dist/$PACKAGE.app/Contents/Info.plist \
+ || fail "Could not add keys to Info.plist. Make sure the program 'plutil' exists and is installed."
+
info "Creating .DMG"
hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG"
diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec
index 0cd01c66..501df88e 100644
--- a/contrib/build-osx/osx.spec
+++ b/contrib/build-osx/osx.spec
@@ -27,16 +27,19 @@ hiddenimports += collect_submodules('safetlib')
hiddenimports += collect_submodules('btchip')
hiddenimports += collect_submodules('keepkeylib')
hiddenimports += collect_submodules('websocket')
+hiddenimports += collect_submodules('ckcc')
datas = [
- (electrum+'electrum/*.json', PYPKG),
- (electrum+'electrum/wordlist/english.txt', PYPKG + '/wordlist'),
- (electrum+'electrum/locale', PYPKG + '/locale')
+ (electrum + PYPKG + '/*.json', PYPKG),
+ (electrum + PYPKG + '/wordlist/english.txt', PYPKG + '/wordlist'),
+ (electrum + PYPKG + '/locale', PYPKG + '/locale'),
+ (electrum + PYPKG + '/plugins', PYPKG + '/plugins'),
]
datas += collect_data_files('trezorlib')
datas += collect_data_files('safetlib')
datas += collect_data_files('btchip')
datas += collect_data_files('keepkeylib')
+datas += collect_data_files('ckcc')
# Add libusb so Trezor and Safe-T mini will work
binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
@@ -63,6 +66,7 @@ a = Analysis([electrum+ MAIN_SCRIPT,
electrum+'electrum/plugins/safe_t/qt.py',
electrum+'electrum/plugins/keepkey/qt.py',
electrum+'electrum/plugins/ledger/qt.py',
+ electrum+'electrum/plugins/coldcard/qt.py',
],
binaries=binaries,
datas=datas,
diff --git a/contrib/build-wine/README.md b/contrib/build-wine/README.md
index 9f1ab6e3..c6348c97 100644
--- a/contrib/build-wine/README.md
+++ b/contrib/build-wine/README.md
@@ -2,7 +2,8 @@ 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.
+
+For reproducible builds, see the `docker` folder.
Usage:
@@ -34,49 +35,3 @@ The binaries are also built by Travis CI, so if you are having problems,
2. Make sure `/opt` is writable by the current user.
3. Run `build.sh`.
4. The generated binaries are in `./dist`.
-
-
-Code Signing
-============
-
-Electrum Windows builds are signed with a Microsoft Authenticode™ code signing
-certificate in addition to the GPG-based signatures.
-
-The advantage of using Authenticode is that Electrum users won't receive a
-Windows SmartScreen warning when starting it.
-
-The release signing procedure involves a signer (the holder of the
-certificate/key) and one or multiple trusted verifiers:
-
-
-| Signer | Verifier |
-|-----------------------------------------------------------|-----------------------------------|
-| Build .exe files using `build.sh` | |
-| Sign .exe with `./sign.sh` | |
-| Upload signed files to download server | |
-| | Build .exe files using `build.sh` |
-| | Compare files using `unsign.sh` |
-| | Sign .exe file using `gpg -b` |
-
-| Signer and verifiers:
-| Upload signatures to 'electrum-signatures' repo, as `$version/$filename.$builder.asc` |
-
-
-
-
-Verify Integrity of signed binary
-=================================
-
-Every user can verify that the official binary was created from the source code in this
-repository. To do so, the Authenticode signature needs to be stripped since the signature
-is not reproducible.
-
-This procedure removes the differences between the signed and unsigned binary:
-
-1. Remove the signature from the signed binary using osslsigncode or signtool.
-2. Set the COFF image checksum for the signed binary to 0x0. This is necessary
- because pyinstaller doesn't generate a checksum.
-3. Append null bytes to the _unsigned_ binary until the byte count is a multiple
- of 8.
-
-The script `unsign.sh` performs these steps.
diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh
index f98a77a1..0a059a5f 100755
--- a/contrib/build-wine/build-electrum-git.sh
+++ b/contrib/build-wine/build-electrum-git.sh
@@ -25,7 +25,7 @@ pushd $WINEPREFIX/drive_c/electrum
git submodule init
git submodule update
-VERSION=`git describe --tags --dirty || printf 'custom'`
+VERSION=`git describe --tags --dirty --always`
echo "Last commit: $VERSION"
pushd ./contrib/deterministic-build/electrum-locale
@@ -34,7 +34,7 @@ if ! which msgfmt > /dev/null 2>&1; then
exit 1
fi
for i in ./locale/*; do
- dir=$i/LC_MESSAGES
+ dir=$WINEPREFIX/drive_c/electrum/electrum/locale/$i/LC_MESSAGES
mkdir -p $dir
msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true
done
@@ -44,7 +44,6 @@ find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
popd
cp $WINEPREFIX/drive_c/electrum/LICENCE .
-cp -r $WINEPREFIX/drive_c/electrum/contrib/deterministic-build/electrum-locale/locale $WINEPREFIX/drive_c/electrum/electrum/
cp $WINEPREFIX/drive_c/electrum/contrib/deterministic-build/electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/electrum/gui/qt/
# Install frozen dependencies
diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
index ae3fba80..875a091a 100644
--- a/contrib/build-wine/deterministic.spec
+++ b/contrib/build-wine/deterministic.spec
@@ -22,6 +22,7 @@ hiddenimports += collect_submodules('safetlib')
hiddenimports += collect_submodules('btchip')
hiddenimports += collect_submodules('keepkeylib')
hiddenimports += collect_submodules('websocket')
+hiddenimports += collect_submodules('ckcc')
# Add libusb binary
binaries = [(PYHOME+"/libusb-1.0.dll", ".")]
@@ -35,12 +36,14 @@ datas = [
(home+'electrum/*.json', 'electrum'),
(home+'electrum/wordlist/english.txt', 'electrum/wordlist'),
(home+'electrum/locale', 'electrum/locale'),
- ('C:\\Program Files (x86)\\ZBar\\bin\\', '.')
+ (home+'electrum/plugins', 'electrum/plugins'),
+ ('C:\\Program Files (x86)\\ZBar\\bin\\', '.'),
]
datas += collect_data_files('trezorlib')
datas += collect_data_files('safetlib')
datas += collect_data_files('btchip')
datas += collect_data_files('keepkeylib')
+datas += collect_data_files('ckcc')
# 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+'run_electrum',
@@ -60,6 +63,7 @@ a = Analysis([home+'run_electrum',
home+'electrum/plugins/safe_t/qt.py',
home+'electrum/plugins/keepkey/qt.py',
home+'electrum/plugins/ledger/qt.py',
+ home+'electrum/plugins/coldcard/qt.py',
#home+'packages/requests/utils.py'
],
binaries=binaries,
diff --git a/contrib/build-wine/docker/Dockerfile b/contrib/build-wine/docker/Dockerfile
index f46e9812..2a2deff7 100644
--- a/contrib/build-wine/docker/Dockerfile
+++ b/contrib/build-wine/docker/Dockerfile
@@ -8,7 +8,8 @@ RUN dpkg --add-architecture i386 && \
wget=1.19.4-1ubuntu2.1 \
gnupg2=2.2.4-1ubuntu1.1 \
dirmngr=2.2.4-1ubuntu1.1 \
- software-properties-common=0.96.24.32.4 \
+ python3-software-properties=0.96.24.32.1 \
+ software-properties-common=0.96.24.32.1 \
&& \
wget -nc https://dl.winehq.org/wine-builds/Release.key && \
apt-key add Release.key && \
diff --git a/contrib/build-wine/docker/README.md b/contrib/build-wine/docker/README.md
index 7cab0f58..0df96ea5 100644
--- a/contrib/build-wine/docker/README.md
+++ b/contrib/build-wine/docker/README.md
@@ -1,6 +1,9 @@
Deterministic Windows binaries with Docker
==========================================
+Produced binaries are deterministic, so you should be able to generate
+binaries that match the official releases.
+
This assumes an Ubuntu host, but it should not be too hard to adapt to another
similar system. The docker commands should be executed in the project's root
folder.
@@ -39,3 +42,49 @@ folder.
Note: the `setup` binary (NSIS installer) is not deterministic yet.
+
+
+Code Signing
+============
+
+Electrum Windows builds are signed with a Microsoft Authenticode™ code signing
+certificate in addition to the GPG-based signatures.
+
+The advantage of using Authenticode is that Electrum users won't receive a
+Windows SmartScreen warning when starting it.
+
+The release signing procedure involves a signer (the holder of the
+certificate/key) and one or multiple trusted verifiers:
+
+
+| Signer | Verifier |
+|-----------------------------------------------------------|-----------------------------------|
+| Build .exe files using `build.sh` | |
+| Sign .exe with `./sign.sh` | |
+| Upload signed files to download server | |
+| | Build .exe files using `build.sh` |
+| | Compare files using `unsign.sh` |
+| | Sign .exe file using `gpg -b` |
+
+| Signer and verifiers: |
+|-----------------------------------------------------------------------------------------------|
+| Upload signatures to 'electrum-signatures' repo, as `$version/$filename.$builder.asc` |
+
+
+
+Verify Integrity of signed binary
+=================================
+
+Every user can verify that the official binary was created from the source code in this
+repository. To do so, the Authenticode signature needs to be stripped since the signature
+is not reproducible.
+
+This procedure removes the differences between the signed and unsigned binary:
+
+1. Remove the signature from the signed binary using osslsigncode or signtool.
+2. Set the COFF image checksum for the signed binary to 0x0. This is necessary
+ because pyinstaller doesn't generate a checksum.
+3. Append null bytes to the _unsigned_ binary until the byte count is a multiple
+ of 8.
+
+The script `unsign.sh` performs these steps.
diff --git a/contrib/deterministic-build/electrum-icons b/contrib/deterministic-build/electrum-icons
index 5af76dc1..0b8cbcca 160000
--- a/contrib/deterministic-build/electrum-icons
+++ b/contrib/deterministic-build/electrum-icons
@@ -1 +1 @@
-Subproject commit 5af76dc1b04c782e622ac409cd802c483c529c1f
+Subproject commit 0b8cbcca428ceb791527bcbb2ef2b36b4ab29c73
diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale
index de999cef..27e36687 160000
--- a/contrib/deterministic-build/electrum-locale
+++ b/contrib/deterministic-build/electrum-locale
@@ -1 +1 @@
-Subproject commit de999ceffd2a864df54451d23f290ef5f333e8ea
+Subproject commit 27e36687f4b0fbd126628bdde80758b63ade7347
diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt
index 02561924..1ae2b837 100644
--- a/contrib/deterministic-build/requirements-binaries.txt
+++ b/contrib/deterministic-build/requirements-binaries.txt
@@ -1,43 +1,45 @@
-pip==10.0.1 \
- --hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \
- --hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68
-pycryptodomex==3.6.4 \
- --hash=sha256:0461e88a7199f9e88f9f90c2c1e109e9e1f7bbb94dc6192e5df52829d31510c1 \
- --hash=sha256:08d0aba5a72e8af5da118ac4b6a5d75befceca7dd92a031b040ed5ff4417cec2 \
- --hash=sha256:0e22d47935d5fa95f556d5f5857576bc6750233964de06a840d58459010c3889 \
- --hash=sha256:10ef21d1728ec0b8afc4f8e1d8d9ea66f317154ea18731a4a05bd996cdc33fdf \
- --hash=sha256:1962b81eef81bf5c42d625816904a22a0bd23d15ca5d49891a54e3c0d0189d84 \
- --hash=sha256:24aae88efe3cbcb4a9cf840b2c352e7de1d6c2c5b3df37ff99b5c7e271e8f3a8 \
- --hash=sha256:43ad6d1d7ca545d53360bf412ee70fcb9ede876b4376fc6db06fc7328f70588c \
- --hash=sha256:4daabe7c0404e673b9029aa43761c779b9b4df2cbe11ccd94daded6a0acd8808 \
- --hash=sha256:4e15af025e02b04b0d0728e8248e4384d3dc7a3a89a020f5bd4d04ef2c5d9d4c \
- --hash=sha256:5b4d3c4a069a05972e0ed7111071bbcb4727ac652b5d7e8f786e8ea2fe63306b \
- --hash=sha256:67ad8b2ad15a99ae70e287454a112f67d2abaf160ee9c97f9daebf2296066447 \
- --hash=sha256:6d7e6fb69d9fd2c57e177f8a9cdf6489a725da77568e3d0a226c7dd18504396a \
- --hash=sha256:7907d7a5adde7cd07d19f129a4afa892b68b0b52a07eaf989e48e2677040b4bf \
- --hash=sha256:88210edafd564c8ff4a68716aaf0627e3bc43e9c192a33d6f5616743f72c2d9b \
- --hash=sha256:8a6b14a90bdcbcdc268acae87126c33bf4250d3842803a93a548d7c10135893a \
- --hash=sha256:94a10446ad61965516aecd610a2dd28d79ab1dfd8723903e1bd19ffa985c208e \
- --hash=sha256:99bda900a0bf6f9e6c69bdeb6114f7f6730b9d36a47bc1fe144263ce85bfc403 \
- --hash=sha256:9dae2e738622bd35ba82fe0b06f773be137a14e6b28defb2e36efc2d809cd28a \
- --hash=sha256:a04cd6021ff2756c38135a95f81b980485507bccbff4d2b8f62e537552270471 \
- --hash=sha256:a3b61625b60dd5e72556520a77464e2ac568c20b8ad12ea1f4443bf5051dc624 \
- --hash=sha256:a9a91fd9e7967a5bad88d542c9fce09323e15d16cb6fa9b8978390e46e68cbdf \
- --hash=sha256:afc44f1b595bd736ec3762dd9a2d0ef276a6ac560c85f643acfc4c0bf0c73384 \
- --hash=sha256:b5f3c8912b36e6abb843a51eecb414a1161f80c0ca0b65066c23aa449b5f98db \
- --hash=sha256:cc07c8b7686dd7093f33067a02b92f4fed860d75ad2bcc4e60624f70fdb94576 \
- --hash=sha256:da646eddbe026306fd1cb2c392a9aee4ebea13f2a9add9af303bb3151786a5d8 \
- --hash=sha256:df93eaccd5c09e6380fab8f15c06a89944415e4bb9af64a94f467ce4c782ff8e \
- --hash=sha256:e667303019770834354c75022ab0324d5ae5bf7cd7015939678033a58f87ee70 \
- --hash=sha256:f921219040ce994c9118b7218b7f7b4e9394e507c97cfc869ce5358437fc26cd
+pip==18.0 \
+ --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
+ --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
+pycryptodomex==3.6.6 \
+ --hash=sha256:0cf562fc5e5ddbe935bb6162d84a7e46e19edba8ac6609587ab9e78dc7c527d4 \
+ --hash=sha256:13b77b7a177a2fd0beb42db84b21d7d4ab646dfd223989a3b5fa6a6901075ae8 \
+ --hash=sha256:1baf0d485853cfa8f87c969148dc040ae81a8de3dcaaeb07b3f40a90ab2ac637 \
+ --hash=sha256:1dac67655206c92eabf827cc3c9f9efd35d699057f12185f01245b22e02db5f8 \
+ --hash=sha256:1dc3abcb853e8b2188d0c8d624dbdc575a1f23ae82feb9a99e4a69b7064df81a \
+ --hash=sha256:24bf22fffba3a7c1132a039d95256ca9e5016798111c7bf7d0091bfddb0e0245 \
+ --hash=sha256:26fde532deaed9781643be654b25fc899b97866b68e50bc015aadd442387c42b \
+ --hash=sha256:2958eceb671795ec4e5f2164e4ec5ddc1788fdf4f72b18390f548de5e11c9dcc \
+ --hash=sha256:34b3218fcdcd59c26de75c4ff8a15e6472e7b88f9e5b3b193cb1c001e7a0e815 \
+ --hash=sha256:3f4d96cfa963960d190d3d6ef5e8f7c2c14815ff816999dff5efaa6bf7cd1134 \
+ --hash=sha256:40063250911af6ec5b3ccc80108e7a3be9d1695dd3d17104a1930861cbdb78b7 \
+ --hash=sha256:4b8669f4b3fa386696b9b0366e69898dd626b119eaead50ee3b2c59c56222272 \
+ --hash=sha256:5144ad0df1009cce6d03b71c2e5843ed96be91ab746e58e8e64ef9bca5209bfe \
+ --hash=sha256:549d0a0884e60d876b975e18e791cf2aece077cb174f0d7d042982b5d36a6279 \
+ --hash=sha256:5b365bc3da05fbc15acd6600d41f84fe8ed903400566bc0d1a0fadc862b41c8e \
+ --hash=sha256:6ab64112924be1905a22656166ec0eeeba275fb15c15adba6470bd030362e160 \
+ --hash=sha256:a06e0e0965145cc0d625e1a031b49705aa2c6b4e16aa0044d4719fb1d639977f \
+ --hash=sha256:a472d2271cfb37940ad80c4698affeaf39552e4c19ceeac4d5290fe71519e3f4 \
+ --hash=sha256:bfc315eef8cdae280e404df64cf71f8673abcc81e646699b15722f41b82a4ed7 \
+ --hash=sha256:c1d4ea2382c1726ff65548f41dc79bd9d0345f3a47ba128768242eea470c6515 \
+ --hash=sha256:c54d3d2e3b099dae61124d7476311ab594df7eeec305f612a5eb79226cf36b53 \
+ --hash=sha256:d0d448484e161786922b41e112b5d7cf76ef5f0c725ea5107ef866bb14a38b12 \
+ --hash=sha256:ddf10a2a071ae749158d8fdad0cbc78f6197219721afa91e0752dd999fddf422 \
+ --hash=sha256:e0cc6ec20e6dfce1a6715ce60d71df3e9491461d525980821f71223026950b0e \
+ --hash=sha256:e5fcb23f936912b6cfb475c7b115aa2e31b7fed409bc947c6190503d9f7d0338 \
+ --hash=sha256:e76d0148ffa5b86f1bcb111bd3950bc61fd50bed8b6b1cf9f5e56784378b55b6 \
+ --hash=sha256:ea09a059e4cf3d99a446f0a80ac9be54802542226cdbf8ffeea79a6ddc77f310 \
+ --hash=sha256:f5844faf085ad3d92fb94d07347c69f193c0bd1f32843329c2a422addeb042c9 \
+ --hash=sha256:f8478a443bb747c2ef1d508a09eeb57ff8c2ebbd8ea0e6e2351d483c23e725b3 \
+ --hash=sha256:ff31cb6cd69c6683bd44e99bc4f36cb74a338cee58a3a9c5c19a8ee4bdd2f3b2
PyQt5==5.10.1 \
--hash=sha256:1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac \
--hash=sha256:4db7113f464c733a99fcb66c4c093a47cf7204ad3f8b3bda502efcc0839ac14b \
--hash=sha256:9c17ab3974c1fc7bbb04cc1c9dae780522c0ebc158613f3025fccae82227b5f7 \
--hash=sha256:f6035baa009acf45e5f460cf88f73580ad5dc0e72330029acd99e477f20a5d61
-setuptools==40.0.0 \
- --hash=sha256:012adb8e25fbfd64c652e99e7bab58799a3aaf05d39ab38561f69190a909015f \
- --hash=sha256:d68abee4eed409fbe8c302ac4d8429a1ffef912cd047a903b5701c024048dd49
+setuptools==40.2.0 \
+ --hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
+ --hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
SIP==4.19.8 \
--hash=sha256:09f9a4e6c28afd0bafedb26ffba43375b97fe7207bd1a0d3513f79b7d168b331 \
--hash=sha256:105edaaa1c8aa486662226360bd3999b4b89dd56de3e314d82b83ed0587d8783 \
diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt
index f06b43e1..361258e4 100644
--- a/contrib/deterministic-build/requirements-hw.txt
+++ b/contrib/deterministic-build/requirements-hw.txt
@@ -1,43 +1,46 @@
-btchip-python==0.1.27 \
- --hash=sha256:e58a941abbb2d8901bf4858baa18012537c60812c7f895f9a039113ecce3032b
-certifi==2018.4.16 \
- --hash=sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7 \
- --hash=sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0
+btchip-python==0.1.28 \
+ --hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83
+certifi==2018.8.24 \
+ --hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \
+ --hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a
chardet==3.0.4 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
+ckcc-protocol==0.7.2 \
+ --hash=sha256:31ee5178cfba8895eb2a6b8d06dc7830b51461a0ff767a670a64707c63e6b264 \
+ --hash=sha256:498db4ccdda018cd9f40210f5bd02ddcc98e7df583170b2eab4035c86c3cc03b
click==6.7 \
--hash=sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d \
--hash=sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b
-Cython==0.28.4 \
- --hash=sha256:01487236575df8f17b46982071438dce4f7eaf8acc8fb99fca3510d343cd7a28 \
- --hash=sha256:0671d17c7a27634d6819246e535241b951141ed0e3f6f2a6d618fd32344dae3e \
- --hash=sha256:0e6190d6971c46729f712dd7307a9c0a8c027bfa5b4d8f2edef106b01759926c \
- --hash=sha256:202587c754901d0678bd6ff89c707f099987928239049a528470c06c6c922cf8 \
- --hash=sha256:345197ba9278cf6a914cb7421dc665a0531a219b0072abf6b0cebfdf68e75725 \
- --hash=sha256:3a296b8d6b02f0e01ab04bedea658f43eef5ad2f8e586a820226ead1a677d9b1 \
- --hash=sha256:484572a2b22823a967be106137a93f7d634db116b3f7accb37dbd760eda2fa9f \
- --hash=sha256:4c67c9c803e50ceff32cc5e4769c50fc8ae8df9c4e5cc592ce8310b5a1076d23 \
- --hash=sha256:539038087c321911745fc2e77049209b1231300d481cb4d682b2f95c724814b3 \
- --hash=sha256:58113e0683c3688594c112103d7e9f2d0092fd2d8297a220240bea22e184dfdd \
- --hash=sha256:65cb25ca4284804293a2404d1be3b5a98818be21a72791649bacbcfa4e431d41 \
- --hash=sha256:699e765da2580e34b08473fc0acef3a2d7bcb7f13eb29401cd25236bcf000080 \
- --hash=sha256:6b54c3470810cea49a8be90814d05c5325ceb9c5bf429fd86c36fc1b32dfc157 \
- --hash=sha256:71ac1629e4eae2ed329be8caf45efea10bfe1af3d8767e12e64b83e4ea5a3250 \
- --hash=sha256:722c179d3df8677f3daf45b1a2764678ed4f0aaddbaa7211a8a08ebfd907c0db \
- --hash=sha256:76ac2b08d3d956d77b574bb43cbf1d37bd58b9d50c04ba281303e695854ebc46 \
- --hash=sha256:7eff1157be9e26bf7494288c89979ca69d593a009e2c7420a739e2cf1e0635f5 \
- --hash=sha256:99546c8696d27d0efa639c77b2f8af6e61dc3a5073caae4f27ffd991ca926f42 \
- --hash=sha256:a0c263b31d335f29c11f4a9e98fbcd908d0731d4ea99bfd27c1c47caaeb4ca2e \
- --hash=sha256:a29c66292605bff962adc26530c030607aa699206b12dfb84f131b0454e15df4 \
- --hash=sha256:a4d3724c5a1ddd86d7d830d8e02c40151839b833791dd4b6fe9e144380fa7d37 \
- --hash=sha256:aed9f33b19d542eea56c38ef3862ca56147f7903648156cd57eabb0fe47c35d6 \
- --hash=sha256:b57e733dd8871d2cc7358c2e0fe33027453afffbcd0ea6a537f54877cad5131c \
- --hash=sha256:d5bf4db62236e82955c40bafbaa18d54b20b5ceefa06fb57c7facc443929f4bd \
- --hash=sha256:d9272dd71ab78e87fa34a0a59bbd6acc9a9c0005c834a6fc8457ff9619dc6795 \
- --hash=sha256:e9d5671bcbb90a41b0832fcb3872fcbaca3d68ff11ea09724dd6cbdf31d947fb \
- --hash=sha256:ee54646afb2b73b293c94cf079682d18d404ebd6c01122dc3980f111aec2d8ae \
- --hash=sha256:f16a87197939977824609005b73f9ebb291b9653a14e5f27afc1c5d6f981ba39
+Cython==0.28.5 \
+ --hash=sha256:022592d419fc754509d0e0461eb2958dbaa45fb60d51c8a61778c58994edbe36 \
+ --hash=sha256:07659f4c57582104d9486c071de512fbd7e087a3a630535298442cc0e20a3f5a \
+ --hash=sha256:13c73e2ffa93a615851e03fad97591954d143b5b62361b9adef81f46a31cd8ef \
+ --hash=sha256:13eab5a2835a84ff62db343035603044c908d2b3b6eec09d67fdf9970acf7ac9 \
+ --hash=sha256:183b35a48f58862c4ec1e821f07bb7b1156c8c8559c85c32ae086f28947474eb \
+ --hash=sha256:2f526b0887128bf20ab2acc905a975f62b5a04ab2f63ecbe5a30fc28285d0e0c \
+ --hash=sha256:32de8637f5e6c5a76667bc7c8fc644bd9314dc19af36db8ce30a0b92ada0f642 \
+ --hash=sha256:4172c183ef4fb2ace6a29cdf7fc9200c5a471a7f775ff691975b774bd9ed3ad2 \
+ --hash=sha256:553956ec06ecbd731ef0c538eb28a5b46bedea7ab89b18237ff28b4b99d65eee \
+ --hash=sha256:660eeb6870687fd3eda91e00ba4e72220545c254c8c4d967fd0c910f4fbb8cbc \
+ --hash=sha256:693a8619ef066ece055ed065a15cf440f9d3ebd1bca60e87ea19144833756433 \
+ --hash=sha256:759c799e9ef418f163b5412e295e14c0a48fe3b4dcba9ab8aab69e9f511cfefd \
+ --hash=sha256:827d3a91b7a7c31ce69e5974496fd9a8ba28eb498b988affb66d0d30de11d934 \
+ --hash=sha256:87e57b5d730cfab225d95e7b23abbc0c6f77598bd66639e93c73ce8afbae6f38 \
+ --hash=sha256:9400e5db8383346b0694a3e794d8bded18a27b21123516dcdf4b79d7ec28e98b \
+ --hash=sha256:9ec27681c5b1b457aacb1cbda5db04aa28b76da2af6e1e1fd15f233eafe6a0b0 \
+ --hash=sha256:ae4784f040a3313c8bd00c8d04934b7ade63dc59692d8f00a5235be8ed72a445 \
+ --hash=sha256:b2ba8310ebd3c0e0b884d5e95bbd99d467d6af922acd1e44fe4b819839b2150e \
+ --hash=sha256:b64575241f64f6ec005a4d4137339fb0ba5e156e826db2fdb5f458060d9979e0 \
+ --hash=sha256:c78ad0df75a9fc03ab28ca1b950c893a208c451a18f76796c3e25817d6994001 \
+ --hash=sha256:cdbb917e41220bd3812234dbe59d15391adbc2c5d91ae11a5273aab9e32ba7ec \
+ --hash=sha256:d2223a80c623e2a8e97953ab945dfaa9385750a494438dcb55562eb1ddd9565a \
+ --hash=sha256:e22f21cf92a9f8f007a280e3b3462c886d9068132a6c698dec10ad6125e3ca1e \
+ --hash=sha256:ea5c16c48e561f4a6f6b8c24807494b77a79e156b8133521c400f22ca712101b \
+ --hash=sha256:ee7a9614d51fe16e32ca5befe72e0808baff481791728449d0b17c8b0fe29eb9 \
+ --hash=sha256:ef86de9299e4ab2ebb129fb84b886bf40b9aced9807c6d6d5f28b46fb905f82c \
+ --hash=sha256:f3e4860f5458a9875caa3de65e255720c0ed2ce71f0bcdab02497b32104f9db8 \
+ --hash=sha256:fc6c20a8ac22202a779ad4c59756647be0826993d2151a03c015e76d2368ae5f
ecdsa==0.13 \
--hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \
--hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa
@@ -56,31 +59,33 @@ idna==2.7 \
--hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16
keepkey==4.0.2 \
--hash=sha256:cddee60ae405841cdff789cbc54168ceaeb2282633420f2be155554c25c69138
-libusb1==1.6.4 \
- --hash=sha256:8c930d9c1d037d9c83924c82608aa6a1adcaa01ca0e4a23ee0e8e18d7eee670d
+libusb1==1.6.6 \
+ --hash=sha256:a49917a2262cf7134396f6720c8be011f14aabfc5cdc53f880cc672c0f39d271
mnemonic==0.18 \
--hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d
pbkdf2==1.3 \
--hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979
-pip==10.0.1 \
- --hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \
- --hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68
-protobuf==3.6.0 \
- --hash=sha256:12985d9f40c104da2f44ec089449214876809b40fdc5d9e43b93b512b9e74056 \
- --hash=sha256:12c97fe27af12fc5d66b23f905ab09dd4fb0c68d5a74a419d914580e6d2e71e3 \
- --hash=sha256:327fb9d8a8247bc780b9ea7ed03c0643bc0d22c139b761c9ec1efc7cc3f0923e \
- --hash=sha256:3895319db04c0b3baed74fb66be7ba9f4cd8e88a432b8e71032cdf08b2dfee23 \
- --hash=sha256:695072063e256d32335d48b9484451f7c7948edc3dbd419469d6a778602682fc \
- --hash=sha256:7d786f3ef5b33a04e6538089674f244a3b0f588155016559d950989010af97d0 \
- --hash=sha256:8bf82bb7a466a54be7272dcb492f71d55a2453a58d862fb74c3f2083f2768543 \
- --hash=sha256:9bbc1ae1c33c1bd3a2fc05a3aec328544d2b039ff0ce6f000063628a32fad777 \
- --hash=sha256:9e992c68103ab5635728d29fcf132c669cb4e2db24d012685210276185009d17 \
- --hash=sha256:9f1087abb67b34e55108bc610936b34363a7aac692023bcbb17e065c253a1f80 \
- --hash=sha256:9fefcb92a3784b446abf3641d9a14dad815bee88e0edd10b9a9e0e144d01a991 \
- --hash=sha256:a37836aa47d1b81c2db1a6b7a5e79926062b5d76bd962115a0e615551be2b48d \
- --hash=sha256:cca22955443c55cf86f963a4ad7057bca95e4dcde84d6a493066d380cfab3bb0 \
- --hash=sha256:d7ac50bc06d31deb07ace6de85556c1d7330e5c0958f3b2af85037d6d1182abf \
- --hash=sha256:dfe6899304b898538f4dc94fa0b281b56b70e40f58afa4c6f807805261cbe2e8
+pip==18.0 \
+ --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
+ --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
+protobuf==3.6.1 \
+ --hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \
+ --hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \
+ --hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \
+ --hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \
+ --hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \
+ --hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \
+ --hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \
+ --hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \
+ --hash=sha256:685bc4ec61a50f7360c9fd18e277b65db90105adbf9c79938bd315435e526b90 \
+ --hash=sha256:92e8418976e52201364a3174e40dc31f5fd8c147186d72380cbda54e0464ee19 \
+ --hash=sha256:9335f79d1940dfb9bcaf8ec881fb8ab47d7a2c721fb8b02949aab8bbf8b68625 \
+ --hash=sha256:a7ee3bb6de78185e5411487bef8bc1c59ebd97e47713cba3c460ef44e99b3db9 \
+ --hash=sha256:ceec283da2323e2431c49de58f80e1718986b79be59c266bb0509cbf90ca5b9e \
+ --hash=sha256:e7a5ccf56444211d79e3204b05087c1460c212a2c7d62f948b996660d0165d68 \
+ --hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10
+pyaes==1.6.1 \
+ --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
pyblake2==1.1.2 \
--hash=sha256:3757f7ad709b0e1b2a6b3919fa79fe3261f166fc375cd521f2be480f8319dde9 \
--hash=sha256:407e02c7f8f36fcec1b7aa114ddca0c1060c598142ea6f6759d03710b946a7e3 \
@@ -94,12 +99,12 @@ pyblake2==1.1.2 \
requests==2.19.1 \
--hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
--hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
-safet==0.1.3 \
- --hash=sha256:ba80fe9f6ba317ab9514a8726cd3792e68eb46dd419f380d48ae4a0ccae646dc \
- --hash=sha256:e5d8e6a87c8bdf1cefd07004181b93fd7631557fdab09d143ba8d1b29291d6dc
-setuptools==40.0.0 \
- --hash=sha256:012adb8e25fbfd64c652e99e7bab58799a3aaf05d39ab38561f69190a909015f \
- --hash=sha256:d68abee4eed409fbe8c302ac4d8429a1ffef912cd047a903b5701c024048dd49
+safet==0.1.4 \
+ --hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \
+ --hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1
+setuptools==40.2.0 \
+ --hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
+ --hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
six==1.11.0 \
--hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
--hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
@@ -109,9 +114,9 @@ trezor==0.10.2 \
urllib3==1.23 \
--hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
--hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
-websocket-client==0.48.0 \
- --hash=sha256:18f1170e6a1b5463986739d9fd45c4308b0d025c1b2f9b88788d8f69e8a5eb4a \
- --hash=sha256:db70953ae4a064698b27ae56dcad84d0ee68b7b43cb40940f537738f38f510c1
+websocket-client==0.52.0 \
+ --hash=sha256:03763384c530b331ec3822d0b52ffdc28c3aeb8a900ac8c98b2ceea3128a7b4e \
+ --hash=sha256:3c9924675eaf0b27ae22feeeab4741bb4149b94820bd3a143eeaf8b62f64d821
wheel==0.31.1 \
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt
index 1b226f9d..45ca2c6a 100644
--- a/contrib/deterministic-build/requirements.txt
+++ b/contrib/deterministic-build/requirements.txt
@@ -1,6 +1,40 @@
-certifi==2018.4.16 \
- --hash=sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7 \
- --hash=sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0
+aiohttp==3.4.4 \
+ --hash=sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b \
+ --hash=sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08 \
+ --hash=sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd \
+ --hash=sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac \
+ --hash=sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650 \
+ --hash=sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa \
+ --hash=sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95 \
+ --hash=sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330 \
+ --hash=sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc \
+ --hash=sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b \
+ --hash=sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de \
+ --hash=sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4 \
+ --hash=sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7 \
+ --hash=sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b \
+ --hash=sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8 \
+ --hash=sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd \
+ --hash=sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2 \
+ --hash=sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698 \
+ --hash=sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95 \
+ --hash=sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6 \
+ --hash=sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0 \
+ --hash=sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07
+aiohttp_socks==0.1.6 \
+ --hash=sha256:943148a3797ba9ffb6df6ddb006ffdd40538885b410589d589bda42a8e8bcd5a
+aiorpcX==0.7.3 \
+ --hash=sha256:24dd4fe2f65f743cb74c8626570470e325bb777bb66d1932e7d2965ae71d1164 \
+ --hash=sha256:5120ca40beef6b6a45d3a7055e343815401385dc607da2fd93baca2762c8a97d
+async_timeout==3.0.0 \
+ --hash=sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c \
+ --hash=sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287
+attrs==18.2.0 \
+ --hash=sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69 \
+ --hash=sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb
+certifi==2018.8.24 \
+ --hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \
+ --hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a
chardet==3.0.4 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
@@ -13,28 +47,60 @@ ecdsa==0.13 \
idna==2.7 \
--hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
--hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16
+idna_ssl==1.1.0 \
+ --hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c
jsonrpclib-pelix==0.3.1 \
--hash=sha256:5417b1508d5a50ec64f6e5b88907f111155d52607b218ff3ba9a777afb2e49e3 \
--hash=sha256:bd89a6093bc4d47dc8a096197aacb827359944a4533be5193f3845f57b9f91b4
-pip==10.0.1 \
- --hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \
- --hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68
-protobuf==3.6.0 \
- --hash=sha256:12985d9f40c104da2f44ec089449214876809b40fdc5d9e43b93b512b9e74056 \
- --hash=sha256:12c97fe27af12fc5d66b23f905ab09dd4fb0c68d5a74a419d914580e6d2e71e3 \
- --hash=sha256:327fb9d8a8247bc780b9ea7ed03c0643bc0d22c139b761c9ec1efc7cc3f0923e \
- --hash=sha256:3895319db04c0b3baed74fb66be7ba9f4cd8e88a432b8e71032cdf08b2dfee23 \
- --hash=sha256:695072063e256d32335d48b9484451f7c7948edc3dbd419469d6a778602682fc \
- --hash=sha256:7d786f3ef5b33a04e6538089674f244a3b0f588155016559d950989010af97d0 \
- --hash=sha256:8bf82bb7a466a54be7272dcb492f71d55a2453a58d862fb74c3f2083f2768543 \
- --hash=sha256:9bbc1ae1c33c1bd3a2fc05a3aec328544d2b039ff0ce6f000063628a32fad777 \
- --hash=sha256:9e992c68103ab5635728d29fcf132c669cb4e2db24d012685210276185009d17 \
- --hash=sha256:9f1087abb67b34e55108bc610936b34363a7aac692023bcbb17e065c253a1f80 \
- --hash=sha256:9fefcb92a3784b446abf3641d9a14dad815bee88e0edd10b9a9e0e144d01a991 \
- --hash=sha256:a37836aa47d1b81c2db1a6b7a5e79926062b5d76bd962115a0e615551be2b48d \
- --hash=sha256:cca22955443c55cf86f963a4ad7057bca95e4dcde84d6a493066d380cfab3bb0 \
- --hash=sha256:d7ac50bc06d31deb07ace6de85556c1d7330e5c0958f3b2af85037d6d1182abf \
- --hash=sha256:dfe6899304b898538f4dc94fa0b281b56b70e40f58afa4c6f807805261cbe2e8
+multidict==4.4.0 \
+ --hash=sha256:112eeeddd226af681dc82b756ed34aa7b6d98f9c4a15760050298c21d715473d \
+ --hash=sha256:13b64ecb692effcabc5e29569ba9b5eb69c35112f990a16d6833ec3a9d9f8ec0 \
+ --hash=sha256:1725373fb8f18c2166f8e0e5789851ccf98453c849b403945fa4ef59a16ca44e \
+ --hash=sha256:2061a50b7cae60a1f987503a995b2fc38e47027a937a355a124306ed9c629041 \
+ --hash=sha256:35b062288a9a478f627c520fd27983160fc97591017d170f966805b428d17e07 \
+ --hash=sha256:467b134bcc227b91b8e2ef8d2931f28b50bf7eb7a04c0403d102ded22e66dbfc \
+ --hash=sha256:475a3ece8bb450e49385414ebfae7f8fdb33f62f1ac0c12935c1cfb1b7c1076a \
+ --hash=sha256:49b885287e227a24545a1126d9ac17ae43138610713dc6219b781cc0ad5c6dfc \
+ --hash=sha256:4c95b2725592adb5c46642be2875c1234c32af841732c5504c17726b92082021 \
+ --hash=sha256:4ea7ed00f4be0f7335c9a2713a65ac3d986be789ce5ebc10821da9664cbe6b85 \
+ --hash=sha256:5e2d5e1d999e941b4a626aea46bdc4206877cf727107fdaa9d46a8a773a6e49b \
+ --hash=sha256:8039c520ef7bb9ec7c3db3df14c570be6362f43c200ae9854d2422d4ffe175a4 \
+ --hash=sha256:81459a0ebcca09c1fcb8fe887ed13cf267d9b60fe33718fc5fd1a2a1ab49470a \
+ --hash=sha256:847c3b7b9ca3268e883685dc1347a4d09f84de7bd7597310044d847590447492 \
+ --hash=sha256:8551d1db45f0ca4e8ec99130767009a29a4e0dc6558a4a6808491bcd3472d325 \
+ --hash=sha256:8fa7679ffe615e0c1c7b80946ab4194669be74848719adf2d7867b5e861eb073 \
+ --hash=sha256:a42a36f09f0f907579ff0fde547f2fde8a739a69efe4a2728835979d2bb5e17b \
+ --hash=sha256:a5fcad0070685c5b2d04b468bf5f4c735f5c176432f495ad055fcc4bc0a79b23 \
+ --hash=sha256:ae22195b2a7494619b73c01129ddcddc0dfaa9e42727404b1d9a77253da3f420 \
+ --hash=sha256:b360e82bdbbd862e1ce2a41cc3bbd0ab614350e813ca74801b34aac0f73465aa \
+ --hash=sha256:b96417899344c5e96bef757f4963a72d02e52653a4e0f99bbea3a531cedac59f \
+ --hash=sha256:b9e921140b797093edfc13ac08dc2a4fd016dd711dc42bb0e1aaf180e48425a7 \
+ --hash=sha256:c5022b94fc330e6d177f3eb38097fb52c7df96ca0e04842c068cf0d9fc38b1e6 \
+ --hash=sha256:cf2b117f2a8d951638efc7592fb72d3eeb2d38cc2194c26ba7f00e7190451d92 \
+ --hash=sha256:d79620b542d9d0e23ae9790ca2fe44f1af40ffad9936efa37bd14954bc3e2818 \
+ --hash=sha256:e2860691c11d10dac7c91bddae44f6211b3da4122d9a2ebb509c2247674d6070 \
+ --hash=sha256:e3a293553715afecf7e10ea02da40593f9d7f48fe48a74fc5dd3ce08a0c46188 \
+ --hash=sha256:e465be3fe7e992e5a6e16731afa6f41cb6ca53afccb4f28ea2fa6457783edf15 \
+ --hash=sha256:e6d27895ef922bc859d969452f247bfbe5345d9aba69b9c8dbe1ea7704f0c5d9
+pip==18.0 \
+ --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
+ --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
+protobuf==3.6.1 \
+ --hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \
+ --hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \
+ --hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \
+ --hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \
+ --hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \
+ --hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \
+ --hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \
+ --hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \
+ --hash=sha256:685bc4ec61a50f7360c9fd18e277b65db90105adbf9c79938bd315435e526b90 \
+ --hash=sha256:92e8418976e52201364a3174e40dc31f5fd8c147186d72380cbda54e0464ee19 \
+ --hash=sha256:9335f79d1940dfb9bcaf8ec881fb8ab47d7a2c721fb8b02949aab8bbf8b68625 \
+ --hash=sha256:a7ee3bb6de78185e5411487bef8bc1c59ebd97e47713cba3c460ef44e99b3db9 \
+ --hash=sha256:ceec283da2323e2431c49de58f80e1718986b79be59c266bb0509cbf90ca5b9e \
+ --hash=sha256:e7a5ccf56444211d79e3204b05087c1460c212a2c7d62f948b996660d0165d68 \
+ --hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10
pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
PySocks==1.6.8 \
@@ -48,22 +114,32 @@ qrcode==6.0 \
requests==2.19.1 \
--hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
--hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
-setuptools==40.0.0 \
- --hash=sha256:012adb8e25fbfd64c652e99e7bab58799a3aaf05d39ab38561f69190a909015f \
- --hash=sha256:d68abee4eed409fbe8c302ac4d8429a1ffef912cd047a903b5701c024048dd49
+setuptools==40.2.0 \
+ --hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
+ --hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
six==1.11.0 \
--hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
--hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
-typing==3.6.4 \
- --hash=sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf \
- --hash=sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8 \
- --hash=sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2
+typing==3.6.6 \
+ --hash=sha256:4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d \
+ --hash=sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4 \
+ --hash=sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a
urllib3==1.23 \
--hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
--hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
wheel==0.31.1 \
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
+yarl==1.2.6 \
+ --hash=sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9 \
+ --hash=sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee \
+ --hash=sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308 \
+ --hash=sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357 \
+ --hash=sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78 \
+ --hash=sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8 \
+ --hash=sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1 \
+ --hash=sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4 \
+ --hash=sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7
colorama==0.3.9 \
--hash=sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda \
--hash=sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1
diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
index 4fe6477f..a6ae0a3a 100644
--- a/contrib/requirements/requirements-hw.txt
+++ b/contrib/requirements/requirements-hw.txt
@@ -3,5 +3,6 @@ trezor[hidapi]>=0.9.0
safet[hidapi]>=0.1.0
keepkey
btchip-python
+ckcc-protocol>=0.7.2
websocket-client
hidapi
diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt
index 99b859c3..3fdf9eac 100644
--- a/contrib/requirements/requirements.txt
+++ b/contrib/requirements/requirements.txt
@@ -5,6 +5,7 @@ qrcode
protobuf
dnspython
jsonrpclib-pelix
-PySocks>=1.6.6
qdarkstyle<3.0
-typing>=3.0.0
+aiorpcx>=0.7.1,<0.8
+aiohttp
+aiohttp_socks
diff --git a/electrum/__init__.py b/electrum/__init__.py
index 48a60c15..556fc329 100644
--- a/electrum/__init__.py
+++ b/electrum/__init__.py
@@ -4,7 +4,7 @@ from .wallet import Wallet
from .storage import WalletStorage
from .coinchooser import COIN_CHOOSERS
from .network import Network, pick_random_server
-from .interface import Connection, Interface
+from .interface import Interface
from .simple_config import SimpleConfig, get_config, set_config
from . import bitcoin
from . import transaction
diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py
index 13d688b6..0fbf4c3e 100644
--- a/electrum/address_synchronizer.py
+++ b/electrum/address_synchronizer.py
@@ -22,12 +22,13 @@
# SOFTWARE.
import threading
+import asyncio
import itertools
from collections import defaultdict
from . import bitcoin
from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY
-from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus
+from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus, aiosafe, CustomTaskGroup
from .transaction import Transaction, TxOutput
from .synchronizer import Synchronizer
from .verifier import SPV
@@ -58,6 +59,8 @@ class AddressSynchronizer(PrintError):
# verifier (SPV) and synchronizer are started in start_threads
self.synchronizer = None
self.verifier = None
+ self.sync_restart_lock = asyncio.Lock()
+ self.group = None
# locks: if you need to take multiple ones, acquire them in the order they are defined here!
self.lock = threading.RLock()
self.transaction_lock = threading.RLock()
@@ -102,6 +105,7 @@ class AddressSynchronizer(PrintError):
return h
def get_address_history_len(self, addr: str) -> int:
+ """Return number of transactions where address is involved."""
return len(self._history_local.get(addr, ()))
def get_txin_address(self, txi):
@@ -133,24 +137,45 @@ class AddressSynchronizer(PrintError):
# add it in case it was previously unconfirmed
self.add_unverified_tx(tx_hash, tx_height)
- def start_threads(self, network):
+ @aiosafe
+ async def on_default_server_changed(self, event):
+ async with self.sync_restart_lock:
+ self.stop_threads()
+ await self._start_threads()
+
+ def start_network(self, network):
self.network = network
if self.network is not None:
- self.verifier = SPV(self.network, self)
- self.synchronizer = Synchronizer(self, network)
- network.add_jobs([self.verifier, self.synchronizer])
- else:
- self.verifier = None
- self.synchronizer = None
+ self.network.register_callback(self.on_default_server_changed, ['default_server_changed'])
+ asyncio.run_coroutine_threadsafe(self._start_threads(), network.asyncio_loop)
+
+ async def _start_threads(self):
+ interface = self.network.interface
+ if interface is None:
+ return # we should get called again soon
+
+ self.verifier = SPV(self.network, self)
+ self.synchronizer = synchronizer = Synchronizer(self)
+ assert self.group is None, 'group already exists'
+ self.group = CustomTaskGroup()
+
+ async def job():
+ async with self.group as group:
+ await group.spawn(self.verifier.main(group))
+ await group.spawn(self.synchronizer.send_subscriptions(group))
+ await group.spawn(self.synchronizer.handle_status(group))
+ await group.spawn(self.synchronizer.main())
+ # we are being cancelled now
+ interface.session.unsubscribe(synchronizer.status_queue)
+ await interface.group.spawn(job)
def stop_threads(self):
if self.network:
- self.network.remove_jobs([self.synchronizer, self.verifier])
- self.synchronizer.release()
self.synchronizer = None
self.verifier = None
- # Now no references to the synchronizer or verifier
- # remain so they will be GC-ed
+ if self.group:
+ asyncio.run_coroutine_threadsafe(self.group.cancel_remaining(), self.network.asyncio_loop)
+ self.group = None
self.storage.put('stored_height', self.get_local_height())
self.save_transactions()
self.save_verified_tx()
@@ -319,6 +344,14 @@ class AddressSynchronizer(PrintError):
self.txi.pop(tx_hash, None)
self.txo.pop(tx_hash, None)
+ def get_depending_transactions(self, tx_hash):
+ """Returns all (grand-)children of tx_hash in this wallet."""
+ children = set()
+ for other_hash in self.spent_outpoints[tx_hash].values():
+ children.add(other_hash)
+ children |= self.get_depending_transactions(other_hash)
+ return children
+
def receive_tx_callback(self, tx_hash, tx, tx_height):
self.add_unverified_tx(tx_hash, tx_height)
self.add_transaction(tx_hash, tx, allow_unrelated=True)
@@ -371,6 +404,8 @@ class AddressSynchronizer(PrintError):
for prevout_hash, d in _spent_outpoints.items():
for prevout_n_str, spending_txid in d.items():
prevout_n = int(prevout_n_str)
+ if spending_txid not in self.transactions:
+ continue # only care about txns we have
self.spent_outpoints[prevout_hash][prevout_n] = spending_txid
@profiler
@@ -626,10 +661,6 @@ class AddressSynchronizer(PrintError):
def is_up_to_date(self):
with self.lock: return self.up_to_date
- def get_num_tx(self, address):
- """ return number of transactions where address is involved """
- return len(self.history.get(address, []))
-
def get_tx_delta(self, tx_hash, address):
"effect of tx on address"
delta = 0
diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py
index a5702e2e..cc63159c 100644
--- a/electrum/base_crash_reporter.py
+++ b/electrum/base_crash_reporter.py
@@ -19,6 +19,7 @@
# 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 asyncio
import json
import locale
import traceback
@@ -26,14 +27,13 @@ import subprocess
import sys
import os
-import requests
-
from .version import ELECTRUM_VERSION
from .import constants
from .i18n import _
+from .util import make_aiohttp_session
-class BaseCrashReporter(object):
+class BaseCrashReporter:
report_server = "https://crashhub.electrum.org"
config_key = "show_crash_reporter"
issue_template = """
Traceback
@@ -60,16 +60,22 @@ class BaseCrashReporter(object):
def __init__(self, exctype, value, tb):
self.exc_args = (exctype, value, tb)
- def send_report(self, endpoint="/crash"):
+ def send_report(self, asyncio_loop, proxy, endpoint="/crash"):
if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
# Gah! Some kind of altcoin wants to send us crash reports.
raise Exception(_("Missing report URL."))
report = self.get_traceback_info()
report.update(self.get_additional_info())
report = json.dumps(report)
- response = requests.post(BaseCrashReporter.report_server + endpoint, data=report)
+ coro = self.do_post(proxy, BaseCrashReporter.report_server + endpoint, data=report)
+ response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(5)
return response
+ async def do_post(self, proxy, url, data):
+ async with make_aiohttp_session(proxy) as session:
+ async with session.post(url, data=data) as resp:
+ return await resp.text()
+
def get_traceback_info(self):
exc_string = str(self.exc_args[1])
stack = traceback.extract_tb(self.exc_args[2])
@@ -125,4 +131,4 @@ class BaseCrashReporter(object):
raise NotImplementedError
def get_os_version(self):
- raise NotImplementedError
+ raise NotImplementedError
diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
index 8e130a7a..0e2d45c6 100644
--- a/electrum/base_wizard.py
+++ b/electrum/base_wizard.py
@@ -240,7 +240,8 @@ class BaseWizard(object):
u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices)
except BaseException as e:
devmgr.print_error('error getting device infos for {}: {}'.format(name, e))
- debug_msg += ' {}:\n {}\n'.format(plugin.name, e)
+ indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
+ debug_msg += ' {}:\n{}\n'.format(plugin.name, indented_error_msg)
continue
devices += list(map(lambda x: (name, x), u))
if not debug_msg:
@@ -346,6 +347,7 @@ class BaseWizard(object):
except ScriptTypeNotSupported:
raise # this is handled in derivation_dialog
except BaseException as e:
+ traceback.print_exc(file=sys.stderr)
self.show_error(e)
return
d = {
diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py
index 1fe405fe..df05fdd0 100644
--- a/electrum/bitcoin.py
+++ b/electrum/bitcoin.py
@@ -24,7 +24,7 @@
# SOFTWARE.
import hashlib
-import hmac
+from typing import List
from .util import bfh, bh2u, BitcoinException, print_error, assert_bytes, to_bytes, inv_dict
from . import version
@@ -626,6 +626,9 @@ def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
return EncodeBase58Check(xpub)
+class InvalidMasterKeyVersionBytes(BitcoinException): pass
+
+
def deserialize_xkey(xkey, prv, *, net=None):
if net is None:
net = constants.net
@@ -640,8 +643,8 @@ def deserialize_xkey(xkey, prv, *, net=None):
header = int('0x' + bh2u(xkey[0:4]), 16)
headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
if header not in headers.values():
- raise BitcoinException('Invalid extended key format: {}'
- .format(hex(header)))
+ raise InvalidMasterKeyVersionBytes('Invalid extended key format: {}'
+ .format(hex(header)))
xtype = list(headers.keys())[list(headers.values()).index(header)]
n = 33 if prv else 32
K_or_k = xkey[13+n:]
@@ -708,6 +711,24 @@ def bip32_derivation(s):
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
yield i
+def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
+ """Convert bip32 path to list of uint32 integers with prime flags
+ m/0/-1/1' -> [0, 0x80000001, 0x80000001]
+
+ based on code in trezorlib
+ """
+ path = []
+ for x in n.split('/')[1:]:
+ if x == '': continue
+ prime = 0
+ if x.endswith("'"):
+ x = x.replace('\'', '')
+ prime = BIP32_PRIME
+ if x.startswith('-'):
+ prime = BIP32_PRIME
+ path.append(abs(int(x)) | prime)
+ return path
+
def is_bip32_derivation(x):
try:
[ i for i in bip32_derivation(x)]
diff --git a/electrum/blockchain.py b/electrum/blockchain.py
index 3de11801..b72b215f 100644
--- a/electrum/blockchain.py
+++ b/electrum/blockchain.py
@@ -28,6 +28,7 @@ from .bitcoin import Hash, hash_encode, int_to_hex, rev_hex
from . import constants
from .util import bfh, bh2u
+
try:
import scrypt
getPoWHash = lambda x: scrypt.hash(x, x, N=1024, r=1, p=1, buflen=32)
@@ -38,6 +39,7 @@ except ImportError:
MAX_TARGET = 0x00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+
class MissingHeader(Exception):
pass
@@ -155,7 +157,10 @@ class Blockchain(util.PrintError):
def check_header(self, header):
header_hash = hash_header(header)
height = header.get('block_height')
- return header_hash == self.get_hash(height)
+ try:
+ return header_hash == self.get_hash(height)
+ except MissingHeader:
+ return False
def fork(parent, header):
forkpoint = header.get('block_height')
@@ -175,9 +180,11 @@ class Blockchain(util.PrintError):
p = self.path()
self._size = os.path.getsize(p)//80 if os.path.exists(p) else 0
- def verify_header(self, header, prev_hash, target):
+ def verify_header(self, header, prev_hash, target, expected_header_hash=None):
_hash = hash_header(header)
_powhash = pow_hash_header(header)
+ if expected_header_hash and expected_header_hash != _hash:
+ raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash))
if prev_hash != header.get('prev_block_hash'):
raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
if constants.net.TESTNET:
@@ -201,10 +208,14 @@ class Blockchain(util.PrintError):
prev_hash = self.get_hash(current_header - 1)
for i in range(num):
target = self.get_target(current_header - 1)
+ try:
+ expected_header_hash = self.get_hash(height)
+ except MissingHeader:
+ expected_header_hash = None
raw_header = data[i*80:(i+1) * 80]
header = deserialize_header(raw_header, current_header)
#print(i)
- self.verify_header(header, prev_hash, target)
+ self.verify_header(header, prev_hash, target, expected_header_hash)
self.save_chunk_part(header)
prev_hash = hash_header(header)
current_header = current_header + 1
@@ -324,17 +335,24 @@ class Blockchain(util.PrintError):
return deserialize_header(h, height)
def get_hash(self, height):
+ def is_height_checkpoint():
+ within_cp_range = height <= constants.net.max_checkpoint()
+ at_chunk_boundary = (height+1) % 2016 == 0
+ return within_cp_range and at_chunk_boundary
+
if height == -1:
return '0000000000000000000000000000000000000000000000000000000000000000'
elif height == 0:
return constants.net.GENESIS
- elif height < len(self.checkpoints) * 2016:
- assert (height+1) % 2016 == 0, height
+ elif is_height_checkpoint():
index = height // 2016
h, t = self.checkpoints[index]
return h
else:
- return hash_header(self.read_header(height))
+ header = self.read_header(height)
+ if header is None:
+ raise MissingHeader(height)
+ return hash_header(header)
def get_target(self, index):
# compute target from chunk x, used in chunk x+1
diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py
index 7dc355d5..5a69cccf 100644
--- a/electrum/coinchooser.py
+++ b/electrum/coinchooser.py
@@ -117,7 +117,7 @@ class CoinChooserBase(PrintError):
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()]
+ output_amounts = [o.value 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)
@@ -354,9 +354,9 @@ class CoinChooserPrivacy(CoinChooserRandom):
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())
+ min_change = min(o.value for o in tx.outputs()) * 0.75
+ max_change = max(o.value for o in tx.outputs()) * 1.33
+ spent_amount = sum(o.value for o in tx.outputs())
def penalty(buckets):
badness = len(buckets) - 1
diff --git a/electrum/commands.py b/electrum/commands.py
index 34672d04..3730f88b 100644
--- a/electrum/commands.py
+++ b/electrum/commands.py
@@ -187,7 +187,7 @@ class Commands:
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))
+ l = copy.deepcopy(self.wallet.get_utxos())
for i in l:
v = i["value"]
i["value"] = str(Decimal(v)/COIN) if v is not None else None
@@ -255,7 +255,7 @@ class Commands:
def broadcast(self, tx):
"""Broadcast a transaction to the network. """
tx = Transaction(tx)
- return self.network.broadcast_transaction(tx)
+ return self.network.broadcast_transaction_from_non_network_thread(tx)
@command('')
def createmultisig(self, num, pubkeys):
@@ -832,7 +832,7 @@ def add_global_options(parser):
group = parser.add_argument_group('global options')
# const is for when no argument is given to verbosity
# default is for when the flag is missing
- group.add_argument("-v", "--verbosity", dest="verbosity", help="Set verbosity filter", default='', const='*', nargs='?')
+ group.add_argument("-v", dest="verbosity", help="Set verbosity filter", default='', const='*', nargs='?')
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")
diff --git a/electrum/constants.py b/electrum/constants.py
index 4b3edbcd..f46f862e 100644
--- a/electrum/constants.py
+++ b/electrum/constants.py
@@ -37,7 +37,14 @@ def read_json(filename, default):
return r
-class BitcoinMainnet:
+class AbstractNet:
+
+ @classmethod
+ def max_checkpoint(cls) -> int:
+ return max(0, len(cls.CHECKPOINTS) * 2016 - 1)
+
+
+class BitcoinMainnet(AbstractNet):
TESTNET = False
WIF_PREFIX = 0xa3
@@ -91,7 +98,7 @@ class BitcoinMainnet:
nAveragingInterval_Version3 = 6
-class BitcoinTestnet:
+class BitcoinTestnet(AbstractNet):
TESTNET = True
WIF_PREFIX = 0xef
diff --git a/electrum/daemon.py b/electrum/daemon.py
index ccdce4a0..c939d109 100644
--- a/electrum/daemon.py
+++ b/electrum/daemon.py
@@ -22,6 +22,7 @@
# 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 asyncio
import ast
import os
import time
@@ -126,10 +127,9 @@ class Daemon(DaemonThread):
self.network = None
else:
self.network = Network(config)
- self.network.start()
self.fx = FxThread(config, self.network)
if self.network:
- self.network.add_jobs([self.fx])
+ self.network.start(self.fx.run())
self.gui = None
self.wallets = {}
# Setup JSONRPC server
@@ -243,7 +243,7 @@ class Daemon(DaemonThread):
if storage.get_action():
return
wallet = Wallet(storage)
- wallet.start_threads(self.network)
+ wallet.start_network(self.network)
self.wallets[path] = wallet
return wallet
diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py
index 72f9b143..10ed3009 100644
--- a/electrum/ecc_fast.py
+++ b/electrum/ecc_fast.py
@@ -72,6 +72,9 @@ def load_library():
secp256k1.secp256k1_ecdsa_signature_parse_compact.argtypes = [c_void_p, c_char_p, c_char_p]
secp256k1.secp256k1_ecdsa_signature_parse_compact.restype = c_int
+ secp256k1.secp256k1_ecdsa_signature_normalize.argtypes = [c_void_p, c_char_p, c_char_p]
+ secp256k1.secp256k1_ecdsa_signature_normalize.restype = c_int
+
secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [c_void_p, c_char_p, c_char_p]
secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = c_int
@@ -181,7 +184,9 @@ def _prepare_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1():
def do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1():
if not _libsecp256k1:
- print_error('[ecc] warning: libsecp256k1 library not available, falling back to python-ecdsa')
+ # FIXME print_error will always print as 'verbosity' is not yet initialised
+ print_error('[ecc] info: libsecp256k1 library not available, falling back to python-ecdsa. '
+ 'This means signing operations will be slower.')
return
if not _patched_functions.prepared_to_patch:
raise Exception("can't patch python-ecdsa without preparations")
diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py
index b20b4ea6..2e9dcd1a 100644
--- a/electrum/exchange_rate.py
+++ b/electrum/exchange_rate.py
@@ -1,19 +1,23 @@
+import asyncio
+import aiohttp
+from aiohttp_socks import SocksConnector, SocksVer
from datetime import datetime
import inspect
-import requests
import sys
import os
import json
-from threading import Thread
import time
import csv
import decimal
from decimal import Decimal
+import concurrent.futures
+import traceback
from .bitcoin import COIN
from .i18n import _
-from .util import PrintError, ThreadJob, make_dir
-
+from .util import PrintError, ThreadJob, make_dir, aiosafe
+from .util import make_aiohttp_session
+from .network import Network
# See https://en.wikipedia.org/wiki/ISO_4217
CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
@@ -23,7 +27,6 @@ CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 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):
@@ -32,34 +35,41 @@ class ExchangeBase(PrintError):
self.on_quotes = on_quotes
self.on_history = on_history
- def get_json(self, site, get_string):
+ async def get_raw(self, site, get_string):
# APIs must have https
url = ''.join(['https://', site, get_string])
- response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}, timeout=10)
- return response.json()
+ async with make_aiohttp_session(Network.get_instance().proxy) as session:
+ async with session.get(url) as response:
+ return await response.text()
- def get_csv(self, site, get_string):
+ async 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'})
- reader = csv.DictReader(response.content.decode().split('\n'))
+ async with make_aiohttp_session(Network.get_instance().proxy) as session:
+ async with session.get(url) as response:
+ return await response.json()
+
+ async def get_csv(self, site, get_string):
+ raw = await self.get_raw(site, get_string)
+ reader = csv.DictReader(raw.split('\n'))
return list(reader)
def name(self):
return self.__class__.__name__
- def update_safe(self, ccy):
+ @aiosafe
+ async def update_safe(self, ccy):
try:
self.print_error("getting fx quotes for", ccy)
- self.quotes = self.get_rates(ccy)
+ self.quotes = await self.get_rates(ccy)
self.print_error("received fx quotes")
except BaseException as e:
self.print_error("failed fx quotes:", e)
+ self.quotes = {}
self.on_quotes()
def update(self, ccy):
- t = Thread(target=self.update_safe, args=(ccy,))
- t.setDaemon(True)
- t.start()
+ asyncio.get_event_loop().create_task(self.update_safe(ccy))
def read_historical_rates(self, ccy, cache_dir):
filename = os.path.join(cache_dir, self.name() + '_'+ ccy)
@@ -78,13 +88,15 @@ class ExchangeBase(PrintError):
self.on_history()
return h
- def get_historical_rates_safe(self, ccy, cache_dir):
+ @aiosafe
+ async def get_historical_rates_safe(self, ccy, cache_dir):
try:
self.print_error("requesting fx history for", ccy)
- h = self.request_history(ccy)
+ h = await self.request_history(ccy)
self.print_error("received fx history for", ccy)
except BaseException as e:
self.print_error("failed fx history:", e)
+ #traceback.print_exc()
return
filename = os.path.join(cache_dir, self.name() + '_' + ccy)
with open(filename, 'w', encoding='utf-8') as f:
@@ -100,9 +112,7 @@ class ExchangeBase(PrintError):
if h is None:
h = self.read_historical_rates(ccy, cache_dir)
if h is None or h['timestamp'] < time.time() - 24*3600:
- t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir))
- t.setDaemon(True)
- t.start()
+ asyncio.get_event_loop().create_task(self.get_historical_rates_safe(ccy, cache_dir))
def history_ccys(self):
return []
@@ -116,8 +126,8 @@ class ExchangeBase(PrintError):
class BitcoinAverage(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short')
+ async def get_rates(self, ccy):
+ json = await self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short')
return dict([(r.replace("BTC", ""), Decimal(json[r]['last']))
for r in json if r != 'timestamp'])
@@ -126,8 +136,8 @@ class BitcoinAverage(ExchangeBase):
'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD',
'ZAR']
- def request_history(self, ccy):
- history = self.get_csv('apiv2.bitcoinaverage.com',
+ async def request_history(self, ccy):
+ history = await self.get_csv('apiv2.bitcoinaverage.com',
"/indices/global/history/BTC%s?period=alltime&format=csv" % ccy)
return dict([(h['DateTime'][:10], h['Average'])
for h in history])
@@ -135,8 +145,8 @@ class BitcoinAverage(ExchangeBase):
class Bitcointoyou(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('bitcointoyou.com', "/API/ticker.aspx")
+ async def get_rates(self, ccy):
+ json = await self.get_json('bitcointoyou.com', "/API/ticker.aspx")
return {'BRL': Decimal(json['ticker']['last'])}
def history_ccys(self):
@@ -145,8 +155,8 @@ class Bitcointoyou(ExchangeBase):
class BitcoinVenezuela(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('api.bitcoinvenezuela.com', '/')
+ async def get_rates(self, ccy):
+ json = await self.get_json('api.bitcoinvenezuela.com', '/')
rates = [(r, json['BTC'][r]) for r in json['BTC']
if json['BTC'][r] is not None] # Giving NULL for LTC
return dict(rates)
@@ -154,64 +164,65 @@ class BitcoinVenezuela(ExchangeBase):
def history_ccys(self):
return ['ARS', 'EUR', 'USD', 'VEF']
- def request_history(self, ccy):
- return self.get_json('api.bitcoinvenezuela.com',
- "/historical/index.php?coin=BTC")[ccy +'_BTC']
+ async def request_history(self, ccy):
+ json = await self.get_json('api.bitcoinvenezuela.com',
+ "/historical/index.php?coin=BTC")
+ return json[ccy +'_BTC']
class Bitbank(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('public.bitbank.cc', '/btc_jpy/ticker')
+ async def get_rates(self, ccy):
+ json = await self.get_json('public.bitbank.cc', '/btc_jpy/ticker')
return {'JPY': Decimal(json['data']['last'])}
class BitFlyer(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('bitflyer.jp', '/api/echo/price')
+ async def get_rates(self, ccy):
+ json = await self.get_json('bitflyer.jp', '/api/echo/price')
return {'JPY': Decimal(json['mid'])}
class Bitmarket(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
+ async def get_rates(self, ccy):
+ json = await self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
return {'PLN': Decimal(json['last'])}
class BitPay(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('bitpay.com', '/api/rates')
+ async def get_rates(self, ccy):
+ json = await self.get_json('bitpay.com', '/api/rates')
return dict([(r['code'], Decimal(r['rate'])) for r in json])
class Bitso(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('api.bitso.com', '/v2/ticker')
+ async def get_rates(self, ccy):
+ json = await self.get_json('api.bitso.com', '/v2/ticker')
return {'MXN': Decimal(json['last'])}
class BitStamp(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('www.bitstamp.net', '/api/ticker/')
+ async def get_rates(self, ccy):
+ json = await self.get_json('www.bitstamp.net', '/api/ticker/')
return {'USD': Decimal(json['last'])}
class Bitvalor(ExchangeBase):
- def get_rates(self,ccy):
- json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
+ async def get_rates(self,ccy):
+ json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
return {'BRL': Decimal(json['ticker_1h']['total']['last'])}
class Bittrex(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('bittrex.com','/api/v1.1/public/getticker?market=BTC-FLO')
+ async def get_rates(self, ccy):
+ json = await self.get_json('bittrex.com','/api/v1.1/public/getticker?market=BTC-FLO')
floPrice_inBTC = json['result']['Last']
json = self.get_json('blockchain.info', '/ticker')
return dict([(r, Decimal(json[r]['15m'] * floPrice_inBTC)) for r in json])
@@ -219,22 +230,22 @@ class Bittrex(ExchangeBase):
class BTCChina(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('data.btcchina.com', '/data/ticker')
+ async def get_rates(self, ccy):
+ json = await self.get_json('data.btcchina.com', '/data/ticker')
return {'CNY': Decimal(json['ticker']['last'])}
class BTCParalelo(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('btcparalelo.com', '/api/price')
+ async def get_rates(self, ccy):
+ json = await self.get_json('btcparalelo.com', '/api/price')
return {'VEF': Decimal(json['price'])}
class Coinbase(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('coinbase.com',
+ async def get_rates(self, ccy):
+ json = await self.get_json('coinbase.com',
'/api/v1/currencies/exchange_rates')
return dict([(r[7:].upper(), Decimal(json[r]))
for r in json if r.startswith('btc_to_')])
@@ -242,13 +253,13 @@ class Coinbase(ExchangeBase):
class CoinDesk(ExchangeBase):
- def get_currencies(self):
- dicts = self.get_json('api.coindesk.com',
+ async def get_currencies(self):
+ dicts = await self.get_json('api.coindesk.com',
'/v1/bpi/supported-currencies.json')
return [d['currency'] for d in dicts]
- def get_rates(self, ccy):
- json = self.get_json('api.coindesk.com',
+ async def get_rates(self, ccy):
+ json = await self.get_json('api.coindesk.com',
'/v1/bpi/currentprice/%s.json' % ccy)
result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])}
return result
@@ -259,35 +270,35 @@ class CoinDesk(ExchangeBase):
def history_ccys(self):
return self.history_starts().keys()
- def request_history(self, ccy):
+ async def request_history(self, ccy):
start = self.history_starts()[ccy]
end = datetime.today().strftime('%Y-%m-%d')
# Note ?currency and ?index don't work as documented. Sigh.
query = ('/v1/bpi/historical/close.json?start=%s&end=%s'
% (start, end))
- json = self.get_json('api.coindesk.com', query)
+ json = await self.get_json('api.coindesk.com', query)
return json['bpi']
class Coinsecure(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('api.coinsecure.in', '/v0/noauth/newticker')
+ async def get_rates(self, ccy):
+ json = await self.get_json('api.coinsecure.in', '/v0/noauth/newticker')
return {'INR': Decimal(json['lastprice'] / 100.0 )}
class Foxbit(ExchangeBase):
- def get_rates(self,ccy):
- json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
+ async def get_rates(self,ccy):
+ json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
return {'BRL': Decimal(json['ticker_1h']['exchanges']['FOX']['last'])}
class itBit(ExchangeBase):
- def get_rates(self, ccy):
+ async def get_rates(self, ccy):
ccys = ['USD', 'EUR', 'SGD']
- json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)
+ json = await self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)
result = dict.fromkeys(ccys)
if ccy in ccys:
result[ccy] = Decimal(json['lastPrice'])
@@ -296,10 +307,10 @@ class itBit(ExchangeBase):
class Kraken(ExchangeBase):
- def get_rates(self, ccy):
+ async def get_rates(self, ccy):
ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY']
pairs = ['XBT%s' % c for c in ccys]
- json = self.get_json('api.kraken.com',
+ json = await self.get_json('api.kraken.com',
'/0/public/Ticker?pair=%s' % ','.join(pairs))
return dict((k[-3:], Decimal(float(v['c'][0])))
for k, v in json['result'].items())
@@ -307,45 +318,45 @@ class Kraken(ExchangeBase):
class LocalBitcoins(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('localbitcoins.com',
+ async def get_rates(self, ccy):
+ json = await self.get_json('localbitcoins.com',
'/bitcoinaverage/ticker-all-currencies/')
return dict([(r, Decimal(json[r]['rates']['last'])) for r in json])
class MercadoBitcoin(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
+ async def get_rates(self, ccy):
+ json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
return {'BRL': Decimal(json['ticker_1h']['exchanges']['MBT']['last'])}
class NegocieCoins(ExchangeBase):
- def get_rates(self,ccy):
- json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
+ async def get_rates(self,ccy):
+ json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
return {'BRL': Decimal(json['ticker_1h']['exchanges']['NEG']['last'])}
class TheRockTrading(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('api.therocktrading.com',
+ async def get_rates(self, ccy):
+ json = await self.get_json('api.therocktrading.com',
'/v1/funds/BTCEUR/ticker')
return {'EUR': Decimal(json['last'])}
class Unocoin(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('www.unocoin.com', 'trade?buy')
+ async def get_rates(self, ccy):
+ json = await self.get_json('www.unocoin.com', 'trade?buy')
return {'INR': Decimal(json)}
class WEX(ExchangeBase):
- def get_rates(self, ccy):
- json_eur = self.get_json('wex.nz', '/api/3/ticker/btc_eur')
- json_rub = self.get_json('wex.nz', '/api/3/ticker/btc_rur')
- json_usd = self.get_json('wex.nz', '/api/3/ticker/btc_usd')
+ async def get_rates(self, ccy):
+ json_eur = await self.get_json('wex.nz', '/api/3/ticker/btc_eur')
+ json_rub = await self.get_json('wex.nz', '/api/3/ticker/btc_rur')
+ json_usd = await self.get_json('wex.nz', '/api/3/ticker/btc_usd')
return {'EUR': Decimal(json_eur['btc_eur']['last']),
'RUB': Decimal(json_rub['btc_rur']['last']),
'USD': Decimal(json_usd['btc_usd']['last'])}
@@ -353,15 +364,15 @@ class WEX(ExchangeBase):
class Winkdex(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('winkdex.com', '/api/v0/price')
+ async def get_rates(self, ccy):
+ json = await self.get_json('winkdex.com', '/api/v0/price')
return {'USD': Decimal(json['price'] / 100.0)}
def history_ccys(self):
return ['USD']
- def request_history(self, ccy):
- json = self.get_json('winkdex.com',
+ async def request_history(self, ccy):
+ json = await self.get_json('winkdex.com',
"/api/v0/series?start_time=1342915200")
history = json['series'][0]['results']
return dict([(h['timestamp'][:10], h['price'] / 100.0)
@@ -369,8 +380,8 @@ class Winkdex(ExchangeBase):
class Zaif(ExchangeBase):
- def get_rates(self, ccy):
- json = self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy')
+ async def get_rates(self, ccy):
+ json = await self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy')
return {'JPY': Decimal(json['last_price'])}
@@ -383,7 +394,6 @@ def dictinvert(d):
return inv
def get_exchanges_and_currencies():
- import os, json
path = os.path.join(os.path.dirname(__file__), 'currencies.json')
try:
with open(path, 'r', encoding='utf-8') as f:
@@ -428,14 +438,21 @@ class FxThread(ThreadJob):
def __init__(self, config, network):
self.config = config
self.network = network
+ if self.network:
+ self.network.register_callback(self.set_proxy, ['proxy_set'])
self.ccy = self.get_currency()
self.history_used_spot = False
self.ccy_combo = None
self.hist_checkbox = None
self.cache_dir = os.path.join(config.path, 'cache')
+ self.trigger = asyncio.Event()
+ self.trigger.set()
self.set_exchange(self.config_exchange())
make_dir(self.cache_dir)
+ def set_proxy(self, trigger_name, *args):
+ self.trigger.set()
+
def get_currencies(self, h):
d = get_exchanges_by_ccy(h)
return sorted(d.keys())
@@ -453,20 +470,26 @@ class FxThread(ThreadJob):
rounded_amount = amount
return fmt_str.format(rounded_amount)
- def run(self):
- # This runs from the plugins thread which catches exceptions
- if self.is_enabled():
- if self.timeout ==0 and self.show_history():
- self.exchange.get_historical_rates(self.ccy, self.cache_dir)
- if self.timeout <= time.time():
- self.timeout = time.time() + 150
+ async def run(self):
+ while True:
+ try:
+ await asyncio.wait_for(self.trigger.wait(), 150)
+ except concurrent.futures.TimeoutError:
+ pass
+ else:
+ self.trigger.clear()
+ if self.is_enabled():
+ if self.show_history():
+ self.exchange.get_historical_rates(self.ccy, self.cache_dir)
+ if self.is_enabled():
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))
+ self.config.set_key('use_exchange_rate', bool(b))
+ self.trigger.set()
def get_history_config(self):
return bool(self.config.get('history_rates'))
@@ -499,7 +522,7 @@ class FxThread(ThreadJob):
def set_currency(self, ccy):
self.ccy = ccy
self.config.set_key('currency', ccy, True)
- self.timeout = 0 # Because self.ccy changes
+ self.trigger.set() # Because self.ccy changes
self.on_quotes()
def set_exchange(self, name):
@@ -510,7 +533,7 @@ class FxThread(ThreadJob):
self.exchange = class_(self.on_quotes, self.on_history)
# A new exchange means new fx quotes, initially empty. Force
# a quote refresh
- self.timeout = 0
+ self.trigger.set()
self.exchange.read_historical_rates(self.ccy, self.cache_dir)
def on_quotes(self):
@@ -521,8 +544,8 @@ class FxThread(ThreadJob):
if self.network:
self.network.trigger_callback('on_history')
- def exchange_rate(self):
- '''Returns None, or the exchange rate as a Decimal'''
+ def exchange_rate(self) -> Decimal:
+ """Returns the exchange rate as a Decimal"""
rate = self.exchange.quotes.get(self.ccy)
if rate is None:
return Decimal('NaN')
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
index ba8e7da2..0dbb5280 100644
--- a/electrum/gui/kivy/main_window.py
+++ b/electrum/gui/kivy/main_window.py
@@ -15,6 +15,7 @@ from electrum.util import profiler, InvalidPassword
from electrum.plugin import run_hook
from electrum.util import format_satoshis, format_satoshis_plain
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
+from electrum import blockchain
from .i18n import _
from kivy.app import App
@@ -69,7 +70,8 @@ Label.register('Roboto',
from electrum.util import (base_units, NoDynamicFeeEstimates, decimal_point_to_base_unit_name,
- base_unit_name_to_decimal_point, NotEnoughFunds)
+ base_unit_name_to_decimal_point, NotEnoughFunds, UnknownBaseUnit,
+ DECIMAL_POINT_DEFAULT)
class ElectrumWindow(App):
@@ -92,8 +94,9 @@ class ElectrumWindow(App):
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)
+ net_params = self.network.get_parameters()
+ net_params = net_params._replace(auto_connect=self.auto_connect)
+ self.network.set_parameters(net_params)
def toggle_auto_connect(self, x):
self.auto_connect = not self.auto_connect
@@ -113,10 +116,10 @@ class ElectrumWindow(App):
from .uix.dialogs.choice_dialog import ChoiceDialog
chains = self.network.get_blockchains()
def cb(name):
- for index, b in self.network.blockchains.items():
+ for index, b in blockchain.blockchains.items():
if name == b.get_name():
self.network.follow_chain(index)
- names = [self.network.blockchains[b].get_name() for b in chains]
+ names = [blockchain.blockchains[b].get_name() for b in chains]
if len(names) > 1:
cur_chain = self.network.blockchain().get_name()
ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()
@@ -153,6 +156,7 @@ class ElectrumWindow(App):
def on_quotes(self, d):
Logger.info("on_quotes")
+ self._trigger_update_status()
self._trigger_update_history()
def on_history(self, d):
@@ -163,8 +167,11 @@ class ElectrumWindow(App):
self._trigger_update_history()
def _get_bu(self):
- decimal_point = self.electrum_config.get('decimal_point', 5)
- return decimal_point_to_base_unit_name(decimal_point)
+ decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
+ try:
+ return decimal_point_to_base_unit_name(decimal_point)
+ except UnknownBaseUnit:
+ return decimal_point_to_base_unit_name(DECIMAL_POINT_DEFAULT)
def _set_bu(self, value):
assert value in base_units.keys()
@@ -262,11 +269,11 @@ class ElectrumWindow(App):
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 {}
+ net_params = self.network.get_parameters()
+ self.server_host = net_params.host
+ self.server_port = net_params.port
+ self.auto_connect = net_params.auto_connect
+ self.proxy_config = net_params.proxy if net_params.proxy else {}
self.plugins = kwargs.get('plugins', [])
self.gui_object = kwargs.get('gui_object', None)
@@ -505,7 +512,7 @@ class ElectrumWindow(App):
def on_wizard_complete(self, wizard, wallet):
if wallet: # wizard returned a wallet
- wallet.start_threads(self.daemon.network)
+ wallet.start_network(self.daemon.network)
self.daemon.add_wallet(wallet)
self.load_wallet(wallet)
elif not self.wallet:
@@ -692,7 +699,7 @@ class ElectrumWindow(App):
if not self.wallet:
self.status = _("No Wallet")
return
- if self.network is None or not self.network.is_running():
+ if self.network is None or not self.network.is_connected():
status = _("Offline")
elif self.network.is_connected():
server_height = self.network.get_server_height()
@@ -873,7 +880,7 @@ class ElectrumWindow(App):
Clock.schedule_once(lambda dt: on_success(tx))
def _broadcast_thread(self, tx, on_complete):
- ok, txid = self.network.broadcast_transaction(tx)
+ ok, txid = self.network.broadcast_transaction_from_non_network_thread(tx)
Clock.schedule_once(lambda dt: on_complete(ok, txid))
def broadcast(self, tx, pr=None):
diff --git a/electrum/gui/kivy/uix/dialogs/crash_reporter.py b/electrum/gui/kivy/uix/dialogs/crash_reporter.py
index 04582b95..f02222fd 100644
--- a/electrum/gui/kivy/uix/dialogs/crash_reporter.py
+++ b/electrum/gui/kivy/uix/dialogs/crash_reporter.py
@@ -1,6 +1,7 @@
import sys
+import json
-import requests
+from aiohttp.client_exceptions import ClientError
from kivy import base, utils
from kivy.clock import Clock
from kivy.core.window import Window
@@ -115,8 +116,10 @@ class CrashReporter(BaseCrashReporter, Factory.Popup):
def send_report(self):
try:
- response = BaseCrashReporter.send_report(self, "/crash.json").json()
- except requests.exceptions.RequestException:
+ loop = self.main_window.network.asyncio_loop
+ proxy = self.main_window.network.proxy
+ response = json.loads(BaseCrashReporter.send_report(self, loop, proxy, "/crash.json"))
+ except (ValueError, ClientError):
self.show_popup(_('Unable to send report'), _("Please check your network connection."))
else:
self.show_popup(_('Report sent'), response["text"])
diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py
index 4e63c4a5..07530a0a 100644
--- a/electrum/gui/kivy/uix/dialogs/settings.py
+++ b/electrum/gui/kivy/uix/dialogs/settings.py
@@ -154,13 +154,16 @@ class SettingsDialog(Factory.Popup):
self._coinselect_dialog.open()
def proxy_status(self):
- server, port, protocol, proxy, auto_connect = self.app.network.get_parameters()
+ net_params = self.app.network.get_parameters()
+ proxy = net_params.proxy
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()
+ net_params = self.app.network.get_parameters()
+ proxy = net_params.proxy
def callback(popup):
+ nonlocal net_params
if popup.ids.mode.text != 'None':
proxy = {
'mode':popup.ids.mode.text,
@@ -171,7 +174,8 @@ class SettingsDialog(Factory.Popup):
}
else:
proxy = None
- self.app.network.set_parameters(server, port, protocol, proxy, auto_connect)
+ net_params = net_params._replace(proxy=proxy)
+ self.app.network.set_parameters(net_params)
item.status = self.proxy_status()
popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/proxy.kv')
popup.ids.mode.text = proxy.get('mode') if proxy else 'None'
diff --git a/electrum/gui/kivy/uix/ui_screens/proxy.kv b/electrum/gui/kivy/uix/ui_screens/proxy.kv
index 738a8ffd..087254ad 100644
--- a/electrum/gui/kivy/uix/ui_screens/proxy.kv
+++ b/electrum/gui/kivy/uix/ui_screens/proxy.kv
@@ -14,7 +14,7 @@ Popup:
height: '48dp'
size_hint_y: None
text: app.proxy_config.get('mode', 'none')
- values: ['none', 'socks4', 'socks5', 'http']
+ values: ['none', 'socks4', 'socks5']
Label:
text: _('Host')
TextInput:
@@ -63,7 +63,7 @@ Popup:
height: '48dp'
text: _('OK')
on_release:
- host, port, protocol, proxy, auto_connect = app.network.get_parameters()
+ net_params = app.network.get_parameters()
proxy = {}
proxy['mode']=str(root.ids.mode.text).lower()
proxy['host']=str(root.ids.host.text)
@@ -71,6 +71,7 @@ Popup:
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)
+ net_params = net_params._replace(proxy=proxy)
+ app.network.set_parameters(net_params)
app.proxy_config = proxy if proxy else {}
nd.dismiss()
diff --git a/electrum/gui/kivy/uix/ui_screens/server.kv b/electrum/gui/kivy/uix/ui_screens/server.kv
index 293d4645..ee959cba 100644
--- a/electrum/gui/kivy/uix/ui_screens/server.kv
+++ b/electrum/gui/kivy/uix/ui_screens/server.kv
@@ -56,8 +56,7 @@ Popup:
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)
+ net_params = app.network.get_parameters()
+ net_params = net_params._replace(host=str(root.ids.host.text), port=str(root.ids.port.text))
+ app.network.set_parameters(net_params)
nd.dismiss()
diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
index 7fa7f4e5..3fd204d7 100644
--- a/electrum/gui/qt/__init__.py
+++ b/electrum/gui/qt/__init__.py
@@ -236,7 +236,7 @@ class ElectrumGui:
if not self.daemon.get_wallet(wallet.storage.path):
# wallet was not in memory
- wallet.start_threads(self.daemon.network)
+ wallet.start_network(self.daemon.network)
self.daemon.add_wallet(wallet)
try:
for w in self.windows:
@@ -260,7 +260,8 @@ class ElectrumGui:
return w
def close_window(self, window):
- self.windows.remove(window)
+ if window in self.windows:
+ self.windows.remove(window)
self.build_tray_menu()
# save wallet path of last open window
if not self.windows:
diff --git a/electrum/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py
index 1774a043..1d30b3ee 100644
--- a/electrum/gui/qt/exception_window.py
+++ b/electrum/gui/qt/exception_window.py
@@ -41,6 +41,7 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin):
def __init__(self, main_window, exctype, value, tb):
BaseCrashReporter.__init__(self, exctype, value, tb)
self.main_window = main_window
+
QWidget.__init__(self)
self.setWindowTitle('Electrum - ' + _('An Error Occurred'))
self.setMinimumSize(600, 300)
@@ -90,14 +91,15 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin):
def send_report(self):
try:
- response = BaseCrashReporter.send_report(self)
+ proxy = self.main_window.network.proxy
+ response = BaseCrashReporter.send_report(self, self.main_window.network.asyncio_loop, proxy)
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' +
str(e) + '\n' +
_("Please report this issue manually."))
return
- QMessageBox.about(self, _("Crash report"), response.text)
+ QMessageBox.about(self, _("Crash report"), response)
self.close()
def on_close(self):
diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py
index edcdc682..50e9bea9 100644
--- a/electrum/gui/qt/history_list.py
+++ b/electrum/gui/qt/history_list.py
@@ -298,6 +298,8 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
else:
tx_hash = item.data(0, Qt.UserRole)
tx = self.wallet.transactions.get(tx_hash)
+ if not tx:
+ return
self.parent.show_transaction(tx)
def update_labels(self):
@@ -331,6 +333,9 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
tx_hash = item.data(0, Qt.UserRole)
if not tx_hash:
return
+ tx = self.wallet.transactions.get(tx_hash)
+ if not tx:
+ return
if column is 0:
column_title = "ID"
column_data = tx_hash
@@ -339,7 +344,6 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
column_data = item.text(column)
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
height = self.wallet.get_tx_height(tx_hash).height
- 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)
diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py
index cd45e6f3..8d201663 100644
--- a/electrum/gui/qt/installwizard.py
+++ b/electrum/gui/qt/installwizard.py
@@ -102,7 +102,6 @@ def wizard_dialog(func):
class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
accept_signal = pyqtSignal()
- synchronized_signal = pyqtSignal(str)
def __init__(self, config, app, plugins, storage):
BaseWizard.__init__(self, config, plugins, storage)
@@ -448,27 +447,6 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
self.exec_layout(playout.layout())
return playout.encrypt_cb.isChecked()
- def show_restore(self, wallet, network):
- # FIXME: these messages are shown after the install wizard is
- # finished and the window closed. On macOS they appear parented
- # with a re-appeared ghost install wizard window...
- if network:
- def task():
- wallet.wait_until_synchronized()
- if wallet.is_found():
- msg = _("Recovery successful")
- else:
- msg = _("No transactions found for this seed")
- self.synchronized_signal.emit(msg)
- self.synchronized_signal.connect(self.show_message)
- t = threading.Thread(target = task)
- t.daemon = True
- t.start()
- else:
- msg = _("This wallet was restored offline. It may "
- "contain more addresses than displayed.")
- self.show_message(msg)
-
@wizard_dialog
def confirm_dialog(self, title, message, run_next):
self.confirm(message, title)
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index b818bdd3..5c3c3847 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -31,6 +31,7 @@ import csv
from decimal import Decimal
import base64
from functools import partial
+import queue
from PyQt5.QtGui import *
from PyQt5.QtCore import *
@@ -49,7 +50,8 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
UserCancelled, NoDynamicFeeEstimates, profiler,
export_meta, import_meta, bh2u, bfh, InvalidPassword,
base_units, base_units_list, base_unit_name_to_decimal_point,
- decimal_point_to_base_unit_name, quantize_feerate)
+ decimal_point_to_base_unit_name, quantize_feerate,
+ UnknownBaseUnit, DECIMAL_POINT_DEFAULT)
from electrum.transaction import Transaction, TxOutput
from electrum.address_synchronizer import AddTransactionException
from electrum.wallet import Multisig_Wallet, CannotBumpFee
@@ -89,7 +91,6 @@ 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)
@@ -119,15 +120,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.not_enough_funds = False
self.pluginsdialog = None
self.require_fee_update = False
- self.tx_notifications = []
self.tl_windows = []
self.tx_external_keypairs = {}
+ self.tx_notification_queue = queue.Queue()
+ self.tx_notification_last_time = 0
+
self.create_status_bar()
self.need_update = threading.Event()
- self.decimal_point = config.get('decimal_point', 5)
- self.num_zeros = int(config.get('num_zeros',0))
+ self.decimal_point = config.get('decimal_point', DECIMAL_POINT_DEFAULT)
+ try:
+ decimal_point_to_base_unit_name(self.decimal_point)
+ except UnknownBaseUnit:
+ self.decimal_point = DECIMAL_POINT_DEFAULT
+ self.num_zeros = int(config.get('num_zeros', 0))
self.completions = QStringListModel()
@@ -176,7 +183,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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
@@ -294,8 +300,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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()
+ # FIXME maybe this event should also include which wallet
+ # the tx is for. now all wallets get this.
+ self.tx_notification_queue.put(args[0])
elif event in ['status', 'banner', 'verified', 'fee']:
# Handle in GUI thread
self.network_signal.emit(event, args)
@@ -351,7 +358,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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()
@@ -557,7 +563,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def donate_to_server(self):
d = self.network.get_donation_address()
if d:
- host = self.network.get_parameters()[0]
+ host = self.network.get_parameters().host
self.pay_to_URI('bitcoin:%s?message=donation for %s'%(d, host))
else:
self.show_error(_('No donation address for this server'))
@@ -582,28 +588,36 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.show_message(msg, title="Electrum - " + _("Reporting Bugs"))
def notify_transactions(self):
- if not self.network or not self.network.is_connected():
+ # note: during initial history sync for a wallet, many txns will be
+ # received multiple times. hence the "total amount received" will be
+ # a lot higher than should be. this is expected though not intended
+ if self.tx_notification_queue.qsize() == 0:
return
- self.print_error("Notifying GUI")
- if len(self.tx_notifications) > 0:
- # Combine the transactions if there are at least three
- num_txns = len(self.tx_notifications)
- if num_txns >= 3:
- total_amount = 0
- for tx in self.tx_notifications:
- is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
- if v > 0:
- total_amount += v
- self.notify(_("{} new transactions received: Total amount received in the new transactions {}")
- .format(num_txns, self.format_amount_and_units(total_amount)))
- self.tx_notifications = []
- else:
- for tx in self.tx_notifications:
- if tx:
- self.tx_notifications.remove(tx)
- is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
- if v > 0:
- self.notify(_("New transaction received: {}").format(self.format_amount_and_units(v)))
+ now = time.time()
+ if self.tx_notification_last_time + 5 > now:
+ return
+ self.tx_notification_last_time = now
+ self.print_error("Notifying GUI about new transactions")
+ txns = []
+ while True:
+ try:
+ txns.append(self.tx_notification_queue.get_nowait())
+ except queue.Empty:
+ break
+ # Combine the transactions if there are at least three
+ if len(txns) >= 3:
+ total_amount = 0
+ for tx in txns:
+ is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
+ if v > 0:
+ total_amount += v
+ self.notify(_("{} new transactions received: Total amount received in the new transactions {}")
+ .format(len(txns), self.format_amount_and_units(total_amount)))
+ else:
+ for tx in txns:
+ is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
+ if v > 0:
+ self.notify(_("New transaction received: {}").format(self.format_amount_and_units(v)))
def notify(self, message):
if self.tray:
@@ -646,6 +660,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.require_fee_update:
self.do_update_fee()
self.require_fee_update = False
+ self.notify_transactions()
+
def format_amount(self, x, is_diff=False, whitespaces=False):
return format_satoshis(x, self.num_zeros, self.decimal_point, is_diff=is_diff, whitespaces=whitespaces)
@@ -707,7 +723,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if not self.wallet:
return
- if self.network is None or not self.network.is_running():
+ if self.network is None:
text = _("Offline")
icon = QIcon(":icons/status_disconnected.png")
@@ -1618,7 +1634,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if pr and pr.has_expired():
self.payment_request = None
return False, _("Payment request has expired")
- status, msg = self.network.broadcast_transaction(tx)
+ status, msg = self.network.broadcast_transaction_from_non_network_thread(tx)
if pr and status is True:
self.invoices.set_paid(pr, tx.txid())
self.invoices.save()
diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py
index ecc7695b..b9b185e8 100644
--- a/electrum/gui/qt/network_dialog.py
+++ b/electrum/gui/qt/network_dialog.py
@@ -31,9 +31,9 @@ from PyQt5.QtWidgets import *
import PyQt5.QtCore as QtCore
from electrum.i18n import _
-from electrum import constants
+from electrum import constants, blockchain
from electrum.util import print_error
-from electrum.network import serialize_server, deserialize_server
+from electrum.interface import serialize_server, deserialize_server
from .util import *
@@ -103,7 +103,7 @@ class NodesListWidget(QTreeWidget):
chains = network.get_blockchains()
n_chains = len(chains)
for k, items in chains.items():
- b = network.blockchains[k]
+ b = blockchain.blockchains[k]
name = b.get_name()
if n_chains >1:
x = QTreeWidgetItem([name + '@%d'%b.get_forkpoint(), '%d'%b.height()])
@@ -239,7 +239,7 @@ class NetworkChoiceLayout(object):
self.proxy_cb.clicked.connect(self.set_proxy)
self.proxy_mode = QComboBox()
- self.proxy_mode.addItems(['SOCKS4', 'SOCKS5', 'HTTP'])
+ self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
self.proxy_host = QLineEdit()
self.proxy_host.setFixedWidth(200)
self.proxy_port = QLineEdit()
@@ -335,9 +335,11 @@ class NetworkChoiceLayout(object):
w.setEnabled(False)
def update(self):
- host, port, protocol, proxy_config, auto_connect = self.network.get_parameters()
+ net_params = self.network.get_parameters()
+ host, port, protocol = net_params.host, net_params.port, net_params.protocol
+ proxy_config, auto_connect = net_params.proxy, net_params.auto_connect
self.server_host.setText(host)
- self.server_port.setText(port)
+ self.server_port.setText(str(port))
self.autoconnect_cb.setChecked(auto_connect)
interface = self.network.interface
@@ -368,7 +370,7 @@ class NetworkChoiceLayout(object):
self.nodes_list_widget.update(self.network)
def fill_in_proxy_settings(self):
- host, port, protocol, proxy_config, auto_connect = self.network.get_parameters()
+ proxy_config = self.network.get_parameters().proxy
if not proxy_config:
proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
@@ -409,9 +411,10 @@ class NetworkChoiceLayout(object):
def follow_server(self, server):
self.network.switch_to_interface(server)
- host, port, protocol, proxy, auto_connect = self.network.get_parameters()
+ net_params = self.network.get_parameters()
host, port, protocol = deserialize_server(server)
- self.network.set_parameters(host, port, protocol, proxy, auto_connect)
+ net_params = net_params._replace(host=host, port=port, protocol=protocol)
+ self.network.set_parameters(net_params)
self.update()
def server_changed(self, x):
@@ -440,14 +443,14 @@ class NetworkChoiceLayout(object):
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())
- auto_connect = self.autoconnect_cb.isChecked()
- self.network.set_parameters(host, port, protocol, proxy, auto_connect)
+ net_params = self.network.get_parameters()
+ net_params = net_params._replace(host=str(self.server_host.text()),
+ port=str(self.server_port.text()),
+ auto_connect=self.autoconnect_cb.isChecked())
+ self.network.set_parameters(net_params)
def set_proxy(self):
- host, port, protocol, proxy, auto_connect = self.network.get_parameters()
+ net_params = self.network.get_parameters()
if self.proxy_cb.isChecked():
proxy = { 'mode':str(self.proxy_mode.currentText()).lower(),
'host':str(self.proxy_host.text()),
@@ -457,7 +460,8 @@ class NetworkChoiceLayout(object):
else:
proxy = None
self.tor_cb.setChecked(False)
- self.network.set_parameters(host, port, protocol, proxy, auto_connect)
+ net_params = net_params._replace(proxy=proxy)
+ self.network.set_parameters(net_params)
def suggest_proxy(self, found_proxy):
self.tor_proxy = found_proxy
diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py
index 78e86536..f75ffb37 100644
--- a/electrum/gui/qt/utxo_list.py
+++ b/electrum/gui/qt/utxo_list.py
@@ -68,7 +68,8 @@ class UTXOList(MyTreeWidget):
if len(selected) == 1:
txid = selected[0].split(':')[0]
tx = self.wallet.transactions.get(txid)
- menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
+ if tx:
+ menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
menu.exec_(self.viewport().mapToGlobal(position))
diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py
index d155efb0..cbb0d340 100644
--- a/electrum/gui/stdio.py
+++ b/electrum/gui/stdio.py
@@ -34,7 +34,7 @@ class ElectrumGui:
self.str_fee = ""
self.wallet = Wallet(storage)
- self.wallet.start_threads(self.network)
+ self.wallet.start_network(self.network)
self.contacts = self.wallet.contacts
self.network.register_callback(self.on_network, ['updated', 'banner'])
@@ -200,7 +200,7 @@ class ElectrumGui:
self.wallet.labels[tx.txid()] = self.str_description
print(_("Please wait..."))
- status, msg = self.network.broadcast_transaction(tx)
+ status, msg = self.network.broadcast_transaction_from_non_network_thread(tx)
if status:
print(_('Payment sent.'))
diff --git a/electrum/gui/text.py b/electrum/gui/text.py
index 61e3ac1d..98134013 100644
--- a/electrum/gui/text.py
+++ b/electrum/gui/text.py
@@ -7,7 +7,10 @@ import electrum
from electrum.util import format_satoshis, set_verbosity
from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
from electrum.transaction import TxOutput
-from .. import Wallet, WalletStorage
+from electrum.wallet import Wallet
+from electrum.storage import WalletStorage
+from electrum.network import NetworkParameters
+from electrum.interface import deserialize_server
_ = lambda x:x
@@ -27,7 +30,7 @@ class ElectrumGui:
password = getpass.getpass('Password:', stream=None)
storage.decrypt(password)
self.wallet = Wallet(storage)
- self.wallet.start_threads(self.network)
+ self.wallet.start_network(self.network)
self.contacts = self.wallet.contacts
locale.setlocale(locale.LC_ALL, '')
@@ -351,7 +354,7 @@ class ElectrumGui:
self.wallet.labels[tx.txid()] = self.str_description
self.show_message(_("Please wait..."), getchar=False)
- status, msg = self.network.broadcast_transaction(tx)
+ status, msg = self.network.broadcast_transaction_from_non_network_thread(tx)
if status:
self.show_message(_('Payment sent.'))
@@ -376,8 +379,9 @@ class ElectrumGui:
def network_dialog(self):
if not self.network:
return
- params = self.network.get_parameters()
- host, port, protocol, proxy_config, auto_connect = params
+ net_params = self.network.get_parameters()
+ host, port, protocol = net_params.host, net_params.port, net_params.protocol
+ proxy_config, auto_connect = net_params.proxy, net_params.auto_connect
srv = 'auto-connect' if auto_connect else self.network.default_server
out = self.run_dialog('Network', [
{'label':'server', 'type':'str', 'value':srv},
@@ -389,13 +393,13 @@ class ElectrumGui:
auto_connect = server == 'auto-connect'
if not auto_connect:
try:
- host, port, protocol = server.split(':')
+ host, port, protocol = deserialize_server(server)
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)
+ self.network.set_parameters(NetworkParameters(host, port, protocol, proxy, auto_connect))
def settings_dialog(self):
fee = str(Decimal(self.config.fee_per_kb()) / COIN)
diff --git a/electrum/interface.py b/electrum/interface.py
index 357d913a..47fdf308 100644
--- a/electrum/interface.py
+++ b/electrum/interface.py
@@ -24,346 +24,497 @@
# SOFTWARE.
import os
import re
-import socket
import ssl
import sys
-import threading
-import time
import traceback
+import asyncio
+from typing import Tuple, Union
-import requests
-
-from .util import print_error
-
-ca_path = requests.certs.where()
+import aiorpcx
+from aiorpcx import ClientSession, Notification
+from .util import PrintError, aiosafe, bfh, AIOSafeSilentException, CustomTaskGroup
from . import util
from . import x509
from . import pem
+from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
+from . import blockchain
+from . import constants
-def Connection(server, queue, config_path):
- """Makes asynchronous connections to a remote Electrum server.
- Returns the running thread that is making the connection.
+class NotificationSession(ClientSession):
- 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
+ def __init__(self, *args, **kwargs):
+ super(NotificationSession, self).__init__(*args, **kwargs)
+ self.subscriptions = {}
+ self.cache = {}
-
-class TcpConnection(threading.Thread, util.PrintError):
- verbosity_filter = 'i'
-
- def __init__(self, server, queue, config_path):
- threading.Thread.__init__(self)
- self.config_path = config_path
- self.queue = queue
- self.server = server
- self.host, self.port, self.protocol = self.server.rsplit(':', 2)
- self.host = str(self.host)
- self.port = int(self.port)
- self.use_ssl = (self.protocol == 's')
- self.daemon = True
-
- def diagnostic_name(self):
- return self.host
-
- def check_host_name(self, peercert, name):
- """Simple certificate/host name checker. Returns True if the
- certificate matches, False otherwise. Does not support
- wildcards."""
- # Check that the peer has supplied a certificate.
- # None/{} is not acceptable.
- if not peercert:
- return False
- if 'subjectAltName' in peercert:
- for typ, val in peercert["subjectAltName"]:
- if typ == "DNS" and val == name:
- return True
- else:
- # Only check the subject DN if there is no subject alternative
- # name.
- cn = None
- for attr, val in peercert["subject"]:
- # Use most-specific (last) commonName attribute.
- if attr == "commonName":
- cn = val
- if cn is not None:
- return cn == name
- return False
-
- def get_simple_socket(self):
- try:
- l = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM)
- except socket.gaierror:
- self.print_error("cannot resolve hostname")
- return
- e = None
- for res in l:
- try:
- s = socket.socket(res[0], socket.SOCK_STREAM)
- s.settimeout(10)
- s.connect(res[4])
- s.settimeout(2)
- s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
- return s
- except BaseException as _e:
- e = _e
- continue
- else:
- self.print_error("failed to connect", str(e))
-
- @staticmethod
- def get_ssl_context(cert_reqs, ca_certs):
- context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_certs)
- context.check_hostname = False
- context.verify_mode = cert_reqs
-
- context.options |= ssl.OP_NO_SSLv2
- context.options |= ssl.OP_NO_SSLv3
- context.options |= ssl.OP_NO_TLSv1
-
- return context
-
- def get_socket(self):
- if self.use_ssl:
- cert_path = os.path.join(self.config_path, 'certs', self.host)
- if not os.path.exists(cert_path):
- is_new = True
- s = self.get_simple_socket()
- if s is None:
- return
- # try with CA first
- try:
- context = self.get_ssl_context(cert_reqs=ssl.CERT_REQUIRED, ca_certs=ca_path)
- s = context.wrap_socket(s, do_handshake_on_connect=True)
- except ssl.SSLError as e:
- self.print_error(e)
- except:
- return
- else:
- try:
- peer_cert = s.getpeercert()
- except OSError:
- return
- if self.check_host_name(peer_cert, self.host):
- self.print_error("SSL certificate signed by CA")
- return s
- # get server certificate.
- # Do not use ssl.get_server_certificate because it does not work with proxy
- s = self.get_simple_socket()
- if s is None:
- return
- try:
- context = self.get_ssl_context(cert_reqs=ssl.CERT_NONE, ca_certs=None)
- s = context.wrap_socket(s)
- except ssl.SSLError as e:
- self.print_error("SSL error retrieving SSL certificate:", e)
- return
- except:
- return
-
- try:
- dercert = s.getpeercert(True)
- except OSError:
- return
- s.close()
- cert = ssl.DER_cert_to_PEM_cert(dercert)
- # workaround android bug
- cert = re.sub("([^\n])-----END CERTIFICATE-----","\\1\n-----END CERTIFICATE-----",cert)
- temporary_path = cert_path + '.temp'
- util.assert_datadir_available(self.config_path)
- with open(temporary_path, "w", encoding='utf-8') as f:
- f.write(cert)
- f.flush()
- os.fsync(f.fileno())
+ async def handle_request(self, request):
+ # note: if server sends malformed request and we raise, the superclass
+ # will catch the exception, count errors, and at some point disconnect
+ if isinstance(request, Notification):
+ params, result = request.args[:-1], request.args[-1]
+ key = self.get_index(request.method, params)
+ if key in self.subscriptions:
+ self.cache[key] = result
+ for queue in self.subscriptions[key]:
+ await queue.put(request.args)
else:
- is_new = False
+ assert False, request.method
- s = self.get_simple_socket()
- if s is None:
- return
+ async def send_request(self, *args, timeout=-1, **kwargs):
+ if timeout == -1:
+ timeout = 20 if not self.proxy else 30
+ return await asyncio.wait_for(
+ super().send_request(*args, **kwargs),
+ timeout)
- if self.use_ssl:
- try:
- context = self.get_ssl_context(cert_reqs=ssl.CERT_REQUIRED,
- ca_certs=(temporary_path if is_new else cert_path))
- s = context.wrap_socket(s, do_handshake_on_connect=True)
- except socket.timeout:
- self.print_error('timeout')
- return
- except ssl.SSLError as e:
- self.print_error("SSL error:", e)
- if e.errno != 1:
- return
- if is_new:
- rej = cert_path + '.rej'
- if os.path.exists(rej):
- os.unlink(rej)
- os.rename(temporary_path, rej)
- else:
- util.assert_datadir_available(self.config_path)
- with open(cert_path, encoding='utf-8') as f:
- cert = f.read()
- try:
- b = pem.dePem(cert, 'CERTIFICATE')
- x = x509.X509(b)
- except:
- traceback.print_exc(file=sys.stderr)
- self.print_error("wrong certificate")
- return
- try:
- x.check_date()
- except:
- self.print_error("certificate has expired:", cert_path)
- os.unlink(cert_path)
- return
- self.print_error("wrong certificate")
- if e.errno == 104:
- return
- return
- except BaseException as e:
- self.print_error(e)
- traceback.print_exc(file=sys.stderr)
- return
+ async def subscribe(self, method, params, queue):
+ key = self.get_index(method, params)
+ if key in self.subscriptions:
+ self.subscriptions[key].append(queue)
+ result = self.cache[key]
+ else:
+ self.subscriptions[key] = [queue]
+ result = await self.send_request(method, params)
+ self.cache[key] = result
+ await queue.put(params + [result])
- if is_new:
- self.print_error("saving certificate")
- os.rename(temporary_path, cert_path)
+ def unsubscribe(self, queue):
+ """Unsubscribe a callback to free object references to enable GC."""
+ # note: we can't unsubscribe from the server, so we keep receiving
+ # subsequent notifications
+ for v in self.subscriptions.values():
+ if queue in v:
+ v.remove(queue)
- return s
-
- def run(self):
- socket = self.get_socket()
- if socket:
- self.print_error("connected")
- self.queue.put((self.server, socket))
+ @classmethod
+ def get_index(cls, method, params):
+ """Hashable index for subscriptions and cache"""
+ return str(method) + repr(params)
-class Interface(util.PrintError):
- """The Interface class handles a socket connected to a single remote
- Electrum server. Its exposed API is:
+# FIXME this is often raised inside a TaskGroup, but then it's not silent :(
+class GracefulDisconnect(AIOSafeSilentException): pass
- - Member functions close(), fileno(), get_responses(), has_timed_out(),
- ping_required(), queue_request(), send_requests()
- - Member variable server.
- """
- def __init__(self, server, socket):
+class ErrorParsingSSLCert(Exception): pass
+
+
+class ErrorGettingSSLCertFromServer(Exception): pass
+
+
+
+def deserialize_server(server_str: str) -> Tuple[str, str, str]:
+ # host might be IPv6 address, hence do rsplit:
+ host, port, protocol = str(server_str).rsplit(':', 2)
+ if protocol not in ('s', 't'):
+ raise ValueError('invalid network protocol: {}'.format(protocol))
+ int(port) # Throw if cannot be converted to int
+ if not (0 < int(port) < 2**16):
+ raise ValueError('port {} is out of valid range'.format(port))
+ return host, port, protocol
+
+
+def serialize_server(host: str, port: Union[str, int], protocol: str) -> str:
+ return str(':'.join([host, str(port), protocol]))
+
+
+class Interface(PrintError):
+
+ def __init__(self, network, server, config_path, proxy):
+ self.exception = None
+ self.ready = asyncio.Future()
self.server = server
- self.host, _, _ = server.rsplit(':', 2)
- self.socket = socket
+ self.host, self.port, self.protocol = deserialize_server(self.server)
+ self.port = int(self.port)
+ self.config_path = config_path
+ self.cert_path = os.path.join(self.config_path, 'certs', self.host)
+ self.blockchain = None
+ self.network = network
- self.pipe = util.SocketPipe(socket)
- self.pipe.set_timeout(0.0) # Don't wait for data
- # Dump network messages. Set at runtime from the console.
- self.debug = False
- self.unsent_requests = []
- self.unanswered_requests = {}
- self.last_send = time.time()
- self.closed_remotely = False
+ self.tip_header = None
+ self.tip = 0
+
+ # TODO combine?
+ self.fut = asyncio.get_event_loop().create_task(self.run())
+ self.group = CustomTaskGroup()
+
+ if proxy:
+ username, pw = proxy.get('user'), proxy.get('password')
+ if not username or not pw:
+ auth = None
+ else:
+ auth = aiorpcx.socks.SOCKSUserAuth(username, pw)
+ if proxy['mode'] == "socks4":
+ self.proxy = aiorpcx.socks.SOCKSProxy((proxy['host'], int(proxy['port'])), aiorpcx.socks.SOCKS4a, auth)
+ elif proxy['mode'] == "socks5":
+ self.proxy = aiorpcx.socks.SOCKSProxy((proxy['host'], int(proxy['port'])), aiorpcx.socks.SOCKS5, auth)
+ else:
+ raise NotImplementedError # http proxy not available with aiorpcx
+ else:
+ self.proxy = None
def diagnostic_name(self):
return self.host
- def fileno(self):
- # Needed for select
- return self.socket.fileno()
-
- def close(self):
- if not self.closed_remotely:
- try:
- self.socket.shutdown(socket.SHUT_RDWR)
- except socket.error:
- pass
- self.socket.close()
-
- def queue_request(self, *args): # method, params, _id
- '''Queue a request, later to be send with send_requests when the
- socket is available for writing.
- '''
- self.request_time = time.time()
- self.unsent_requests.append(args)
-
- def num_requests(self):
- '''Keep unanswered requests below 100'''
- n = 100 - len(self.unanswered_requests)
- return min(n, len(self.unsent_requests))
-
- def send_requests(self):
- '''Sends queued requests. Returns False on failure.'''
- self.last_send = time.time()
- make_dict = lambda m, p, i: {'method': m, 'params': p, 'id': i}
- n = self.num_requests()
- wire_requests = self.unsent_requests[0:n]
+ async def is_server_ca_signed(self, sslc):
try:
- self.pipe.send_all([make_dict(*r) for r in wire_requests])
- except BaseException as e:
- self.print_error("pipe send error:", e)
+ await self.open_session(sslc, exit_early=True)
+ except ssl.SSLError as e:
+ assert e.reason == 'CERTIFICATE_VERIFY_FAILED'
return False
- self.unsent_requests = self.unsent_requests[n:]
- for request in wire_requests:
- if self.debug:
- self.print_error("-->", request)
- self.unanswered_requests[request[2]] = request
return True
- def ping_required(self):
- '''Returns True if a ping should be sent.'''
- return time.time() - self.last_send > 300
+ async def _try_saving_ssl_cert_for_first_time(self, ca_ssl_context):
+ try:
+ ca_signed = await self.is_server_ca_signed(ca_ssl_context)
+ except (OSError, aiorpcx.socks.SOCKSFailure) as e:
+ raise ErrorGettingSSLCertFromServer(e) from e
+ if ca_signed:
+ with open(self.cert_path, 'w') as f:
+ # empty file means this is CA signed, not self-signed
+ f.write('')
+ else:
+ await self.save_certificate()
- 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))
+ def _is_saved_ssl_cert_available(self):
+ if not os.path.exists(self.cert_path):
+ return False
+ with open(self.cert_path, 'r') as f:
+ contents = f.read()
+ if contents == '': # CA signed
return True
+ # pinned self-signed cert
+ try:
+ b = pem.dePem(contents, 'CERTIFICATE')
+ except SyntaxError as e:
+ self.print_error("error parsing already saved cert:", e)
+ raise ErrorParsingSSLCert(e) from e
+ try:
+ x = x509.X509(b)
+ except Exception as e:
+ self.print_error("error parsing already saved cert:", e)
+ raise ErrorParsingSSLCert(e) from e
+ try:
+ x.check_date()
+ return True
+ except x509.CertificateError as e:
+ self.print_error("certificate has expired:", e)
+ os.unlink(self.cert_path) # delete pinned cert only in this case
+ return False
- return False
+ async def _get_ssl_context(self):
+ if self.protocol != 's':
+ # using plaintext TCP
+ return None
- 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
+ # see if we already have cert for this server; or get it for the first time
+ ca_sslc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
+ if not self._is_saved_ssl_cert_available():
+ await self._try_saving_ssl_cert_for_first_time(ca_sslc)
+ # now we have a file saved in our certificate store
+ siz = os.stat(self.cert_path).st_size
+ if siz == 0:
+ # CA signed cert
+ sslc = ca_sslc
+ else:
+ # pinned self-signed cert
+ sslc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=self.cert_path)
+ sslc.check_hostname = 0
+ return sslc
+
+ @aiosafe
+ async def run(self):
+ try:
+ ssl_context = await self._get_ssl_context()
+ except (ErrorParsingSSLCert, ErrorGettingSSLCertFromServer) as e:
+ self.exception = e
+ return
+ try:
+ await self.open_session(ssl_context, exit_early=False)
+ except (asyncio.CancelledError, OSError, aiorpcx.socks.SOCKSFailure) as e:
+ self.print_error('disconnecting due to: {} {}'.format(e, type(e)))
+ self.exception = e
+ return
+ # should never get here (can only exit via exception)
+ assert False
+
+ def mark_ready(self):
+ assert self.tip_header
+ chain = blockchain.check_header(self.tip_header)
+ if not chain:
+ self.blockchain = blockchain.blockchains[0]
+ else:
+ self.blockchain = chain
+
+ self.print_error("set blockchain with height", self.blockchain.height())
+
+ if not self.ready.done():
+ self.ready.set_result(1)
+
+ async def save_certificate(self):
+ if not os.path.exists(self.cert_path):
+ # we may need to retry this a few times, in case the handshake hasn't completed
+ for _ in range(10):
+ dercert = await self.get_certificate()
+ if dercert:
+ self.print_error("succeeded in getting cert")
+ with open(self.cert_path, 'w') as f:
+ cert = ssl.DER_cert_to_PEM_cert(dercert)
+ # workaround android bug
+ cert = re.sub("([^\n])-----END CERTIFICATE-----","\\1\n-----END CERTIFICATE-----",cert)
+ f.write(cert)
+ # even though close flushes we can't fsync when closed.
+ # and we must flush before fsyncing, cause flush flushes to OS buffer
+ # fsync writes to OS buffer to disk
+ f.flush()
+ os.fsync(f.fileno())
break
+ await asyncio.sleep(1)
+ else:
+ raise Exception("could not get certificate")
- return responses
+ async def get_certificate(self):
+ sslc = ssl.SSLContext()
+ try:
+ async with aiorpcx.ClientSession(self.host, self.port, ssl=sslc, proxy=self.proxy) as session:
+ return session.transport._ssl_protocol._sslpipe._sslobj.getpeercert(True)
+ except ValueError:
+ return None
+
+ async def get_block_header(self, height, assert_mode):
+ # use lower timeout as we usually have network.bhi_lock here
+ timeout = 5 if not self.proxy else 10
+ res = await self.session.send_request('blockchain.block.header', [height], timeout=timeout)
+ return blockchain.deserialize_header(bytes.fromhex(res), height)
+
+ async def request_chunk(self, idx, tip):
+ return await self.network.request_chunk(idx, tip, self.session)
+
+ async def open_session(self, sslc, exit_early):
+ header_queue = asyncio.Queue()
+ self.session = NotificationSession(self.host, self.port, ssl=sslc, proxy=self.proxy)
+ async with self.session as session:
+ try:
+ ver = await session.send_request('server.version', [ELECTRUM_VERSION, PROTOCOL_VERSION])
+ except aiorpcx.jsonrpc.RPCError as e:
+ raise GracefulDisconnect(e) # probably 'unsupported protocol version'
+ if exit_early:
+ return
+ self.print_error(ver, self.host)
+ await session.subscribe('blockchain.headers.subscribe', [], header_queue)
+ async with self.group as group:
+ await group.spawn(self.ping())
+ await group.spawn(self.run_fetch_blocks(header_queue))
+ await group.spawn(self.monitor_connection())
+ # NOTE: group.__aexit__ will be called here; this is needed to notice exceptions in the group!
+
+ async def monitor_connection(self):
+ while True:
+ await asyncio.sleep(1)
+ if not self.session or self.session.is_closing():
+ raise GracefulDisconnect('server closed session')
+
+ async def ping(self):
+ while True:
+ await asyncio.sleep(300)
+ await self.session.send_request('server.ping')
+
+ def close(self):
+ self.fut.cancel()
+ asyncio.get_event_loop().create_task(self.group.cancel_remaining())
+
+ async def run_fetch_blocks(self, header_queue):
+ while True:
+ self.network.notify('updated')
+ item = await header_queue.get()
+ item = item[0]
+ height = item['height']
+ item = blockchain.deserialize_header(bfh(item['hex']), item['height'])
+ self.tip_header = item
+ self.tip = height
+ if self.tip < constants.net.max_checkpoint():
+ raise GracefulDisconnect('server tip below max checkpoint')
+ if not self.ready.done():
+ self.mark_ready()
+ async with self.network.bhi_lock:
+ if self.blockchain.height() < item['block_height']-1:
+ _, height = await self.sync_until(height, None)
+ if self.blockchain.height() >= height and self.blockchain.check_header(item):
+ # another interface amended the blockchain
+ self.print_error("skipping header", height)
+ continue
+ if self.tip < height:
+ height = self.tip
+ _, height = await self.step(height, item)
+
+ async def sync_until(self, height, next_height=None):
+ if next_height is None:
+ next_height = self.tip
+ last = None
+ while last is None or height < next_height:
+ if next_height > height + 10:
+ self.print_error("requesting chunk from height {}".format(height))
+ could_connect, num_headers = await self.request_chunk(height, next_height)
+ if not could_connect:
+ if height <= constants.net.max_checkpoint():
+ raise Exception('server chain conflicts with checkpoints or genesis')
+ last, height = await self.step(height)
+ continue
+ self.network.notify('updated')
+ height = (height // 2016 * 2016) + num_headers
+ if height > next_height:
+ assert False, (height, self.tip)
+ last = 'catchup'
+ else:
+ last, height = await self.step(height)
+ return last, height
+
+ async def step(self, height, header=None):
+ assert height != 0
+ if header is None:
+ header = await self.get_block_header(height, 'catchup')
+ chain = self.blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
+ if chain: return 'catchup', height
+ can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height)
+
+ bad_header = None
+ if not can_connect:
+ self.print_error("can't connect", height)
+ #backward
+ bad = height
+ bad_header = header
+ height -= 1
+ checkp = False
+ if height <= constants.net.max_checkpoint():
+ height = constants.net.max_checkpoint()
+ checkp = True
+
+ header = await self.get_block_header(height, 'backward')
+ chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
+ can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height)
+ if checkp and not (can_connect or chain):
+ raise Exception("server chain conflicts with checkpoints. {} {}".format(can_connect, chain))
+ while not chain and not can_connect:
+ bad = height
+ bad_header = header
+ delta = self.tip - height
+ next_height = self.tip - 2 * delta
+ checkp = False
+ if next_height <= constants.net.max_checkpoint():
+ next_height = constants.net.max_checkpoint()
+ checkp = True
+ height = next_height
+
+ header = await self.get_block_header(height, 'backward')
+ chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
+ can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height)
+ if checkp and not (can_connect or chain):
+ raise Exception("server chain conflicts with checkpoints. {} {}".format(can_connect, chain))
+ self.print_error("exiting backward mode at", height)
+ if can_connect:
+ self.print_error("could connect", height)
+ chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
+
+ if type(can_connect) is bool:
+ # mock
+ height += 1
+ if height > self.tip:
+ assert False
+ return 'catchup', height
+ self.blockchain = can_connect
+ height += 1
+ self.blockchain.save_header(header)
+ return 'catchup', height
+
+ if not chain:
+ raise Exception("not chain") # line 931 in 8e69174374aee87d73cd2f8005fbbe87c93eee9c's network.py
+
+ # binary
+ if type(chain) in [int, bool]:
+ pass # mock
+ else:
+ self.blockchain = chain
+ good = height
+ height = (bad + good) // 2
+ header = await self.get_block_header(height, 'binary')
+ while True:
+ self.print_error("binary step")
+ chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
+ if chain:
+ assert bad != height, (bad, height)
+ good = height
+ self.blockchain = self.blockchain if type(chain) in [bool, int] else chain
+ else:
+ bad = height
+ assert good != height
+ bad_header = header
+ if bad != good + 1:
+ height = (bad + good) // 2
+ header = await self.get_block_header(height, 'binary')
+ continue
+ mock = bad_header and 'mock' in bad_header and bad_header['mock']['connect'](height)
+ real = not mock and self.blockchain.can_connect(bad_header, check_height=False)
+ if not real and not mock:
+ raise Exception('unexpected bad header during binary' + str(bad_header)) # line 948 in 8e69174374aee87d73cd2f8005fbbe87c93eee9c's network.py
+ branch = blockchain.blockchains.get(bad)
+ if branch is not None:
+ ismocking = False
+ if type(branch) is dict:
+ ismocking = True
+ # FIXME: it does not seem sufficient to check that the branch
+ # contains the bad_header. what if self.blockchain doesn't?
+ # the chains shouldn't be joined then. observe the incorrect
+ # joining on regtest with a server that has a fork of height
+ # one. the problem is observed only if forking is not during
+ # electrum runtime
+ if not ismocking and branch.check_header(bad_header) \
+ or ismocking and branch['check'](bad_header):
+ self.print_error('joining chain', bad)
+ height += 1
+ return 'join', height
+ else:
+ if not ismocking and branch.parent().check_header(header) \
+ or ismocking and branch['parent']['check'](header):
+ self.print_error('reorg', bad, self.tip)
+ self.blockchain = branch.parent() if not ismocking else branch['parent']
+ height = bad
+ header = await self.get_block_header(height, 'binary')
+ else:
+ if ismocking:
+ height = bad + 1
+ self.print_error("TODO replace blockchain")
+ return 'conflict', height
+ self.print_error('forkpoint conflicts with existing fork', branch.path())
+ branch.write(b'', 0)
+ branch.save_header(bad_header)
+ self.blockchain = branch
+ height = bad + 1
+ return 'conflict', height
+ else:
+ bh = self.blockchain.height()
+ if bh > good:
+ forkfun = self.blockchain.fork
+ if 'mock' in bad_header:
+ chain = bad_header['mock']['check'](bad_header)
+ forkfun = bad_header['mock']['fork'] if 'fork' in bad_header['mock'] else forkfun
+ else:
+ chain = self.blockchain.check_header(bad_header)
+ if not chain:
+ b = forkfun(bad_header)
+ assert bad not in blockchain.blockchains, (bad, list(blockchain.blockchains.keys()))
+ blockchain.blockchains[bad] = b
+ self.blockchain = b
+ height = b.forkpoint + 1
+ assert b.forkpoint == bad
+ return 'fork', height
+ else:
+ assert bh == good
+ if bh < self.tip:
+ self.print_error("catching up from %d"% (bh + 1))
+ height = bh + 1
+ return 'no_fork', height
def check_cert(host, cert):
diff --git a/electrum/network.py b/electrum/network.py
index 344d60f0..06d33935 100644
--- a/electrum/network.py
+++ b/electrum/network.py
@@ -23,32 +23,29 @@
import time
import queue
import os
-import errno
import random
import re
-import select
from collections import defaultdict
import threading
import socket
import json
import sys
import ipaddress
+import asyncio
+from typing import NamedTuple, Optional
import dns
import dns.resolver
-import socks
+from aiorpcx import TaskGroup
from . import util
-from .util import print_error
-from . import bitcoin
+from .util import PrintError, print_error, aiosafe, bfh
from .bitcoin import COIN
from . import constants
-from .interface import Connection, Interface
from . import blockchain
-from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
-from .i18n import _
-from .blockchain import InvalidHeader
-
+from .interface import Interface, serialize_server, deserialize_server
+from .version import PROTOCOL_VERSION
+from .simple_config import SimpleConfig
NODES_RETRY_INTERVAL = 60
SERVER_RETRY_INTERVAL = 10
@@ -83,7 +80,7 @@ def parse_servers(result):
def filter_version(servers):
def is_recent(version):
try:
- return util.normalize_version(version) >= util.normalize_version(PROTOCOL_VERSION)
+ return util.versiontuple(version) >= util.versiontuple(PROTOCOL_VERSION)
except Exception as e:
return False
return {k: v for k, v in servers.items() if is_recent(v.get('version'))}
@@ -111,9 +108,14 @@ def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
return random.choice(eligible) if eligible else None
-from .simple_config import SimpleConfig
+NetworkParameters = NamedTuple("NetworkParameters", [("host", str),
+ ("port", str),
+ ("protocol", str),
+ ("proxy", Optional[dict]),
+ ("auto_connect", bool)])
-proxy_modes = ['socks4', 'socks5', 'http']
+
+proxy_modes = ['socks4', 'socks5']
def serialize_proxy(p):
@@ -123,12 +125,13 @@ def serialize_proxy(p):
p.get('user', ''), p.get('password', '')])
-def deserialize_proxy(s):
+def deserialize_proxy(s: str) -> Optional[dict]:
if not isinstance(s, str):
return None
if s.lower() == 'none':
return None
proxy = { "mode":"socks5", "host":"localhost" }
+ # FIXME raw IPv6 address fails here
args = s.split(':')
n = 0
if proxy_modes.count(args[n]) == 1:
@@ -150,19 +153,10 @@ def deserialize_proxy(s):
return proxy
-def deserialize_server(server_str):
- host, port, protocol = str(server_str).rsplit(':', 2)
- if protocol not in 'st':
- raise ValueError('invalid network protocol: {}'.format(protocol))
- int(port) # Throw if cannot be converted to int
- return host, port, protocol
+INSTANCE = None
-def serialize_server(host, port, protocol):
- return str(':'.join([host, port, protocol]))
-
-
-class Network(util.DaemonThread):
+class Network(PrintError):
"""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
@@ -177,15 +171,16 @@ class Network(util.DaemonThread):
verbosity_filter = 'n'
def __init__(self, config=None):
+ global INSTANCE
+ INSTANCE = self
if config is None:
config = {} # Do not use mutables as default values!
- util.DaemonThread.__init__(self)
self.config = SimpleConfig(config) if isinstance(config, dict) else config
self.num_server = 10 if not self.config.get('oneserver') else 0
- self.blockchains = blockchain.read_blockchains(self.config) # note: needs self.blockchains_lock
- self.print_error("blockchains", self.blockchains.keys())
+ blockchain.blockchains = blockchain.read_blockchains(self.config) # note: needs self.blockchains_lock
+ self.print_error("blockchains", list(blockchain.blockchains.keys()))
self.blockchain_index = config.get('blockchain_index', 0)
- if self.blockchain_index not in self.blockchains.keys():
+ if self.blockchain_index not in blockchain.blockchains.keys():
self.blockchain_index = 0
# Server for addresses and transactions
self.default_server = self.config.get('server', None)
@@ -200,11 +195,11 @@ class Network(util.DaemonThread):
self.default_server = pick_random_server()
# locks: if you need to take multiple ones, acquire them in the order they are defined here!
+ self.bhi_lock = asyncio.Lock()
self.interface_lock = threading.RLock() # <- re-entrant
self.callback_lock = threading.Lock()
self.pending_sends_lock = threading.Lock()
self.recent_servers_lock = threading.RLock() # <- re-entrant
- self.subscribed_addresses_lock = threading.Lock()
self.blockchains_lock = threading.Lock()
self.pending_sends = []
@@ -216,9 +211,6 @@ class Network(util.DaemonThread):
self.banner = ''
self.donation_address = ''
self.relay_fee = None
- # callbacks passed with subscriptions
- self.subscriptions = defaultdict(list) # note: needs self.callback_lock
- self.sub_cache = {} # note: needs self.interface_lock
# callbacks set by the GUI
self.callbacks = defaultdict(list) # note: needs self.callback_lock
@@ -226,7 +218,6 @@ class Network(util.DaemonThread):
util.make_dir(dir_path)
# subscriptions and requests
- self.subscribed_addresses = set() # note: needs self.subscribed_addresses_lock
self.h2addr = {}
# Requests from client we've not seen a response to
self.unanswered_requests = {}
@@ -244,6 +235,11 @@ class Network(util.DaemonThread):
self.socket_queue = queue.Queue()
self.start_network(deserialize_server(self.default_server)[2],
deserialize_proxy(self.config.get('proxy')))
+ self.asyncio_loop = asyncio.get_event_loop()
+
+ @staticmethod
+ def get_instance():
+ return INSTANCE
def with_interface_lock(func):
def func_wrapper(self, *args, **kwargs):
@@ -271,7 +267,12 @@ class Network(util.DaemonThread):
def trigger_callback(self, event, *args):
with self.callback_lock:
callbacks = self.callbacks[event][:]
- [callback(event, *args) for callback in callbacks]
+ for callback in callbacks:
+ if asyncio.iscoroutinefunction(callback):
+ # FIXME: if callback throws, we will lose the traceback
+ asyncio.run_coroutine_threadsafe(callback(event, *args), self.asyncio_loop)
+ else:
+ callback(event, *args)
def read_recent_servers(self):
if not self.config.path:
@@ -316,53 +317,39 @@ class Network(util.DaemonThread):
self.notify('status')
def is_connected(self):
- return self.interface is not None
+ return self.interface is not None and self.interface.ready.done()
def is_connecting(self):
return self.connection_status == 'connecting'
- @with_interface_lock
- def queue_request(self, method, params, interface=None):
- # If you want to queue a request on any interface it must go
- # through this function so message ids are properly tracked
- if interface is None:
- interface = self.interface
- if interface is None:
- self.print_error('warning: dropping request', method, params)
- return
- message_id = self.message_id
- self.message_id += 1
- if self.debug:
- self.print_error(interface.host, "-->", method, params, message_id)
- interface.queue_request(method, params, message_id)
- return message_id
+ async def request_server_info(self, interface):
+ await interface.ready
+ session = interface.session
+ self.banner = await session.send_request('server.banner')
+ self.notify('banner')
+ self.donation_address = await session.send_request('server.donation_address')
+ self.irc_servers = parse_servers(await session.send_request('server.peers.subscribe'))
+ self.notify('servers')
+ await self.request_fee_estimates(interface)
+ relayfee = await session.send_request('blockchain.relayfee')
+ self.relay_fee = int(relayfee * COIN) if relayfee is not None else None
- @with_interface_lock
- def send_subscriptions(self):
- assert self.interface
- self.print_error('sending subscriptions to', self.interface.server, len(self.unanswered_requests), len(self.subscribed_addresses))
- self.sub_cache.clear()
- # Resend unanswered requests
- requests = self.unanswered_requests.values()
- self.unanswered_requests = {}
- for request in requests:
- message_id = self.queue_request(request[0], request[1])
- self.unanswered_requests[message_id] = request
- self.queue_request('server.banner', [])
- self.queue_request('server.donation_address', [])
- self.queue_request('server.peers.subscribe', [])
- self.request_fee_estimates()
- self.queue_request('blockchain.relayfee', [])
- with self.subscribed_addresses_lock:
- for h in self.subscribed_addresses:
- self.queue_request('blockchain.scripthash.subscribe', [h])
-
- def request_fee_estimates(self):
+ async def request_fee_estimates(self, interface):
+ session = interface.session
from .simple_config import FEE_ETA_TARGETS
self.config.requested_fee_estimates()
- self.queue_request('mempool.get_fee_histogram', [])
- for i in FEE_ETA_TARGETS:
- self.queue_request('blockchain.estimatefee', [i])
+ async with TaskGroup() as group:
+ histogram_task = await group.spawn(session.send_request('mempool.get_fee_histogram'))
+ fee_tasks = []
+ for i in FEE_ETA_TARGETS:
+ fee_tasks.append((i, await group.spawn(session.send_request('blockchain.estimatefee', [i]))))
+ self.config.mempool_fees = histogram_task.result()
+ self.notify('fee_histogram')
+ for i, task in fee_tasks:
+ fee = int(task.result() * COIN)
+ self.config.update_fee_estimates(i, fee)
+ self.print_error("fee_estimates[%d]" % i, fee)
+ self.notify('fee')
def get_status_value(self, key):
if key == 'status':
@@ -387,9 +374,9 @@ class Network(util.DaemonThread):
else:
self.trigger_callback(key, self.get_status_value(key))
- def get_parameters(self):
+ def get_parameters(self) -> NetworkParameters:
host, port, protocol = deserialize_server(self.default_server)
- return host, port, protocol, self.proxy, self.auto_connect
+ return NetworkParameters(host, port, protocol, self.proxy, self.auto_connect)
def get_donation_address(self):
if self.is_connected():
@@ -424,7 +411,7 @@ class Network(util.DaemonThread):
self.print_error("connecting to %s as new interface" % server)
self.set_status('connecting')
self.connecting.add(server)
- Connection(server, self.socket_queue, self.config.path)
+ self.socket_queue.put(server)
def start_random_interface(self):
with self.interface_lock:
@@ -432,32 +419,18 @@ class Network(util.DaemonThread):
server = pick_random_server(self.get_servers(), self.protocol, exclude_set)
if server:
self.start_interface(server)
+ return 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):
+ def set_proxy(self, proxy: Optional[dict]):
self.proxy = proxy
# Store these somewhere so we can un-monkey-patch
- if not hasattr(socket, "_socketobject"):
- socket._socketobject = socket.socket
+ if not hasattr(socket, "_getaddrinfo"):
socket._getaddrinfo = socket.getaddrinfo
if proxy:
self.print_error('setting proxy', proxy)
- proxy_mode = proxy_modes.index(proxy["mode"]) + 1
- socks.setdefaultproxy(proxy_mode,
- proxy["host"],
- int(proxy["port"]),
- # socks.py seems to want either None or a non-empty string
- username=(proxy.get("user", "") or None),
- password=(proxy.get("password", "") or None))
- socket.socket = socks.socksocket
# prevent dns leaks, see http://stackoverflow.com/questions/13184205/dns-over-proxy
socket.getaddrinfo = lambda *args: [(socket.AF_INET, socket.SOCK_STREAM, 6, '', (args[0], args[1]))]
else:
- socket.socket = socket._socketobject
if sys.platform == 'win32':
# On Windows, socket.getaddrinfo takes a mutex, and might hold it for up to 10 seconds
# when dns-resolving. To speed it up drastically, we resolve dns ourselves, outside that lock.
@@ -465,6 +438,7 @@ class Network(util.DaemonThread):
socket.getaddrinfo = self._fast_getaddrinfo
else:
socket.getaddrinfo = socket._getaddrinfo
+ self.trigger_callback('proxy_set', self.proxy)
@staticmethod
def _fast_getaddrinfo(host, *args, **kwargs):
@@ -495,14 +469,14 @@ class Network(util.DaemonThread):
return socket._getaddrinfo(addr, *args, **kwargs)
@with_interface_lock
- def start_network(self, protocol, proxy):
+ def start_network(self, protocol: str, proxy: Optional[dict]):
assert not self.interface and not self.interfaces
assert not self.connecting and self.socket_queue.empty()
self.print_error('starting network')
self.disconnected_servers = set([]) # note: needs self.interface_lock
self.protocol = protocol
self.set_proxy(proxy)
- self.start_interfaces()
+ self.start_interface(self.default_server)
@with_interface_lock
def stop_network(self):
@@ -513,13 +487,15 @@ class Network(util.DaemonThread):
self.close_interface(self.interface)
assert self.interface is None
assert not self.interfaces
- self.connecting = set()
+ self.connecting.clear()
# Get a new queue - no old pending connections thanks!
self.socket_queue = queue.Queue()
- def set_parameters(self, host, port, protocol, proxy, auto_connect):
+ def set_parameters(self, net_params: NetworkParameters):
+ proxy = net_params.proxy
proxy_str = serialize_proxy(proxy)
- server = serialize_server(host, port, protocol)
+ host, port, protocol = net_params.host, net_params.port, net_params.protocol
+ server_str = serialize_server(host, port, protocol)
# sanitize parameters
try:
deserialize_server(serialize_server(host, port, protocol))
@@ -528,21 +504,21 @@ class Network(util.DaemonThread):
int(proxy['port'])
except:
return
- self.config.set_key('auto_connect', auto_connect, False)
+ self.config.set_key('auto_connect', net_params.auto_connect, False)
self.config.set_key("proxy", proxy_str, False)
- self.config.set_key("server", server, True)
+ self.config.set_key("server", server_str, True)
# abort if changes were not allowed by config
- if self.config.get('server') != server or self.config.get('proxy') != proxy_str:
+ if self.config.get('server') != server_str or self.config.get('proxy') != proxy_str:
return
- self.auto_connect = auto_connect
+ self.auto_connect = net_params.auto_connect
if self.proxy != proxy or self.protocol != protocol:
# Restart the network defaulting to the given server
with self.interface_lock:
self.stop_network()
- self.default_server = server
+ self.default_server = server_str
self.start_network(protocol, proxy)
- elif self.default_server != server:
- self.switch_to_interface(server)
+ elif self.default_server != server_str:
+ self.switch_to_interface(server_str)
else:
self.switch_lagging_interface()
self.notify('updated')
@@ -561,7 +537,12 @@ class Network(util.DaemonThread):
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())))
+ def filt(x):
+ a = x[1].tip_header
+ b = header
+ assert type(a) is type(b)
+ return a == b
+ filtered = list(map(lambda x: x[0], filter(filt, self.interfaces.items())))
if filtered:
choice = random.choice(filtered)
self.switch_to_interface(choice)
@@ -581,11 +562,20 @@ class Network(util.DaemonThread):
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)
+ if self.interface is not None:
+ # Stop any current interface in order to terminate subscriptions,
+ # and to cancel tasks in interface.group.
+ # However, for headers sub, give preference to this interface
+ # over unknown ones, i.e. start it again right away.
+ old_server = self.interface.server
+ self.close_interface(self.interface)
+ if len(self.interfaces) <= self.num_server:
+ self.start_interface(old_server)
+
self.interface = i
- self.send_subscriptions()
+ asyncio.get_event_loop().create_task(
+ i.group.spawn(self.request_server_info(i)))
+ self.trigger_callback('default_server_changed')
self.set_status('connected')
self.notify('updated')
self.notify('interfaces')
@@ -608,164 +598,6 @@ class Network(util.DaemonThread):
self.recent_servers = self.recent_servers[0:20]
self.save_recent_servers()
- def process_response(self, interface, response, callbacks):
- if self.debug:
- self.print_error(interface.host, "<--", response)
- error = response.get('error')
- result = response.get('result')
- method = response.get('method')
- params = response.get('params')
-
- # We handle some responses; return the rest to the client.
- if method == 'server.version':
- interface.server_version = result
- elif method == 'blockchain.headers.subscribe':
- if error is None:
- self.on_notify_header(interface, result)
- else:
- # no point in keeping this connection without headers sub
- self.connection_down(interface.server)
- return
- elif method == 'server.peers.subscribe':
- if error is None:
- self.irc_servers = parse_servers(result)
- self.notify('servers')
- elif method == 'server.banner':
- if error is None:
- self.banner = result
- self.notify('banner')
- elif method == 'server.donation_address':
- if error is None:
- self.donation_address = result
- elif method == 'mempool.get_fee_histogram':
- if error is None:
- self.print_error('fee_histogram', result)
- self.config.mempool_fees = result
- self.notify('fee_histogram')
- elif method == 'blockchain.estimatefee':
- if error is None and result > 0:
- i = params[0]
- fee = int(result*COIN)
- self.config.update_fee_estimates(i, fee)
- self.print_error("fee_estimates[%d]" % i, fee)
- self.notify('fee')
- elif method == 'blockchain.relayfee':
- if error is None:
- self.relay_fee = int(result * COIN) if result is not None else None
- self.print_error("relayfee", self.relay_fee)
- elif method == 'blockchain.block.headers':
- self.on_block_headers(interface, response)
- elif method == 'blockchain.block.get_header':
- self.on_get_header(interface, response)
-
- for callback in callbacks:
- callback(response)
-
- @classmethod
- def get_index(cls, method, params):
- """ hashable index for subscriptions and cache"""
- return str(method) + (':' + str(params[0]) if params else '')
-
- def process_responses(self, interface):
- responses = interface.get_responses()
- for request, response in responses:
- if request:
- method, params, message_id = request
- k = self.get_index(method, params)
- # client requests go through self.send() with a
- # callback, are only sent to the current interface,
- # and are placed in the unanswered_requests dictionary
- client_req = self.unanswered_requests.pop(message_id, None)
- if client_req:
- if interface != self.interface:
- # we probably changed the current interface
- # in the meantime; drop this.
- return
- callbacks = [client_req[2]]
- else:
- # fixme: will only work for subscriptions
- k = self.get_index(method, params)
- callbacks = list(self.subscriptions.get(k, []))
-
- # Copy the request method and params to the response
- response['method'] = method
- response['params'] = params
- # Only once we've received a response to an addr subscription
- # add it to the list; avoids double-sends on reconnection
- if method == 'blockchain.scripthash.subscribe':
- with self.subscribed_addresses_lock:
- self.subscribed_addresses.add(params[0])
- else:
- if not response: # Closed remotely / misbehaving
- self.connection_down(interface.server)
- break
- # Rewrite response shape to match subscription request response
- method = response.get('method')
- params = response.get('params')
- k = self.get_index(method, params)
- if method == 'blockchain.headers.subscribe':
- response['result'] = params[0]
- response['params'] = []
- elif method == 'blockchain.scripthash.subscribe':
- response['params'] = [params[0]] # addr
- response['result'] = params[1]
- callbacks = list(self.subscriptions.get(k, []))
-
- # update cache if it's a subscription
- if method.endswith('.subscribe'):
- with self.interface_lock:
- self.sub_cache[k] = response
- # Response is now in canonical form
- self.process_response(interface, response, callbacks)
-
- def send(self, messages, callback):
- '''Messages is a list of (method, params) tuples'''
- messages = list(messages)
- with self.pending_sends_lock:
- self.pending_sends.append((messages, callback))
-
- @with_interface_lock
- def process_pending_sends(self):
- # Requests needs connectivity. If we don't have an interface,
- # we cannot process them.
- if not self.interface:
- return
-
- with self.pending_sends_lock:
- sends = self.pending_sends
- self.pending_sends = []
-
- for messages, callback in sends:
- for method, params in messages:
- r = None
- if method.endswith('.subscribe'):
- k = self.get_index(method, params)
- # add callback to list
- l = list(self.subscriptions.get(k, []))
- if callback not in l:
- l.append(callback)
- with self.callback_lock:
- self.subscriptions[k] = l
- # check cached response for subscriptions
- r = self.sub_cache.get(k)
-
- if r is not None:
- self.print_error("cache hit", k)
- callback(r)
- else:
- message_id = self.queue_request(method, params)
- self.unanswered_requests[message_id] = method, params, callback
-
- def unsubscribe(self, callback):
- '''Unsubscribe a callback to free object references to enable GC.'''
- # Note: we can't unsubscribe from the server, so if we receive
- # subsequent notifications process_response() will emit a harmless
- # "received unexpected notification" warning
- with self.callback_lock:
- for v in self.subscriptions.values():
- if callback in v:
- v.remove(callback)
-
@with_interface_lock
def connection_down(self, server):
'''A connection to server either went down, or was never made.
@@ -777,64 +609,183 @@ class Network(util.DaemonThread):
self.close_interface(self.interfaces[server])
self.notify('interfaces')
with self.blockchains_lock:
- for b in self.blockchains.values():
+ for b in blockchain.blockchains.values():
if b.catch_up == server:
b.catch_up = None
- def new_interface(self, server, socket):
+ @aiosafe
+ async def new_interface(self, server):
# 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
+
+ interface = Interface(self, server, self.config.path, self.proxy)
+ timeout = 10 if not self.proxy else 20
+ try:
+ await asyncio.wait_for(interface.ready, timeout)
+ except BaseException as e:
+ #import traceback
+ #traceback.print_exc()
+ self.print_error(interface.server, "couldn't launch because", str(e), str(type(e)))
+ self.connection_down(interface.server)
+ return
+ finally:
+ try: self.connecting.remove(server)
+ except KeyError: pass
+
with self.interface_lock:
self.interfaces[server] = interface
- # server.version should be the first message
- params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
- self.queue_request('server.version', params, interface)
- self.queue_request('blockchain.headers.subscribe', [True], interface)
+
if server == self.default_server:
self.switch_to_interface(server)
- #self.notify('interfaces')
- def maintain_sockets(self):
- '''Socket maintenance.'''
- # Responses to connection attempts?
- while not self.socket_queue.empty():
- server, socket = self.socket_queue.get()
- if server in self.connecting:
- self.connecting.remove(server)
+ self.notify('interfaces')
- if socket:
- self.new_interface(server, socket)
- else:
- self.connection_down(server)
+ def init_headers_file(self):
+ b = blockchain.blockchains[0]
+ filename = b.path()
+ length = 80 * len(constants.net.CHECKPOINTS) * 2016
+ if not os.path.exists(filename) or os.path.getsize(filename) < length:
+ with open(filename, 'wb') as f:
+ if length>0:
+ f.seek(length-1)
+ f.write(b'\x00')
+ with b.lock:
+ b.update_size()
+
+ async def get_merkle_for_transaction(self, tx_hash, tx_height):
+ return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])
+
+ def broadcast_transaction_from_non_network_thread(self, tx, timeout=10):
+ # note: calling this from the network thread will deadlock it
+ fut = asyncio.run_coroutine_threadsafe(self.broadcast_transaction(tx, timeout=timeout), self.asyncio_loop)
+ return fut.result()
+
+ async def broadcast_transaction(self, tx, timeout=10):
+ try:
+ out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout)
+ except asyncio.TimeoutError as e:
+ return False, "error: operation timed out"
+ except Exception as e:
+ return False, "error: " + str(e)
+
+ if out != tx.txid():
+ return False, "error: " + out
+ return True, out
+
+ async def request_chunk(self, height, tip, session=None, can_return_early=False):
+ if session is None: session = self.interface.session
+ index = height // 2016
+ if can_return_early and index in self.requested_chunks:
+ return
+ size = 2016
+ if tip is not None:
+ size = min(size, tip - index * 2016)
+ size = max(size, 0)
+ try:
+ self.requested_chunks.add(index)
+ res = await session.send_request('blockchain.block.headers', [index * 2016, size])
+ finally:
+ try: self.requested_chunks.remove(index)
+ except KeyError: pass
+ conn = self.blockchain().connect_chunk(index, res['hex'])
+ if not conn:
+ return conn, 0
+ return conn, res['count']
+
+ @with_interface_lock
+ def blockchain(self):
+ if self.interface and self.interface.blockchain is not None:
+ self.blockchain_index = self.interface.blockchain.forkpoint
+ return blockchain.blockchains[self.blockchain_index]
+
+ @with_interface_lock
+ def get_blockchains(self):
+ out = {}
+ with self.blockchains_lock:
+ blockchain_items = list(blockchain.blockchains.items())
+ for k, b in blockchain_items:
+ r = list(filter(lambda i: i.blockchain==b, list(self.interfaces.values())))
+ if r:
+ out[k] = r
+ return out
+
+ def follow_chain(self, index):
+ bc = blockchain.blockchains.get(index)
+ if bc:
+ self.blockchain_index = index
+ self.config.set_key('blockchain_index', index)
+ with self.interface_lock:
+ interfaces = list(self.interfaces.values())
+ for i in interfaces:
+ if i.blockchain == bc:
+ self.switch_to_interface(i.server)
+ break
+ else:
+ raise Exception('blockchain not found', index)
- # Send pings and shut down stale interfaces
- # must use copy of values
with self.interface_lock:
- interfaces = list(self.interfaces.values())
- for interface in interfaces:
- if interface.has_timed_out():
- self.connection_down(interface.server)
- elif interface.ping_required():
- self.queue_request('server.ping', [], interface)
+ if self.interface:
+ net_params = self.get_parameters()
+ host, port, protocol = deserialize_server(self.interface.server)
+ net_params = net_params._replace(host=host, port=port, protocol=protocol)
+ self.set_parameters(net_params)
- now = time.time()
- # nodes
- with self.interface_lock:
- if len(self.interfaces) + len(self.connecting) < self.num_server:
+ def get_local_height(self):
+ return self.blockchain().height()
+
+ def export_checkpoints(self, path):
+ # run manually from the console to generate checkpoints
+ cp = self.blockchain().get_checkpoints()
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(json.dumps(cp, indent=4))
+
+ def start(self, fx=None):
+ self.main_taskgroup = TaskGroup()
+ async def main():
+ self.init_headers_file()
+ async with self.main_taskgroup as group:
+ await group.spawn(self.maintain_sessions())
+ if fx: await group.spawn(fx)
+ self._wrapper_thread = threading.Thread(target=self.asyncio_loop.run_until_complete, args=(main(),))
+ self._wrapper_thread.start()
+
+ def stop(self):
+ asyncio.run_coroutine_threadsafe(self.main_taskgroup.cancel_remaining(), self.asyncio_loop)
+
+ def join(self):
+ self._wrapper_thread.join(1)
+
+ async def maintain_sessions(self):
+ while True:
+ while self.socket_queue.qsize() > 0:
+ server = self.socket_queue.get()
+ asyncio.get_event_loop().create_task(self.new_interface(server))
+ remove = []
+ for k, i in self.interfaces.items():
+ if i.fut.done() and not i.exception:
+ assert False, "interface future should not finish without exception"
+ if i.exception:
+ if not i.fut.done():
+ try: i.fut.cancel()
+ except Exception as e: self.print_error('exception while cancelling fut', e)
+ try:
+ raise i.exception
+ except BaseException as e:
+ self.print_error(i.server, "errored because:", str(e), str(type(e)))
+ remove.append(k)
+ for k in remove:
+ self.connection_down(k)
+
+ # nodes
+ now = time.time()
+ for i in range(self.num_server - len(self.interfaces) - len(self.connecting)):
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
+ if now - self.nodes_retry_time > NODES_RETRY_INTERVAL:
+ self.print_error('network: retrying connections')
+ self.disconnected_servers = set([])
+ self.nodes_retry_time = now
- # main interface
- with self.interface_lock:
+ # main interface
if not self.is_connected():
if self.auto_connect:
if not self.is_connecting():
@@ -848,473 +799,9 @@ class Network(util.DaemonThread):
self.switch_to_interface(self.default_server)
else:
if self.config.is_fee_estimates_update_required():
- self.request_fee_estimates()
+ await self.interface.group.spawn(self.attempt_fee_estimate_update())
- def request_chunk(self, interface, index):
- if index in self.requested_chunks:
- return
- interface.print_error("requesting chunk %d" % index)
- self.requested_chunks.add(index)
- height = index * 2016
- self.queue_request('blockchain.block.headers', [height, 2016],
- interface)
+ await asyncio.sleep(0.1)
- def on_block_headers(self, interface, response):
- '''Handle receiving a chunk of block headers'''
- error = response.get('error')
- result = response.get('result')
- params = response.get('params')
- blockchain = interface.blockchain
- if result is None or params is None or error is not None:
- interface.print_error(error or 'bad response')
- return
- # Ignore unsolicited chunks
- height = params[0]
- index = height // 2016
- if index * 2016 != height or index not in self.requested_chunks:
- interface.print_error("received chunk %d (unsolicited)" % index)
- return
- else:
- interface.print_error("received chunk %d" % index)
- self.requested_chunks.remove(index)
- hexdata = result['hex']
- connect = blockchain.connect_chunk(index, hexdata)
- if not connect:
- self.connection_down(interface.server)
- return
- if index >= len(blockchain.checkpoints):
- # If not finished, get the next chunk
- if blockchain.height() < interface.tip:
- self.request_chunk(interface, index+1)
- else:
- interface.mode = 'default'
- interface.print_error('catch up done', blockchain.height())
- blockchain.catch_up = None
- else:
- # the verifier must have asked for this chunk
- pass
- self.notify('updated')
-
- def on_get_header(self, interface, response):
- '''Handle receiving a single block header'''
- header = response.get('result')
- if not header:
- interface.print_error(response)
- self.connection_down(interface.server)
- return
- height = header.get('block_height')
- #interface.print_error('got header', height, blockchain.hash_header(header))
- if interface.request != height:
- interface.print_error("unsolicited header",interface.request, height)
- self.connection_down(interface.server)
- return
- chain = blockchain.check_header(header)
- if interface.mode == 'backward':
- can_connect = blockchain.can_connect(header)
- if can_connect and can_connect.catch_up is None:
- interface.mode = 'catch_up'
- interface.blockchain = can_connect
- interface.blockchain.save_header(header)
- next_height = height + 1
- interface.blockchain.catch_up = interface.server
- elif chain:
- # FIXME should await "initial chunk download".
- # binary search will NOT do the correct thing if we don't yet
- # have all headers up to the fork height
- interface.print_error("binary search")
- interface.mode = 'binary'
- interface.blockchain = chain
- interface.good = height
- next_height = (interface.bad + interface.good) // 2
- assert next_height >= self.max_checkpoint(), (interface.bad, interface.good)
- else:
- if height == 0:
- self.connection_down(interface.server)
- next_height = None
- else:
- interface.bad = height
- interface.bad_header = header
- delta = interface.tip - height
- next_height = max(self.max_checkpoint(), interface.tip - 2 * delta)
- if height == next_height:
- self.connection_down(interface.server)
- next_height = None
-
- elif interface.mode == 'binary':
- if chain:
- interface.good = height
- interface.blockchain = chain
- else:
- interface.bad = height
- interface.bad_header = header
- if interface.bad != interface.good + 1:
- next_height = (interface.bad + interface.good) // 2
- assert next_height >= self.max_checkpoint()
- elif not interface.blockchain.can_connect(interface.bad_header, check_height=False):
- self.connection_down(interface.server)
- next_height = None
- else:
- branch = self.blockchains.get(interface.bad)
- if branch is not None:
- if branch.check_header(interface.bad_header):
- interface.print_error('joining chain', interface.bad)
- next_height = None
- elif branch.parent().check_header(header):
- interface.print_error('reorg', interface.bad, interface.tip)
- interface.blockchain = branch.parent()
- next_height = interface.bad
- else:
- interface.print_error('forkpoint conflicts with existing fork', branch.path())
- branch.write(b'', 0)
- branch.save_header(interface.bad_header)
- interface.mode = 'catch_up'
- interface.blockchain = branch
- next_height = interface.bad + 1
- interface.blockchain.catch_up = interface.server
- else:
- bh = interface.blockchain.height()
- next_height = None
- if bh > interface.good:
- if not interface.blockchain.check_header(interface.bad_header):
- b = interface.blockchain.fork(interface.bad_header)
- with self.blockchains_lock:
- self.blockchains[interface.bad] = b
- interface.blockchain = b
- interface.print_error("new chain", b.forkpoint)
- interface.mode = 'catch_up'
- maybe_next_height = interface.bad + 1
- if maybe_next_height <= interface.tip:
- next_height = maybe_next_height
- interface.blockchain.catch_up = interface.server
- else:
- assert bh == interface.good
- if interface.blockchain.catch_up is None and bh < interface.tip:
- interface.print_error("catching up from %d"% (bh + 1))
- interface.mode = 'catch_up'
- next_height = bh + 1
- interface.blockchain.catch_up = interface.server
-
- self.notify('updated')
-
- elif interface.mode == 'catch_up':
- can_connect = interface.blockchain.can_connect(header)
- if can_connect:
- interface.blockchain.save_header(header)
- next_height = height + 1 if height < interface.tip else None
- else:
- # go back
- interface.print_error("cannot connect", height)
- interface.mode = 'backward'
- interface.bad = height
- interface.bad_header = header
- next_height = height - 1
-
- if next_height is None:
- # exit catch_up state
- interface.print_error('catch up done', interface.blockchain.height())
- interface.blockchain.catch_up = None
- self.switch_lagging_interface()
- self.notify('updated')
-
- else:
- raise Exception(interface.mode)
- # If not finished, get the next header
- if next_height is not None:
- if interface.mode == 'catch_up' and interface.tip > next_height + 50:
- self.request_chunk(interface, next_height // 2016)
- else:
- self.request_header(interface, next_height)
- else:
- interface.mode = 'default'
- interface.request = None
- self.notify('updated')
-
- # refresh network dialog
- self.notify('interfaces')
-
- def maintain_requests(self):
- with self.interface_lock:
- interfaces = list(self.interfaces.values())
- for interface in interfaces:
- if interface.request and time.time() - interface.request_time > 20:
- interface.print_error("blockchain request timed out")
- self.connection_down(interface.server)
- continue
-
- def wait_on_sockets(self):
- # Python docs say Windows doesn't like empty selects.
- # Sleep to prevent busy looping
- if not self.interfaces:
- time.sleep(0.1)
- return
- with self.interface_lock:
- interfaces = list(self.interfaces.values())
- rin = [i for i in interfaces]
- win = [i for i in interfaces if i.num_requests()]
- try:
- rout, wout, xout = select.select(rin, win, [], 0.1)
- except socket.error as e:
- if e.errno == errno.EINTR:
- return
- raise
- assert not xout
- for interface in wout:
- interface.send_requests()
- for interface in rout:
- self.process_responses(interface)
-
- def init_headers_file(self):
- b = self.blockchains[0]
- filename = b.path()
- length = 80 * len(constants.net.CHECKPOINTS) * 2016
- if not os.path.exists(filename) or os.path.getsize(filename) < length:
- with open(filename, 'wb') as f:
- if length>0:
- f.seek(length-1)
- f.write(b'\x00')
- with b.lock:
- b.update_size()
-
- def run(self):
- self.init_headers_file()
- while self.is_running():
- self.maintain_sockets()
- self.wait_on_sockets()
- self.maintain_requests()
- self.run_jobs() # Synchronizer and Verifier
- self.process_pending_sends()
- self.stop_network()
- self.on_stop()
-
- def on_notify_header(self, interface, header_dict):
- try:
- header_hex, height = header_dict['hex'], header_dict['height']
- except KeyError:
- # no point in keeping this connection without headers sub
- self.connection_down(interface.server)
- return
- try:
- header = blockchain.deserialize_header(util.bfh(header_hex), height)
- except InvalidHeader:
- self.connection_down(interface.server)
- return
- #interface.print_error('notified of header', height, blockchain.hash_header(header))
- if height < self.max_checkpoint():
- self.connection_down(interface.server)
- return
- interface.tip_header = header
- interface.tip = height
- if interface.mode != 'default':
- return
- b = blockchain.check_header(header)
- if b:
- interface.blockchain = b
- self.switch_lagging_interface()
- self.notify('updated')
- self.notify('interfaces')
- return
- b = blockchain.can_connect(header)
- if b:
- interface.blockchain = b
- b.save_header(header)
- self.switch_lagging_interface()
- self.notify('updated')
- self.notify('interfaces')
- return
- with self.blockchains_lock:
- tip = max([x.height() for x in self.blockchains.values()])
- if tip >=0:
- interface.mode = 'backward'
- interface.bad = height
- interface.bad_header = header
- self.request_header(interface, min(tip +1, height - 1))
- else:
- chain = self.blockchains[0]
- if chain.catch_up is None:
- chain.catch_up = interface
- interface.mode = 'catch_up'
- interface.blockchain = chain
- with self.blockchains_lock:
- self.print_error("switching to catchup mode", tip, self.blockchains)
- self.request_header(interface, 0)
- else:
- self.print_error("chain already catching up with", chain.catch_up.server)
-
- @with_interface_lock
- def blockchain(self):
- if self.interface and self.interface.blockchain is not None:
- self.blockchain_index = self.interface.blockchain.forkpoint
- return self.blockchains[self.blockchain_index]
-
- @with_interface_lock
- def get_blockchains(self):
- out = {}
- with self.blockchains_lock:
- blockchain_items = list(self.blockchains.items())
- for k, b in blockchain_items:
- r = list(filter(lambda i: i.blockchain==b, list(self.interfaces.values())))
- if r:
- out[k] = r
- return out
-
- def follow_chain(self, index):
- blockchain = self.blockchains.get(index)
- if blockchain:
- self.blockchain_index = index
- self.config.set_key('blockchain_index', index)
- with self.interface_lock:
- interfaces = list(self.interfaces.values())
- for i in interfaces:
- if i.blockchain == blockchain:
- self.switch_to_interface(i.server)
- break
- else:
- raise Exception('blockchain not found', index)
-
- with self.interface_lock:
- if self.interface:
- server = self.interface.server
- host, port, protocol, proxy, auto_connect = self.get_parameters()
- host, port, protocol = server.split(':')
- self.set_parameters(host, port, protocol, proxy, auto_connect)
-
- def get_local_height(self):
- return self.blockchain().height()
-
- @staticmethod
- def __wait_for(it):
- """Wait for the result of calling lambda `it`."""
- q = queue.Queue()
- it(q.put)
- try:
- result = q.get(block=True, timeout=30)
- except queue.Empty:
- raise util.TimeoutException(_('Server did not answer'))
-
- if result.get('error'):
- raise Exception(result.get('error'))
-
- return result.get('result')
-
- @staticmethod
- def __with_default_synchronous_callback(invocation, callback):
- """ Use this method if you want to make the network request
- synchronous. """
- if not callback:
- return Network.__wait_for(invocation)
-
- invocation(callback)
-
- def request_header(self, interface, height):
- self.queue_request('blockchain.block.get_header', [height], interface)
- interface.request = height
- interface.req_time = time.time()
-
- def map_scripthash_to_address(self, callback):
- def cb2(x):
- x2 = x.copy()
- p = x2.pop('params')
- addr = self.h2addr[p[0]]
- x2['params'] = [addr]
- callback(x2)
- return cb2
-
- def subscribe_to_addresses(self, addresses, callback):
- hash2address = {
- bitcoin.address_to_scripthash(address): address
- for address in addresses}
- self.h2addr.update(hash2address)
- msgs = [
- ('blockchain.scripthash.subscribe', [x])
- for x in hash2address.keys()]
- self.send(msgs, self.map_scripthash_to_address(callback))
-
- def request_address_history(self, address, callback):
- h = bitcoin.address_to_scripthash(address)
- self.h2addr.update({h: address})
- self.send([('blockchain.scripthash.get_history', [h])], self.map_scripthash_to_address(callback))
-
- # NOTE this method handles exceptions and a special edge case, counter to
- # what the other ElectrumX methods do. This is unexpected.
- def broadcast_transaction(self, transaction, callback=None):
- command = 'blockchain.transaction.broadcast'
- invocation = lambda c: self.send([(command, [str(transaction)])], c)
-
- if callback:
- invocation(callback)
- return
-
- try:
- out = Network.__wait_for(invocation)
- except BaseException as e:
- return False, "error: " + str(e)
-
- if out != transaction.txid():
- return False, "error: " + out
-
- return True, out
-
- def get_history_for_scripthash(self, hash, callback=None):
- command = 'blockchain.scripthash.get_history'
- invocation = lambda c: self.send([(command, [hash])], c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def subscribe_to_headers(self, callback=None):
- command = 'blockchain.headers.subscribe'
- invocation = lambda c: self.send([(command, [True])], c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def subscribe_to_address(self, address, callback=None):
- command = 'blockchain.address.subscribe'
- invocation = lambda c: self.send([(command, [address])], c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def get_merkle_for_transaction(self, tx_hash, tx_height, callback=None):
- command = 'blockchain.transaction.get_merkle'
- invocation = lambda c: self.send([(command, [tx_hash, tx_height])], c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def subscribe_to_scripthash(self, scripthash, callback=None):
- command = 'blockchain.scripthash.subscribe'
- invocation = lambda c: self.send([(command, [scripthash])], c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def get_transaction(self, transaction_hash, callback=None):
- command = 'blockchain.transaction.get'
- invocation = lambda c: self.send([(command, [transaction_hash])], c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def get_transactions(self, transaction_hashes, callback=None):
- command = 'blockchain.transaction.get'
- messages = [(command, [tx_hash]) for tx_hash in transaction_hashes]
- invocation = lambda c: self.send(messages, c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def listunspent_for_scripthash(self, scripthash, callback=None):
- command = 'blockchain.scripthash.listunspent'
- invocation = lambda c: self.send([(command, [scripthash])], c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def get_balance_for_scripthash(self, scripthash, callback=None):
- command = 'blockchain.scripthash.get_balance'
- invocation = lambda c: self.send([(command, [scripthash])], c)
-
- return Network.__with_default_synchronous_callback(invocation, callback)
-
- def export_checkpoints(self, path):
- # run manually from the console to generate checkpoints
- cp = self.blockchain().get_checkpoints()
- with open(path, 'w', encoding='utf-8') as f:
- f.write(json.dumps(cp, indent=4))
-
- @classmethod
- def max_checkpoint(cls):
- return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1)
+ async def attempt_fee_estimate_update(self):
+ await self.request_fee_estimates(self.interface)
diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
index 7010b93f..78df7566 100644
--- a/electrum/paymentrequest.py
+++ b/electrum/paymentrequest.py
@@ -92,7 +92,8 @@ def get_payment_request(url):
data = None
error = "payment URL not pointing to a valid file"
else:
- raise Exception("unknown scheme", url)
+ data = None
+ error = "Unknown scheme for payment request. URL: {}".format(url)
pr = PaymentRequest(data, error)
return pr
diff --git a/electrum/plugin.py b/electrum/plugin.py
index 4ea8e53d..f269fb89 100644
--- a/electrum/plugin.py
+++ b/electrum/plugin.py
@@ -457,7 +457,8 @@ class DeviceMgr(ThreadJob, PrintError):
'''Returns a list of DeviceInfo objects: one for each connected,
unpaired device accepted by the plugin.'''
if not plugin.libraries_available:
- raise Exception('Missing libraries for {}'.format(plugin.name))
+ message = plugin.get_library_not_available_message()
+ raise Exception(message)
if devices is None:
devices = self.scan_devices()
devices = [dev for dev in devices if not self.xpub_by_id(dev.id_)]
diff --git a/electrum/plugins/coldcard/README.md b/electrum/plugins/coldcard/README.md
new file mode 100644
index 00000000..651cd804
--- /dev/null
+++ b/electrum/plugins/coldcard/README.md
@@ -0,0 +1,65 @@
+
+# Coldcard Hardware Wallet Plugin
+
+## Just the glue please
+
+This code connects the public USB API and Electrum. Leverages all
+the good work that's been done by the Electrum team to support
+hardware wallets.
+
+## Background
+
+The Coldcard has a larger screen (128x64) and a number pad. For
+this reason, all PIN code entry is done directly on the device.
+Coldcard does not appear on the USB bus until unlocked with appropriate
+PIN. Initial setup, and seed generation must be done offline.
+
+Coldcard uses an emerging standard for unsigned tranasctions:
+
+PSBT = Partially Signed Bitcoin Transaction = BIP174
+
+However, this spec is still under heavy discussion and in flux. At
+this point, the PSBT files generated will only be compatible with
+Coldcard.
+
+The Coldcard can be used 100% offline: it can generate a skeleton
+Electrum wallet and save it to MicroSD card. Transport that file
+to Electrum and it will fetch history, blockchain details and then
+operate in "unpaired" mode.
+
+Spending transactions can be saved to MicroSD using the "Export PSBT"
+button on the transaction preview dialog (when this plugin is
+owner of the wallet). That PSBT can be signed on the Coldcard
+(again using MicroSD both ways). The result is a ready-to-transmit
+bitcoin transaction, which can be transmitted using Tools > Load
+Transaction > From File in Electrum or really any tool.
+
+
+
+## TODO Items
+
+- No effort yet to support translations or languages other than English, sorry.
+- Coldcard PSBT format is not likely to be compatible with other devices, because the BIP174 is still in flux.
+- Segwit support not 100% complete: can pay to them, but cannot setup wallet to receive them.
+- Limited support for segwit wrapped in P2SH.
+- Someday we could support multisig hardware wallets based on PSBT where each participant
+ is using different devices/systems for signing, however, that belongs in an independant
+ plugin that is PSBT focused and might not require a Coldcard to be present.
+
+### Ctags
+
+- I find this command useful (at top level) ... but I'm a VIM user.
+
+ ctags -f .tags electrum `find . -name ENV -prune -o -name \*.py`
+
+
+### Working with latest ckcc-protocol
+
+- at top level, do this:
+
+ pip install -e git+ssh://git@github.com/Coldcard/ckcc-protocol.git#egg=ckcc-protocol
+
+- but you'll need the https version of that, not ssh like I can.
+- also a branch name would be good in there
+- do `pip uninstall ckcc` first
+- see
diff --git a/electrum/plugins/coldcard/__init__.py b/electrum/plugins/coldcard/__init__.py
new file mode 100644
index 00000000..7cb033f4
--- /dev/null
+++ b/electrum/plugins/coldcard/__init__.py
@@ -0,0 +1,7 @@
+from electrum.i18n import _
+
+fullname = 'Coldcard Wallet'
+description = 'Provides support for the Coldcard hardware wallet from Coinkite'
+requires = [('ckcc-protocol', 'github.com/Coldcard/ckcc-protocol')]
+registers_keystore = ('hardware', 'coldcard', _("Coldcard Wallet"))
+available_for = ['qt', 'cmdline']
diff --git a/electrum/plugins/coldcard/cmdline.py b/electrum/plugins/coldcard/cmdline.py
new file mode 100644
index 00000000..5fddd02a
--- /dev/null
+++ b/electrum/plugins/coldcard/cmdline.py
@@ -0,0 +1,47 @@
+from electrum.plugin import hook
+from .coldcard import ColdcardPlugin
+from electrum.util import print_msg, print_error, raw_input, print_stderr
+
+class ColdcardCmdLineHandler:
+
+ def get_passphrase(self, msg, confirm):
+ raise NotImplementedError
+
+ def get_pin(self, msg):
+ raise NotImplementedError
+
+ def prompt_auth(self, msg):
+ raise NotImplementedError
+
+ 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_stderr(msg)
+
+ def show_error(self, msg, blocking=False):
+ print_error(msg)
+
+ def update_status(self, b):
+ print_error('hw device status', b)
+
+ def finished(self):
+ pass
+
+class Plugin(ColdcardPlugin):
+ handler = ColdcardCmdLineHandler()
+
+ @hook
+ def init_keystore(self, keystore):
+ if not isinstance(keystore, self.keystore_class):
+ return
+ keystore.handler = self.handler
+
+ def create_handler(self, window):
+ return self.handler
+
+# EOF
diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
new file mode 100644
index 00000000..5327aa5e
--- /dev/null
+++ b/electrum/plugins/coldcard/coldcard.py
@@ -0,0 +1,693 @@
+#
+# Coldcard Electrum plugin main code.
+#
+#
+from struct import pack, unpack
+import hashlib
+import os, sys, time, io
+import traceback
+
+from electrum import bitcoin
+from electrum.bitcoin import serialize_xpub, deserialize_xpub, InvalidMasterKeyVersionBytes
+from electrum import constants
+from electrum.bitcoin import TYPE_ADDRESS, int_to_hex
+from electrum.i18n import _
+from electrum.plugin import BasePlugin, Device
+from electrum.keystore import Hardware_KeyStore, xpubkey_to_pubkey, Xpub
+from electrum.transaction import Transaction
+from electrum.wallet import Standard_Wallet
+from electrum.crypto import hash_160
+from ..hw_wallet import HW_PluginBase
+from ..hw_wallet.plugin import is_any_tx_output_on_change_branch
+from electrum.util import print_error, bfh, bh2u, versiontuple
+from electrum.base_wizard import ScriptTypeNotSupported
+
+try:
+ import hid
+ from ckcc.protocol import CCProtocolPacker, CCProtocolUnpacker
+ from ckcc.protocol import CCProtoError, CCUserRefused, CCBusyError
+ from ckcc.constants import (MAX_MSG_LEN, MAX_BLK_LEN, MSG_SIGNING_MAX_LENGTH, MAX_TXN_LEN,
+ AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH)
+ from ckcc.constants import (
+ PSBT_GLOBAL_UNSIGNED_TX, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO,
+ PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT,
+ PSBT_IN_BIP32_DERIVATION, PSBT_OUT_BIP32_DERIVATION)
+
+ from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID, CKCC_SIMULATOR_PATH
+
+ requirements_ok = True
+
+
+ class ElectrumColdcardDevice(ColdcardDevice):
+ # avoid use of pycoin for MiTM message signature test
+ def mitm_verify(self, sig, expect_xpub):
+ # verify a signature (65 bytes) over the session key, using the master bip32 node
+ # - customized to use specific EC library of Electrum.
+ from electrum.ecc import ECPubkey
+
+ xtype, depth, parent_fingerprint, child_number, chain_code, K_or_k \
+ = bitcoin.deserialize_xpub(expect_xpub)
+
+ pubkey = ECPubkey(K_or_k)
+ try:
+ pubkey.verify_message_hash(sig[1:65], self.session_key)
+ return True
+ except:
+ return False
+
+except ImportError:
+ requirements_ok = False
+
+ COINKITE_VID = 0xd13e
+ CKCC_PID = 0xcc10
+
+CKCC_SIMULATED_PID = CKCC_PID ^ 0x55aa
+
+def my_var_int(l):
+ # Bitcoin serialization of integers... directly into binary!
+ if l < 253:
+ return pack("B", l)
+ elif l < 0x10000:
+ return pack("' % (self.dev.master_fingerprint,
+ self.label())
+
+ def verify_connection(self, expected_xfp, expected_xpub):
+ ex = (expected_xfp, expected_xpub)
+
+ if self._expected_device == ex:
+ # all is as expected
+ return
+
+ if ( (self._expected_device is not None)
+ or (self.dev.master_fingerprint != expected_xfp)
+ or (self.dev.master_xpub != expected_xpub)):
+ # probably indicating programing error, not hacking
+ raise RuntimeError("Expecting 0x%08x but that's not whats connected?!" %
+ expected_xfp)
+
+ # check signature over session key
+ # - mitm might have lied about xfp and xpub up to here
+ # - important that we use value capture at wallet creation time, not some value
+ # we read over USB today
+ self.dev.check_mitm(expected_xpub=expected_xpub)
+
+ self._expected_device = ex
+
+ print_error("[coldcard]", "Successfully verified against MiTM")
+
+ def is_pairable(self):
+ # can't do anything w/ devices that aren't setup (but not normally reachable)
+ return bool(self.dev.master_xpub)
+
+ def timeout(self, cutoff):
+ # nothing to do?
+ pass
+
+ def close(self):
+ # close the HID device (so can be reused)
+ self.dev.close()
+ self.dev = None
+
+ def is_initialized(self):
+ return bool(self.dev.master_xpub)
+
+ def label(self):
+ # 'label' of this Coldcard. Warning: gets saved into wallet file, which might
+ # not be encrypted, so better for privacy if based on xpub/fingerprint rather than
+ # USB serial number.
+ if self.dev.is_simulator:
+ lab = 'Coldcard Simulator 0x%08x' % self.dev.master_fingerprint
+ elif not self.dev.master_fingerprint:
+ # failback; not expected
+ lab = 'Coldcard #' + self.dev.serial
+ else:
+ lab = 'Coldcard 0x%08x' % self.dev.master_fingerprint
+
+ # Hack zone: during initial setup I need the xfp and master xpub but
+ # very few objects are passed between the various steps of base_wizard.
+ # Solution: return a string with some hidden metadata
+ # - see
+ # - needs to work w/ deepcopy
+ class LabelStr(str):
+ def __new__(cls, s, xfp=None, xpub=None):
+ self = super().__new__(cls, str(s))
+ self.xfp = getattr(s, 'xfp', xfp)
+ self.xpub = getattr(s, 'xpub', xpub)
+ return self
+
+ return LabelStr(lab, self.dev.master_fingerprint, self.dev.master_xpub)
+
+ def has_usable_connection_with_device(self):
+ # Do end-to-end ping test
+ try:
+ self.ping_check()
+ return True
+ except:
+ return False
+
+ def get_xpub(self, bip32_path, xtype):
+ assert xtype in ColdcardPlugin.SUPPORTED_XTYPES
+ print_error('[coldcard]', 'Derive xtype = %r' % xtype)
+ xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000)
+ # TODO handle timeout?
+ # change type of xpub to the requested type
+ try:
+ __, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
+ except InvalidMasterKeyVersionBytes:
+ raise Exception(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.')
+ .format(self.device)) from None
+ if xtype != 'standard':
+ xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
+ return xpub
+
+ def ping_check(self):
+ # check connection is working
+ assert self.dev.session_key, 'not encrypted?'
+ req = b'1234 Electrum Plugin 4321' # free up to 59 bytes
+ try:
+ echo = self.dev.send_recv(CCProtocolPacker.ping(req))
+ assert echo == req
+ except:
+ raise RuntimeError("Communication trouble with Coldcard")
+
+ def show_address(self, path, addr_fmt):
+ # prompt user w/ addres, also returns it immediately.
+ return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
+
+ def get_version(self):
+ # gives list of strings
+ return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\n')
+
+ def sign_message_start(self, path, msg):
+ # this starts the UX experience.
+ self.dev.send_recv(CCProtocolPacker.sign_message(msg, path), timeout=None)
+
+ def sign_message_poll(self):
+ # poll device... if user has approved, will get tuple: (addr, sig) else None
+ return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
+
+ def sign_transaction_start(self, raw_psbt, finalize=True):
+ # Multiple steps to sign:
+ # - upload binary
+ # - start signing UX
+ # - wait for coldcard to complete process, or have it refused.
+ # - download resulting txn
+ assert 20 <= len(raw_psbt) < MAX_TXN_LEN, 'PSBT is too big'
+ dlen, chk = self.dev.upload_file(raw_psbt)
+
+ resp = self.dev.send_recv(CCProtocolPacker.sign_transaction(dlen, chk, finalize=finalize),
+ timeout=None)
+
+ if resp != None:
+ raise ValueError(resp)
+
+ def sign_transaction_poll(self):
+ # poll device... if user has approved, will get tuple: (legnth, checksum) else None
+ return self.dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)
+
+ def download_file(self, length, checksum, file_number=1):
+ # get a file
+ return self.dev.download_file(length, checksum, file_number=file_number)
+
+
+
+class Coldcard_KeyStore(Hardware_KeyStore):
+ hw_type = 'coldcard'
+ device = 'Coldcard'
+
+ 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.ux_busy = False
+
+ # Seems like only the derivation path and resulting **derived** xpub is stored in
+ # the wallet file... however, we need to know at least the fingerprint of the master
+ # xpub to verify against MiTM, and also so we can put the right value into the subkey paths
+ # of PSBT files that might be generated offline.
+ # - save the fingerprint of the master xpub, as "xfp"
+ # - it's a LE32 int, but hex more natural way to see it
+ # - device reports these value during encryption setup process
+ lab = d['label']
+ if hasattr(lab, 'xfp'):
+ # initial setup
+ self.ckcc_xfp = lab.xfp
+ self.ckcc_xpub = lab.xpub
+ else:
+ # wallet load: fatal if missing, we need them!
+ self.ckcc_xfp = d['ckcc_xfp']
+ self.ckcc_xpub = d['ckcc_xpub']
+
+ def dump(self):
+ # our additions to the stored data about keystore -- only during creation?
+ d = Hardware_KeyStore.dump(self)
+
+ d['ckcc_xfp'] = self.ckcc_xfp
+ d['ckcc_xpub'] = self.ckcc_xpub
+
+ return d
+
+ def get_derivation(self):
+ return self.derivation
+
+ def get_client(self):
+ # called when user tries to do something like view address, sign somthing.
+ # - not called during probing/setup
+ rv = self.plugin.get_client(self)
+ if rv:
+ rv.verify_connection(self.ckcc_xfp, self.ckcc_xpub)
+
+ return rv
+
+ def give_error(self, message, clear_client=False):
+ print_error(message)
+ if not self.ux_busy:
+ self.handler.show_error(message)
+ else:
+ self.ux_busy = False
+ if clear_client:
+ self.client = None
+ raise Exception(message)
+
+ def wrap_busy(func):
+ # decorator: function takes over the UX on the device.
+ def wrapper(self, *args, **kwargs):
+ try:
+ self.ux_busy = True
+ return func(self, *args, **kwargs)
+ finally:
+ self.ux_busy = False
+ return wrapper
+
+ def decrypt_message(self, pubkey, message, password):
+ raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device))
+
+ @wrap_busy
+ def sign_message(self, sequence, message, password):
+ # Sign a message on device. Since we have big screen, of course we
+ # have to show the message unabiguously there first!
+ try:
+ msg = message.encode('ascii', errors='strict')
+ assert 1 <= len(msg) <= MSG_SIGNING_MAX_LENGTH
+ except (UnicodeError, AssertionError):
+ # there are other restrictions on message content,
+ # but let the device enforce and report those
+ self.handler.show_error('Only short (%d max) ASCII messages can be signed.'
+ % MSG_SIGNING_MAX_LENGTH)
+ return b''
+
+ client = self.get_client()
+ path = self.get_derivation() + ("/%d/%d" % sequence)
+ try:
+ cl = self.get_client()
+ try:
+ self.handler.show_message("Signing message (using %s)..." % path)
+
+ cl.sign_message_start(path, msg)
+
+ while 1:
+ # How to kill some time, without locking UI?
+ time.sleep(0.250)
+
+ resp = cl.sign_message_poll()
+ if resp is not None:
+ break
+
+ finally:
+ self.handler.finished()
+
+ assert len(resp) == 2
+ addr, raw_sig = resp
+
+ # already encoded in Bitcoin fashion, binary.
+ assert 40 < len(raw_sig) <= 65
+
+ return raw_sig
+
+ except (CCUserRefused, CCBusyError) as exc:
+ self.handler.show_error(str(exc))
+ except CCProtoError as exc:
+ traceback.print_exc(file=sys.stderr)
+ self.handler.show_error('{}\n\n{}'.format(
+ _('Error showing address') + ':', str(exc)))
+ except Exception as e:
+ self.give_error(e, True)
+
+ # give empty bytes for error cases; it seems to clear the old signature box
+ return b''
+
+ def build_psbt(self, tx, wallet=None, xfp=None):
+ # Render a PSBT file, for upload to Coldcard.
+ #
+ if xfp is None:
+ # need fingerprint of MASTER xpub, not the derived key
+ xfp = self.ckcc_xfp
+
+ inputs = tx.inputs()
+
+ if 'prev_tx' not in inputs[0]:
+ # fetch info about inputs, if needed?
+ # - needed during export PSBT flow, not normal online signing
+ assert wallet, 'need wallet reference'
+ wallet.add_hw_info(tx)
+
+ # wallet.add_hw_info installs this attr
+ assert hasattr(tx, 'output_info'), 'need data about outputs'
+
+ # Build map of pubkey needed as derivation from master, in PSBT binary format
+ # 1) binary version of the common subpath for all keys
+ # m/ => fingerprint LE32
+ # a/b/c => ints
+ base_path = pack(' not multisig, must be bip32
+ if type(wallet) is not Standard_Wallet:
+ keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))
+ return
+
+ sequence = wallet.get_address_index(address)
+ txin_type = wallet.get_txin_type(address)
+ keystore.show_address(sequence, txin_type)
+
+# EOF
diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py
new file mode 100644
index 00000000..583a845b
--- /dev/null
+++ b/electrum/plugins/coldcard/qt.py
@@ -0,0 +1,241 @@
+import time
+
+from electrum.i18n import _
+from electrum.plugin import hook
+from electrum.wallet import Standard_Wallet
+from electrum.gui.qt.util import *
+
+from .coldcard import ColdcardPlugin
+from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
+
+
+class Plugin(ColdcardPlugin, QtPluginBase):
+ icon_unpaired = ":icons/coldcard_unpaired.png"
+ icon_paired = ":icons/coldcard.png"
+
+ def create_handler(self, window):
+ return Coldcard_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 Coldcard"), show_address)
+
+ @hook
+ def transaction_dialog(self, dia):
+ # see gui/qt/transaction_dialog.py
+
+ keystore = dia.wallet.get_keystore()
+ if type(keystore) != self.keystore_class:
+ # not a Coldcard wallet, hide feature
+ return
+
+ # - add a new button, near "export"
+ btn = QPushButton(_("Save PSBT"))
+ btn.clicked.connect(lambda unused: self.export_psbt(dia))
+ if dia.tx.is_complete():
+ # but disable it for signed transactions (nothing to do if already signed)
+ btn.setDisabled(True)
+
+ dia.sharing_buttons.append(btn)
+
+ def export_psbt(self, dia):
+ # Called from hook in transaction dialog
+ tx = dia.tx
+
+ if tx.is_complete():
+ # if they sign while dialog is open, it can transition from unsigned to signed,
+ # which we don't support here, so do nothing
+ return
+
+ # can only expect Coldcard wallets to work with these files (right now)
+ keystore = dia.wallet.get_keystore()
+ assert type(keystore) == self.keystore_class
+
+ # convert to PSBT
+ raw_psbt = keystore.build_psbt(tx, wallet=dia.wallet)
+
+ name = (dia.wallet.basename() + time.strftime('-%y%m%d-%H%M.psbt')).replace(' ', '-')
+ fileName = dia.main_window.getSaveFileName(_("Select where to save the PSBT file"),
+ name, "*.psbt")
+ if fileName:
+ with open(fileName, "wb+") as f:
+ f.write(raw_psbt)
+ dia.show_message(_("Transaction exported successfully"))
+ dia.saved = True
+
+ def show_settings_dialog(self, window, keystore):
+ # When they click on the icon for CC we come here.
+ device_id = self.choose_device(window, keystore)
+ if device_id:
+ CKCCSettingsDialog(window, self, keystore, device_id).exec_()
+
+
+class Coldcard_Handler(QtHandlerBase):
+ setup_signal = pyqtSignal()
+ #auth_signal = pyqtSignal(object)
+
+ def __init__(self, win):
+ super(Coldcard_Handler, self).__init__(win, 'Coldcard')
+ self.setup_signal.connect(self.setup_dialog)
+ #self.auth_signal.connect(self.auth_dialog)
+
+
+ def message_dialog(self, msg):
+ self.clear_dialog()
+ self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Coldcard Status"))
+ l = QLabel(msg)
+ vbox = QVBoxLayout(dialog)
+ vbox.addWidget(l)
+ dialog.show()
+
+ def get_setup(self):
+ self.done.clear()
+ self.setup_signal.emit()
+ self.done.wait()
+ return
+
+ def setup_dialog(self):
+ self.show_error(_('Please initialize your Coldcard while disconnected.'))
+ return
+
+class CKCCSettingsDialog(WindowModalDialog):
+ '''This dialog doesn't require a device be paired with a wallet.
+ We want users to be able to wipe a device even if they've forgotten
+ their PIN.'''
+
+ def __init__(self, window, plugin, keystore, device_id):
+ title = _("{} Settings").format(plugin.device)
+ super(CKCCSettingsDialog, self).__init__(window, title)
+ self.setMaximumWidth(540)
+
+ devmgr = plugin.device_manager()
+ config = devmgr.config
+ handler = keystore.handler
+ self.thread = thread = keystore.thread
+
+ def connect_and_doit():
+ client = devmgr.client_by_id(device_id)
+ if not client:
+ raise RuntimeError("Device not connected")
+ return client
+
+ body = QWidget()
+ body_layout = QVBoxLayout(body)
+ grid = QGridLayout()
+ grid.setColumnStretch(2, 1)
+
+ # see
+ title = QLabel('''
+Coldcard Wallet
+
from Coinkite Inc.
+
coldcardwallet.com''')
+ title.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
+
+ grid.addWidget(title , 0,0, 1,2, Qt.AlignHCenter)
+ y = 3
+
+ rows = [
+ ('fw_version', _("Firmware Version")),
+ ('fw_built', _("Build Date")),
+ ('bl_version', _("Bootloader")),
+ ('xfp', _("Master Fingerprint")),
+ ('serial', _("USB Serial")),
+ ]
+ for row_num, (member_name, label) in enumerate(rows):
+ widget = QLabel('000000000000')
+ widget.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
+
+ grid.addWidget(QLabel(label), y, 0, 1,1, Qt.AlignRight)
+ grid.addWidget(widget, y, 1, 1, 1, Qt.AlignLeft)
+ setattr(self, member_name, widget)
+ y += 1
+ body_layout.addLayout(grid)
+
+ upg_btn = QPushButton('Upgrade')
+ #upg_btn.setDefault(False)
+ def _start_upgrade():
+ thread.add(connect_and_doit, on_success=self.start_upgrade)
+ upg_btn.clicked.connect(_start_upgrade)
+
+ y += 3
+ grid.addWidget(upg_btn, y, 0)
+ grid.addWidget(CloseButton(self), y, 1)
+
+ dialog_vbox = QVBoxLayout(self)
+ dialog_vbox.addWidget(body)
+
+ # Fetch values and show them
+ thread.add(connect_and_doit, on_success=self.show_values)
+
+ def show_values(self, client):
+ dev = client.dev
+
+ self.xfp.setText('0x%08x' % dev.master_fingerprint)
+ self.serial.setText('%s' % dev.serial)
+
+ # ask device for versions: allow extras for future
+ fw_date, fw_rel, bl_rel, *rfu = client.get_version()
+
+ self.fw_version.setText('%s' % fw_rel)
+ self.fw_built.setText('%s' % fw_date)
+ self.bl_version.setText('%s' % bl_rel)
+
+ def start_upgrade(self, client):
+ # ask for a filename (must have already downloaded it)
+ mw = get_parent_main_window(self)
+ dev = client.dev
+
+ fileName = mw.getOpenFileName("Select upgraded firmware file", "*.dfu")
+ if not fileName:
+ return
+
+ from ckcc.utils import dfu_parse
+ from ckcc.sigheader import FW_HEADER_SIZE, FW_HEADER_OFFSET, FW_HEADER_MAGIC
+ from ckcc.protocol import CCProtocolPacker
+ from hashlib import sha256
+ import struct
+
+ try:
+ with open(fileName, 'rb') as fd:
+
+ # unwrap firmware from the DFU
+ offset, size, *ignored = dfu_parse(fd)
+
+ fd.seek(offset)
+ firmware = fd.read(size)
+
+ hpos = FW_HEADER_OFFSET
+ hdr = bytes(firmware[hpos:hpos + FW_HEADER_SIZE]) # needed later too
+ magic = struct.unpack_from(" str:
+ """Returns the version of the 3rd party python library
+ for the hw wallet. For example '0.9.0'
+
+ Returns 'unknown' if library is found but cannot determine version.
+ Raises 'ImportError' if library is not found.
+ """
+ raise NotImplementedError()
+
+ def check_libraries_available(self) -> bool:
+ try:
+ library_version = self.get_library_version()
+ except ImportError:
+ return False
+ if library_version == 'unknown' or \
+ versiontuple(library_version) < self.minimum_library:
+ self.libraries_available_message = (
+ _("Library version for '{}' is too old.").format(self.name)
+ + '\nInstalled: {}, Needed: {}'
+ .format(library_version, self.minimum_library))
+ self.print_stderr(self.libraries_available_message)
+ return False
+ return True
+
+ def get_library_not_available_message(self) -> str:
+ if hasattr(self, 'libraries_available_message'):
+ message = self.libraries_available_message
+ else:
+ message = _("Missing libraries for {}.").format(self.name)
+ message += '\n' + _("Make sure you install it with python3")
+ return message
+
def is_any_tx_output_on_change_branch(tx):
if not hasattr(tx, 'output_info'):
@@ -85,7 +119,7 @@ def is_any_tx_output_on_change_branch(tx):
for _type, address, amount in tx.outputs():
info = tx.output_info.get(address)
if info is not None:
- index, xpubs, m = info
+ index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
if index[0] == 1:
return True
return False
diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py
index 214ea2b5..2b5215eb 100644
--- a/electrum/plugins/hw_wallet/qt.py
+++ b/electrum/plugins/hw_wallet/qt.py
@@ -189,11 +189,7 @@ class QtPluginBase(object):
if not isinstance(keystore, self.keystore_class):
continue
if not self.libraries_available:
- if hasattr(self, 'libraries_available_message'):
- message = self.libraries_available_message + '\n'
- else:
- message = _("Cannot find python library for") + " '%s'.\n" % self.name
- message += _("Make sure you install it with python3")
+ message = keystore.plugin.get_library_not_available_message()
window.show_error(message)
return
tooltip = self.device + '\n' + (keystore.label or 'unnamed')
diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py
index e1c7b2aa..f4e46d15 100644
--- a/electrum/plugins/keepkey/clientbase.py
+++ b/electrum/plugins/keepkey/clientbase.py
@@ -4,7 +4,7 @@ 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
+from electrum.bitcoin import serialize_xpub, convert_bip32_path_to_list_of_uint32
class GuiMixin(object):
@@ -141,21 +141,7 @@ class KeepKeyClientBase(GuiMixin, PrintError):
@staticmethod
def expand_path(n):
- '''Convert bip32 path to list of uint32 integers with prime flags
- 0/-1/1' -> [0, 0x80000001, 0x80000001]'''
- # This code is similar to code in trezorlib where it unfortunately
- # is not declared as a staticmethod. Our n has an extra element.
- PRIME_DERIVATION_FLAG = 0x80000000
- path = []
- for x in n.split('/')[1:]:
- prime = 0
- if x.endswith("'"):
- x = x.replace('\'', '')
- prime = PRIME_DERIVATION_FLAG
- if x.startswith('-'):
- prime = PRIME_DERIVATION_FLAG
- path.append(abs(int(x)) | prime)
- return path
+ return convert_bip32_path_to_list_of_uint32(n)
def cancel(self):
'''Provided here as in keepkeylib but not trezorlib.'''
diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py
index 66a208a4..097aea33 100644
--- a/electrum/plugins/keepkey/keepkey.py
+++ b/electrum/plugins/keepkey/keepkey.py
@@ -351,8 +351,7 @@ class KeepKeyPlugin(HW_PluginBase):
def tx_outputs(self, derivation, tx, segwit=False):
- def create_output_by_derivation(info):
- index, xpubs, m = info
+ def create_output_by_derivation():
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)
@@ -407,7 +406,7 @@ class KeepKeyPlugin(HW_PluginBase):
info = tx.output_info.get(address)
if info is not None and not has_change:
- index, xpubs, m = info
+ index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
on_change_branch = index[0] == 1
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
@@ -416,7 +415,7 @@ class KeepKeyPlugin(HW_PluginBase):
has_change = True
if use_create_by_derivation:
- txoutputtype = create_output_by_derivation(info)
+ txoutputtype = create_output_by_derivation()
else:
txoutputtype = create_output_by_address()
outputs.append(txoutputtype)
diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py
index d5fa50b2..623023cb 100644
--- a/electrum/plugins/labels/labels.py
+++ b/electrum/plugins/labels/labels.py
@@ -1,6 +1,5 @@
+import asyncio
import hashlib
-import requests
-import threading
import json
import sys
import traceback
@@ -10,7 +9,7 @@ import base64
from electrum.plugin import BasePlugin, hook
from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv
from electrum.i18n import _
-
+from electrum.util import aiosafe, make_aiohttp_session
class LabelsPlugin(BasePlugin):
@@ -18,11 +17,11 @@ class LabelsPlugin(BasePlugin):
BasePlugin.__init__(self, parent, config, name)
self.target_host = 'labels.electrum.org'
self.wallets = {}
+ self.proxy = None
def encode(self, wallet, msg):
password, iv, wallet_id = self.wallets[wallet]
- encrypted = aes_encrypt_with_iv(password, iv,
- msg.encode('utf8'))
+ encrypted = aes_encrypt_with_iv(password, iv, msg.encode('utf8'))
return base64.b64encode(encrypted).decode()
def decode(self, wallet, message):
@@ -55,37 +54,27 @@ class LabelsPlugin(BasePlugin):
"walletNonce": nonce,
"externalId": self.encode(wallet, item),
"encryptedLabel": self.encode(wallet, label)}
- t = threading.Thread(target=self.do_request_safe,
- args=["POST", "/label", False, bundle])
- t.setDaemon(True)
- t.start()
+ asyncio.get_event_loop().create_task(self.do_post_safe("/label", bundle))
# Caller will write the wallet
self.set_nonce(wallet, nonce + 1)
- def do_request(self, method, url = "/labels", is_batch=False, data=None):
+ @aiosafe
+ async def do_post_safe(self, *args):
+ await self.do_post(*args)
+
+ async def do_get(self, url = "/labels"):
url = 'https://' + self.target_host + url
- kwargs = {'headers': {}}
- if method == 'GET' and data:
- kwargs['params'] = data
- elif method == 'POST' and data:
- kwargs['data'] = json.dumps(data)
- kwargs['headers']['Content-Type'] = 'application/json'
- response = requests.request(method, url, **kwargs)
- if response.status_code != 200:
- raise Exception(response.status_code, response.text)
- response = response.json()
- if "error" in response:
- raise Exception(response["error"])
- return response
+ async with make_aiohttp_session(self.proxy) as session:
+ async with session.get(url) as result:
+ return await result.json()
- def do_request_safe(self, *args, **kwargs):
- try:
- self.do_request(*args, **kwargs)
- except BaseException as e:
- #traceback.print_exc(file=sys.stderr)
- self.print_error('error doing request')
+ async def do_post(self, url = "/labels", data=None):
+ url = 'https://' + self.target_host + url
+ async with make_aiohttp_session(self.proxy) as session:
+ async with session.post(url, json=data) as result:
+ return await result.json()
- def push_thread(self, wallet):
+ async def push_thread(self, wallet):
wallet_data = self.wallets.get(wallet, None)
if not wallet_data:
raise Exception('Wallet {} not loaded'.format(wallet))
@@ -102,16 +91,16 @@ class LabelsPlugin(BasePlugin):
continue
bundle["labels"].append({'encryptedLabel': encoded_value,
'externalId': encoded_key})
- self.do_request("POST", "/labels", True, bundle)
+ await self.do_post("/labels", bundle)
- def pull_thread(self, wallet, force):
+ async def pull_thread(self, wallet, force):
wallet_data = self.wallets.get(wallet, None)
if not wallet_data:
raise Exception('Wallet {} not loaded'.format(wallet))
wallet_id = wallet_data[2]
nonce = 1 if force else self.get_nonce(wallet) - 1
self.print_error("asking for labels since nonce", nonce)
- response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) ))
+ response = await self.do_get("/labels/since/%d/for/%s" % (nonce, wallet_id))
if response["labels"] is None:
self.print_error('no new labels')
return
@@ -140,12 +129,15 @@ class LabelsPlugin(BasePlugin):
self.set_nonce(wallet, response["nonce"] + 1)
self.on_pulled(wallet)
- def pull_thread_safe(self, wallet, force):
- try:
- self.pull_thread(wallet, force)
- except BaseException as e:
- # traceback.print_exc(file=sys.stderr)
- self.print_error('could not retrieve labels')
+ @aiosafe
+ async def pull_safe_thread(self, wallet, force):
+ await self.pull_thread(wallet, force)
+
+ def pull(self, wallet, force):
+ return asyncio.run_coroutine_threadsafe(self.pull_thread(wallet, force), wallet.network.asyncio_loop).result()
+
+ def push(self, wallet):
+ return asyncio.run_coroutine_threadsafe(self.push_thread(wallet), wallet.network.asyncio_loop).result()
def start_wallet(self, wallet):
nonce = self.get_nonce(wallet)
@@ -159,9 +151,14 @@ class LabelsPlugin(BasePlugin):
wallet_id = hashlib.sha256(mpk).hexdigest()
self.wallets[wallet] = (password, iv, wallet_id)
# If there is an auth token we can try to actually start syncing
- t = threading.Thread(target=self.pull_thread_safe, args=(wallet, False))
- t.setDaemon(True)
- t.start()
+ asyncio.get_event_loop().create_task(self.pull_safe_thread(wallet, False))
+ self.proxy = wallet.network.proxy
+ wallet.network.register_callback(self.set_proxy, ['proxy_set'])
def stop_wallet(self, wallet):
+ wallet.network.unregister_callback('proxy_set')
self.wallets.pop(wallet, None)
+
+ def set_proxy(self, evt_name, new_proxy):
+ self.proxy = new_proxy
+ self.print_error("proxy set")
diff --git a/electrum/plugins/labels/qt.py b/electrum/plugins/labels/qt.py
index df4ae55c..2a66d98e 100644
--- a/electrum/plugins/labels/qt.py
+++ b/electrum/plugins/labels/qt.py
@@ -38,11 +38,11 @@ class Plugin(LabelsPlugin):
hbox = QHBoxLayout()
hbox.addWidget(QLabel("Label sync options:"))
upload = ThreadedButton("Force upload",
- partial(self.push_thread, wallet),
+ partial(self.push, wallet),
partial(self.done_processing_success, d),
partial(self.done_processing_error, d))
download = ThreadedButton("Force download",
- partial(self.pull_thread, wallet, True),
+ partial(self.pull, wallet, True),
partial(self.done_processing_success, d),
partial(self.done_processing_error, d))
vbox = QVBoxLayout()
diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
index 939a7a5a..cdac591e 100644
--- a/electrum/plugins/ledger/ledger.py
+++ b/electrum/plugins/ledger/ledger.py
@@ -399,7 +399,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
info = tx.output_info.get(o.address)
if (info is not None) and len(tx.outputs()) > 1 \
and not has_change:
- index, xpubs, m = info
+ index = info.address_index
on_change_branch = index[0] == 1
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
diff --git a/electrum/plugins/revealer/hmac_drbg.py b/electrum/plugins/revealer/hmac_drbg.py
new file mode 100644
index 00000000..ca533469
--- /dev/null
+++ b/electrum/plugins/revealer/hmac_drbg.py
@@ -0,0 +1,51 @@
+'''
+Copyright (c) 2014 David Lazar
+
+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 hmac
+
+class DRBG(object):
+ def __init__(self, seed):
+ self.key = b'\x00' * 64
+ self.val = b'\x01' * 64
+ self.reseed(seed)
+
+ def hmac(self, key, val):
+ return hmac.new(key, val, hashlib.sha512).digest()
+
+ def reseed(self, data=b''):
+ self.key = self.hmac(self.key, self.val + b'\x00' + data)
+ self.val = self.hmac(self.key, self.val)
+
+ if data:
+ self.key = self.hmac(self.key, self.val + b'\x01' + data)
+ self.val = self.hmac(self.key, self.val)
+
+ def generate(self, n):
+ xs = b''
+ while len(xs) < n:
+ self.val = self.hmac(self.key, self.val)
+ xs += self.val
+
+ self.reseed()
+
+ return xs[:n]
diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py
index 198d1cc4..d64dc585 100644
--- a/electrum/plugins/revealer/qt.py
+++ b/electrum/plugins/revealer/qt.py
@@ -22,16 +22,17 @@ import qrcode
import traceback
from hashlib import sha256
from decimal import Decimal
+import binascii
from PyQt5.QtPrintSupport import QPrinter
from electrum.plugin import BasePlugin, hook
from electrum.i18n import _
from electrum.util import to_bytes, make_dir
-
from electrum.gui.qt.util import *
from electrum.gui.qt.qrtextedit import ScanQRTextEdit
+from .hmac_drbg import DRBG
class Plugin(BasePlugin):
@@ -47,7 +48,7 @@ class Plugin(BasePlugin):
self.calibration_h = self.config.get('calibration_h')
self.calibration_v = self.config.get('calibration_v')
- self.version = '0'
+ self.version = '1'
self.size = (159, 97)
self.f_size = QSize(1014*2, 642*2)
self.abstand_h = 21
@@ -130,7 +131,7 @@ class Plugin(BasePlugin):
s = self.get_noise()
b = self.is_noise(s)
if b:
- self.noise_seed = s[:-3]
+ self.noise_seed = s[1:-3]
self.user_input = True
self.next_button.setEnabled(b)
@@ -155,6 +156,9 @@ class Plugin(BasePlugin):
else:
return False
else:
+
+ if (len(txt)>0 and txt[0]=='0'):
+ self.d.show_message(''.join(["",_("Warning: "), "", _("Revealers starting with 0 had a vulnerability and are not supported.")]))
self.user_input = False
return False
@@ -211,12 +215,12 @@ class Plugin(BasePlugin):
grid = QGridLayout()
self.vbox.addLayout(grid)
- cprint = QPushButton(_("Generate encrypted seed PDF"))
+ cprint = QPushButton(_("Generate encrypted seed backup"))
cprint.clicked.connect(partial(self.seed_img, True))
self.vbox.addWidget(cprint)
self.vbox.addSpacing(14)
- self.vbox.addWidget(WWLabel(_("and/or type any secret below:")))
+ self.vbox.addWidget(WWLabel(_("OR type any secret below:")))
self.text = ScanQRTextEdit()
self.text.setTabChangesFocus(True)
self.text.setMaximumHeight(70)
@@ -231,12 +235,19 @@ class Plugin(BasePlugin):
self.vbox.addWidget(self.max_chars)
self.max_chars.setVisible(False)
- self.ctext = QPushButton(_("Generate custom secret encrypted PDF"))
+ self.ctext = QPushButton(_("Generate custom secret encrypted backup"))
self.ctext.clicked.connect(self.t)
self.vbox.addWidget(self.ctext)
self.ctext.setEnabled(False)
+ self.vbox.addSpacing(11)
+ self.vbox.addWidget(
+ QLabel(''.join(["" + _("WARNING") + ": " + _("Revealer is a one-time-pad and should be used only once."), '
',
+ _("Multiple secrets encrypted for the same Revealer can be attacked."), '
',
+ ])))
+ self.vbox.addSpacing(11)
+
self.vbox.addSpacing(21)
self.vbox.addLayout(Buttons(CloseButton(d)))
return bool(d.exec_())
@@ -263,20 +274,21 @@ class Plugin(BasePlugin):
painter.begin(bitmap)
QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(__file__), 'SourceSansPro-Bold.otf') )
if len(txt) < 102 :
- fontsize = 12
+ fontsize = 15
linespace = 15
max_letters = 17
max_lines = 6
max_words = 3
else:
- fontsize = 9
+ fontsize = 12
linespace = 10
- max_letters = 24
+ max_letters = 23
max_lines = 9
max_words = int(max_letters/4)
font = QFont('Source Sans Pro', fontsize, QFont.Bold)
font.setLetterSpacing(QFont.PercentageSpacing, 100)
+ font.setPixelSize(fontsize)
painter.setFont(font)
seed_array = txt.split(' ')
@@ -304,21 +316,27 @@ class Plugin(BasePlugin):
if(self.noise_seed == False):
self.noise_seed = random.SystemRandom().getrandbits(128)
- self.hex_noise = format(self.noise_seed, '02x')
+ self.hex_noise = format(self.noise_seed, '032x')
self.hex_noise = self.version + str(self.hex_noise)
if (self.user_input == True):
self.noise_seed = int(self.noise_seed, 16)
- self.hex_noise = self.version + str(format(self.noise_seed, '02x'))
-
+ self.hex_noise = self.version + str(format(self.noise_seed, '032x'))
self.code_id = self.code_hashid(self.hex_noise)
self.hex_noise = ' '.join(self.hex_noise[i:i+4] for i in range(0,len(self.hex_noise),4))
- random.seed(self.noise_seed)
+ entropy = binascii.unhexlify(str(format(self.noise_seed, '032x')))
+ code_id = binascii.unhexlify(self.version + self.code_id)
+
+ drbg = DRBG(entropy + code_id)
+ noise_array=bin(int.from_bytes(drbg.generate(1929), 'big'))[2:]
+
+ i=0
for x in range(w):
for y in range(h):
- rawnoise.setPixel(x,y,random.randint(0, 1))
+ rawnoise.setPixel(x,y,int(noise_array[i]))
+ i+=1
self.rawnoise = rawnoise
if create_revealer==True:
@@ -339,8 +357,9 @@ class Plugin(BasePlugin):
revealer = self.pixelcode_2x2(self.rawnoise)
revealer.invertPixels()
revealer = QBitmap.fromImage(revealer)
+ revealer = revealer.scaled(self.f_size, Qt.KeepAspectRatio)
revealer = self.overlay_marks(revealer)
- revealer = revealer.scaled(1014, 642)
+
self.filename = 'Revealer - '
revealer.save(self.base_dir + self.filename + self.version+'_'+self.code_id + '.png')
self.toPdf(QImage(revealer))
@@ -519,6 +538,7 @@ class Plugin(BasePlugin):
f_size = 37
QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(__file__), 'DejaVuSansMono-Bold.ttf'))
font = QFont("DejaVu Sans Mono", f_size-11, QFont.Bold)
+ font.setPixelSize(35)
painter.setFont(font)
if not calibration_sheet:
diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py
index 68a4544c..da383809 100644
--- a/electrum/plugins/safe_t/clientbase.py
+++ b/electrum/plugins/safe_t/clientbase.py
@@ -4,7 +4,7 @@ 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
+from electrum.bitcoin import serialize_xpub, convert_bip32_path_to_list_of_uint32
class GuiMixin(object):
@@ -143,21 +143,7 @@ class SafeTClientBase(GuiMixin, PrintError):
@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 safetlib where it unfortunately
- # is not declared as a staticmethod. Our n has an extra element.
- PRIME_DERIVATION_FLAG = 0x80000000
- path = []
- for x in n.split('/')[1:]:
- prime = 0
- if x.endswith("'"):
- x = x.replace('\'', '')
- prime = PRIME_DERIVATION_FLAG
- if x.startswith('-'):
- prime = PRIME_DERIVATION_FLAG
- path.append(abs(int(x)) | prime)
- return path
+ return convert_bip32_path_to_list_of_uint32(n)
def cancel(self):
'''Provided here as in keepkeylib but not safetlib.'''
diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py
index b9580aa1..a9114f2d 100644
--- a/electrum/plugins/safe_t/qt.py
+++ b/electrum/plugins/safe_t/qt.py
@@ -76,10 +76,10 @@ class QtPlugin(QtPluginBase):
return
for keystore in wallet.get_keystores():
if type(keystore) == self.keystore_class:
- def show_address():
+ def show_address(keystore=keystore):
keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
- menu.addAction(_("Show on {}").format(self.device), show_address)
- break
+ device_name = "{} ({})".format(self.device, keystore.label)
+ menu.addAction(_("Show on {}").format(device_name), show_address)
def show_settings_dialog(self, window, keystore):
device_id = self.choose_device(window, keystore)
diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py
index 22008977..f6ef634c 100644
--- a/electrum/plugins/safe_t/safe_t.py
+++ b/electrum/plugins/safe_t/safe_t.py
@@ -9,7 +9,7 @@ from electrum import constants
from electrum.i18n import _
from electrum.plugin import BasePlugin, Device
from electrum.transaction import deserialize, Transaction
-from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey, xtype_from_derivation
+from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase
@@ -19,9 +19,6 @@ from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validat
# Safe-T mini 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 SafeTKeyStore(Hardware_KeyStore):
hw_type = 'safe_t'
@@ -30,15 +27,6 @@ class SafeTKeyStore(Hardware_KeyStore):
def get_derivation(self):
return self.derivation
- def get_script_gen(self):
- xtype = xtype_from_derivation(self.derivation)
- if xtype in ('p2wpkh', 'p2wsh'):
- return SCRIPT_GEN_NATIVE_SEGWIT
- elif xtype in ('p2wpkh-p2sh', 'p2wsh-p2sh'):
- return SCRIPT_GEN_P2SH_SEGWIT
- else:
- return SCRIPT_GEN_LEGACY
-
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
@@ -94,25 +82,8 @@ class SafeTPlugin(HW_PluginBase):
def __init__(self, parent, config, name):
HW_PluginBase.__init__(self, parent, config, name)
- try:
- # Minimal test if python-safet is installed
- import safetlib
- try:
- library_version = safetlib.__version__
- except AttributeError:
- # python-safet only introduced __version__ in 0.1.0
- library_version = 'unknown'
- if library_version == 'unknown' or \
- versiontuple(library_version) < self.minimum_library:
- self.libraries_available_message = (
- _("Library version for '{}' is too old.").format(name)
- + '\nInstalled: {}, Needed: {}'
- .format(library_version, self.minimum_library))
- self.print_stderr(self.libraries_available_message)
- raise ImportError()
- self.libraries_available = True
- except ImportError:
- self.libraries_available = False
+ self.libraries_available = self.check_libraries_available()
+ if not self.libraries_available:
return
from . import client
@@ -125,6 +96,13 @@ class SafeTPlugin(HW_PluginBase):
self.transport_handler = transport.SafeTTransport()
self.device_manager().register_enumerate_func(self.enumerate)
+ def get_library_version(self):
+ import safetlib
+ try:
+ return safetlib.__version__
+ except AttributeError:
+ return 'unknown'
+
def enumerate(self):
devices = self.transport_handler.enumerate_devices()
return [Device(d.get_path(), -1, d.get_path(), 'Safe-T mini', 0) for d in devices]
@@ -294,23 +272,34 @@ class SafeTPlugin(HW_PluginBase):
client.used()
return xpub
- def get_safet_input_script_type(self, script_gen, is_multisig):
- if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
+ def get_safet_input_script_type(self, electrum_txin_type: str):
+ if electrum_txin_type in ('p2wpkh', 'p2wsh'):
return self.types.InputScriptType.SPENDWITNESS
- elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
+ if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):
return self.types.InputScriptType.SPENDP2SHWITNESS
- else:
- if is_multisig:
- return self.types.InputScriptType.SPENDMULTISIG
- else:
- return self.types.InputScriptType.SPENDADDRESS
+ if electrum_txin_type in ('p2pkh', ):
+ return self.types.InputScriptType.SPENDADDRESS
+ if electrum_txin_type in ('p2sh', ):
+ return self.types.InputScriptType.SPENDMULTISIG
+ raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
+
+ def get_safet_output_script_type(self, electrum_txin_type: str):
+ if electrum_txin_type in ('p2wpkh', 'p2wsh'):
+ return self.types.OutputScriptType.PAYTOWITNESS
+ if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):
+ return self.types.OutputScriptType.PAYTOP2SHWITNESS
+ if electrum_txin_type in ('p2pkh', ):
+ return self.types.OutputScriptType.PAYTOADDRESS
+ if electrum_txin_type in ('p2sh', ):
+ return self.types.OutputScriptType.PAYTOMULTISIG
+ raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
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())
+ inputs = self.tx_inputs(tx, True)
+ outputs = self.tx_outputs(keystore.get_derivation(), tx)
signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0]
signatures = [(bh2u(x) + '01') for x in signatures]
tx.update_signatures(signatures)
@@ -330,8 +319,7 @@ class SafeTPlugin(HW_PluginBase):
address_n = client.expand_path(address_path)
xpubs = wallet.get_master_public_keys()
if len(xpubs) == 1:
- script_gen = keystore.get_script_gen()
- script_type = self.get_safet_input_script_type(script_gen, is_multisig=False)
+ script_type = self.get_safet_input_script_type(wallet.txin_type)
client.get_address(self.get_coin_name(), address_n, True, script_type=script_type)
else:
def f(xpub):
@@ -345,11 +333,10 @@ class SafeTPlugin(HW_PluginBase):
signatures=[b''] * wallet.n,
m=wallet.m,
)
- script_gen = keystore.get_script_gen()
- script_type = self.get_safet_input_script_type(script_gen, is_multisig=True)
+ script_type = self.get_safet_input_script_type(wallet.txin_type)
client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
- def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY):
+ def tx_inputs(self, tx, for_sig=False):
inputs = []
for txin in tx.inputs():
txinputtype = self.types.TxInputType()
@@ -364,7 +351,7 @@ class SafeTPlugin(HW_PluginBase):
xpub, s = parse_xpubkey(x_pubkey)
xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
txinputtype._extend_address_n(xpub_n + s)
- txinputtype.script_type = self.get_safet_input_script_type(script_gen, is_multisig=False)
+ txinputtype.script_type = self.get_safet_input_script_type(txin['type'])
else:
def f(x_pubkey):
if is_xpubkey(x_pubkey):
@@ -379,7 +366,7 @@ class SafeTPlugin(HW_PluginBase):
signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))),
m=txin.get('num_sig'),
)
- script_type = self.get_safet_input_script_type(script_gen, is_multisig=True)
+ script_type = self.get_safet_input_script_type(txin['type'])
txinputtype = self.types.TxInputType(
script_type=script_type,
multisig=multisig
@@ -411,17 +398,11 @@ class SafeTPlugin(HW_PluginBase):
return inputs
- def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY):
+ def tx_outputs(self, derivation, tx):
- def create_output_by_derivation(info):
- index, xpubs, m = info
+ def create_output_by_derivation():
+ script_type = self.get_trezor_output_script_type(info.script_type)
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,
@@ -429,12 +410,6 @@ class SafeTPlugin(HW_PluginBase):
address_n=address_n,
)
else:
- if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
- script_type = self.types.OutputScriptType.PAYTOWITNESS
- elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
- script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS
- else:
- script_type = self.types.OutputScriptType.PAYTOMULTISIG
address_n = self.client_class.expand_path("/%d/%d" % index)
pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs]
multisig = self.types.MultisigRedeemScriptType(
@@ -469,7 +444,7 @@ class SafeTPlugin(HW_PluginBase):
info = tx.output_info.get(address)
if info is not None and not has_change:
- index, xpubs, m = info
+ index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
on_change_branch = index[0] == 1
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
@@ -480,7 +455,7 @@ class SafeTPlugin(HW_PluginBase):
has_change = True
if use_create_by_derivation:
- txoutputtype = create_output_by_derivation(info)
+ txoutputtype = create_output_by_derivation()
else:
txoutputtype = create_output_by_address()
outputs.append(txoutputtype)
diff --git a/electrum/plugins/safe_t/transport.py b/electrum/plugins/safe_t/transport.py
index 59dc915f..3753434d 100644
--- a/electrum/plugins/safe_t/transport.py
+++ b/electrum/plugins/safe_t/transport.py
@@ -24,11 +24,11 @@ class SafeTTransport(PrintError):
transports.append(HidTransport)
except BaseException:
pass
- try:
- from safetlib.transport_udp import UdpTransport
- transports.append(UdpTransport)
- except BaseException:
- pass
+ #try:
+ # from safetlib.transport_udp import UdpTransport
+ # transports.append(UdpTransport)
+ #except BaseException:
+ # pass
try:
from safetlib.transport_webusb import WebUsbTransport
transports.append(WebUsbTransport)
@@ -47,11 +47,11 @@ class SafeTTransport(PrintError):
transports.append(HidTransport)
except BaseException:
pass
- try:
- from safetlib.transport.udp import UdpTransport
- transports.append(UdpTransport)
- except BaseException:
- pass
+ #try:
+ # from safetlib.transport.udp import UdpTransport
+ # transports.append(UdpTransport)
+ #except BaseException:
+ # pass
try:
from safetlib.transport.webusb import WebUsbTransport
transports.append(WebUsbTransport)
diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py
index b91e4af4..cb6ccc5e 100644
--- a/electrum/plugins/trezor/clientbase.py
+++ b/electrum/plugins/trezor/clientbase.py
@@ -4,7 +4,7 @@ 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
+from electrum.bitcoin import serialize_xpub, convert_bip32_path_to_list_of_uint32
class GuiMixin(object):
@@ -152,21 +152,7 @@ class TrezorClientBase(GuiMixin, PrintError):
@staticmethod
def expand_path(n):
- '''Convert bip32 path to list of uint32 integers with prime flags
- 0/-1/1' -> [0, 0x80000001, 0x80000001]'''
- # This code is similar to code in trezorlib where it unfortunately
- # is not declared as a staticmethod. Our n has an extra element.
- PRIME_DERIVATION_FLAG = 0x80000000
- path = []
- for x in n.split('/')[1:]:
- prime = 0
- if x.endswith("'"):
- x = x.replace('\'', '')
- prime = PRIME_DERIVATION_FLAG
- if x.startswith('-'):
- prime = PRIME_DERIVATION_FLAG
- path.append(abs(int(x)) | prime)
- return path
+ return convert_bip32_path_to_list_of_uint32(n)
def cancel(self):
'''Provided here as in keepkeylib but not trezorlib.'''
diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py
index 53340b94..04632817 100644
--- a/electrum/plugins/trezor/qt.py
+++ b/electrum/plugins/trezor/qt.py
@@ -171,10 +171,10 @@ class QtPlugin(QtPluginBase):
return
for keystore in wallet.get_keystores():
if type(keystore) == self.keystore_class:
- def show_address():
+ def show_address(keystore=keystore):
keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
- menu.addAction(_("Show on {}").format(self.device), show_address)
- break
+ device_name = "{} ({})".format(self.device, keystore.label)
+ menu.addAction(_("Show on {}").format(device_name), show_address)
def show_settings_dialog(self, window, keystore):
device_id = self.choose_device(window, keystore)
diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py
index e9893824..0c479437 100644
--- a/electrum/plugins/trezor/trezor.py
+++ b/electrum/plugins/trezor/trezor.py
@@ -9,7 +9,7 @@ from electrum import constants
from electrum.i18n import _
from electrum.plugin import BasePlugin, Device
from electrum.transaction import deserialize, Transaction
-from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey, xtype_from_derivation
+from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase
@@ -20,9 +20,6 @@ from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validat
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX = range(0, 2)
-# script "generation"
-SCRIPT_GEN_LEGACY, SCRIPT_GEN_P2SH_SEGWIT, SCRIPT_GEN_NATIVE_SEGWIT = range(0, 3)
-
class TrezorKeyStore(Hardware_KeyStore):
hw_type = 'trezor'
@@ -31,15 +28,6 @@ class TrezorKeyStore(Hardware_KeyStore):
def get_derivation(self):
return self.derivation
- def get_script_gen(self):
- xtype = xtype_from_derivation(self.derivation)
- if xtype in ('p2wpkh', 'p2wsh'):
- return SCRIPT_GEN_NATIVE_SEGWIT
- elif xtype in ('p2wpkh-p2sh', 'p2wsh-p2sh'):
- return SCRIPT_GEN_P2SH_SEGWIT
- else:
- return SCRIPT_GEN_LEGACY
-
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
@@ -95,25 +83,8 @@ class TrezorPlugin(HW_PluginBase):
def __init__(self, parent, config, name):
HW_PluginBase.__init__(self, parent, config, name)
- try:
- # Minimal test if python-trezor is installed
- import trezorlib
- try:
- library_version = trezorlib.__version__
- except AttributeError:
- # python-trezor only introduced __version__ in 0.9.0
- library_version = 'unknown'
- if library_version == 'unknown' or \
- versiontuple(library_version) < self.minimum_library:
- self.libraries_available_message = (
- _("Library version for '{}' is too old.").format(name)
- + '\nInstalled: {}, Needed: {}'
- .format(library_version, self.minimum_library))
- self.print_stderr(self.libraries_available_message)
- raise ImportError()
- self.libraries_available = True
- except ImportError:
- self.libraries_available = False
+ self.libraries_available = self.check_libraries_available()
+ if not self.libraries_available:
return
from . import client
@@ -126,6 +97,13 @@ class TrezorPlugin(HW_PluginBase):
self.transport_handler = transport.TrezorTransport()
self.device_manager().register_enumerate_func(self.enumerate)
+ def get_library_version(self):
+ import trezorlib
+ try:
+ return trezorlib.__version__
+ except AttributeError:
+ return 'unknown'
+
def enumerate(self):
devices = self.transport_handler.enumerate_devices()
return [Device(d.get_path(), -1, d.get_path(), 'TREZOR', 0) for d in devices]
@@ -305,23 +283,34 @@ class TrezorPlugin(HW_PluginBase):
client.used()
return xpub
- def get_trezor_input_script_type(self, script_gen, is_multisig):
- if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
+ def get_trezor_input_script_type(self, electrum_txin_type: str):
+ if electrum_txin_type in ('p2wpkh', 'p2wsh'):
return self.types.InputScriptType.SPENDWITNESS
- elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
+ if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):
return self.types.InputScriptType.SPENDP2SHWITNESS
- else:
- if is_multisig:
- return self.types.InputScriptType.SPENDMULTISIG
- else:
- return self.types.InputScriptType.SPENDADDRESS
+ if electrum_txin_type in ('p2pkh', ):
+ return self.types.InputScriptType.SPENDADDRESS
+ if electrum_txin_type in ('p2sh', ):
+ return self.types.InputScriptType.SPENDMULTISIG
+ raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
+
+ def get_trezor_output_script_type(self, electrum_txin_type: str):
+ if electrum_txin_type in ('p2wpkh', 'p2wsh'):
+ return self.types.OutputScriptType.PAYTOWITNESS
+ if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):
+ return self.types.OutputScriptType.PAYTOP2SHWITNESS
+ if electrum_txin_type in ('p2pkh', ):
+ return self.types.OutputScriptType.PAYTOADDRESS
+ if electrum_txin_type in ('p2sh', ):
+ return self.types.OutputScriptType.PAYTOMULTISIG
+ raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
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())
+ inputs = self.tx_inputs(tx, True)
+ outputs = self.tx_outputs(keystore.get_derivation(), tx)
signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0]
signatures = [(bh2u(x) + '01') for x in signatures]
tx.update_signatures(signatures)
@@ -341,8 +330,7 @@ class TrezorPlugin(HW_PluginBase):
address_n = client.expand_path(address_path)
xpubs = wallet.get_master_public_keys()
if len(xpubs) == 1:
- script_gen = keystore.get_script_gen()
- script_type = self.get_trezor_input_script_type(script_gen, is_multisig=False)
+ script_type = self.get_trezor_input_script_type(wallet.txin_type)
client.get_address(self.get_coin_name(), address_n, True, script_type=script_type)
else:
def f(xpub):
@@ -356,11 +344,10 @@ class TrezorPlugin(HW_PluginBase):
signatures=[b''] * wallet.n,
m=wallet.m,
)
- script_gen = keystore.get_script_gen()
- script_type = self.get_trezor_input_script_type(script_gen, is_multisig=True)
+ script_type = self.get_trezor_input_script_type(wallet.txin_type)
client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
- def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY):
+ def tx_inputs(self, tx, for_sig=False):
inputs = []
for txin in tx.inputs():
txinputtype = self.types.TxInputType()
@@ -375,7 +362,7 @@ class TrezorPlugin(HW_PluginBase):
xpub, s = parse_xpubkey(x_pubkey)
xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
txinputtype._extend_address_n(xpub_n + s)
- txinputtype.script_type = self.get_trezor_input_script_type(script_gen, is_multisig=False)
+ txinputtype.script_type = self.get_trezor_input_script_type(txin['type'])
else:
def f(x_pubkey):
if is_xpubkey(x_pubkey):
@@ -390,7 +377,7 @@ class TrezorPlugin(HW_PluginBase):
signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))),
m=txin.get('num_sig'),
)
- script_type = self.get_trezor_input_script_type(script_gen, is_multisig=True)
+ script_type = self.get_trezor_input_script_type(txin['type'])
txinputtype = self.types.TxInputType(
script_type=script_type,
multisig=multisig
@@ -422,17 +409,11 @@ class TrezorPlugin(HW_PluginBase):
return inputs
- def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY):
+ def tx_outputs(self, derivation, tx):
- def create_output_by_derivation(info):
- index, xpubs, m = info
+ def create_output_by_derivation():
+ script_type = self.get_trezor_output_script_type(info.script_type)
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,
@@ -440,12 +421,6 @@ class TrezorPlugin(HW_PluginBase):
address_n=address_n,
)
else:
- if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
- script_type = self.types.OutputScriptType.PAYTOWITNESS
- elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
- script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS
- else:
- script_type = self.types.OutputScriptType.PAYTOMULTISIG
address_n = self.client_class.expand_path("/%d/%d" % index)
pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs]
multisig = self.types.MultisigRedeemScriptType(
@@ -480,7 +455,7 @@ class TrezorPlugin(HW_PluginBase):
info = tx.output_info.get(address)
if info is not None and not has_change:
- index, xpubs, m = info
+ index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
on_change_branch = index[0] == 1
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
@@ -491,7 +466,7 @@ class TrezorPlugin(HW_PluginBase):
has_change = True
if use_create_by_derivation:
- txoutputtype = create_output_by_derivation(info)
+ txoutputtype = create_output_by_derivation()
else:
txoutputtype = create_output_by_address()
outputs.append(txoutputtype)
diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py
index ccd24e33..8227a3a8 100644
--- a/electrum/plugins/trustedcoin/trustedcoin.py
+++ b/electrum/plugins/trustedcoin/trustedcoin.py
@@ -22,10 +22,9 @@
# 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 asyncio
import socket
import os
-import requests
import json
import base64
from urllib.parse import urljoin
@@ -38,8 +37,9 @@ from electrum.mnemonic import Mnemonic
from electrum.wallet import Multisig_Wallet, Deterministic_Wallet
from electrum.i18n import _
from electrum.plugin import BasePlugin, hook
-from electrum.util import NotEnoughFunds
+from electrum.util import NotEnoughFunds, make_aiohttp_session
from electrum.storage import STO_EV_USER_PW
+from electrum.network import Network
# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server
def get_signing_xpub():
@@ -104,34 +104,47 @@ class TrustedCoinCosignerClient(object):
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'
+ network = Network.get_instance()
+ if network:
+ return asyncio.run_coroutine_threadsafe(self._send_request(method, relative_url, data), network.asyncio_loop).result()
+ else:
+ raise ErrorConnectingServer('You are offline.')
+
+ async def handle_response(self, resp):
+ if resp.status != 200:
+ try:
+ r = await resp.json()
+ message = r['message']
+ except:
+ message = await resp.text()
+ raise TrustedCoinException(message, resp.status)
+ try:
+ return await resp.json()
+ except:
+ return await resp.text()
+
+ async def _send_request(self, method, relative_url, data):
url = urljoin(self.base_url, relative_url)
if self.debug:
print('%s %s %s' % (method, url, data))
+ headers = {}
+ if self.user_agent:
+ headers['user-agent'] = self.user_agent
try:
- response = requests.request(method, url, **kwargs)
+ proxy = Network.get_instance().proxy
+ async with make_aiohttp_session(proxy) as session:
+ if method == 'get':
+ async with session.get(url, params=data, headers=headers) as resp:
+ return await self.handle_response(resp)
+ elif method == 'post':
+ async with session.post(url, json=data, headers=headers) as resp:
+ return await self.handle_response(resp)
+ else:
+ assert False
+ except TrustedCoinException:
+ raise
except Exception as e:
raise ErrorConnectingServer(e)
- if self.debug:
- print(response.text)
- if response.status_code != 200:
- message = str(response.text)
- if response.headers.get('content-type') == 'application/json':
- r = response.json()
- if 'message' in r:
- message = r['message']
- raise TrustedCoinException(message, response.status_code)
- if response.headers.get('content-type') == 'application/json':
- return response.json()
- else:
- return response.text
def get_terms_of_service(self, billing_plan='electrum-per-tx-otp'):
"""
diff --git a/electrum/qrscanner.py b/electrum/qrscanner.py
index 56e0a18a..463d5ec6 100644
--- a/electrum/qrscanner.py
+++ b/electrum/qrscanner.py
@@ -74,10 +74,11 @@ def _find_system_cameras():
devices = {} # Name -> device
if os.path.exists(device_root):
for device in os.listdir(device_root):
+ path = os.path.join(device_root, device, 'name')
try:
- with open(os.path.join(device_root, device, 'name')) as f:
+ with open(path, encoding='utf-8') as f:
name = f.read()
- except IOError:
+ except Exception:
continue
name = name.strip('\n')
devices[name] = os.path.join("/dev", device)
diff --git a/electrum/simple_config.py b/electrum/simple_config.py
index 8d7846be..91fc20c0 100644
--- a/electrum/simple_config.py
+++ b/electrum/simple_config.py
@@ -241,7 +241,7 @@ class SimpleConfig(PrintError):
# command line -w option
if self.get('wallet_path'):
- return os.path.join(self.get('cwd'), 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')
diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py
index 48635df8..a74961a3 100644
--- a/electrum/synchronizer.py
+++ b/electrum/synchronizer.py
@@ -22,102 +22,65 @@
# 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 asyncio
import hashlib
-# from .bitcoin import Hash, hash_encode
+from aiorpcx import TaskGroup
+
from .transaction import Transaction
-from .util import ThreadJob, bh2u
+from .util import bh2u, PrintError
+from .bitcoin import address_to_scripthash
-class Synchronizer(ThreadJob):
+def history_status(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())
+
+
+class Synchronizer(PrintError):
'''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):
+ def __init__(self, wallet):
self.wallet = wallet
- self.network = network
- self.new_addresses = set()
- # Entries are (tx_hash, tx_height) tuples
self.requested_tx = {}
self.requested_histories = {}
self.requested_addrs = set()
- self.lock = Lock()
-
- self.initialized = False
- self.initialize()
-
- def parse_response(self, response):
- if response.get('error'):
- self.print_error("response error:", response)
- return None, None
- return response['params'], response['result']
+ self.scripthash_to_address = {}
+ # Queues
+ self.add_queue = asyncio.Queue()
+ self.status_queue = asyncio.Queue()
def is_up_to_date(self):
- return (not self.requested_tx and not self.requested_histories
- and not self.requested_addrs)
+ return (not self.requested_addrs
+ and not self.requested_histories
+ and not self.requested_tx)
- def release(self):
- self.network.unsubscribe(self.on_address_status)
+ def add(self, addr):
+ self.requested_addrs.add(addr)
+ self.add_queue.put_nowait(addr)
- def add(self, address):
- '''This can be called from the proxy or GUI threads.'''
- with self.lock:
- self.new_addresses.add(address)
-
- def subscribe_to_addresses(self, addresses):
- if addresses:
- self.requested_addrs |= addresses
- self.network.subscribe_to_addresses(addresses, self.on_address_status)
-
- def get_status(self, h):
- if not h:
- return None
- status = ''
- for tx_hash, height in h:
- status += tx_hash + ':%d:' % height
- return bh2u(hashlib.sha256(status.encode('ascii')).digest())
-
- def on_address_status(self, response):
- if self.wallet.synchronizer is None and self.initialized:
- return # we have been killed, this was just an orphan callback
- params, result = self.parse_response(response)
- if not params:
- return
- addr = params[0]
+ async def on_address_status(self, addr, status):
history = self.wallet.history.get(addr, [])
- if self.get_status(history) != result:
- # note that at this point 'result' can be None;
- # if we had a history for addr but now the server is telling us
- # there is no history
- if addr not in self.requested_histories:
- self.requested_histories[addr] = result
- self.network.request_address_history(addr, self.on_address_history)
- # remove addr from list only after it is added to requested_histories
- if addr in self.requested_addrs: # Notifications won't be in
- self.requested_addrs.remove(addr)
-
- def on_address_history(self, response):
- if self.wallet.synchronizer is None and self.initialized:
- return # we have been killed, this was just an orphan callback
- params, result = self.parse_response(response)
- if not params:
+ if history_status(history) == status:
return
- addr = params[0]
- try:
- server_status = self.requested_histories[addr]
- except KeyError:
- # note: server_status can be None even if we asked for the history,
- # so it is not sufficient to test that
- self.print_error("receiving history (unsolicited)", addr, len(result))
+ # note that at this point 'result' can be None;
+ # if we had a history for addr but now the server is telling us
+ # there is no history
+ if addr in self.requested_histories:
return
+ # request address history
+ self.requested_histories[addr] = status
+ h = address_to_scripthash(addr)
+ result = await self.session.send_request("blockchain.scripthash.get_history", [h])
self.print_error("receiving history", addr, len(result))
hashes = set(map(lambda item: item['tx_hash'], result))
hist = list(map(lambda item: (item['tx_hash'], item['height']), result))
@@ -128,23 +91,36 @@ class Synchronizer(ThreadJob):
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:
+ elif history_status(hist) != 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)
+ await self.request_missing_txs(hist)
+
# Remove request; this allows up_to_date to be True
self.requested_histories.pop(addr)
- def on_tx_response(self, response):
- if self.wallet.synchronizer is None and self.initialized:
- return # we have been killed, this was just an orphan callback
- params, result = self.parse_response(response)
- if not params:
- return
- tx_hash = params[0]
+ if self.wallet.network: self.wallet.network.notify('updated')
+
+ async def request_missing_txs(self, hist):
+ # "hist" is a list of [tx_hash, tx_height] lists
+ transaction_hashes = []
+ for tx_hash, tx_height in hist:
+ if tx_hash in self.requested_tx:
+ continue
+ if tx_hash in self.wallet.transactions:
+ continue
+ transaction_hashes.append(tx_hash)
+ self.requested_tx[tx_hash] = tx_height
+
+ async with TaskGroup() as group:
+ for tx_hash in transaction_hashes:
+ await group.spawn(self.get_transaction, tx_hash)
+
+ async def get_transaction(self, tx_hash):
+ result = await self.session.send_request('blockchain.transaction.get', [tx_hash])
tx = Transaction(result)
try:
tx.deserialize()
@@ -160,54 +136,47 @@ class Synchronizer(ThreadJob):
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')
+ self.wallet.network.trigger_callback('new_transaction', tx)
- def request_missing_txs(self, hist):
- # "hist" is a list of [tx_hash, tx_height] lists
- transaction_hashes = []
- for tx_hash, tx_height in hist:
- if tx_hash in self.requested_tx:
- continue
- if tx_hash in self.wallet.transactions:
- continue
- transaction_hashes.append(tx_hash)
- self.requested_tx[tx_hash] = tx_height
+ async def subscribe_to_address(self, addr):
+ h = address_to_scripthash(addr)
+ self.scripthash_to_address[h] = addr
+ await self.session.subscribe('blockchain.scripthash.subscribe', [h], self.status_queue)
+ self.requested_addrs.remove(addr)
- self.network.get_transactions(transaction_hashes, self.on_tx_response)
+ async def send_subscriptions(self, group: TaskGroup):
+ while True:
+ addr = await self.add_queue.get()
+ await group.spawn(self.subscribe_to_address, addr)
- 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)
+ async def handle_status(self, group: TaskGroup):
+ while True:
+ h, status = await self.status_queue.get()
+ addr = self.scripthash_to_address[h]
+ await group.spawn(self.on_address_status, addr, status)
- if self.requested_tx:
- self.print_error("missing tx", self.requested_tx)
- self.subscribe_to_addresses(set(self.wallet.get_addresses()))
- self.initialized = True
+ @property
+ def session(self):
+ s = self.wallet.network.interface.session
+ assert s is not None
+ return s
- 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')
+ async def main(self):
+ # request missing txns, if any
+ async with TaskGroup() as group:
+ 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
+ await group.spawn(self.request_missing_txs, history)
+ # add addresses to bootstrap
+ for addr in self.wallet.get_addresses():
+ self.add(addr)
+ # main loop
+ while True:
+ await asyncio.sleep(0.1)
+ self.wallet.synchronize()
+ 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.wallet.network.trigger_callback('updated')
diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py
index 8c01f774..f14fb63c 100644
--- a/electrum/tests/test_bitcoin.py
+++ b/electrum/tests/test_bitcoin.py
@@ -1,5 +1,4 @@
import base64
-import unittest
import sys
from electrum.bitcoin import (
@@ -11,7 +10,7 @@ from electrum.bitcoin import (
deserialize_privkey, serialize_privkey, is_segwit_address,
is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub,
xpub_type, is_xprv, is_bip32_derivation, seed_type, EncodeBase58Check,
- script_num_to_hex, push_script, add_number_to_script, int_to_hex)
+ script_num_to_hex, push_script, add_number_to_script, int_to_hex, convert_bip32_path_to_list_of_uint32)
from electrum import ecc, crypto, constants
from electrum.ecc import number_to_string, string_to_number
from electrum.transaction import opcodes
@@ -48,10 +47,10 @@ def needs_test_with_all_ecc_implementations(func):
# first test without libsecp
func(*args, **kwargs)
finally:
- # if libsecp is not available, we are done
- if not ecc_fast._libsecp256k1:
- return
ecc_fast.do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1()
+ # if libsecp is not available, we are done
+ if not ecc_fast._libsecp256k1:
+ return
# if libsecp is available, test again now
func(*args, **kwargs)
return run_test
@@ -74,10 +73,10 @@ def needs_test_with_all_aes_implementations(func):
# first test without pycryptodomex
func(*args, **kwargs)
finally:
- # if pycryptodomex is not available, we are done
- if not _aes:
- return
crypto.AES = _aes
+ # if pycryptodomex is not available, we are done
+ if not _aes:
+ return
# if pycryptodomex is available, test again now
func(*args, **kwargs)
return run_test
@@ -470,6 +469,11 @@ class Test_xprv_xpub(SequentialTestCase):
self.assertFalse(is_bip32_derivation(""))
self.assertFalse(is_bip32_derivation("m/q8462"))
+ def test_convert_bip32_path_to_list_of_uint32(self):
+ self.assertEqual([0, 0x80000001, 0x80000001], convert_bip32_path_to_list_of_uint32("m/0/-1/1'"))
+ self.assertEqual([], convert_bip32_path_to_list_of_uint32("m/"))
+ self.assertEqual([2147483692, 2147488889, 221], convert_bip32_path_to_list_of_uint32("m/44'/5241'/221"))
+
def test_xtype_from_derivation(self):
self.assertEqual('standard', xtype_from_derivation("m/44'"))
self.assertEqual('standard', xtype_from_derivation("m/44'/"))
diff --git a/electrum/tests/test_interface.py b/electrum/tests/test_interface.py
deleted file mode 100644
index 402588ca..00000000
--- a/electrum/tests/test_interface.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import unittest
-
-from electrum import interface
-
-from . import SequentialTestCase
-
-
-class TestInterface(SequentialTestCase):
-
- def test_match_host_name(self):
- self.assertTrue(interface._match_hostname('asd.fgh.com', 'asd.fgh.com'))
- self.assertFalse(interface._match_hostname('asd.fgh.com', 'asd.zxc.com'))
- self.assertTrue(interface._match_hostname('asd.fgh.com', '*.fgh.com'))
- self.assertFalse(interface._match_hostname('asd.fgh.com', '*fgh.com'))
- self.assertFalse(interface._match_hostname('asd.fgh.com', '*.zxc.com'))
-
- def test_check_host_name(self):
- i = interface.TcpConnection(server=':1:', queue=None, config_path=None)
-
- self.assertFalse(i.check_host_name(None, None))
- self.assertFalse(i.check_host_name(
- peercert={'subjectAltName': []}, name=''))
- self.assertTrue(i.check_host_name(
- peercert={'subjectAltName': [('DNS', 'foo.bar.com')]},
- name='foo.bar.com'))
- self.assertTrue(i.check_host_name(
- peercert={'subject': [('commonName', 'foo.bar.com')]},
- name='foo.bar.com'))
diff --git a/electrum/tests/test_mnemonic.py b/electrum/tests/test_mnemonic.py
index 191ce902..1f5f18f1 100644
--- a/electrum/tests/test_mnemonic.py
+++ b/electrum/tests/test_mnemonic.py
@@ -1,19 +1,68 @@
-import unittest
from electrum import keystore
from electrum import mnemonic
from electrum import old_mnemonic
-from electrum.util import bh2u
+from electrum.util import bh2u, bfh
+from electrum.bitcoin import is_new_seed
+from electrum.version import SEED_PREFIX_SW
from . import SequentialTestCase
+from .test_wallet_vertical import UNICODE_HORROR
+
+
+SEED_WORDS_JAPANESE = 'なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん'
+assert bh2u(SEED_WORDS_JAPANESE.encode('utf8')) == 'e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e3819920e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818be38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195e3819fe38293'
+
+SEED_WORDS_CHINESE = '眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬'
+assert bh2u(SEED_WORDS_CHINESE.encode('utf8')) == 'e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e7968620e882a120e9818220e586ac'
+
+PASSPHRASE_CHINESE = '给我一些测试向量谷歌'
+assert bh2u(PASSPHRASE_CHINESE.encode('utf8')) == 'e7bb99e68891e4b880e4ba9be6b58be8af95e59091e9878fe8b0b7e6ad8c'
class Test_NewMnemonic(SequentialTestCase):
- def test_to_seed(self):
+ def test_mnemonic_to_seed_basic(self):
seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic='foobar', passphrase='none')
- self.assertEqual(bh2u(seed),
- '741b72fd15effece6bfe5a26a52184f66811bd2be363190e07a42cca442b1a5b'
- 'b22b3ad0eb338197287e6d314866c7fba863ac65d3f156087a5052ebc7157fce')
+ self.assertEqual('741b72fd15effece6bfe5a26a52184f66811bd2be363190e07a42cca442b1a5bb22b3ad0eb338197287e6d314866c7fba863ac65d3f156087a5052ebc7157fce',
+ bh2u(seed))
+
+ def test_mnemonic_to_seed_japanese(self):
+ words = SEED_WORDS_JAPANESE
+ self.assertTrue(is_new_seed(words))
+
+ m = mnemonic.Mnemonic(lang='ja')
+ self.assertEqual(1938439226660562861250521787963972783469, m.mnemonic_decode(words))
+
+ seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=words, passphrase='')
+ self.assertEqual('d3eaf0e44ddae3a5769cb08a26918e8b308258bcb057bb704c6f69713245c0b35cb92c03df9c9ece5eff826091b4e74041e010b701d44d610976ce8bfb66a8ad',
+ bh2u(seed))
+
+ def test_mnemonic_to_seed_japanese_with_unicode_horror(self):
+ words = SEED_WORDS_JAPANESE
+ self.assertTrue(is_new_seed(words))
+
+ seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=words, passphrase=UNICODE_HORROR)
+ self.assertEqual('251ee6b45b38ba0849e8f40794540f7e2c6d9d604c31d68d3ac50c034f8b64e4bc037c5e1e985a2fed8aad23560e690b03b120daf2e84dceb1d7857dda042457',
+ bh2u(seed))
+
+ def test_mnemonic_to_seed_chinese(self):
+ words = SEED_WORDS_CHINESE
+ self.assertTrue(is_new_seed(words, prefix=SEED_PREFIX_SW))
+
+ m = mnemonic.Mnemonic(lang='zh')
+ self.assertEqual(3083737086352778425940060465574397809099, m.mnemonic_decode(words))
+
+ seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=words, passphrase='')
+ self.assertEqual('0b9077db7b5a50dbb6f61821e2d35e255068a5847e221138048a20e12d80b673ce306b6fe7ac174ebc6751e11b7037be6ee9f17db8040bb44f8466d519ce2abf',
+ bh2u(seed))
+
+ def test_mnemonic_to_seed_chinese_with_passphrase(self):
+ words = SEED_WORDS_CHINESE
+ passphrase = PASSPHRASE_CHINESE
+ self.assertTrue(is_new_seed(words, prefix=SEED_PREFIX_SW))
+ seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=words, passphrase=passphrase)
+ self.assertEqual('6c03dd0615cf59963620c0af6840b52e867468cc64f20a1f4c8155705738e87b8edb0fc8a6cee4085776cb3a629ff88bb1a38f37085efdbf11ce9ec5a7fa5f71',
+ bh2u(seed))
def test_random_seeds(self):
iters = 10
diff --git a/electrum/tests/test_network.py b/electrum/tests/test_network.py
new file mode 100644
index 00000000..b28dff77
--- /dev/null
+++ b/electrum/tests/test_network.py
@@ -0,0 +1,119 @@
+import asyncio
+import tempfile
+import unittest
+
+from electrum import constants
+from electrum.simple_config import SimpleConfig
+from electrum import blockchain
+from electrum.interface import Interface
+
+class MockInterface(Interface):
+ def __init__(self, config):
+ self.config = config
+ super().__init__(None, 'mock-server:50000:t', self.config.electrum_path(), None)
+ self.q = asyncio.Queue()
+ self.blockchain = blockchain.Blockchain(self.config, 2002, None)
+ self.tip = 12
+ async def get_block_header(self, height, assert_mode):
+ assert self.q.qsize() > 0, (height, assert_mode)
+ item = await self.q.get()
+ print("step with height", height, item)
+ assert item['block_height'] == height, (item['block_height'], height)
+ assert assert_mode in item['mock'], (assert_mode, item)
+ return item
+
+class TestNetwork(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ constants.set_regtest()
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+ constants.set_mainnet()
+
+ def setUp(self):
+ self.config = SimpleConfig({'electrum_path': tempfile.mkdtemp(prefix="test_network")})
+ self.interface = MockInterface(self.config)
+
+ def test_new_fork(self):
+ blockchain.blockchains = {}
+ self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
+ def mock_connect(height):
+ return height == 6
+ self.interface.q.put_nowait({'block_height': 7, 'mock': {'backward':1,'check': lambda x: False, 'connect': mock_connect, 'fork': self.mock_fork}})
+ self.interface.q.put_nowait({'block_height': 2, 'mock': {'backward':1,'check':lambda x: True, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 4, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}})
+ self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}})
+ self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}})
+ ifa = self.interface
+ self.assertEqual(('fork', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=8)))
+ self.assertEqual(self.interface.q.qsize(), 0)
+
+ def test_new_can_connect_during_backward(self):
+ blockchain.blockchains = {}
+ self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
+ def mock_connect(height):
+ return height == 2
+ self.interface.q.put_nowait({'block_height': 7, 'mock': {'backward':1, 'check': lambda x: False, 'connect': mock_connect, 'fork': self.mock_fork}})
+ self.interface.q.put_nowait({'block_height': 2, 'mock': {'backward':1, 'check': lambda x: False, 'connect': mock_connect, 'fork': self.mock_fork}})
+ self.interface.q.put_nowait({'block_height': 3, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: True}})
+ self.interface.q.put_nowait({'block_height': 4, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: True}})
+ ifa = self.interface
+ self.assertEqual(('catchup', 5), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=5)))
+ self.assertEqual(self.interface.q.qsize(), 0)
+
+ def mock_fork(self, bad_header):
+ return blockchain.Blockchain(self.config, bad_header['block_height'], None)
+
+ def test_new_chain_false_during_binary(self):
+ blockchain.blockchains = {}
+ self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
+ mock_connect = lambda height: height == 3
+ self.interface.q.put_nowait({'block_height': 7, 'mock': {'backward':1, 'check': lambda x: False, 'connect': mock_connect}})
+ self.interface.q.put_nowait({'block_height': 2, 'mock': {'backward':1, 'check': lambda x: True, 'connect': mock_connect}})
+ self.interface.q.put_nowait({'block_height': 4, 'mock': {'binary':1, 'check': lambda x: False, 'fork': self.mock_fork, 'connect': mock_connect}})
+ self.interface.q.put_nowait({'block_height': 3, 'mock': {'binary':1, 'check': lambda x: True, 'connect': lambda x: True}})
+ self.interface.q.put_nowait({'block_height': 5, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: True}})
+ self.interface.q.put_nowait({'block_height': 6, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: True}})
+ ifa = self.interface
+ self.assertEqual(('catchup', 7), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7)))
+ self.assertEqual(self.interface.q.qsize(), 0)
+
+ def test_new_join(self):
+ blockchain.blockchains = {7: {'check': lambda bad_header: True}}
+ self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 7, 'mock': {'backward':1, 'check': lambda x: False, 'connect': lambda height: height == 6}})
+ self.interface.q.put_nowait({'block_height': 2, 'mock': {'backward':1, 'check': lambda x: True, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 4, 'mock': {'binary':1, 'check': lambda x: True, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1, 'check': lambda x: True, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1, 'check': lambda x: True, 'connect': lambda x: True}})
+ ifa = self.interface
+ self.assertEqual(('join', 7), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7)))
+ self.assertEqual(self.interface.q.qsize(), 0)
+
+ def test_new_reorg(self):
+ times = 0
+ def check(header):
+ nonlocal times
+ self.assertEqual(header['block_height'], 7)
+ times += 1
+ return times != 1
+ blockchain.blockchains = {7: {'check': check, 'parent': {'check': lambda x: True}}}
+ self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 7, 'mock': {'backward':1, 'check': lambda x: False, 'connect': lambda height: height == 6}})
+ self.interface.q.put_nowait({'block_height': 2, 'mock': {'backward':1, 'check': lambda x: 1, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 4, 'mock': {'binary':1, 'check': lambda x: 1, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1, 'check': lambda x: 1, 'connect': lambda x: False}})
+ self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1, 'check': lambda x: 1, 'connect': lambda x: True}})
+ self.interface.q.put_nowait({'block_height': 7, 'mock': {'binary':1, 'check': lambda x: False, 'connect': lambda x: True}})
+ ifa = self.interface
+ self.assertEqual(('join', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=8)))
+ self.assertEqual(self.interface.q.qsize(), 0)
+ self.assertEqual(times, 2)
+
+if __name__=="__main__":
+ constants.set_regtest()
+ unittest.main()
diff --git a/electrum/tests/test_simple_config.py b/electrum/tests/test_simple_config.py
index 6a3dbd02..4ec2a7e4 100644
--- a/electrum/tests/test_simple_config.py
+++ b/electrum/tests/test_simple_config.py
@@ -1,7 +1,6 @@
import ast
import sys
import os
-import unittest
import tempfile
import shutil
diff --git a/electrum/tests/test_storage_upgrade.py b/electrum/tests/test_storage_upgrade.py
index 8117176f..8d24e279 100644
--- a/electrum/tests/test_storage_upgrade.py
+++ b/electrum/tests/test_storage_upgrade.py
@@ -6,8 +6,6 @@ from electrum.wallet import Wallet
from .test_wallet import WalletTestCase
-from . import SequentialTestCase
-
# TODO add other wallet types: 2fa, xpub-only
# TODO hw wallet with client version 2.6.x (single-, and multiacc)
diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py
index 4e43c3aa..c1212945 100644
--- a/electrum/tests/test_transaction.py
+++ b/electrum/tests/test_transaction.py
@@ -1,5 +1,3 @@
-import unittest
-
from electrum import transaction
from electrum.bitcoin import TYPE_ADDRESS
from electrum.keystore import xpubkey_to_address
@@ -307,6 +305,12 @@ class TestTransaction(SequentialTestCase):
txid = '9b9f39e314662a7433aadaa5c94a2f1e24c7e7bf55fc9e1f83abd72be933eb95'
self._run_naive_tests_on_tx(raw_tx, txid)
+ # see https://bitcoin.stackexchange.com/questions/38006/txout-script-criteria-scriptpubkey-critieria
+ def test_txid_invalid_op_return(self):
+ raw_tx = '01000000019ac03d5ae6a875d970128ef9086cef276a1919684a6988023cc7254691d97e6d010000006b4830450221009d41dc793ba24e65f571473d40b299b6459087cea1509f0d381740b1ac863cb6022039c425906fcaf51b2b84d8092569fb3213de43abaff2180e2a799d4fcb4dd0aa012102d5ede09a8ae667d0f855ef90325e27f6ce35bbe60a1e6e87af7f5b3c652140fdffffffff080100000000000000010101000000000000000202010100000000000000014c0100000000000000034c02010100000000000000014d0100000000000000044dffff010100000000000000014e0100000000000000064effffffff0100000000'
+ txid = 'ebc9fa1196a59e192352d76c0f6e73167046b9d37b8302b6bb6968dfd279b767'
+ self._run_naive_tests_on_tx(raw_tx, txid)
+
# these transactions are from Bitcoin Core unit tests --->
# https://github.com/bitcoin/bitcoin/blob/11376b5583a283772c82f6d32d0007cdbf5b8ef0/src/test/data/tx_valid.json
diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py
index 1ffe1945..a88bd8c3 100644
--- a/electrum/tests/test_util.py
+++ b/electrum/tests/test_util.py
@@ -1,4 +1,3 @@
-import unittest
from electrum.util import format_satoshis, parse_URI
from . import SequentialTestCase
diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py
index a1997024..9117392e 100644
--- a/electrum/tests/test_wallet.py
+++ b/electrum/tests/test_wallet.py
@@ -1,7 +1,6 @@
import shutil
import tempfile
import sys
-import unittest
import os
import json
diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py
index 1a38c922..5162a78f 100644
--- a/electrum/tests/test_wallet_vertical.py
+++ b/electrum/tests/test_wallet_vertical.py
@@ -1,10 +1,9 @@
-import unittest
from unittest import mock
import shutil
import tempfile
from typing import Sequence
-from electrum import storage, bitcoin, keystore, constants
+from electrum import storage, bitcoin, keystore
from electrum import Transaction
from electrum import SimpleConfig
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
@@ -21,7 +20,7 @@ from .test_bitcoin import needs_test_with_all_ecc_implementations
_UNICODE_HORROR_HEX = 'e282bf20f09f988020f09f98882020202020e3818620e38191e3819fe381be20e3828fe3828b2077cda2cda2cd9d68cda16fcda2cda120ccb8cda26bccb5cd9f6eccb4cd98c7ab77ccb8cc9b73cd9820cc80cc8177cd98cda2e1b8a9ccb561d289cca1cda27420cca7cc9568cc816fccb572cd8fccb5726f7273cca120ccb6cda1cda06cc4afccb665cd9fcd9f20ccb6cd9d696ecda220cd8f74cc9568ccb7cca1cd9f6520cd9fcd9f64cc9b61cd9c72cc95cda16bcca2cca820cda168ccb465cd8f61ccb7cca2cca17274cc81cd8f20ccb4ccb7cda0c3b2ccb5ccb666ccb82075cca7cd986ec3adcc9bcd9c63cda2cd8f6fccb7cd8f64ccb8cda265cca1cd9d3fcd9e'
UNICODE_HORROR = bfh(_UNICODE_HORROR_HEX).decode('utf-8')
-# '₿ 😀 😈 う けたま わる w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏o̷͏d̸͢e̡͝?͞'
+assert UNICODE_HORROR == '₿ 😀 😈 う けたま わる w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏o̷͏d̸͢e̡͝?͞'
class WalletIntegrityHelper:
diff --git a/electrum/tests/test_x509.py b/electrum/tests/test_x509.py
new file mode 100644
index 00000000..75d11a8c
--- /dev/null
+++ b/electrum/tests/test_x509.py
@@ -0,0 +1,6 @@
+import unittest
+from electrum.x509 import X509
+class TestX509(unittest.TestCase):
+ def test_generalizedtime(self):
+ full = X509(b'0\x82\x05F0\x82\x03.\x02\t\x00\xfeV\xd6\xb5?\xb1j\xe40\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x000d1\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x130\x11\x06\x03U\x04\x08\x0c\nCalifornia1!0\x1f\x06\x03U\x04\n\x0c\x18Internet Widgits Pty Ltd1\x1d0\x1b\x06\x03U\x04\x03\x0c\x14testnet.qtornado.com0 \x17\r180206010225Z\x18\x0f21180113010225Z0d1\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x130\x11\x06\x03U\x04\x08\x0c\nCalifornia1!0\x1f\x06\x03U\x04\n\x0c\x18Internet Widgits Pty Ltd1\x1d0\x1b\x06\x03U\x04\x03\x0c\x14testnet.qtornado.com0\x82\x02"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x02\x0f\x000\x82\x02\n\x02\x82\x02\x01\x00\xc2B\xe0\xa8\xd9$M\xbc)Wx\x0cv\x00\xc0\xfa2Ew:\xce\xa7\xcb\xc8\r?\xea\xc5R(\xc7\xc3Y\xe7zq=\xcd\x8d\xe3\x86\x9ecSI\xc7\x84\xf2~\x91\xd4\x19\xc2;\x97\xe81e\xf2\xeb\xf1\xadw\xa3p\x88A*-\r\xb6Yt\x98R\xe8\x8a\xf9\xb5>"F\xac\x19%\xc8~\x1d\xac\x93A\xffk\xce\xdb\xfc9\x05\xa0\xad\xf9V\x0f0\xa2b\xd0@\xe4\xf1\xb1\xe8\xb1\x10[&\xa1\xff\x13\xcfQ\xb7\x805\xef\xe7tL\xe5|\x08W\x8c\xd72\x9d\'\xeb\x92)3N\x01M\x06\xa9\xdc\xe4\'\x13\x90x\xd8\x830\x97\xa8\xcc2d \xfa\x91\x04\xd0\x1b\xe7\xaa t\x87\xba]\xb5w\x05(\xba\x07\xc2X$~?L\xc5\x03\xb2\xdeQ\xf3\xf3\xdab\xd9\x92\xd9\x86^:\x93\xc9\x86~\xd1\x94\xd4\x80\x9c\xff0\xc6m\xf4\xf0\xd6\x18\x96l\x1d\x0c\xe8\x15 \x8c\x89\xcb\xa4*\xd9\xefg\x844\x81\xb3\xce\xa1\x8a|\xf9h\xc3\xe1!\xfeZ`\xb71\x97Kj\x0b"\xd3\x98T\r\xd9\xbb\xc6\x0b\x81\x81k\x0e\xd01\x16\x91\xe4A\x8c\x1a\xe9W\xd4=<\xd4m_\xd4m\xa4H\x14\xc0\xae\x12\xab\x808\xf1\xf9_\xbb\xfb\xd0U\x0e\\\xd3.?\xa36\xe1hstU"\x17P\xcb>\x83\x9c\xaa\x9b\xb7\xe5\xb4\xb5W\xdc\xc1\xee\x91K\x12\xc2\xe1U\xaf\xf7I`\x83\x91\x0c\xc0\xcb\x15\x13!V\xa9\xc1\xca\x1b\x80\xff\xd8\x1f\xd8_+\x83\xcd\xcb%\xd6\xb7\xdc\x8a2\xa8Q\x1f\xbb.\xdf\x05\xb7hD\xab\xea\xe9\xfb.\xdd\x93\xd1\xf0\xb8r\xb9t.\xab\xf6]\xac\xc9U9\x87\x9e\xe36 \x87\xe7eo\x98\xac\xf4\x87\x8e\xf4\xa86\xd3\xcapy\xee\xa0]\xdbA\xb9\x00\xe9_R\xc8\xf7\xca\x13\xc6\xb1Z|c\xe8v\xa24\xac?k\xf1\xc4\x97\x18\x07\xbaU\xc9\xf5? \x95\x8f\x11\xa7\xc9\x8eY\x9c\xdfnx?\x88\xba\x90\xef\x94WU\xb5\xcf\x0b"\xe8\xfe\xa6.\x0cr-\xaf3\x8a\xe6v\xf9\xb91\x87\x91\xc6\xb1\xe9\xb9UP\xf5\x14\xb7\x99\x80\xc0\xc5}\x9a~\x7f\x06\x1e\xb8\x05\xd5\xa2LXO\\73i\x82\xcd\xc6#\xb7\xa4q\xd7\xd4y\xb1d\xaf\xa8\t\x9e1K\xd94\xaf7\x08\x8c);\xd2\xed\x91\xc6\xed\x83\x90\r\xef\x85\xf0\xfeJi\x02;\xf0\x0b\x03\xe7\xc1\x84\xd45\xaeP\xc2Lp\x1akb\xcaP\xe9\xfc\xc1\xc8VPQu\x85\x92l\x12\xb99{\x91\xd0\xa6d\n\xde\xf85\x93e\xfa\\\xf9cKx8\x84"s\xb8\xe52~\x97\x05\xc3\xf6\x1c\xca\x0b\xda\x8b\x90\xfeu5,\x94,\x99\xf9\x9a\xf3T\x8dAZ\xc7\xe9\x95-\x98\xf2\xbaL\x89\xc0?\xba1\xb5\\t|RY_\xc6\xabr\xe8')
+ full.check_date()
diff --git a/electrum/transaction.py b/electrum/transaction.py
index 2fe2218f..f487db39 100644
--- a/electrum/transaction.py
+++ b/electrum/transaction.py
@@ -27,7 +27,7 @@
# Note: The deserialization code originally comes from ABE.
-from typing import Sequence, Union, NamedTuple
+from typing import Sequence, Union, NamedTuple, Tuple, Optional, Iterable
from .util import print_error, profiler
@@ -60,10 +60,20 @@ class NotRecognizedRedeemScript(Exception):
pass
+class MalformedBitcoinScript(Exception):
+ pass
+
+
TxOutput = NamedTuple("TxOutput", [('type', int), ('address', str), ('value', Union[int, str])])
# ^ value is str when the output is set to max: '!'
+TxOutputHwInfo = NamedTuple("TxOutputHwInfo", [('address_index', Tuple),
+ ('sorted_xpubs', Iterable[str]),
+ ('num_sig', Optional[int]),
+ ('script_type', str)])
+
+
class BCDataStream(object):
def __init__(self):
self.input = None
@@ -258,13 +268,16 @@ def script_GetOp(_bytes : bytes):
if opcode <= opcodes.OP_PUSHDATA4:
nSize = opcode
if opcode == opcodes.OP_PUSHDATA1:
- nSize = _bytes[i]
+ try: nSize = _bytes[i]
+ except IndexError: raise MalformedBitcoinScript()
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 decoded is None:
+ return False
if len(decoded) != len(to_match):
- return False;
+ 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.
@@ -408,7 +411,10 @@ def parse_scriptSig(d, _bytes):
def parse_redeemScript_multisig(redeem_script: bytes):
- dec2 = [ x for x in script_GetOp(redeem_script) ]
+ try:
+ dec2 = [ x for x in script_GetOp(redeem_script) ]
+ except MalformedBitcoinScript:
+ raise NotRecognizedRedeemScript()
try:
m = dec2[0][0] - opcodes.OP_1 + 1
n = dec2[-2][0] - opcodes.OP_1 + 1
@@ -429,7 +435,10 @@ def parse_redeemScript_multisig(redeem_script: bytes):
def get_address_from_output_script(_bytes, *, net=None):
- decoded = [x for x in script_GetOp(_bytes)]
+ try:
+ decoded = [x for x in script_GetOp(_bytes)]
+ except MalformedBitcoinScript:
+ decoded = None
# The Genesis Block, self-payments, and pay-by-IP-address payments look like:
# 65 BYTES:... CHECKSIG
@@ -1102,7 +1111,6 @@ class Transaction:
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.
diff --git a/electrum/util.py b/electrum/util.py
index 1a3f69f5..027c2d33 100644
--- a/electrum/util.py
+++ b/electrum/util.py
@@ -34,13 +34,17 @@ import hmac
import stat
import inspect
from locale import localeconv
+import asyncio
+import urllib.request, urllib.parse, urllib.error
+import queue
+
+import aiohttp
+from aiohttp_socks import SocksConnector, SocksVer
+from aiorpcx import TaskGroup
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()}
@@ -49,13 +53,18 @@ base_units = {'FLO':8, 'mFLO':5, 'bits':2, 'sat':0}
base_units_inverse = inv_dict(base_units)
base_units_list = ['FLO', 'mFLO', 'bits', 'sat'] # list(dict) does not guarantee order
+DECIMAL_POINT_DEFAULT = 5 # mBTC
+
+
+class UnknownBaseUnit(Exception): pass
+
def decimal_point_to_base_unit_name(dp: int) -> str:
# e.g. 8 -> "BTC"
try:
return base_units_inverse[dp]
except KeyError:
- raise Exception('Unknown base unit')
+ raise UnknownBaseUnit(dp) from None
def base_unit_name_to_decimal_point(unit_name: str) -> int:
@@ -63,12 +72,9 @@ def base_unit_name_to_decimal_point(unit_name: str) -> int:
try:
return base_units[unit_name]
except KeyError:
- raise Exception('Unknown base unit')
+ raise UnknownBaseUnit(unit_name) from None
-def normalize_version(v):
- return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
-
class NotEnoughFunds(Exception): pass
@@ -898,6 +904,27 @@ def make_dir(path, allow_symlink=True):
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+class AIOSafeSilentException(Exception): pass
+
+
+def aiosafe(f):
+ # save exception in object.
+ # f must be a method of a PrintError instance.
+ # aiosafe calls should not be nested
+ async def f2(*args, **kwargs):
+ self = args[0]
+ try:
+ return await f(*args, **kwargs)
+ except AIOSafeSilentException as e:
+ self.exception = e
+ except asyncio.CancelledError as e:
+ self.exception = e
+ except BaseException as e:
+ self.print_error("Exception in", f.__name__, ":", e.__class__.__name__, str(e))
+ traceback.print_exc(file=sys.stderr)
+ self.exception = e
+ return f2
+
TxMinedStatus = NamedTuple("TxMinedStatus", [("height", int),
("conf", int),
("timestamp", int),
@@ -907,3 +934,26 @@ VerifiedTxInfo = NamedTuple("VerifiedTxInfo", [("height", int),
("txpos", int),
("header_hash", str),
("tx_comment", str)])
+
+def make_aiohttp_session(proxy):
+ if proxy:
+ connector = SocksConnector(
+ socks_ver=SocksVer.SOCKS5 if proxy['mode'] == 'socks5' else SocksVer.SOCKS4,
+ host=proxy['host'],
+ port=int(proxy['port']),
+ username=proxy.get('user', None),
+ password=proxy.get('password', None),
+ rdns=True
+ )
+ return aiohttp.ClientSession(headers={'User-Agent' : 'Electrum'}, timeout=aiohttp.ClientTimeout(total=10), connector=connector)
+ else:
+ return aiohttp.ClientSession(headers={'User-Agent' : 'Electrum'}, timeout=aiohttp.ClientTimeout(total=10))
+
+
+class CustomTaskGroup(TaskGroup):
+
+ def spawn(self, *args, **kwargs):
+ # don't complain if group is already closed.
+ if self._closed:
+ raise asyncio.CancelledError()
+ return super().spawn(*args, **kwargs)
diff --git a/electrum/verifier.py b/electrum/verifier.py
index 8278d1b4..26b8857e 100644
--- a/electrum/verifier.py
+++ b/electrum/verifier.py
@@ -21,12 +21,16 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
+import asyncio
from typing import Sequence, Optional
+from aiorpcx import TaskGroup
+
from .util import ThreadJob, bh2u, VerifiedTxInfo
from .bitcoin import Hash, hash_decode, hash_encode
from .transaction import Transaction
from .blockchain import hash_header
+from .interface import GracefulDisconnect
class MerkleVerificationFailure(Exception): pass
@@ -45,17 +49,20 @@ class SPV(ThreadJob):
self.merkle_roots = {} # txid -> merkle root (once it has been verified)
self.requested_merkle = set() # txid set of pending requests
- def run(self):
- interface = self.network.interface
- if not interface:
- return
+ async def main(self, group: TaskGroup):
+ while True:
+ await self._request_proofs(group)
+ await asyncio.sleep(0.1)
- blockchain = interface.blockchain
+ async def _request_proofs(self, group: TaskGroup):
+ blockchain = self.network.blockchain()
if not blockchain:
+ self.print_error("no blockchain")
return
local_height = self.network.get_local_height()
unverified = self.wallet.get_unverified_txs()
+
for tx_hash, tx_height in unverified.items():
# do not request merkle branch before headers are available
if tx_height <= 0 or tx_height > local_height:
@@ -65,31 +72,21 @@ class SPV(ThreadJob):
if header is None:
index = tx_height // 2016
if index < len(blockchain.checkpoints):
- self.network.request_chunk(interface, index)
+ await group.spawn(self.network.request_chunk(tx_height, None, can_return_early=True))
elif (tx_hash not in self.requested_merkle
and tx_hash not in self.merkle_roots):
- self.network.get_merkle_for_transaction(
- tx_hash,
- tx_height,
- self.verify_merkle)
self.print_error('requested merkle', tx_hash)
self.requested_merkle.add(tx_hash)
+ await group.spawn(self._request_and_verify_single_proof, tx_hash, tx_height)
if self.network.blockchain() != self.blockchain:
self.blockchain = self.network.blockchain()
- self.undo_verifications()
+ self._undo_verifications()
- def verify_merkle(self, response):
- if self.wallet.verifier is None:
- return # we have been killed, this was just an orphan callback
- if response.get('error'):
- self.print_error('received an error:', response)
- return
- params = response['params']
- merkle = response['result']
+ async def _request_and_verify_single_proof(self, tx_hash, tx_height):
+ merkle = await self.network.get_merkle_for_transaction(tx_hash, tx_height)
# 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_branch = merkle.get('merkle')
@@ -98,14 +95,10 @@ class SPV(ThreadJob):
verify_tx_is_in_block(tx_hash, merkle_branch, pos, header, tx_height)
except MerkleVerificationFailure as e:
self.print_error(str(e))
- # FIXME: we should make a fresh connection to a server
- # to recover from this, as this TX will now never verify
- return
+ raise GracefulDisconnect(e)
# we passed all the tests
self.merkle_roots[tx_hash] = header.get('merkle_root')
try:
- # note: we could pop in the beginning, but then we would request
- # this proof again in case of verification failure from the same server
self.requested_merkle.remove(tx_hash)
except KeyError: pass
self.print_error("verified %s" % tx_hash)
@@ -145,7 +138,7 @@ class SPV(ThreadJob):
else:
raise InnerNodeOfSpvProofIsValidTx()
- def undo_verifications(self):
+ def _undo_verifications(self):
height = self.blockchain.get_forkpoint()
tx_hashes = self.wallet.undo_verifications(self.blockchain, height)
for tx_hash in tx_hashes:
diff --git a/electrum/version.py b/electrum/version.py
index 4f3791cd..53387af3 100644
--- a/electrum/version.py
+++ b/electrum/version.py
@@ -1,7 +1,7 @@
-ELECTRUM_VERSION = '3.2.2' # version of the client package
-APK_VERSION = '3.2.2.0' # read by buildozer.spec
+ELECTRUM_VERSION = '3.2.3' # version of the client package
+APK_VERSION = '3.2.3.1' # read by buildozer.spec
-PROTOCOL_VERSION = '1.2' # protocol version requested
+PROTOCOL_VERSION = '1.4' # protocol version requested
# The hash of the mnemonic seed must begin with this
SEED_PREFIX = '01' # Standard wallet
diff --git a/electrum/wallet.py b/electrum/wallet.py
index b30588e8..b299ee74 100644
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -51,7 +51,7 @@ from .keystore import load_keystore, Hardware_KeyStore
from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW
from . import transaction, bitcoin, coinchooser, paymentrequest, contacts
-from .transaction import Transaction, TxOutput
+from .transaction import Transaction, TxOutput, TxOutputHwInfo
from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED)
@@ -179,6 +179,8 @@ class Abstract_Wallet(AddressSynchronizer):
self.fiat_value = storage.get('fiat_value', {})
self.receive_requests = storage.get('payment_requests', {})
+ self.calc_unused_change_addresses()
+
# save wallet type the first time
if self.storage.get('wallet_type') is None:
self.storage.put('wallet_type', self.wallet_type)
@@ -225,6 +227,16 @@ class Abstract_Wallet(AddressSynchronizer):
def synchronize(self):
pass
+ def calc_unused_change_addresses(self):
+ with self.lock:
+ if hasattr(self, '_unused_change_addresses'):
+ addrs = self._unused_change_addresses
+ else:
+ addrs = self.get_change_addresses()
+ self._unused_change_addresses = [addr for addr in addrs if
+ self.get_address_history_len(addr) == 0]
+ return list(self._unused_change_addresses)
+
def is_deterministic(self):
return self.keystore.is_deterministic()
@@ -272,8 +284,6 @@ class Abstract_Wallet(AddressSynchronizer):
return
def is_mine(self, address):
- if not super().is_mine(address):
- return False
try:
self.get_address_index(address)
except KeyError:
@@ -416,7 +426,7 @@ class Abstract_Wallet(AddressSynchronizer):
else:
income += value
# fiat computations
- if fx and fx.is_enabled():
+ if fx and fx.is_enabled() and fx.get_history_config():
fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
fiat_default = fiat_value is None
fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) #
@@ -452,7 +462,7 @@ class Abstract_Wallet(AddressSynchronizer):
'income': Satoshis(income),
'expenditures': Satoshis(expenditures)
}
- if fx and fx.is_enabled():
+ if fx and fx.is_enabled() and fx.get_history_config():
unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy)
summary['capital_gains'] = Fiat(capital_gains, fx.ccy)
summary['fiat_income'] = Fiat(fiat_income, fx.ccy)
@@ -557,21 +567,22 @@ class Abstract_Wallet(AddressSynchronizer):
self.add_input_info(item)
# change address
+ # if we leave it empty, coin_chooser will set it
+ change_addrs = []
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)]
+ elif self.use_change:
+ # Recalc and get unused change addresses
+ addrs = self.calc_unused_change_addresses()
+ # New change addresses are created only after a few
+ # confirmations.
+ if addrs:
+ # if there are any unused, select all
+ change_addrs = addrs
else:
- # coin_chooser will set change address
- change_addrs = []
+ # if there are none, take one randomly from the last few
+ addrs = self.get_change_addresses()[-self.gap_limit_for_change:]
+ change_addrs = [random.choice(addrs)] if addrs else []
# Fee estimator
if fixed_fee is None:
@@ -637,22 +648,24 @@ class Abstract_Wallet(AddressSynchronizer):
self.set_up_to_date(False)
while not self.is_up_to_date():
if callback:
- msg = "%s\n%s %d"%(
+ msg = "{}\n{} {}".format(
_("Please wait..."),
_("Addresses generated:"),
- len(self.addresses(True)))
+ len(self.get_addresses()))
callback(msg)
time.sleep(0.1)
def wait_for_network():
while not self.network.is_connected():
if callback:
- msg = "%s \n" % (_("Connecting..."))
+ msg = "{} \n".format(_("Connecting..."))
callback(msg)
time.sleep(0.1)
# wait until we are connected, because the user
# might have selected another server
if self.network:
+ self.print_error("waiting for network...")
wait_for_network()
+ self.print_error("waiting while wallet is syncing...")
wait_for_wallet()
else:
self.synchronize()
@@ -793,7 +806,8 @@ class Abstract_Wallet(AddressSynchronizer):
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
+ num_sig = self.m if isinstance(self, Multisig_Wallet) else None
+ info[addr] = TxOutputHwInfo(index, sorted_xpubs, num_sig, self.txin_type)
tx.output_info = info
def sign_transaction(self, tx, password):
@@ -1074,17 +1088,6 @@ class Abstract_Wallet(AddressSynchronizer):
index = self.get_address_index(addr)
return self.keystore.decrypt_message(index, message, password)
- def get_depending_transactions(self, tx_hash):
- """Returns all (grand-)children of tx_hash in this wallet."""
- children = set()
- # TODO rewrite this to use self.spent_outpoints
- for other_hash, tx in self.transactions.items():
- for input in (tx.inputs()):
- if input["prevout_hash"] == tx_hash:
- children.add(other_hash)
- children |= self.get_depending_transactions(other_hash)
- return children
-
def txin_value(self, txin):
txid = txin['prevout_hash']
prev_n = txin['prevout_n']
@@ -1131,7 +1134,8 @@ class Abstract_Wallet(AddressSynchronizer):
return result
if self.txi.get(txid, {}) != {}:
result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
- self.coin_price_cache[cache_key] = result
+ if not result.is_nan():
+ self.coin_price_cache[cache_key] = result
return result
else:
fiat_value = self.get_fiat_value(txid, ccy)
@@ -1437,6 +1441,9 @@ class Deterministic_Wallet(Abstract_Wallet):
self._addr_to_addr_index[address] = (for_change, n)
self.save_addresses()
self.add_address(address)
+ if for_change:
+ # note: if it's actually used, it will get filtered later
+ self._unused_change_addresses.append(address)
return address
def synchronize_sequence(self, for_change):
diff --git a/electrum/x509.py b/electrum/x509.py
index e4b94d40..f03d3051 100644
--- a/electrum/x509.py
+++ b/electrum/x509.py
@@ -26,6 +26,7 @@ from . import util
from .util import profiler, bh2u
import ecdsa
import hashlib
+import time
# algo OIDs
ALGO_RSA_SHA1 = '1.2.840.113549.1.1.5'
@@ -178,6 +179,14 @@ class ASN1_Node(bytes):
p[oid] = value
return p
+ def decode_time(self, ii):
+ GENERALIZED_TIMESTAMP_FMT = '%Y%m%d%H%M%SZ'
+ UTCTIME_TIMESTAMP_FMT = '%y%m%d%H%M%SZ'
+
+ try:
+ return time.strptime(self.get_value_of_type(ii, 'UTCTime').decode('ascii'), UTCTIME_TIMESTAMP_FMT)
+ except TypeError:
+ return time.strptime(self.get_value_of_type(ii, 'GeneralizedTime').decode('ascii'), GENERALIZED_TIMESTAMP_FMT)
class X509(object):
def __init__(self, b):
@@ -210,15 +219,9 @@ class X509(object):
# 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
+ self.notBefore = der.decode_time(ii)
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
+ self.notAfter = der.decode_time(ii)
# subject
subject = der.next_node(validity)
@@ -293,14 +296,10 @@ class X509(object):
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:
+ now = time.gmtime()
+ if self.notBefore > now:
raise CertificateError('Certificate has not entered its valid date range. (%s)' % self.get_common_name())
- if not_after <= now:
+ if self.notAfter <= now:
raise CertificateError('Certificate has expired. (%s)' % self.get_common_name())
def getFingerprint(self):
diff --git a/icons.qrc b/icons.qrc
index feb07a02..1ff4ea57 100644
--- a/icons.qrc
+++ b/icons.qrc
@@ -51,6 +51,8 @@
icons/speaker.png
icons/trezor_unpaired.png
icons/trezor.png
+ icons/coldcard.png
+ icons/coldcard_unpaired.png
icons/trustedcoin-status.png
icons/trustedcoin-wizard.png
icons/unconfirmed.png
diff --git a/icons/coldcard.png b/icons/coldcard.png
new file mode 100644
index 00000000..74104d76
Binary files /dev/null and b/icons/coldcard.png differ
diff --git a/icons/coldcard_unpaired.png b/icons/coldcard_unpaired.png
new file mode 100644
index 00000000..2560407e
Binary files /dev/null and b/icons/coldcard_unpaired.png differ
diff --git a/run_electrum b/run_electrum
index 46654ec1..0a43adbc 100755
--- a/run_electrum
+++ b/run_electrum
@@ -48,6 +48,7 @@ def check_imports():
import qrcode
import google.protobuf
import jsonrpclib
+ import aiorpcx
except ImportError as e:
sys.exit("Error: %s. Try 'sudo pip install '"%str(e))
# the following imports are for pyinstaller
@@ -134,7 +135,7 @@ def run_non_RPC(config):
if not config.get('offline'):
network = Network(config)
network.start()
- wallet.start_threads(network)
+ wallet.start_network(network)
print_msg("Recovering wallet...")
wallet.synchronize()
wallet.wait_until_synchronized()
@@ -335,6 +336,14 @@ if __name__ == '__main__':
sys.argv.remove('help')
sys.argv.append('-h')
+ # old '-v' syntax
+ try:
+ i = sys.argv.index('-v')
+ except ValueError:
+ pass
+ else:
+ sys.argv[i] = '-v*'
+
# read arguments from stdin pipe and prompt
for i, arg in enumerate(sys.argv):
if arg == '-':
diff --git a/setup.py b/setup.py
index 18c5cb88..9da14c01 100755
--- a/setup.py
+++ b/setup.py
@@ -2,12 +2,15 @@
# python setup.py sdist --format=zip,gztar
-from setuptools import setup, find_packages
import os
import sys
import platform
import imp
import argparse
+import subprocess
+
+from setuptools import setup, find_packages
+from setuptools.command.install import install
with open('contrib/requirements/requirements.txt') as f:
requirements = f.read().splitlines()
@@ -17,8 +20,8 @@ with open('contrib/requirements/requirements-hw.txt') as f:
version = imp.load_source('version', 'electrum/version.py')
-if sys.version_info[:3] < (3, 4, 0):
- sys.exit("Error: Electrum requires Python version >= 3.4.0...")
+if sys.version_info[:3] < (3, 6, 0):
+ sys.exit("Error: Electrum requires Python version >= 3.6.0...")
data_files = []
@@ -43,13 +46,32 @@ if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']:
extras_require = {
'hardware': requirements_hw,
'fast': ['pycryptodomex'],
+ 'gui': ['pyqt5'],
}
-extras_require['full'] = extras_require['hardware'] + extras_require['fast']
+extras_require['full'] = [pkg for sublist in list(extras_require.values()) for pkg in sublist]
+
+
+class CustomInstallCommand(install):
+ def run(self):
+ install.run(self)
+ # potentially build Qt icons file
+ try:
+ import PyQt5
+ except ImportError:
+ pass
+ else:
+ try:
+ path = os.path.join(self.install_lib, "electrum/gui/qt/icons_rc.py")
+ if not os.path.exists(path):
+ subprocess.call(["pyrcc5", "icons.qrc", "-o", path])
+ except Exception as e:
+ print('Warning: building icons file failed with {}'.format(e))
setup(
name="Electrum",
version=version.ELECTRUM_VERSION,
+ python_requires='>=3.6',
install_requires=requirements,
extras_require=extras_require,
packages=[
@@ -75,5 +97,8 @@ setup(
author_email="thomasv@electrum.org",
license="MIT Licence",
url="https://electrum.org",
- long_description="""Lightweight Bitcoin Wallet"""
+ long_description="""Lightweight Bitcoin Wallet""",
+ cmdclass={
+ 'install': CustomInstallCommand,
+ },
)
diff --git a/tox.ini b/tox.ini
index b00d2923..895f302a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py35, py36
+envlist = py36
[testenv]
deps=