Solved upstream merge conflicts
This commit is contained in:
commit
9e0777a7b4
15
.gitignore
vendored
15
.gitignore
vendored
@ -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
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
sudo: false
|
||||
sudo: true
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
|
||||
@ -5,7 +5,7 @@ Electrum - Lightweight Bitcoin client
|
||||
|
||||
Licence: MIT Licence
|
||||
Author: Thomas Voegtlin
|
||||
Language: Python
|
||||
Language: Python (>= 3.6)
|
||||
Homepage: https://electrum.org/
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 && \
|
||||
|
||||
@ -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
|
||||
@ -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 \
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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"])
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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.'))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
1047
electrum/network.py
1047
electrum/network.py
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
|
||||
@ -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_)]
|
||||
|
||||
65
electrum/plugins/coldcard/README.md
Normal file
65
electrum/plugins/coldcard/README.md
Normal 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>
|
||||
7
electrum/plugins/coldcard/__init__.py
Normal file
7
electrum/plugins/coldcard/__init__.py
Normal 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']
|
||||
47
electrum/plugins/coldcard/cmdline.py
Normal file
47
electrum/plugins/coldcard/cmdline.py
Normal 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
|
||||
693
electrum/plugins/coldcard/coldcard.py
Normal file
693
electrum/plugins/coldcard/coldcard.py
Normal 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
|
||||
241
electrum/plugins/coldcard/qt.py
Normal file
241
electrum/plugins/coldcard/qt.py
Normal 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()
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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.'''
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
51
electrum/plugins/revealer/hmac_drbg.py
Normal file
51
electrum/plugins/revealer/hmac_drbg.py
Normal 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]
|
||||
@ -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:
|
||||
|
||||
@ -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.'''
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.'''
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'):
|
||||
"""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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'/"))
|
||||
|
||||
@ -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'))
|
||||
@ -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
|
||||
|
||||
119
electrum/tests/test_network.py
Normal file
119
electrum/tests/test_network.py
Normal 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()
|
||||
@ -1,7 +1,6 @@
|
||||
import ast
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import unittest
|
||||
from electrum.util import format_satoshis, parse_URI
|
||||
|
||||
from . import SequentialTestCase
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import shutil
|
||||
import tempfile
|
||||
import sys
|
||||
import unittest
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
6
electrum/tests/test_x509.py
Normal file
6
electrum/tests/test_x509.py
Normal 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()
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
BIN
icons/coldcard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 528 B |
BIN
icons/coldcard_unpaired.png
Normal file
BIN
icons/coldcard_unpaired.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 788 B |
11
run_electrum
11
run_electrum
@ -48,6 +48,7 @@ def check_imports():
|
||||
import qrcode
|
||||
import google.protobuf
|
||||
import jsonrpclib
|
||||
import aiorpcx
|
||||
except ImportError as e:
|
||||
sys.exit("Error: %s. Try 'sudo pip install <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 == '-':
|
||||
|
||||
35
setup.py
35
setup.py
@ -2,12 +2,15 @@
|
||||
|
||||
# python setup.py sdist --format=zip,gztar
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import imp
|
||||
import argparse
|
||||
import subprocess
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools.command.install import install
|
||||
|
||||
with open('contrib/requirements/requirements.txt') as f:
|
||||
requirements = f.read().splitlines()
|
||||
@ -17,8 +20,8 @@ with open('contrib/requirements/requirements-hw.txt') as f:
|
||||
|
||||
version = imp.load_source('version', 'electrum/version.py')
|
||||
|
||||
if sys.version_info[:3] < (3, 4, 0):
|
||||
sys.exit("Error: Electrum requires Python version >= 3.4.0...")
|
||||
if sys.version_info[:3] < (3, 6, 0):
|
||||
sys.exit("Error: Electrum requires Python version >= 3.6.0...")
|
||||
|
||||
data_files = []
|
||||
|
||||
@ -43,13 +46,32 @@ if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']:
|
||||
extras_require = {
|
||||
'hardware': requirements_hw,
|
||||
'fast': ['pycryptodomex'],
|
||||
'gui': ['pyqt5'],
|
||||
}
|
||||
extras_require['full'] = extras_require['hardware'] + extras_require['fast']
|
||||
extras_require['full'] = [pkg for sublist in list(extras_require.values()) for pkg in sublist]
|
||||
|
||||
|
||||
class CustomInstallCommand(install):
|
||||
def run(self):
|
||||
install.run(self)
|
||||
# potentially build Qt icons file
|
||||
try:
|
||||
import PyQt5
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
path = os.path.join(self.install_lib, "electrum/gui/qt/icons_rc.py")
|
||||
if not os.path.exists(path):
|
||||
subprocess.call(["pyrcc5", "icons.qrc", "-o", path])
|
||||
except Exception as e:
|
||||
print('Warning: building icons file failed with {}'.format(e))
|
||||
|
||||
|
||||
setup(
|
||||
name="Electrum",
|
||||
version=version.ELECTRUM_VERSION,
|
||||
python_requires='>=3.6',
|
||||
install_requires=requirements,
|
||||
extras_require=extras_require,
|
||||
packages=[
|
||||
@ -75,5 +97,8 @@ setup(
|
||||
author_email="thomasv@electrum.org",
|
||||
license="MIT Licence",
|
||||
url="https://electrum.org",
|
||||
long_description="""Lightweight Bitcoin Wallet"""
|
||||
long_description="""Lightweight Bitcoin Wallet""",
|
||||
cmdclass={
|
||||
'install': CustomInstallCommand,
|
||||
},
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user