Solved upstream merge conflicts

This commit is contained in:
Vivek Teega 2018-09-12 03:38:43 +05:30
commit 9e0777a7b4
97 changed files with 3317 additions and 2129 deletions

15
.gitignore vendored
View File

@ -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

View File

@ -1,8 +1,9 @@
sudo: false
sudo: true
dist: xenial
language: python
python:
- 3.5
- 3.6
- 3.7
addons:
apt:
sources:

View File

@ -5,7 +5,7 @@ Electrum - Lightweight Bitcoin client
Licence: MIT Licence
Author: Thomas Voegtlin
Language: Python
Language: Python (>= 3.6)
Homepage: https://electrum.org/

View File

@ -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

View File

@ -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 '<array><dict> <key>CFBundleURLName</key> <string>bitcoin</string> <key>CFBundleURLSchemes</key> <array><string>bitcoin</string></array> </dict></array>' \
-- 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"

View File

@ -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,

View File

@ -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.

View File

@ -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

View File

@ -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,

View File

@ -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 && \

View File

@ -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.

@ -1 +1 @@
Subproject commit 5af76dc1b04c782e622ac409cd802c483c529c1f
Subproject commit 0b8cbcca428ceb791527bcbb2ef2b36b4ab29c73

@ -1 +1 @@
Subproject commit de999ceffd2a864df54451d23f290ef5f333e8ea
Subproject commit 27e36687f4b0fbd126628bdde80758b63ade7347

View File

@ -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 \

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = """<h2>Traceback</h2>
@ -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

View File

@ -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 = {

View File

@ -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)]

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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')

View File

@ -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):

View File

@ -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"])

View File

@ -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'

View File

@ -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()

View File

@ -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()

View File

@ -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:

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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))

View File

@ -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.'))

View File

@ -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)

View File

@ -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):

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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_)]

View File

@ -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.
<https://coldcardwallet.com>
## 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 <https://stackoverflow.com/questions/4830856>

View File

@ -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']

View File

@ -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

View File

@ -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("<BH", 253, l)
elif l < 0x100000000:
return pack("<BI", 254, l)
else:
return pack("<BQ", 255, l)
def xfp_from_xpub(xpub):
# sometime we need to BIP32 fingerprint value: 4 bytes of ripemd(sha256(pubkey))
# UNTESTED
kk = bfh(Xpub.get_pubkey_from_xpub(xpub, []))
assert len(kk) == 33
xfp, = unpack('<I', hash_160(kk)[0:4])
return xfp
class CKCCClient:
# Challenge: I haven't found anywhere that defines a base class for this 'client',
# nor an API (interface) to be met. Winging it. Gets called from lib/plugins.py mostly?
def __init__(self, plugin, handler, dev_path, is_simulator=False):
self.device = plugin.device
self.handler = handler
# if we know what the (xfp, xpub) "should be" then track it here
self._expected_device = None
if is_simulator:
self.dev = ElectrumColdcardDevice(dev_path, encrypt=True)
else:
# open the real HID device
import hid
hd = hid.device(path=dev_path)
hd.open_path(dev_path)
self.dev = ElectrumColdcardDevice(dev=hd, encrypt=True)
# NOTE: MiTM test is delayed until we have a hint as to what XPUB we
# should expect. It's also kinda slow.
def __repr__(self):
return '<CKCCClient: xfp=%08x label=%r>' % (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 <https://stackoverflow.com/questions/7172772/abc-for-string>
# - 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('<I', xfp)
for x in self.get_derivation()[2:].split('/'):
if x.endswith("'"):
x = int(x[:-1]) | 0x80000000
else:
x = int(x)
base_path += pack('<I', x)
# 2) all used keys in transaction
subkeys = {}
derivations = self.get_tx_derivations(tx)
for xpubkey in derivations:
pubkey = xpubkey_to_pubkey(xpubkey)
# assuming depth two, non-harded: change + index
aa, bb = derivations[xpubkey]
assert 0 <= aa < 0x80000000
assert 0 <= bb < 0x80000000
subkeys[bfh(pubkey)] = base_path + pack('<II', aa, bb)
for txin in inputs:
if txin['type'] == 'coinbase':
self.give_error("Coinbase not supported") # but why not?
if txin['type'] in ['p2sh']:
self.give_error('Not ready for multisig transactions yet')
#if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
#if txin['type'] in ['p2wpkh', 'p2wsh']:
# Construct PSBT from start to finish.
out_fd = io.BytesIO()
out_fd.write(b'psbt\xff')
def write_kv(ktype, val, key=b''):
# serialize helper: write w/ size and key byte
out_fd.write(my_var_int(1 + len(key)))
out_fd.write(bytes([ktype]) + key)
if isinstance(val, str):
val = bfh(val)
out_fd.write(my_var_int(len(val)))
out_fd.write(val)
# global section: just the unsigned txn
class CustomTXSerialization(Transaction):
@classmethod
def input_script(cls, txin, estimate_size=False):
return ''
unsigned = bfh(CustomTXSerialization(tx.serialize()).serialize_to_network(witness=False))
write_kv(PSBT_GLOBAL_UNSIGNED_TX, unsigned)
# end globals section
out_fd.write(b'\x00')
# inputs section
for txin in inputs:
utxo = txin['prev_tx'].outputs()[txin['prevout_n']]
spendable = txin['prev_tx'].serialize_output(utxo)
write_kv(PSBT_IN_WITNESS_UTXO, spendable)
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
pubkeys = [bfh(k) for k in pubkeys]
for k in pubkeys:
write_kv(PSBT_IN_BIP32_DERIVATION, subkeys[k], k)
out_fd.write(b'\x00')
# outputs section
for o in tx.outputs():
# can be empty, but must be present, and helpful to show change inputs
# wallet.add_hw_info() adds some data about change outputs into tx.output_info
if o.address in tx.output_info:
# this address "is_mine" but might not be change (I like to sent to myself)
output_info = tx.output_info.get(o.address)
index, xpubs = output_info.address_index, output_info.sorted_xpubs
if index[0] == 1 and len(index) == 2:
# it is a change output (based on our standard derivation path)
assert len(xpubs) == 1 # not expecting multisig
xpubkey = xpubs[0]
# document its bip32 derivation in output section
aa, bb = index
assert 0 <= aa < 0x80000000
assert 0 <= bb < 0x80000000
deriv = base_path + pack('<II', aa, bb)
pubkey = self.get_pubkey_from_xpub(xpubkey, index)
write_kv(PSBT_OUT_BIP32_DERIVATION, deriv, bfh(pubkey))
out_fd.write(b'\x00')
return out_fd.getvalue()
@wrap_busy
def sign_transaction(self, tx, password):
# Build a PSBT in memory, upload it for signing.
# - we can also work offline (without paired device present)
if tx.is_complete():
return
client = self.get_client()
assert client.dev.master_fingerprint == self.ckcc_xfp
raw_psbt = self.build_psbt(tx)
#open('debug.psbt', 'wb').write(out_fd.getvalue())
try:
try:
self.handler.show_message("Authorize Transaction...")
client.sign_transaction_start(raw_psbt, True)
while 1:
# How to kill some time, without locking UI?
time.sleep(0.250)
resp = client.sign_transaction_poll()
if resp is not None:
break
rlen, rsha = resp
# download the resulting txn.
new_raw = client.download_file(rlen, rsha)
finally:
self.handler.finished()
except (CCUserRefused, CCBusyError) as exc:
print_error('[coldcard]', 'Did not sign:', str(exc))
self.handler.show_error(str(exc))
return
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.give_error(e, True)
return
# trust the coldcard to re-searilize final product right?
tx.update(bh2u(new_raw))
@staticmethod
def _encode_txin_type(txin_type):
# Map from Electrum code names to our code numbers.
return {'standard': AF_CLASSIC, 'p2pkh': AF_CLASSIC,
'p2sh': AF_P2SH,
'p2wpkh-p2sh': AF_P2WPKH_P2SH,
'p2wpkh': AF_P2WPKH,
'p2wsh-p2sh': AF_P2WSH_P2SH,
'p2wsh': AF_P2WSH,
}[txin_type]
@wrap_busy
def show_address(self, sequence, txin_type):
client = self.get_client()
address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
addr_fmt = self._encode_txin_type(txin_type)
try:
try:
self.handler.show_message(_("Showing address ..."))
dev_addr = client.show_address(address_path, addr_fmt)
# we could double check address here
finally:
self.handler.finished()
except CCProtoError as exc:
traceback.print_exc(file=sys.stderr)
self.handler.show_error('{}\n\n{}'.format(
_('Error showing address') + ':', str(exc)))
except BaseException as exc:
traceback.print_exc(file=sys.stderr)
self.handler.show_error(exc)
class ColdcardPlugin(HW_PluginBase):
keystore_class = Coldcard_KeyStore
minimum_library = (0, 7, 2)
client = None
DEVICE_IDS = [
(COINKITE_VID, CKCC_PID),
(COINKITE_VID, CKCC_SIMULATED_PID)
]
#SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
SUPPORTED_XTYPES = ('standard', 'p2wpkh')
def __init__(self, parent, config, name):
HW_PluginBase.__init__(self, parent, config, name)
self.libraries_available = self.check_libraries_available() and requirements_ok
if not self.libraries_available:
return
self.device_manager().register_devices(self.DEVICE_IDS)
self.device_manager().register_enumerate_func(self.detect_simulator)
def get_library_version(self):
import ckcc
try:
return ckcc.__version__
except AttributeError:
return 'unknown'
def detect_simulator(self):
# if there is a simulator running on this machine,
# return details about it so it's offered as a pairing choice
fn = CKCC_SIMULATOR_PATH
if os.path.exists(fn):
return [Device(fn, -1, fn, (COINKITE_VID, CKCC_SIMULATED_PID), 0)]
return []
def create_client(self, device, handler):
if handler:
self.handler = handler
# We are given a HID device, or at least some details about it.
# Not sure why not we aren't just given a HID library handle, but
# the 'path' is unabiguous, so we'll use that.
try:
rv = CKCCClient(self, handler, device.path,
is_simulator=(device.product_key[1] == CKCC_SIMULATED_PID))
return rv
except:
self.print_error('late failure connecting to device?')
return None
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
if client is None:
raise Exception(_('Failed to create a client for this device.') + '\n' +
_('Make sure it is in the correct state.'))
client.handler = self.create_handler(wizard)
def get_xpub(self, device_id, derivation, xtype, wizard):
# this seems to be part of the pairing process only, not during normal ops?
# base_wizard:on_hw_derivation
if xtype not in self.SUPPORTED_XTYPES:
raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device))
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
client.handler = self.create_handler(wizard)
client.ping_check()
xpub = client.get_xpub(derivation, xtype)
return xpub
def get_client(self, keystore, force_pair=True):
# All client interaction should not be in the main GUI thread
devmgr = self.device_manager()
handler = keystore.handler
with devmgr.hid_lock:
client = devmgr.client_for_keystore(self, handler, keystore, force_pair)
# returns the client for a given keystore. can use xpub
#if client:
# client.used()
if client is not None:
client.ping_check()
return client
def show_address(self, wallet, address, keystore=None):
if keystore is None:
keystore = wallet.get_keystore()
if not self.show_address_helper(wallet, address, keystore):
return
# Standard_Wallet => 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

View File

@ -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 <http://doc.qt.io/archives/qt-4.8/richtext-html-subset.html>
title = QLabel('''<center>
<span style="font-size: x-large">Coldcard Wallet</span>
<br><span style="font-size: medium">from Coinkite Inc.</span>
<br><a href="https://coldcardwallet.com">coldcardwallet.com</a>''')
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('<tt>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('<tt>0x%08x' % dev.master_fingerprint)
self.serial.setText('<tt>%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('<tt>%s' % fw_rel)
self.fw_built.setText('<tt>%s' % fw_date)
self.bl_version.setText('<tt>%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("<I", hdr)[0]
if magic != FW_HEADER_MAGIC:
raise ValueError("Bad magic")
except Exception as exc:
mw.show_error("Does not appear to be a Coldcard firmware file.\n\n%s" % exc)
return
# TODO:
# - detect if they are trying to downgrade; aint gonna work
# - warn them about the reboot?
# - length checks
# - add progress local bar
mw.show_message("Ready to Upgrade.\n\nBe patient. Unit will reboot itself when complete.")
def doit():
dlen, _ = dev.upload_file(firmware, verify=True)
assert dlen == len(firmware)
# append the firmware header a second time
result = dev.send_recv(CCProtocolPacker.upload(size, size+FW_HEADER_SIZE, hdr))
# make it reboot into bootlaoder which might install it
dev.send_recv(CCProtocolPacker.reboot())
self.thread.add(doit)
self.close()

View File

@ -538,7 +538,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
assert o.type == TYPE_ADDRESS
info = tx.output_info.get(o.address)
if info is not None:
index, xpubs, m = info
index = info.address_index
changePath = self.get_derivation() + "/%d/%d" % index
changePubkey = self.derive_pubkey(index[0], index[1])
pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath}

View File

@ -27,7 +27,7 @@
from electrum.plugin import BasePlugin, hook
from electrum.i18n import _
from electrum.bitcoin import is_address, TYPE_SCRIPT
from electrum.util import bfh
from electrum.util import bfh, versiontuple
from electrum.transaction import opcodes, TxOutput
@ -38,6 +38,8 @@ class HW_PluginBase(BasePlugin):
# libraries_available, libraries_URL, minimum_firmware,
# wallet_class, ckd_public, types, HidTransport
minimum_library = (0, )
def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name)
self.device = self.keystore_class.device
@ -78,6 +80,38 @@ class HW_PluginBase(BasePlugin):
return False
return True
def get_library_version(self) -> 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

View File

@ -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')

View File

@ -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.'''

View File

@ -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)

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1,51 @@
'''
Copyright (c) 2014 David Lazar <lazard@mit.edu>
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]

View File

@ -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(["<b>",_("Warning: "), "</b>", _("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(["<b>" + _("WARNING") + "</b>: " + _("Revealer is a one-time-pad and should be used only once."), '<br/>',
_("Multiple secrets encrypted for the same Revealer can be attacked."), '<br/>',
])))
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:

View File

@ -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.'''

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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.'''

View File

@ -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)

View File

@ -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)

View File

@ -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'):
"""

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -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'/"))

View File

@ -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'))

View File

@ -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

View File

@ -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()

View File

@ -1,7 +1,6 @@
import ast
import sys
import os
import unittest
import tempfile
import shutil

View File

@ -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)

View File

@ -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

View File

@ -1,4 +1,3 @@
import unittest
from electrum.util import format_satoshis, parse_URI
from . import SequentialTestCase

View File

@ -1,7 +1,6 @@
import shutil
import tempfile
import sys
import unittest
import os
import json

View File

@ -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:

View File

@ -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<r\x0c\xd5Q\xd0L\x02\xcb\x19\x19\xd6\xdf$\xcej\xa8l\xbd\x81\x803\x95\x0e\x907&\x81J\x88\xaf\xa23\xb4q\x96\x08\xa9]}\xb8Rs\x89{\x04\x88/\xc1m\x8c\xe8\\X\x95 \x1cj\xf2(t\xd7\xef\x10-r\xb6\x17L\xce_\x1bf\xc0c\x18\x83\x99\xdf\xd5\xad\x88\xcd \xae\x07 \xed\xb6\xfc[\x9a/f\x92\xce^\x9c\xd9\x064\xb4\xcc\x1d,d\x99\xee\x9a4\xbe\xde0\x92\x8f/keq\x94\x9frf1\xda\xadM_\x11C\x19\x01\xf0\xe0I\x84W\xf9\xaa\xd3\x12ex\x89"\xbfQ\x1f\xbdU\xa0\x92\xa3\x9d\xdb?\x86\x82\x0b\x1e\xe0\x8aSq\xce%\xea4\xfb\x82\x92\x0f\xcf\xaa\xe2\r\xedd\xba\xff\x85\xa2+\xb0x9\xba\'\xd3\xf5\xd6\xfa\xb43\x0b\xd4\xf4\xca\xa5\xb1\xe4[\xe7\xf7\xc3\xd3\xdd\x85)\xac5E\x17\xae\x03fCC(\x06\x1cU\xedM\x90r\xe87\x8d}\xf1i\xfdO\x83\x05\x83\x83y\xd9f,\xe1\xba\xf0\\y\x8d\x08`\xb1\x02\x03\x01\x00\x010\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x00\x03\x82\x02\x01\x00,.\x12jC3\x9fdF\x15\x16\xea*1\x0b[\xfa-\xcf\x80\x17\xf0\xfa\xf4\x96C\xff\xf9\xe9\xa2N\xda\xf1&6\x9ecV~\xea[\x07\xc1R\x03\x95\xd4\x84B\xe2r\x92\xad<mp\xf1\xcb\xb3\x8b\xbf \x08\x12\x1e6\xe3\xad\xbd1\x81\xbe\xaex\x002\xb6\xf9\xa0\xf6\xb7E^"\r\xa0w\x08\x14\xe7\x84\x03q2\x9c\xac\xce>\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()

View File

@ -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('<H', _bytes, i)
try: (nSize,) = struct.unpack_from('<H', _bytes, i)
except struct.error: raise MalformedBitcoinScript()
i += 2
elif opcode == opcodes.OP_PUSHDATA4:
(nSize,) = struct.unpack_from('<I', _bytes, i)
try: (nSize,) = struct.unpack_from('<I', _bytes, i)
except struct.error: raise MalformedBitcoinScript()
i += 4
vch = _bytes[i:i + nSize]
i += nSize
@ -276,21 +289,11 @@ def script_GetOpName(opcode):
return (opcodes.whatis(opcode)).replace("OP_", "")
def decode_script(bytes):
result = ''
for (opcode, vch, i) in script_GetOp(bytes):
if len(result) > 0: result += " "
if opcode <= opcodes.OP_PUSHDATA4:
result += "%d:"%(opcode,)
result += short_hex(vch)
else:
result += script_GetOpName(opcode)
return result
def match_decoded(decoded, to_match):
if 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.

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -51,6 +51,8 @@
<file>icons/speaker.png</file>
<file>icons/trezor_unpaired.png</file>
<file>icons/trezor.png</file>
<file>icons/coldcard.png</file>
<file>icons/coldcard_unpaired.png</file>
<file>icons/trustedcoin-status.png</file>
<file>icons/trustedcoin-wizard.png</file>
<file>icons/unconfirmed.png</file>

BIN
icons/coldcard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

BIN
icons/coldcard_unpaired.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

View File

@ -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 <module-name>'"%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 == '-':

View File

@ -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,
},
)

View File

@ -1,5 +1,5 @@
[tox]
envlist = py35, py36
envlist = py36
[testenv]
deps=