Compare commits

..

1 Commits

Author SHA1 Message Date
Vivek Teega
e423e34c72 changes for the android app 2018-09-28 18:01:04 +05:30
302 changed files with 6736 additions and 82534 deletions

4
.gitignore vendored
View File

@ -6,7 +6,7 @@ dist/
*.egg/ *.egg/
contrib/pyinstaller/ contrib/pyinstaller/
Electrum.egg-info/ Electrum.egg-info/
electrum/locale/ locale/
.devlocaltmp/ .devlocaltmp/
*_trial_temp *_trial_temp
packages packages
@ -14,9 +14,11 @@ env/
.buildozer/ .buildozer/
bin/ bin/
/app.fil /app.fil
.idea .idea
# icons # icons
electrum/gui/qt/icons_rc.py
electrum/gui/kivy/theming/light-0.png electrum/gui/kivy/theming/light-0.png
electrum/gui/kivy/theming/light.atlas electrum/gui/kivy/theming/light.atlas

6
.gitmodules vendored
View File

@ -1,6 +1,6 @@
[submodule "contrib/deterministic-build/electrum-icons"]
path = contrib/deterministic-build/electrum-icons
url = https://github.com/spesmilo/electrum-icons
[submodule "contrib/deterministic-build/electrum-locale"] [submodule "contrib/deterministic-build/electrum-locale"]
path = contrib/deterministic-build/electrum-locale path = contrib/deterministic-build/electrum-locale
url = https://github.com/spesmilo/electrum-locale url = https://github.com/spesmilo/electrum-locale
[submodule "contrib/CalinsQRReader"]
path = contrib/osx/CalinsQRReader
url = https://github.com/spesmilo/CalinsQRReader

View File

@ -4,16 +4,12 @@ language: python
python: python:
- 3.6 - 3.6
- 3.7 - 3.7
git:
depth: false
addons: addons:
apt: apt:
sources: sources:
- sourceline: 'ppa:tah83/secp256k1' - sourceline: 'ppa:tah83/secp256k1'
packages: packages:
- libsecp256k1-0 - libsecp256k1-0
before_install:
- git tag
install: install:
- pip install -r contrib/requirements/requirements-travis.txt - pip install -r contrib/requirements/requirements-travis.txt
cache: cache:
@ -23,12 +19,11 @@ cache:
script: script:
- tox - tox
after_success: after_success:
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install requests && contrib/make_locale; fi - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi
- coveralls - coveralls
jobs: jobs:
include: include:
- stage: binary builds - stage: binary builds
name: "Windows build"
sudo: true sudo: true
language: c language: c
python: false python: false
@ -37,57 +32,25 @@ jobs:
services: services:
- docker - docker
install: install:
- sudo docker build --no-cache -t electrum-wine-builder-img ./contrib/build-wine/docker/ - sudo docker build --no-cache -t electrum-wine-builder-img ./contrib/build-wine/docker/
script: script:
- sudo docker run --name electrum-wine-builder-cont -v $PWD:/opt/wine64/drive_c/electrum --rm --workdir /opt/wine64/drive_c/electrum/contrib/build-wine electrum-wine-builder-img ./build.sh - sudo docker run --name electrum-wine-builder-cont -v $PWD:/opt/wine64/drive_c/electrum --rm --workdir /opt/wine64/drive_c/electrum/contrib/build-wine electrum-wine-builder-img ./build.sh
after_success: true after_success: true
- name: "Android build" - os: osx
language: python
python: 3.7
env:
# reset API key to not have make_locale upload stuff here
- crowdin_api_key=
services:
- docker
install:
- pip install requests && ./contrib/make_locale
- ./contrib/make_packages
- sudo docker build --no-cache -t electrum-android-builder-img electrum/gui/kivy/tools
script:
- sudo chown -R 1000:1000 .
# Output something every minute or Travis kills the job
- while sleep 60; do echo "=====[ $SECONDS seconds still running ]====="; done &
- sudo docker run -it -u 1000:1000 --rm --name electrum-android-builder-cont -v $PWD:/home/user/wspace/electrum --workdir /home/user/wspace/electrum electrum-android-builder-img ./contrib/make_apk
# kill background sleep loop
- kill %1
- ls -la bin
- if [ $(ls bin | grep -c Electrum-*) -eq 0 ]; then exit 1; fi
after_success: true
- name: "MacOS build"
os: osx
language: c language: c
env: env:
- TARGET_OS=macOS - TARGET_OS=macOS
python: false python: false
install: install:
- git fetch --all --tags - git fetch --all --tags
script: ./contrib/osx/make_osx - git fetch origin --unshallow
script: ./contrib/build-osx/make_osx
after_script: ls -lah dist && md5 dist/* after_script: ls -lah dist && md5 dist/*
after_success: true after_success: true
- name: "AppImage build"
sudo: true
language: c
python: false
services:
- docker
install:
- sudo docker build --no-cache -t electrum-appimage-builder-img ./contrib/build-linux/appimage/
script:
- sudo docker run --name electrum-appimage-builder-cont -v $PWD:/opt/electrum --rm --workdir /opt/electrum/contrib/build-linux/appimage electrum-appimage-builder-img ./build.sh
after_success: true
- stage: release check - stage: release check
install: install:
- git fetch --all --tags - git fetch --all --tags
- git fetch origin --unshallow
script: script:
- ./contrib/deterministic-build/check_submodules.sh - ./contrib/deterministic-build/check_submodules.sh
after_success: true after_success: true

13
AUTHORS
View File

@ -1,5 +1,3 @@
Electrum-BTC
------------
ThomasV - Creator and maintainer. ThomasV - Creator and maintainer.
Animazing / Tachikoma - Styled the new GUI. Mac version. Animazing / Tachikoma - Styled the new GUI. Mac version.
Azelphur - GUI stuff. Azelphur - GUI stuff.
@ -11,13 +9,4 @@ Genjix - Porting pro-mode functionality to lite-gui and worked on server
Slush - Work on the server. Designed the original Stratum spec. Slush - Work on the server. Designed the original Stratum spec.
Julian Toash (Tuxavant) - Various fixes to the client. Julian Toash (Tuxavant) - Various fixes to the client.
rdymac - Website and translations. rdymac - Website and translations.
kyuupichan - Miscellaneous. kyuupichan - Miscellaneous.
FLO-Electrum
------------
vivekteega - Maintainer and remaining stuff
Bitspill - Bootstraped the project with core FLO changes
Rohit Tripathy - Ideation and problem solving
akhil2015 - Flodata and scrypt hashing

22
Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>bitcoin</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bitcoin</string>
</array>
</dict>
</array>
<key>LSArchitecturePriority</key>
<array>
<string>x86_64</string>
<string>i386</string>
</array>
</dict>
</plist>

View File

@ -1,5 +1,6 @@
include LICENCE RELEASE-NOTES AUTHORS include LICENCE RELEASE-NOTES AUTHORS
include README.rst include README.rst
include electrum.conf.sample
include electrum.desktop include electrum.desktop
include *.py include *.py
include run_electrum include run_electrum
@ -7,11 +8,11 @@ include contrib/requirements/requirements.txt
include contrib/requirements/requirements-hw.txt include contrib/requirements/requirements-hw.txt
recursive-include packages *.py recursive-include packages *.py
recursive-include packages cacert.pem recursive-include packages cacert.pem
include icons.qrc
graft icons
graft electrum graft electrum
prune electrum/tests prune electrum/tests
global-exclude __pycache__ global-exclude __pycache__
global-exclude *.py[co~] global-exclude *.py[co]
global-exclude *.py.orig
global-exclude *.py.rej

View File

@ -32,7 +32,7 @@ Qt interface, install the Qt dependencies::
sudo apt-get install python3-pyqt5 sudo apt-get install python3-pyqt5
If you downloaded the official package (tar.gz), you can run If you downloaded the official package (tar.gz), you can run
Electrum from its root directory without installing it on your Electrum from its root directory, without installing it on your
system; all the python dependencies are included in the 'packages' system; all the python dependencies are included in the 'packages'
directory. To run Electrum from its root directory, just do:: directory. To run Electrum from its root directory, just do::
@ -41,10 +41,10 @@ directory. To run Electrum from its root directory, just do::
You can also install Electrum on your system, by running this command:: You can also install Electrum on your system, by running this command::
sudo apt-get install python3-setuptools sudo apt-get install python3-setuptools
python3 -m pip install .[fast] pip3 install .[fast]
This will download and install the Python dependencies used by This will download and install the Python dependencies used by
Electrum instead of using the 'packages' directory. Electrum, instead of using the 'packages' directory.
The 'fast' extra contains some optional dependencies that we think The 'fast' extra contains some optional dependencies that we think
are often useful but they are not strictly needed. are often useful but they are not strictly needed.
@ -64,8 +64,16 @@ Check out the code from GitHub::
Run install (this should install dependencies):: Run install (this should install dependencies)::
python3 -m pip install .[fast] pip3 install .[fast]
Render the SVG icons to PNGs (optional)::
for i in lock unlock confirmed status_lagging status_disconnected status_connected_proxy status_connected status_waiting preferences; do convert -background none icons/$i.svg icons/$i.png; done
Compile the icons file for Qt::
sudo apt-get install pyqt5-dev-tools
pyrcc5 icons.qrc -o electrum/gui/qt/icons_rc.py
Compile the protobuf description file:: Compile the protobuf description file::
@ -83,25 +91,25 @@ Create translations (optional)::
Creating Binaries Creating Binaries
================= =================
Linux
-----
See :code:`contrib/build-linux/README.md`. To create binaries, create the 'packages' directory::
./contrib/make_packages
This directory contains the python dependencies used by Electrum.
Mac OS X / macOS Mac OS X / macOS
---------------- --------
See :code:`contrib/osx/README.md`.
See `contrib/build-osx/`.
Windows Windows
------- -------
See :code:`contrib/build-wine/docker/README.md`. See `contrib/build-wine/`.
Android Android
------- -------
See :code:`electrum/gui/kivy/Readme.md`. See `electrum/gui/kivy/Readme.md` file.

View File

@ -1,77 +1,3 @@
# Release 3.3.4 - (February 13, 2019)
* AppImage: we now also distribute self-contained binaries for x86_64
Linux in the form of an AppImage (#5042). The Python interpreter,
PyQt5, libsecp256k1, PyCryptodomex, zbar, hidapi/libusb (including
hardware wallet libraries) are all bundled. Note that users of
hw wallets still need to set udev rules themselves.
* hw wallets: fix a regression during transaction signing that prompts
the user too many times for confirmations (commit 2729909)
* transactions now set nVersion to 2, to mimic Bitcoin Core
* fix Qt bug that made all hw wallets unusable on Windows 8.1 (#4960)
* fix bugs in wallet creation wizard that resulted in corrupted
wallets being created in rare cases (#5082, #5057)
* fix compatibility with Qt 5.12 (#5109)
# Release 3.3.3 - (January 25, 2019)
* Do not expose users to server error messages (#4968)
* Notify users of new releases. Release announcements must be signed,
and they are verified byElectrum using a hardcoded Bitcoin address.
* Hardware wallet fixes (#4991, #4993, #5006)
* Display only QR code in QRcode Window
* Fixed code signing on MacOS
* Randomise locktime of transactions
# Release 3.3.2 - (December 21, 2018)
* Fix Qt history export bug
* Improve network timeouts
* Prepend server transaction_broadcast error messages with
explanatory message. Render error messages as plain text.
# Release 3.3.1 - (December 20, 2018)
* Qt: Fix invoices tab crash (#4941)
* Android: Minor GUI improvements
# Release 3.3.0 - Hodler's Edition (December 19, 2018)
* The network layer has been rewritten using asyncio and aiorpcx.
In addition to easier maintenance, this makes the client
more robust against misbehaving servers.
* The minimum python version was increased to 3.6
* The blockchain headers and fork handling logic has been generalized.
Clients by default now follow chain based on most work, not length.
* New wallet creation defaults to native segwit (bech32).
* Segwit 2FA: TrustedCoin now supports native segwit p2wsh
two-factor wallets.
* RBF batching (opt-in): If the wallet has an unconfirmed RBF
transaction, new payments will be added to that transaction,
instead of creating new transactions.
* MacOS: support QR code scanner in binaries.
* Android APK:
- build using Google NDK instead of Crystax NDK
- target API 28
- do not use external storage (previously for block headers)
* hardware wallets:
- Coldcard now supports spending from p2wpkh-p2sh,
fixed p2pkh signing for fw 1.1.0
- Archos Safe-T mini: fix #4726 signing issue
- KeepKey: full segwit support
- Trezor: refactoring and compat with python-trezor 0.11
- Digital BitBox: support firmware v5.0.0
* fix bitcoin URI handling when app already running (#4796)
* Qt listings rewritten:
the History tab now uses QAbstractItemModel, the other tabs use
QStandardItemModel. Performance should be better for large wallets.
* Several other minor bugfixes and usability improvements.
# Release 3.2.3 - (September 3, 2018) # Release 3.2.3 - (September 3, 2018)
* hardware wallet: the Safe-T mini from Archos is now supported. * hardware wallet: the Safe-T mini from Archos is now supported.
@ -297,7 +223,7 @@ issue #3374. Users should upgrade to 3.0.5.
* Qt GUI: sweeping now uses the Send tab, allowing fees to be set * Qt GUI: sweeping now uses the Send tab, allowing fees to be set
* Windows: if using the installer binary, there is now a separate shortcut * Windows: if using the installer binary, there is now a separate shortcut
for "Electrum Testnet" for "Electrum Testnet"
* Digital Bitbox: added support for p2sh-segwit * Digital Bitbox: added suport for p2sh-segwit
* OS notifications for incoming transactions * OS notifications for incoming transactions
* better transaction size estimation: * better transaction size estimation:
- fees for segwit txns were somewhat underestimated (#3347) - fees for segwit txns were somewhat underestimated (#3347)
@ -525,7 +451,7 @@ issue #3374. Users should upgrade to 3.0.5.
# Release 2.7.7 # Release 2.7.7
* Fix utf8 encoding bug with old wallet seeds (issue #1967) * Fix utf8 encoding bug with old wallet seeds (issue #1967)
* Fix delete request from menu (issue #1968) * Fix delete request from menu (isue #1968)
# Release 2.7.6 # Release 2.7.6
* Fixes a critical bug with imported private keys (issue #1966). Keys * Fixes a critical bug with imported private keys (issue #1966). Keys
@ -888,7 +814,7 @@ issue #3374. Users should upgrade to 3.0.5.
* New 'Receive' tab in the GUI: * New 'Receive' tab in the GUI:
- create and manage payment requests, with QR Codes - create and manage payment requests, with QR Codes
- the former 'Receive' tab was renamed to 'Addresses' - the former 'Receive' tab was renamed to 'Addresses'
- the former Point of Sale plugin is replaced by a resizable - the former Point of Sale plugin is replaced by a resizeable
window that pops up if you click on the QR code window that pops up if you click on the QR code
* The 'Send' tab in the Qt GUI supports transactions with multiple * The 'Send' tab in the Qt GUI supports transactions with multiple
@ -911,7 +837,7 @@ issue #3374. Users should upgrade to 3.0.5.
* The client accepts servers with a CA-signed SSL certificate. * The client accepts servers with a CA-signed SSL certificate.
* ECIES encrypt/decrypt methods, available in the GUI and using * ECIES encrypt/decrypt methods, availabe in the GUI and using
the command line: the command line:
encrypt <pubkey> <message> encrypt <pubkey> <message>
decrypt <pubkey> <message> decrypt <pubkey> <message>
@ -984,7 +910,7 @@ bugfixes: connection problems, transactions staying unverified
# Release 1.8.1 # Release 1.8.1
* Notification option when receiving new transactions * Notification option when receiving new tranactions
* Confirm dialogue before sending large amounts * Confirm dialogue before sending large amounts
* Alternative datafile location for non-windows systems * Alternative datafile location for non-windows systems
* Fix offline wallet creation * Fix offline wallet creation

View File

@ -1,20 +0,0 @@
Source tarballs
===============
1. Build locale files
```
contrib/make_locale
```
2. Prepare python dependencies used by Electrum.
```
contrib/make_packages
```
3. Create source tarball.
```
contrib/make_tgz
```

View File

@ -1,25 +0,0 @@
FROM ubuntu:14.04@sha256:cac55e5d97fad634d954d00a5c2a56d80576a08dcc01036011f26b88263f1578
ENV LC_ALL=C.UTF-8 LANG=C.UTF-8
RUN apt-get update -q && \
apt-get install -qy \
git \
wget \
make \
autotools-dev \
autoconf \
libtool \
xz-utils \
libssl-dev \
zlib1g-dev \
libffi6 \
libffi-dev \
libusb-1.0-0-dev \
libudev-dev \
gettext \
libzbar0 \
&& \
rm -rf /var/lib/apt/lists/* && \
apt-get autoremove -y && \
apt-get clean

View File

@ -1,41 +0,0 @@
AppImage binary for Electrum
============================
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.
1. Install Docker
```
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get update
$ sudo apt-get install -y docker-ce
```
2. Build image
```
$ sudo docker build --no-cache -t electrum-appimage-builder-img contrib/build-linux/appimage
```
3. Build binary
```
$ sudo docker run -it \
--name electrum-appimage-builder-cont \
-v $PWD:/opt/electrum \
--rm \
--workdir /opt/electrum/contrib/build-linux/appimage \
electrum-appimage-builder-img \
./build.sh
```
4. The generated binary is in `./dist`.
## FAQ
### How can I see what is included in the AppImage?
Execute the binary as follows: `./electrum*.AppImage --appimage-extract`

View File

@ -1,11 +0,0 @@
#!/bin/bash
set -e
APPDIR="$(dirname "$(readlink -e "$0")")"
export LD_LIBRARY_PATH="${APPDIR}/usr/lib/:${APPDIR}/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}"
export PATH="${APPDIR}/usr/bin:${PATH}"
export LDFLAGS="-L${APPDIR}/usr/lib/x86_64-linux-gnu -L${APPDIR}/usr/lib"
exec "${APPDIR}/usr/bin/python3.6" -s "${APPDIR}/usr/bin/electrum" "$@"

View File

@ -1,197 +0,0 @@
#!/bin/bash
set -e
PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.."
CONTRIB="$PROJECT_ROOT/contrib"
DISTDIR="$PROJECT_ROOT/dist"
BUILDDIR="$CONTRIB/build-linux/appimage/build/appimage"
APPDIR="$BUILDDIR/electrum.AppDir"
CACHEDIR="$CONTRIB/build-linux/appimage/.cache/appimage"
# pinned versions
PYTHON_VERSION=3.6.8
PKG2APPIMAGE_COMMIT="83483c2971fcaa1cb0c1253acd6c731ef8404381"
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
VERSION=`git describe --tags --dirty --always`
APPIMAGE="$DISTDIR/electrum-$VERSION-x86_64.AppImage"
rm -rf "$BUILDDIR"
mkdir -p "$APPDIR" "$CACHEDIR" "$DISTDIR"
. "$CONTRIB"/build_tools_util.sh
info "downloading some dependencies."
download_if_not_exist "$CACHEDIR/functions.sh" "https://raw.githubusercontent.com/AppImage/pkg2appimage/$PKG2APPIMAGE_COMMIT/functions.sh"
verify_hash "$CACHEDIR/functions.sh" "a73a21a6c1d1e15c0a9f47f017ae833873d1dc6aa74a4c840c0b901bf1dcf09c"
download_if_not_exist "$CACHEDIR/appimagetool" "https://github.com/probonopd/AppImageKit/releases/download/11/appimagetool-x86_64.AppImage"
verify_hash "$CACHEDIR/appimagetool" "c13026b9ebaa20a17e7e0a4c818a901f0faba759801d8ceab3bb6007dde00372"
download_if_not_exist "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz"
verify_hash "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "35446241e995773b1bed7d196f4b624dadcadc8429f26282e756b2fb8a351193"
info "building python."
tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILDDIR"
(
cd "$BUILDDIR/Python-$PYTHON_VERSION"
export SOURCE_DATE_EPOCH=1530212462
./configure \
--cache-file="$CACHEDIR/python.config.cache" \
--prefix="$APPDIR/usr" \
--enable-ipv6 \
--enable-shared \
--with-threads \
-q
make -s
make -s install > /dev/null
)
info "building libsecp256k1."
(
git clone https://github.com/bitcoin-core/secp256k1 "$CACHEDIR"/secp256k1 || (cd "$CACHEDIR"/secp256k1 && git pull)
cd "$CACHEDIR"/secp256k1
git reset --hard "$LIBSECP_VERSION"
git clean -f -x -q
export SOURCE_DATE_EPOCH=1530212462
./autogen.sh
echo "LDFLAGS = -no-undefined" >> Makefile.am
./configure \
--prefix="$APPDIR/usr" \
--enable-module-recovery \
--enable-experimental \
--enable-module-ecdh \
--disable-jni \
-q
make -s
make -s install > /dev/null
)
appdir_python() {
env \
PYTHONNOUSERSITE=1 \
LD_LIBRARY_PATH="$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}" \
"$APPDIR/usr/bin/python3.6" "$@"
}
python='appdir_python'
info "installing pip."
"$python" -m ensurepip
info "preparing electrum-locale."
(
cd "$PROJECT_ROOT"
git submodule update --init
pushd "$CONTRIB"/deterministic-build/electrum-locale
if ! which msgfmt > /dev/null 2>&1; then
echo "Please install gettext"
exit 1
fi
for i in ./locale/*; do
dir="$PROJECT_ROOT/electrum/$i/LC_MESSAGES"
mkdir -p $dir
msgfmt --output-file="$dir/electrum.mo" "$i/electrum.po" || true
done
popd
)
info "installing electrum and its dependencies."
mkdir -p "$CACHEDIR/pip_cache"
"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements.txt"
"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-binaries.txt"
"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-hw.txt"
"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" "$PROJECT_ROOT"
info "copying zbar"
cp "/usr/lib/libzbar.so.0" "$APPDIR/usr/lib/libzbar.so.0"
info "desktop integration."
cp "$PROJECT_ROOT/electrum.desktop" "$APPDIR/electrum.desktop"
cp "$PROJECT_ROOT/electrum/gui/icons/electrum.png" "$APPDIR/electrum.png"
# add launcher
cp "$CONTRIB/build-linux/appimage/apprun.sh" "$APPDIR/AppRun"
info "finalizing AppDir."
(
export PKG2AICOMMIT="$PKG2APPIMAGE_COMMIT"
. "$CACHEDIR/functions.sh"
cd "$APPDIR"
# copy system dependencies
# note: temporarily move PyQt5 out of the way so
# we don't try to bundle its system dependencies.
mv "$APPDIR/usr/lib/python3.6/site-packages/PyQt5" "$BUILDDIR"
copy_deps; copy_deps; copy_deps
move_lib
mv "$BUILDDIR/PyQt5" "$APPDIR/usr/lib/python3.6/site-packages"
# apply global appimage blacklist to exclude stuff
# move usr/include out of the way to preserve usr/include/python3.6m.
mv usr/include usr/include.tmp
delete_blacklisted
mv usr/include.tmp usr/include
)
info "stripping binaries from debug symbols."
strip_binaries()
{
chmod u+w -R "$APPDIR"
{
printf '%s\0' "$APPDIR/usr/bin/python3.6"
find "$APPDIR" -type f -regex '.*\.so\(\.[0-9.]+\)?$' -print0
} | xargs -0 --no-run-if-empty --verbose -n1 strip
}
strip_binaries
remove_emptydirs()
{
find "$APPDIR" -type d -empty -print0 | xargs -0 --no-run-if-empty rmdir -vp --ignore-fail-on-non-empty
}
remove_emptydirs
info "removing some unneeded stuff to decrease binary size."
rm -rf "$APPDIR"/usr/lib/python3.6/test
rm -rf "$APPDIR"/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/translations/qtwebengine_locales
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/resources/qtwebengine_*
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/qml
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Web*
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Designer*
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Qml*
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Quick*
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Location*
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Test*
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Xml*
info "creating the AppImage."
(
cd "$BUILDDIR"
chmod +x "$CACHEDIR/appimagetool"
"$CACHEDIR/appimagetool" --appimage-extract
env VERSION="$VERSION" ./squashfs-root/AppRun --no-appstream --verbose "$APPDIR" "$APPIMAGE"
)
info "done."
ls -la "$DISTDIR"
sha256sum "$DISTDIR"/*

View File

@ -0,0 +1,36 @@
Building Mac OS binaries
========================
This guide explains how to build Electrum binaries for macOS systems.
The build process consists of two steps:
## 1. Building the binary
This needs to be done on a system running macOS or OS X. We use El Capitan (10.11.6) as building it on High Sierra
makes the binaries incompatible with older versions.
Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`).
cd electrum
./contrib/build-osx/make_osx
This creates a folder named Electrum.app.
## 2. Building the image
The usual way to distribute macOS applications is to use image files containing the
application. Although these images can be created on a Mac with the built-in `hdiutil`,
they are not deterministic.
Instead, we use the toolchain that Bitcoin uses: genisoimage and libdmg-hfsplus.
These tools do not work on macOS, so you need a separate Linux machine (or VM).
Copy the Electrum.app directory over and install the dependencies, e.g.:
apt install libcap-dev cmake make gcc faketime
Then you can just invoke `package.sh` with the path to the app:
cd electrum
./contrib/build-osx/package.sh ~/Electrum.app/

12
contrib/build-osx/base.sh Normal file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
RED='\033[0;31m'
BLUE='\033[0,34m'
NC='\033[0m' # No Color
function info {
printf "\r💬 ${BLUE}INFO:${NC} ${1}\n"
}
function fail {
printf "\r🗯 ${RED}ERROR:${NC} ${1}\n"
exit 1
}

View File

@ -5,7 +5,7 @@ PYTHON_VERSION=3.6.4
BUILDDIR=/tmp/electrum-build BUILDDIR=/tmp/electrum-build
PACKAGE=Electrum PACKAGE=Electrum
GIT_REPO=https://github.com/spesmilo/electrum GIT_REPO=https://github.com/spesmilo/electrum
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7" LIBSECP_VERSION=452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4
. $(dirname "$0")/base.sh . $(dirname "$0")/base.sh
@ -16,25 +16,6 @@ export PYTHONHASHSEED=22
VERSION=`git describe --tags --dirty --always` VERSION=`git describe --tags --dirty --always`
which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue" which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue"
which xcodebuild > /dev/null 2>&1 || fail "Please install Xcode and xcode command line tools to continue"
# Code Signing: See https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html
APP_SIGN=""
if [ -n "$1" ]; then
# Test the identity is valid for signing by doing this hack. There is no other way to do this.
cp -f /bin/ls ./CODESIGN_TEST
codesign -s "$1" --dryrun -f ./CODESIGN_TEST > /dev/null 2>&1
res=$?
rm -f ./CODESIGN_TEST
if ((res)); then
fail "Code signing identity \"$1\" appears to be invalid."
fi
unset res
APP_SIGN="$1"
info "Code signing enabled using identity \"$APP_SIGN\""
else
warn "Code signing DISABLED. Specify a valid macOS Developer identity installed on the system as the first argument to this script to enable signing."
fi
info "Installing Python $PYTHON_VERSION" info "Installing Python $PYTHON_VERSION"
export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH" export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH"
@ -48,14 +29,8 @@ pyenv global $PYTHON_VERSION || \
fail "Unable to use Python $PYTHON_VERSION" fail "Unable to use Python $PYTHON_VERSION"
info "install dependencies specific to binaries"
# note that this also installs pinned versions of both pip and setuptools
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user \
|| fail "Could not install pyinstaller"
info "Installing pyinstaller" info "Installing pyinstaller"
python3 -m pip install -I --user pyinstaller==3.4 --no-use-pep517 || fail "Could not install pyinstaller" python3 -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952 -I --user || fail "Could not install pyinstaller"
info "Using these versions for building $PACKAGE:" info "Using these versions for building $PACKAGE:"
sw_vers sw_vers
@ -72,14 +47,13 @@ rm -rf $BUILDDIR > /dev/null 2>&1
mkdir $BUILDDIR mkdir $BUILDDIR
cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./electrum/locale/ cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./electrum/locale/
cp ./contrib/deterministic-build/electrum-icons/icons_rc.py ./electrum/gui/qt/
info "Downloading libusb..." info "Downloading libusb..."
curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \ curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \
tar xz --directory $BUILDDIR tar xz --directory $BUILDDIR
cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/osx cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/build-osx
echo "82c368dfd4da017ceb32b12ca885576f325503428a4966cc09302cbd62702493 contrib/osx/libusb-1.0.dylib" | \
shasum -a 256 -c || fail "libusb checksum mismatched"
info "Building libsecp256k1" info "Building libsecp256k1"
brew install autoconf automake libtool brew install autoconf automake libtool
@ -91,20 +65,12 @@ git clean -f -x -q
./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --disable-jni ./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --disable-jni
make make
popd popd
cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/osx cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/build-osx
info "Building CalinsQRReader..."
d=contrib/osx/CalinsQRReader
pushd $d
rm -fr build
# prefer building using xcode ourselves. otherwise fallback to prebuilt binary
xcodebuild || cp -r prebuilt_qr build || fail "Could not build CalinsQRReader"
popd
DoCodeSignMaybe "CalinsQRReader.app" "${d}/build/Release/CalinsQRReader.app" "$APP_SIGN" # If APP_SIGN is empty will be a noop
info "Installing requirements..." info "Installing requirements..."
python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user || \ python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user || \
fail "Could not install requirements" fail "Could not install requirements"
info "Installing hardware wallet requirements..." info "Installing hardware wallet requirements..."
@ -112,7 +78,7 @@ python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --u
fail "Could not install hardware wallet requirements" fail "Could not install hardware wallet requirements"
info "Building $PACKAGE..." info "Building $PACKAGE..."
python3 -m pip install --user . > /dev/null || fail "Could not build $PACKAGE" python3 setup.py install --user > /dev/null || fail "Could not build $PACKAGE"
info "Faking timestamps..." info "Faking timestamps..."
for d in ~/Library/Python/ ~/.pyenv .; do for d in ~/Library/Python/ ~/.pyenv .; do
@ -122,7 +88,7 @@ for d in ~/Library/Python/ ~/.pyenv .; do
done done
info "Building binary" info "Building binary"
APP_SIGN="$APP_SIGN" pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/osx/osx.spec || fail "Could not build 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" info "Adding bitcoin URI types to Info.plist"
plutil -insert 'CFBundleURLTypes' \ plutil -insert 'CFBundleURLTypes' \
@ -130,14 +96,5 @@ plutil -insert 'CFBundleURLTypes' \
-- dist/$PACKAGE.app/Contents/Info.plist \ -- dist/$PACKAGE.app/Contents/Info.plist \
|| fail "Could not add keys to Info.plist. Make sure the program 'plutil' exists and is installed." || fail "Could not add keys to Info.plist. Make sure the program 'plutil' exists and is installed."
DoCodeSignMaybe "app bundle" "dist/${PACKAGE}.app" "$APP_SIGN" # If APP_SIGN is empty will be a noop
info "Creating .DMG" info "Creating .DMG"
hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG" hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG"
DoCodeSignMaybe ".DMG" "dist/electrum-${VERSION}.dmg" "$APP_SIGN" # If APP_SIGN is empty will be a noop
if [ -z "$APP_SIGN" ]; then
warn "App was built successfully but was not code signed. Users may get security warnings from macOS."
warn "Specify a valid code signing identity as the first argument to this script to enable code signing."
fi

View File

@ -2,50 +2,13 @@
from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
import sys, os import sys
import os
PACKAGE='Electrum' PACKAGE='Electrum'
PYPKG='electrum' PYPKG='electrum'
MAIN_SCRIPT='run_electrum' MAIN_SCRIPT='run_electrum'
ICONS_FILE=PYPKG + '/gui/icons/electrum.icns' ICONS_FILE='electrum.icns'
APP_SIGN = os.environ.get('APP_SIGN', '')
def fail(*msg):
RED='\033[0;31m'
NC='\033[0m' # No Color
print("\r🗯 {}ERROR:{}".format(RED, NC), *msg)
sys.exit(1)
def codesign(identity, binary):
d = os.path.dirname(binary)
saved_dir=None
if d:
# switch to directory of the binary so codesign verbose messages don't include long path
saved_dir = os.path.abspath(os.path.curdir)
os.chdir(d)
binary = os.path.basename(binary)
os.system("codesign -v -f -s '{}' '{}'".format(identity, binary))==0 or fail("Could not code sign " + binary)
if saved_dir:
os.chdir(saved_dir)
def monkey_patch_pyinstaller_for_codesigning(identity):
# Monkey-patch PyInstaller so that we app-sign all binaries *after* they are modified by PyInstaller
# If we app-sign before that point, the signature will be invalid because PyInstaller modifies
# @loader_path in the Mach-O loader table.
try:
import PyInstaller.depend.dylib
_saved_func = PyInstaller.depend.dylib.mac_set_relative_dylib_deps
except (ImportError, NameError, AttributeError):
# Hmm. Likely wrong PyInstaller version.
fail("Could not monkey-patch PyInstaller for code signing. Please ensure that you are using PyInstaller 3.4.")
_signed = set()
def my_func(fn, distname):
_saved_func(fn, distname)
if (fn, distname) not in _signed:
codesign(identity, fn)
_signed.add((fn,distname)) # remember we signed it so we don't sign again
PyInstaller.depend.dylib.mac_set_relative_dylib_deps = my_func
for i, x in enumerate(sys.argv): for i, x in enumerate(sys.argv):
if x == '--name': if x == '--name':
@ -66,17 +29,11 @@ hiddenimports += collect_submodules('keepkeylib')
hiddenimports += collect_submodules('websocket') hiddenimports += collect_submodules('websocket')
hiddenimports += collect_submodules('ckcc') hiddenimports += collect_submodules('ckcc')
# safetlib imports PyQt5.Qt. We use a local updated copy of pinmatrix.py until they
# release a new version that includes https://github.com/archos-safe-t/python-safet/commit/b1eab3dba4c04fdfc1fcf17b66662c28c5f2380e
hiddenimports.remove('safetlib.qt.pinmatrix')
datas = [ datas = [
(electrum + PYPKG + '/*.json', PYPKG), (electrum + PYPKG + '/*.json', PYPKG),
(electrum + PYPKG + '/wordlist/english.txt', PYPKG + '/wordlist'), (electrum + PYPKG + '/wordlist/english.txt', PYPKG + '/wordlist'),
(electrum + PYPKG + '/locale', PYPKG + '/locale'), (electrum + PYPKG + '/locale', PYPKG + '/locale'),
(electrum + PYPKG + '/plugins', PYPKG + '/plugins'), (electrum + PYPKG + '/plugins', PYPKG + '/plugins'),
(electrum + PYPKG + '/gui/icons', PYPKG + '/gui/icons'),
] ]
datas += collect_data_files('trezorlib') datas += collect_data_files('trezorlib')
datas += collect_data_files('safetlib') datas += collect_data_files('safetlib')
@ -84,12 +41,9 @@ datas += collect_data_files('btchip')
datas += collect_data_files('keepkeylib') datas += collect_data_files('keepkeylib')
datas += collect_data_files('ckcc') datas += collect_data_files('ckcc')
# Add the QR Scanner helper app
datas += [(electrum + "contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app", "./contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app")]
# Add libusb so Trezor and Safe-T mini will work # Add libusb so Trezor and Safe-T mini will work
binaries = [(electrum + "contrib/osx/libusb-1.0.dylib", ".")] binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
binaries += [(electrum + "contrib/osx/libsecp256k1.0.dylib", ".")] binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")]
# Workaround for "Retro Look": # Workaround for "Retro Look":
binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]] binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
@ -106,6 +60,7 @@ a = Analysis([electrum+ MAIN_SCRIPT,
electrum+'electrum/commands.py', electrum+'electrum/commands.py',
electrum+'electrum/plugins/cosigner_pool/qt.py', electrum+'electrum/plugins/cosigner_pool/qt.py',
electrum+'electrum/plugins/email_requests/qt.py', electrum+'electrum/plugins/email_requests/qt.py',
electrum+'electrum/plugins/trezor/client.py',
electrum+'electrum/plugins/trezor/qt.py', electrum+'electrum/plugins/trezor/qt.py',
electrum+'electrum/plugins/safe_t/client.py', electrum+'electrum/plugins/safe_t/client.py',
electrum+'electrum/plugins/safe_t/qt.py', electrum+'electrum/plugins/safe_t/qt.py',
@ -124,19 +79,6 @@ for d in a.datas:
a.datas.remove(d) a.datas.remove(d)
break break
# Strip out parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815
qt_bins2remove=('qtweb', 'qt3d', 'qtgame', 'qtdesigner', 'qtquick', 'qtlocation', 'qttest', 'qtxml')
print("Removing Qt binaries:", *qt_bins2remove)
for x in a.binaries.copy():
for r in qt_bins2remove:
if x[0].lower().startswith(r):
a.binaries.remove(x)
print('----> Removed x =', x)
# If code signing, monkey-patch in a code signing step to pyinstaller. See: https://github.com/spesmilo/electrum/issues/4994
if APP_SIGN:
monkey_patch_pyinstaller_for_codesigning(APP_SIGN)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(pyz, exe = EXE(pyz,

View File

@ -85,4 +85,4 @@ dmg dmg Electrum_uncompressed.dmg electrum-$VERSION.dmg || fail "Unable to creat
rm Electrum_uncompressed.dmg rm Electrum_uncompressed.dmg
echo "Done." echo "Done."
sha256sum electrum-$VERSION.dmg md5sum electrum-$VERSION.dmg

View File

@ -1,13 +1,14 @@
#!/bin/bash #!/bin/bash
NAME_ROOT=electrum NAME_ROOT=electrum
PYTHON_VERSION=3.6.6
# These settings probably don't need any change # These settings probably don't need any change
export WINEPREFIX=/opt/wine64 export WINEPREFIX=/opt/wine64
export PYTHONDONTWRITEBYTECODE=1 export PYTHONDONTWRITEBYTECODE=1
export PYTHONHASHSEED=22 export PYTHONHASHSEED=22
PYHOME=c:/python3 PYHOME=c:/python$PYTHON_VERSION
PYTHON="wine $PYHOME/python.exe -OO -B" PYTHON="wine $PYHOME/python.exe -OO -B"
@ -20,7 +21,7 @@ cd tmp
pushd $WINEPREFIX/drive_c/electrum pushd $WINEPREFIX/drive_c/electrum
# Load electrum-locale for this release # Load electrum-icons and electrum-locale for this release
git submodule init git submodule init
git submodule update git submodule update
@ -33,7 +34,7 @@ if ! which msgfmt > /dev/null 2>&1; then
exit 1 exit 1
fi fi
for i in ./locale/*; do for i in ./locale/*; do
dir=$WINEPREFIX/drive_c/electrum/electrum/$i/LC_MESSAGES dir=$WINEPREFIX/drive_c/electrum/electrum/locale/$i/LC_MESSAGES
mkdir -p $dir mkdir -p $dir
msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true
done done
@ -43,6 +44,7 @@ find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
popd popd
cp $WINEPREFIX/drive_c/electrum/LICENCE . cp $WINEPREFIX/drive_c/electrum/LICENCE .
cp $WINEPREFIX/drive_c/electrum/contrib/deterministic-build/electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/electrum/gui/qt/
# Install frozen dependencies # Install frozen dependencies
$PYTHON -m pip install -r ../../deterministic-build/requirements.txt $PYTHON -m pip install -r ../../deterministic-build/requirements.txt
@ -50,7 +52,7 @@ $PYTHON -m pip install -r ../../deterministic-build/requirements.txt
$PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt $PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt
pushd $WINEPREFIX/drive_c/electrum pushd $WINEPREFIX/drive_c/electrum
$PYTHON -m pip install . $PYTHON setup.py install
popd popd
cd .. cd ..
@ -58,7 +60,7 @@ cd ..
rm -rf dist/ rm -rf dist/
# build standalone and portable versions # build standalone and portable versions
wine "$PYHOME/scripts/pyinstaller.exe" --noconfirm --ascii --clean --name $NAME_ROOT-$VERSION -w deterministic.spec wine "C:/python$PYTHON_VERSION/scripts/pyinstaller.exe" --noconfirm --ascii --clean --name $NAME_ROOT-$VERSION -w deterministic.spec
# set timestamps in dist, in order to make the installer reproducible # set timestamps in dist, in order to make the installer reproducible
pushd dist pushd dist
@ -74,4 +76,4 @@ mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe
cd .. cd ..
echo "Done." echo "Done."
sha256sum dist/electrum*exe md5sum dist/electrum*exe

View File

@ -29,8 +29,7 @@ else
git pull git pull
fi fi
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7" git reset --hard 452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4
git reset --hard "$LIBSECP_VERSION"
git clean -f -x -q git clean -f -x -q
build_dll i686-w64-mingw32 # 64-bit would be: x86_64-w64-mingw32 build_dll i686-w64-mingw32 # 64-bit would be: x86_64-w64-mingw32

View File

@ -10,7 +10,8 @@ for i, x in enumerate(sys.argv):
else: else:
raise Exception('no name') raise Exception('no name')
PYHOME = 'c:/python3' PYTHON_VERSION = '3.6.6'
PYHOME = 'c:/python' + PYTHON_VERSION
home = 'C:\\electrum\\' home = 'C:\\electrum\\'
@ -23,11 +24,6 @@ hiddenimports += collect_submodules('keepkeylib')
hiddenimports += collect_submodules('websocket') hiddenimports += collect_submodules('websocket')
hiddenimports += collect_submodules('ckcc') hiddenimports += collect_submodules('ckcc')
# safetlib imports PyQt5.Qt. We use a local updated copy of pinmatrix.py until they
# release a new version that includes https://github.com/archos-safe-t/python-safet/commit/b1eab3dba4c04fdfc1fcf17b66662c28c5f2380e
hiddenimports.remove('safetlib.qt.pinmatrix')
# Add libusb binary # Add libusb binary
binaries = [(PYHOME+"/libusb-1.0.dll", ".")] binaries = [(PYHOME+"/libusb-1.0.dll", ".")]
@ -42,7 +38,6 @@ datas = [
(home+'electrum/locale', 'electrum/locale'), (home+'electrum/locale', 'electrum/locale'),
(home+'electrum/plugins', 'electrum/plugins'), (home+'electrum/plugins', 'electrum/plugins'),
('C:\\Program Files (x86)\\ZBar\\bin\\', '.'), ('C:\\Program Files (x86)\\ZBar\\bin\\', '.'),
(home+'electrum/gui/icons', 'electrum/gui/icons'),
] ]
datas += collect_data_files('trezorlib') datas += collect_data_files('trezorlib')
datas += collect_data_files('safetlib') datas += collect_data_files('safetlib')
@ -62,6 +57,7 @@ a = Analysis([home+'run_electrum',
home+'electrum/commands.py', home+'electrum/commands.py',
home+'electrum/plugins/cosigner_pool/qt.py', home+'electrum/plugins/cosigner_pool/qt.py',
home+'electrum/plugins/email_requests/qt.py', home+'electrum/plugins/email_requests/qt.py',
home+'electrum/plugins/trezor/client.py',
home+'electrum/plugins/trezor/qt.py', home+'electrum/plugins/trezor/qt.py',
home+'electrum/plugins/safe_t/client.py', home+'electrum/plugins/safe_t/client.py',
home+'electrum/plugins/safe_t/qt.py', home+'electrum/plugins/safe_t/qt.py',
@ -83,24 +79,6 @@ for d in a.datas:
a.datas.remove(d) a.datas.remove(d)
break break
# Strip out parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815
qt_bins2remove=('qt5web', 'qt53d', 'qt5game', 'qt5designer', 'qt5quick',
'qt5location', 'qt5test', 'qt5xml', r'pyqt5\qt\qml\qtquick')
print("Removing Qt binaries:", *qt_bins2remove)
for x in a.binaries.copy():
for r in qt_bins2remove:
if x[0].lower().startswith(r):
a.binaries.remove(x)
print('----> Removed x =', x)
qt_data2remove=(r'pyqt5\qt\translations\qtwebengine_locales', )
print("Removing Qt datas:", *qt_data2remove)
for x in a.datas.copy():
for r in qt_data2remove:
if x[0].lower().startswith(r):
a.datas.remove(x)
print('----> Removed x =', x)
# hotfix for #3171 (pre-Win10 binaries) # hotfix for #3171 (pre-Win10 binaries)
a.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\windows')] a.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\windows')]
@ -119,7 +97,7 @@ exe_standalone = EXE(
debug=False, debug=False,
strip=None, strip=None,
upx=False, upx=False,
icon=home+'electrum/gui/icons/electrum.ico', icon=home+'icons/electrum.ico',
console=False) console=False)
# console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used # console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used
@ -132,7 +110,7 @@ exe_portable = EXE(
debug=False, debug=False,
strip=None, strip=None,
upx=False, upx=False,
icon=home+'electrum/gui/icons/electrum.ico', icon=home+'icons/electrum.ico',
console=False) console=False)
##### #####
@ -146,7 +124,7 @@ exe_dependent = EXE(
debug=False, debug=False,
strip=None, strip=None,
upx=False, upx=False,
icon=home+'electrum/gui/icons/electrum.ico', icon=home+'icons/electrum.ico',
console=False) console=False)
coll = COLLECT( coll = COLLECT(
@ -157,6 +135,6 @@ coll = COLLECT(
strip=None, strip=None,
upx=True, upx=True,
debug=False, debug=False,
icon=home+'electrum/gui/icons/electrum.ico', icon=home+'icons/electrum.ico',
console=False, console=False,
name=os.path.join('dist', 'electrum')) name=os.path.join('dist', 'electrum'))

View File

@ -6,36 +6,29 @@ RUN dpkg --add-architecture i386 && \
apt-get update -q && \ apt-get update -q && \
apt-get install -qy \ apt-get install -qy \
wget=1.19.4-1ubuntu2.1 \ wget=1.19.4-1ubuntu2.1 \
gnupg2=2.2.4-1ubuntu1.2 \ gnupg2=2.2.4-1ubuntu1.1 \
dirmngr=2.2.4-1ubuntu1.2 \ dirmngr=2.2.4-1ubuntu1.1 \
python3-software-properties=0.96.24.32.1 \ python3-software-properties=0.96.24.32.1 \
software-properties-common=0.96.24.32.1 software-properties-common=0.96.24.32.1 \
&& \
RUN apt-get update -q && \ wget -nc https://dl.winehq.org/wine-builds/Release.key && \
apt-get install -qy \ apt-key add Release.key && \
git=1:2.17.1-1ubuntu0.4 \ apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/ && \
apt-get update -q && \
apt-get install -qy \
wine-stable-amd64:amd64=3.0.1~bionic \
wine-stable-i386:i386=3.0.1~bionic \
wine-stable:amd64=3.0.1~bionic \
winehq-stable:amd64=3.0.1~bionic \
git=1:2.17.1-1ubuntu0.1 \
p7zip-full=16.02+dfsg-6 \ p7zip-full=16.02+dfsg-6 \
make=4.1-9.1ubuntu1 \ make=4.1-9.1ubuntu1 \
mingw-w64=5.0.3-1 \ mingw-w64=5.0.3-1 \
autotools-dev=20180224.1 \ autotools-dev=20180224.1 \
autoconf=2.69-11 \ autoconf=2.69-11 \
libtool=2.4.6-2 \ libtool=2.4.6-2 \
gettext=0.19.8.1-6 gettext=0.19.8.1-6 \
&& \
RUN wget -nc https://dl.winehq.org/wine-builds/Release.key && \ rm -rf /var/lib/apt/lists/* && \
echo "c51bcb8cc4a12abfbd7c7660eaf90f49674d15e222c262f27e6c96429111b822 Release.key" | sha256sum -c - && \
apt-key add Release.key && \
wget -nc https://dl.winehq.org/wine-builds/winehq.key && \
echo "78b185fabdb323971d13bd329fefc8038e08559aa51c4996de18db0639a51df6 winehq.key" | sha256sum -c - && \
apt-key add winehq.key && \
apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/ && \
apt-get update -q && \
apt-get install -qy \
wine-stable-amd64:amd64=4.0~bionic \
wine-stable-i386:i386=4.0~bionic \
wine-stable:amd64=4.0~bionic \
winehq-stable:amd64=4.0~bionic
RUN rm -rf /var/lib/apt/lists/* && \
apt-get autoremove -y && \ apt-get autoremove -y && \
apt-get clean apt-get clean

View File

@ -20,29 +20,16 @@ folder.
2. Build image 2. Build image
``` ```
$ sudo docker build -t electrum-wine-builder-img contrib/build-wine/docker $ sudo docker build --no-cache -t electrum-wine-builder-img contrib/build-wine/docker
``` ```
Note: see [this](https://stackoverflow.com/a/40516974/7499128) if having dns problems Note: see [this](https://stackoverflow.com/a/40516974/7499128) if having dns problems
3. Build Windows binaries 3. Build Windows binaries
It's recommended to build from a fresh clone
(but you can skip this if reproducibility is not necessary).
```
$ FRESH_CLONE=contrib/build-wine/fresh_clone && \
rm -rf $FRESH_CLONE && \
mkdir -p $FRESH_CLONE && \
cd $FRESH_CLONE && \
git clone https://github.com/spesmilo/electrum.git && \
cd electrum
```
And then build from this directory:
``` ```
$ git checkout $REV $ git checkout $REV
$ sudo docker run -it \ $ sudo docker run \
--name electrum-wine-builder-cont \ --name electrum-wine-builder-cont \
-v $PWD:/opt/wine64/drive_c/electrum \ -v $PWD:/opt/wine64/drive_c/electrum \
--rm \ --rm \

View File

@ -58,7 +58,7 @@
VIAddVersionKey ProductName "${PRODUCT_NAME} Installer" VIAddVersionKey ProductName "${PRODUCT_NAME} Installer"
VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}" VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}"
VIAddVersionKey CompanyName "${PRODUCT_NAME}" VIAddVersionKey CompanyName "${PRODUCT_NAME}"
VIAddVersionKey LegalCopyright "2013-2018 ${PRODUCT_PUBLISHER}" VIAddVersionKey LegalCopyright "2013-2016 ${PRODUCT_PUBLISHER}"
VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer" VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer"
VIAddVersionKey FileVersion ${PRODUCT_VERSION} VIAddVersionKey FileVersion ${PRODUCT_VERSION}
VIAddVersionKey ProductVersion ${PRODUCT_VERSION} VIAddVersionKey ProductVersion ${PRODUCT_VERSION}
@ -72,7 +72,7 @@
!define MUI_ABORTWARNING !define MUI_ABORTWARNING
!define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?" !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?"
!define MUI_ICON "c:\electrum\electrum\gui\icons\electrum.ico" !define MUI_ICON "c:\electrum\icons\electrum.ico"
;-------------------------------- ;--------------------------------
;Pages ;Pages
@ -111,7 +111,7 @@ Section
;Files to pack into the installer ;Files to pack into the installer
File /r "dist\electrum\*.*" File /r "dist\electrum\*.*"
File "c:\electrum\electrum\gui\icons\electrum.ico" File "c:\electrum\icons\electrum.ico"
;Store installation folder ;Store installation folder
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR

View File

@ -1,9 +1,9 @@
#!/bin/bash #!/bin/bash
# Please update these carefully, some versions won't work under Wine # Please update these carefully, some versions won't work under Wine
NSIS_FILENAME=nsis-3.04-setup.exe NSIS_FILENAME=nsis-3.03-setup.exe
NSIS_URL=https://prdownloads.sourceforge.net/nsis/$NSIS_FILENAME?download NSIS_URL=https://prdownloads.sourceforge.net/nsis/$NSIS_FILENAME?download
NSIS_SHA256=4e1db5a7400e348b1b46a4a11b6d9557fd84368e4ad3d4bc4c1be636c89638aa NSIS_SHA256=bd3b15ab62ec6b0c7a00f46022d441af03277be893326f6fea8e212dc2d77743
ZBAR_FILENAME=zbarw-20121031-setup.exe ZBAR_FILENAME=zbarw-20121031-setup.exe
ZBAR_URL=https://sourceforge.net/projects/zbarw/files/$ZBAR_FILENAME/download ZBAR_URL=https://sourceforge.net/projects/zbarw/files/$ZBAR_FILENAME/download
@ -13,25 +13,78 @@ LIBUSB_FILENAME=libusb-1.0.22.7z
LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.22/$LIBUSB_FILENAME?download LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.22/$LIBUSB_FILENAME?download
LIBUSB_SHA256=671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b LIBUSB_SHA256=671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b
PYTHON_VERSION=3.6.8 PYTHON_VERSION=3.6.6
## These settings probably don't need change ## These settings probably don't need change
export WINEPREFIX=/opt/wine64 export WINEPREFIX=/opt/wine64
#export WINEARCH='win32' #export WINEARCH='win32'
PYTHON_FOLDER="python3" PYHOME=c:/python$PYTHON_VERSION
PYHOME="c:/$PYTHON_FOLDER"
PYTHON="wine $PYHOME/python.exe -OO -B" PYTHON="wine $PYHOME/python.exe -OO -B"
# Let's begin! # based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg
here="$(dirname "$(readlink -e "$0")")" verify_signature() {
set -e local file=$1 keyring=$2 out=
if out=$(gpg --no-default-keyring --keyring "$keyring" --status-fd 1 --verify "$file" 2>/dev/null) &&
echo "$out" | grep -qs "^\[GNUPG:\] VALIDSIG "; then
return 0
else
echo "$out" >&2
exit 1
fi
}
. $here/../build_tools_util.sh verify_hash() {
local file=$1 expected_hash=$2
actual_hash=$(sha256sum $file | awk '{print $1}')
if [ "$actual_hash" == "$expected_hash" ]; then
return 0
else
echo "$file $actual_hash (unexpected hash)" >&2
rm "$file"
exit 1
fi
}
download_if_not_exist() {
local file_name=$1 url=$2
if [ ! -e $file_name ] ; then
wget -O $PWD/$file_name "$url"
fi
}
# https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh
retry() {
local result=0
local count=1
while [ $count -le 3 ]; do
[ $result -ne 0 ] && {
echo -e "\nThe command \"$@\" failed. Retrying, $count of 3.\n" >&2
}
! { "$@"; result=$?; }
[ $result -eq 0 ] && break
count=$(($count + 1))
sleep 1
done
[ $count -gt 3 ] && {
echo -e "\nThe command \"$@\" failed 3 times.\n" >&2
}
return $result
}
# Let's begin!
here=$(dirname $(readlink -e $0))
set -e
wine 'wineboot' wine 'wineboot'
# HACK to work around https://bugs.winehq.org/show_bug.cgi?id=42474#c22
# needed for python 3.6+
rm -f /opt/wine-stable/lib/wine/fakedlls/api-ms-win-core-path-l1-1-0.dll
rm -f /opt/wine-stable/lib/wine/api-ms-win-core-path-l1-1-0.dll.so
cd /tmp/electrum-build cd /tmp/electrum-build
@ -52,21 +105,31 @@ for msifile in core dev exe lib pip tools; do
wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi"
wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc" wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc"
verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV
wine msiexec /i "${msifile}.msi" /qb TARGETDIR=$PYHOME wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION
done done
# Install dependencies specific to binaries # upgrade pip
# note that this also installs pinned versions of both pip and setuptools $PYTHON -m pip install pip --upgrade
$PYTHON -m pip install -r "$here"/../deterministic-build/requirements-binaries.txt
# Install pywin32-ctypes (needed by pyinstaller)
$PYTHON -m pip install pywin32-ctypes==0.1.2
# install PySocks
$PYTHON -m pip install win_inet_pton==1.0.1
$PYTHON -m pip install -r $here/../deterministic-build/requirements-binaries.txt
# Install PyInstaller # Install PyInstaller
$PYTHON -m pip install pyinstaller==3.4 --no-use-pep517 $PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip
# Install ZBar # Install ZBar
download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL" download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL"
verify_hash $ZBAR_FILENAME "$ZBAR_SHA256" verify_hash $ZBAR_FILENAME "$ZBAR_SHA256"
wine "$PWD/$ZBAR_FILENAME" /S wine "$PWD/$ZBAR_FILENAME" /S
# Upgrade setuptools (so Electrum can be installed later)
$PYTHON -m pip install setuptools --upgrade
# Install NSIS installer # Install NSIS installer
download_if_not_exist $NSIS_FILENAME "$NSIS_URL" download_if_not_exist $NSIS_FILENAME "$NSIS_URL"
verify_hash $NSIS_FILENAME "$NSIS_SHA256" verify_hash $NSIS_FILENAME "$NSIS_SHA256"
@ -76,7 +139,10 @@ download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL"
verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256" verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256"
7z x -olibusb $LIBUSB_FILENAME -aoa 7z x -olibusb $LIBUSB_FILENAME -aoa
cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/$PYTHON_FOLDER/ cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/python$PYTHON_VERSION/
# add dlls needed for pyinstaller:
cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/
mkdir -p $WINEPREFIX/drive_c/tmp mkdir -p $WINEPREFIX/drive_c/tmp
cp secp256k1/libsecp256k1.dll $WINEPREFIX/drive_c/tmp/ cp secp256k1/libsecp256k1.dll $WINEPREFIX/drive_c/tmp/

View File

@ -1,69 +0,0 @@
#!/usr/bin/env bash
RED='\033[0;31m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
function info {
printf "\r💬 ${BLUE}INFO:${NC} ${1}\n"
}
function fail {
printf "\r🗯 ${RED}ERROR:${NC} ${1}\n"
exit 1
}
function warn {
printf "\r⚠ ${YELLOW}WARNING:${NC} ${1}\n"
}
# based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg
function verify_signature() {
local file=$1 keyring=$2 out=
if out=$(gpg --no-default-keyring --keyring "$keyring" --status-fd 1 --verify "$file" 2>/dev/null) &&
echo "$out" | grep -qs "^\[GNUPG:\] VALIDSIG "; then
return 0
else
echo "$out" >&2
exit 1
fi
}
function verify_hash() {
local file=$1 expected_hash=$2
actual_hash=$(sha256sum $file | awk '{print $1}')
if [ "$actual_hash" == "$expected_hash" ]; then
return 0
else
echo "$file $actual_hash (unexpected hash)" >&2
rm "$file"
exit 1
fi
}
function download_if_not_exist() {
local file_name=$1 url=$2
if [ ! -e $file_name ] ; then
wget -O $file_name "$url"
fi
}
# https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh
function retry() {
local result=0
local count=1
while [ $count -le 3 ]; do
[ $result -ne 0 ] && {
echo -e "\nThe command \"$@\" failed. Retrying, $count of 3.\n" >&2
}
! { "$@"; result=$?; }
[ $result -eq 0 ] && break
count=$(($count + 1))
sleep 1
done
[ $count -gt 3 ] && {
echo -e "\nThe command \"$@\" failed 3 times.\n" >&2
}
return $result
}

View File

@ -18,6 +18,13 @@ function get_git_mtime {
fail=0 fail=0
for f in icons/* "icons.qrc"; do
if (( $(get_git_mtime "$f") > $(get_git_mtime "contrib/deterministic-build/electrum-icons/") )); then
echo "Modification time of $f (" $(get_git_mtime --readable "$f") ") is newer than"\
"last update of electrum-icons"
fail=1
fi
done
if [ $(date +%s -d "2 weeks ago") -gt $(get_git_mtime "contrib/deterministic-build/electrum-locale/") ]; then if [ $(date +%s -d "2 weeks ago") -gt $(get_git_mtime "contrib/deterministic-build/electrum-locale/") ]; then
echo "Last update from electrum-locale is older than 2 weeks."\ echo "Last update from electrum-locale is older than 2 weeks."\

@ -0,0 +1 @@
Subproject commit 0b8cbcca428ceb791527bcbb2ef2b36b4ab29c73

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

View File

@ -1,10 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
try: import requests
import requests
except ImportError as e:
sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'")
def check_restriction(p, r): def check_restriction(p, r):

View File

@ -1,56 +1,58 @@
pip==19.0.1 \ pip==18.0 \
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \ --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5 --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
pycryptodomex==3.7.3 \ pycryptodomex==3.6.6 \
--hash=sha256:0bda549e20db1eb8e29fb365d10acf84b224d813b1131c828fc830b2ce313dcd \ --hash=sha256:0cf562fc5e5ddbe935bb6162d84a7e46e19edba8ac6609587ab9e78dc7c527d4 \
--hash=sha256:1210c0818e5334237b16d99b5785aa0cee815d9997ee258bd5e2936af8e8aa50 \ --hash=sha256:13b77b7a177a2fd0beb42db84b21d7d4ab646dfd223989a3b5fa6a6901075ae8 \
--hash=sha256:2090dc8cd7843eae75bd504b9be86792baa171fc5a758ea3f60188ab67ca95cf \ --hash=sha256:1baf0d485853cfa8f87c969148dc040ae81a8de3dcaaeb07b3f40a90ab2ac637 \
--hash=sha256:22e6784b65dfdd357bf9a8a842db445192b227103e2c3137a28c489c46742135 \ --hash=sha256:1dac67655206c92eabf827cc3c9f9efd35d699057f12185f01245b22e02db5f8 \
--hash=sha256:2edb8c3965a77e3092b5c5c1233ffd32de083f335202013f52d662404191ac79 \ --hash=sha256:1dc3abcb853e8b2188d0c8d624dbdc575a1f23ae82feb9a99e4a69b7064df81a \
--hash=sha256:310fe269ac870135ff610d272e88dcb594ee58f40ac237a688d7c972cbca43e8 \ --hash=sha256:24bf22fffba3a7c1132a039d95256ca9e5016798111c7bf7d0091bfddb0e0245 \
--hash=sha256:456136b7d459f000794a67b23558351c72e21f0c2d4fcaa09fc99dae7844b0ef \ --hash=sha256:26fde532deaed9781643be654b25fc899b97866b68e50bc015aadd442387c42b \
--hash=sha256:463e49a9c5f1fa7bd36aff8debae0b5c487868c1fb66704529f2ad7e92f0cc9f \ --hash=sha256:2958eceb671795ec4e5f2164e4ec5ddc1788fdf4f72b18390f548de5e11c9dcc \
--hash=sha256:4a33b2828799ef8be789a462e6645ea6fe2c42b0df03e6763ccbfd1789c453e6 \ --hash=sha256:34b3218fcdcd59c26de75c4ff8a15e6472e7b88f9e5b3b193cb1c001e7a0e815 \
--hash=sha256:5ff02dff1b03929e6339226b318aa59bd0b5c362f96e3e0eb7f3401d30594ed3 \ --hash=sha256:3f4d96cfa963960d190d3d6ef5e8f7c2c14815ff816999dff5efaa6bf7cd1134 \
--hash=sha256:6b1db8234b8ee2b30435d9e991389c2eeae4d45e09e471ffe757ba1dfae682bb \ --hash=sha256:40063250911af6ec5b3ccc80108e7a3be9d1695dd3d17104a1930861cbdb78b7 \
--hash=sha256:6eb67ee02de143cd19e36a52bd3869a9dc53e9184cd6bed5c39ff71dee2f6a45 \ --hash=sha256:4b8669f4b3fa386696b9b0366e69898dd626b119eaead50ee3b2c59c56222272 \
--hash=sha256:6f42eea5afc7eee29494fdfddc6bb7173953d4197d9200e4f67096c2a24bc21b \ --hash=sha256:5144ad0df1009cce6d03b71c2e5843ed96be91ab746e58e8e64ef9bca5209bfe \
--hash=sha256:87bc8082e2de2247df7d0b161234f8edb1384294362cc0c8db9324463097578b \ --hash=sha256:549d0a0884e60d876b975e18e791cf2aece077cb174f0d7d042982b5d36a6279 \
--hash=sha256:8df93d34bc0e3a28a27652070164683a07d8a50c628119d6e0f7710f4d01b42f \ --hash=sha256:5b365bc3da05fbc15acd6600d41f84fe8ed903400566bc0d1a0fadc862b41c8e \
--hash=sha256:989952c39e8fef1c959f0a0f85656e29c41c01162e33a3f5fd8ce71e47262ae9 \ --hash=sha256:6ab64112924be1905a22656166ec0eeeba275fb15c15adba6470bd030362e160 \
--hash=sha256:a4a203077e2f312ec8677dde80a5c4e6fe5a82a46173a8edc8da668602a3e073 \ --hash=sha256:a06e0e0965145cc0d625e1a031b49705aa2c6b4e16aa0044d4719fb1d639977f \
--hash=sha256:a793c1242dffd39f585ae356344e8935d30f01f6be7d4c62ffc87af376a2f5f9 \ --hash=sha256:a472d2271cfb37940ad80c4698affeaf39552e4c19ceeac4d5290fe71519e3f4 \
--hash=sha256:b70fe991564e178af02ccf89435a8f9e8d052707a7c4b95bf6027cb785da3175 \ --hash=sha256:bfc315eef8cdae280e404df64cf71f8673abcc81e646699b15722f41b82a4ed7 \
--hash=sha256:b83594196e3661cb78c97b80a62fbfbba2add459dfd532b58e7a7c62dd06aab4 \ --hash=sha256:c1d4ea2382c1726ff65548f41dc79bd9d0345f3a47ba128768242eea470c6515 \
--hash=sha256:ba27725237d0a3ea66ec2b6b387259471840908836711a3b215160808dffed0f \ --hash=sha256:c54d3d2e3b099dae61124d7476311ab594df7eeec305f612a5eb79226cf36b53 \
--hash=sha256:d1ab8ad1113cdc553ca50c4d5f0142198c317497364c0c70443d69f7ad1c9288 \ --hash=sha256:d0d448484e161786922b41e112b5d7cf76ef5f0c725ea5107ef866bb14a38b12 \
--hash=sha256:dce039a8a8a318d7af83cae3fd08d58cefd2120075dfac0ae14d706974040f63 \ --hash=sha256:ddf10a2a071ae749158d8fdad0cbc78f6197219721afa91e0752dd999fddf422 \
--hash=sha256:e3213037ea33c85ab705579268cbc8a4433357e9fb99ec7ce9fdcc4d4eec1d50 \ --hash=sha256:e0cc6ec20e6dfce1a6715ce60d71df3e9491461d525980821f71223026950b0e \
--hash=sha256:ec8d8023d31ef72026d46e9fb301ff8759eff5336bcf3d1510836375f53f96a9 \ --hash=sha256:e5fcb23f936912b6cfb475c7b115aa2e31b7fed409bc947c6190503d9f7d0338 \
--hash=sha256:ece65730d50aa57a1330d86d81582a2d1587b2ca51cb34f586da8551ddc68fee \ --hash=sha256:e76d0148ffa5b86f1bcb111bd3950bc61fd50bed8b6b1cf9f5e56784378b55b6 \
--hash=sha256:ed21fc515e224727793e4cc3fb3d00f33f59e3a167d3ad6ac1475ab3b05c2f9e \ --hash=sha256:ea09a059e4cf3d99a446f0a80ac9be54802542226cdbf8ffeea79a6ddc77f310 \
--hash=sha256:eec1132d878153d61a05424f35f089f951bd6095a4f6c60bdd2ef8919d44425e --hash=sha256:f5844faf085ad3d92fb94d07347c69f193c0bd1f32843329c2a422addeb042c9 \
PyQt5==5.11.3 \ --hash=sha256:f8478a443bb747c2ef1d508a09eeb57ff8c2ebbd8ea0e6e2351d483c23e725b3 \
--hash=sha256:517e4339135c4874b799af0d484bc2e8c27b54850113a68eec40a0b56534f450 \ --hash=sha256:ff31cb6cd69c6683bd44e99bc4f36cb74a338cee58a3a9c5c19a8ee4bdd2f3b2
--hash=sha256:ac1eb5a114b6e7788e8be378be41c5e54b17d5158994504e85e43b5fca006a39 \ PyQt5==5.10.1 \
--hash=sha256:d2309296a5a79d0a1c0e6c387c30f0398b65523a6dcc8a19cc172e46b949e00d \ --hash=sha256:1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac \
--hash=sha256:e85936bae1581bcb908847d2038e5b34237a5e6acc03130099a78930770e7ead --hash=sha256:4db7113f464c733a99fcb66c4c093a47cf7204ad3f8b3bda502efcc0839ac14b \
PyQt5-sip==4.19.13 \ --hash=sha256:9c17ab3974c1fc7bbb04cc1c9dae780522c0ebc158613f3025fccae82227b5f7 \
--hash=sha256:125f77c087572c9272219cda030a63c2f996b8507592b2a54d7ef9b75f9f054d \ --hash=sha256:f6035baa009acf45e5f460cf88f73580ad5dc0e72330029acd99e477f20a5d61
--hash=sha256:14c37b06e3fb7c2234cb208fa461ec4e62b4ba6d8b32ca3753c0b2cfd61b00e3 \ setuptools==40.2.0 \
--hash=sha256:1cb2cf52979f9085fc0eab7e0b2438eb4430d4aea8edec89762527e17317175b \ --hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
--hash=sha256:4babef08bccbf223ec34464e1ed0a23caeaeea390ca9a3529227d9a57f0d6ee4 \ --hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
--hash=sha256:53cb9c1208511cda0b9ed11cffee992a5a2f5d96eb88722569b2ce65ecf6b960 \ SIP==4.19.8 \
--hash=sha256:549449d9461d6c665cbe8af4a3808805c5e6e037cd2ce4fd93308d44a049bfac \ --hash=sha256:09f9a4e6c28afd0bafedb26ffba43375b97fe7207bd1a0d3513f79b7d168b331 \
--hash=sha256:5f5b3089b200ff33de3f636b398e7199b57a6b5c1bb724bdb884580a072a14b5 \ --hash=sha256:105edaaa1c8aa486662226360bd3999b4b89dd56de3e314d82b83ed0587d8783 \
--hash=sha256:a4d9bf6e1fa2dd6e73f1873f1a47cee11a6ba0cf9ba8cf7002b28c76823600d0 \ --hash=sha256:1bb10aac55bd5ab0e2ee74b3047aa2016cfa7932077c73f602a6f6541af8cd51 \
--hash=sha256:a4ee6026216f1fbe25c8847f9e0fbce907df5b908f84816e21af16ec7666e6fe \ --hash=sha256:265ddf69235dd70571b7d4da20849303b436192e875ce7226be7144ca702a45c \
--hash=sha256:a91a308a5e0cc99de1e97afd8f09f46dd7ca20cfaa5890ef254113eebaa1adff \ --hash=sha256:52074f7cb5488e8b75b52f34ec2230bc75d22986c7fe5cd3f2d266c23f3349a7 \
--hash=sha256:b0342540da479d2713edc68fb21f307473f68da896ad5c04215dae97630e0069 \ --hash=sha256:5ff887a33839de8fc77d7f69aed0259b67a384dc91a1dc7588e328b0b980bde2 \
--hash=sha256:f997e21b4e26a3397cb7b255b8d1db5b9772c8e0c94b6d870a5a0ab5c27eacaa --hash=sha256:74da4ddd20c5b35c19cda753ce1e8e1f71616931391caeac2de7a1715945c679 \
setuptools==40.8.0 \ --hash=sha256:7d69e9cf4f8253a3c0dfc5ba6bb9ac8087b8239851f22998e98cb35cfe497b68 \
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \ --hash=sha256:97bb93ee0ef01ba90f57be2b606e08002660affd5bc380776dd8b0fcaa9e093a \
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab --hash=sha256:cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85 \
wheel==0.32.3 \ --hash=sha256:d9023422127b94d11c1a84bfa94933e959c484f2c79553c1ef23c69fe00d25f8 \
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \ --hash=sha256:e72955e12f4fccf27aa421be383453d697b8a44bde2cc26b08d876fd492d0174
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44 wheel==0.31.1 \
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f

View File

@ -1,56 +1,52 @@
btchip-python==0.1.28 \ btchip-python==0.1.28 \
--hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83 --hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83
certifi==2018.11.29 \ certifi==2018.8.24 \
--hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \ --hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \
--hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033 --hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a
chardet==3.0.4 \ chardet==3.0.4 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
ckcc-protocol==0.7.2 \ ckcc-protocol==0.7.2 \
--hash=sha256:31ee5178cfba8895eb2a6b8d06dc7830b51461a0ff767a670a64707c63e6b264 \ --hash=sha256:31ee5178cfba8895eb2a6b8d06dc7830b51461a0ff767a670a64707c63e6b264 \
--hash=sha256:498db4ccdda018cd9f40210f5bd02ddcc98e7df583170b2eab4035c86c3cc03b --hash=sha256:498db4ccdda018cd9f40210f5bd02ddcc98e7df583170b2eab4035c86c3cc03b
click==7.0 \ click==6.7 \
--hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ --hash=sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d \
--hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 --hash=sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b
construct==2.9.45 \ Cython==0.28.5 \
--hash=sha256:2271a0efd0798679dea825ff47e22a4c550456a5db0ba8baa82f7eae0af0118c --hash=sha256:022592d419fc754509d0e0461eb2958dbaa45fb60d51c8a61778c58994edbe36 \
Cython==0.29.4 \ --hash=sha256:07659f4c57582104d9486c071de512fbd7e087a3a630535298442cc0e20a3f5a \
--hash=sha256:004eeb2fc64e9db4a3bc0d65583d69769c7242d29d9335121cbab776688dc122 \ --hash=sha256:13c73e2ffa93a615851e03fad97591954d143b5b62361b9adef81f46a31cd8ef \
--hash=sha256:028ee8571884a129e0d5c4d48296f6b3ea679668c096bb65fe8b2ff7ac29d707 \ --hash=sha256:13eab5a2835a84ff62db343035603044c908d2b3b6eec09d67fdf9970acf7ac9 \
--hash=sha256:162b8b794ca9210c7039d54b6d96cd342e0404e41e7e467baae69f0252d7e52a \ --hash=sha256:183b35a48f58862c4ec1e821f07bb7b1156c8c8559c85c32ae086f28947474eb \
--hash=sha256:1aba4cf581d203e8fa3b6a7b432b09416e4f93c0d1f7744834acacfe3e9db424 \ --hash=sha256:2f526b0887128bf20ab2acc905a975f62b5a04ab2f63ecbe5a30fc28285d0e0c \
--hash=sha256:1be8f08c87b92a880f2fd19f93293e738ca8647834ad05625635320cec9ecad4 \ --hash=sha256:32de8637f5e6c5a76667bc7c8fc644bd9314dc19af36db8ce30a0b92ada0f642 \
--hash=sha256:21c707a811912aeb65abe8a66e5adebc759889661c8f4cf677523cd33c609084 \ --hash=sha256:4172c183ef4fb2ace6a29cdf7fc9200c5a471a7f775ff691975b774bd9ed3ad2 \
--hash=sha256:234de250ef09ba667fc6a8f6ba07712d3fe5bb8d92d70d2b958d4c56e3172c4a \ --hash=sha256:553956ec06ecbd731ef0c538eb28a5b46bedea7ab89b18237ff28b4b99d65eee \
--hash=sha256:33dad82003df518e1242ac3b0592fc63c49d65d0d37b696cb43b7d35085e6bd5 \ --hash=sha256:660eeb6870687fd3eda91e00ba4e72220545c254c8c4d967fd0c910f4fbb8cbc \
--hash=sha256:54ee6cbc1397b27670e598ae15cab36e826a01605f63bf267a5fd2642bd8a147 \ --hash=sha256:693a8619ef066ece055ed065a15cf440f9d3ebd1bca60e87ea19144833756433 \
--hash=sha256:6058c57657d2704c9fad8a56458173d2f525dce4083ca46e9b99b1b35da2b27f \ --hash=sha256:759c799e9ef418f163b5412e295e14c0a48fe3b4dcba9ab8aab69e9f511cfefd \
--hash=sha256:6d3065f39ea1354eba4807e2752e97d57f26d6f68bc4a4c561264ca4300c46cb \ --hash=sha256:827d3a91b7a7c31ce69e5974496fd9a8ba28eb498b988affb66d0d30de11d934 \
--hash=sha256:7059e5acac1d7a82e75e553924d9ea59b0e79203adf903cb999287fbcc8f50f1 \ --hash=sha256:87e57b5d730cfab225d95e7b23abbc0c6f77598bd66639e93c73ce8afbae6f38 \
--hash=sha256:71c31e01f20a3a7273f6f38760d29170ee89e895be540481130cb173ef6b7246 \ --hash=sha256:9400e5db8383346b0694a3e794d8bded18a27b21123516dcdf4b79d7ec28e98b \
--hash=sha256:89225447801e8bd0f6d8e2c0807ded83af8ad7bf4086b5ecf1f22c5a68d1b3e3 \ --hash=sha256:9ec27681c5b1b457aacb1cbda5db04aa28b76da2af6e1e1fd15f233eafe6a0b0 \
--hash=sha256:9783f11fe4a4af66b0aa0da68fda833c10b95edd9099a6dbe710d03bcb96adf2 \ --hash=sha256:ae4784f040a3313c8bd00c8d04934b7ade63dc59692d8f00a5235be8ed72a445 \
--hash=sha256:9a0be0aac30d71fe490a2b0377fca6e13a5242ecc01d09c7a358f1f2fcb07a80 \ --hash=sha256:b2ba8310ebd3c0e0b884d5e95bbd99d467d6af922acd1e44fe4b819839b2150e \
--hash=sha256:9a2cccc26dcf2df1e0048cdf63bd714f1d5dfad457f03b9938c5cc3eef74c9ab \ --hash=sha256:b64575241f64f6ec005a4d4137339fb0ba5e156e826db2fdb5f458060d9979e0 \
--hash=sha256:b0889310f8558eb406a4a853d63553b90c621476f1b5b80b46b1ff57eef198cf \ --hash=sha256:c78ad0df75a9fc03ab28ca1b950c893a208c451a18f76796c3e25817d6994001 \
--hash=sha256:c46ef7b771c88512435399e5ffbc3a70079d4945123d6fbfc6211b4cfdc4e546 \ --hash=sha256:cdbb917e41220bd3812234dbe59d15391adbc2c5d91ae11a5273aab9e32ba7ec \
--hash=sha256:c71a77c1047d65e5b4e614053cbb7b567c36359b2bc1d27fba23b984ab6dddd0 \ --hash=sha256:d2223a80c623e2a8e97953ab945dfaa9385750a494438dcb55562eb1ddd9565a \
--hash=sha256:c9361811a1a49db11efce54fedd01a5544af8db074fce471c720bdb85ec9c7a8 \ --hash=sha256:e22f21cf92a9f8f007a280e3b3462c886d9068132a6c698dec10ad6125e3ca1e \
--hash=sha256:d021a8326a1d2cdb182b0dd7f49bb42d8a4e6ddfb3c8d388ee5be26d57d49f3b \ --hash=sha256:ea5c16c48e561f4a6f6b8c24807494b77a79e156b8133521c400f22ca712101b \
--hash=sha256:d1ee3d39c73a094ae5b6e2f9263ae0dc61af1b549a0869ade8c3c30325ed9f26 \ --hash=sha256:ee7a9614d51fe16e32ca5befe72e0808baff481791728449d0b17c8b0fe29eb9 \
--hash=sha256:d49d7cf82192edc6e386262a07ceb3515028afbd9009dd8ec669d2c0a9f20128 \ --hash=sha256:ef86de9299e4ab2ebb129fb84b886bf40b9aced9807c6d6d5f28b46fb905f82c \
--hash=sha256:dc5fc1fa072a98f152e46465aaf3e02b3ea36a9d3b8c79bfabd47b0e3ad9226c \ --hash=sha256:f3e4860f5458a9875caa3de65e255720c0ed2ce71f0bcdab02497b32104f9db8 \
--hash=sha256:e290fed7fe73860657af564e596fff87e75cfda861c067e89212970a47826cc6 \ --hash=sha256:fc6c20a8ac22202a779ad4c59756647be0826993d2151a03c015e76d2368ae5f
--hash=sha256:fcf9a9a566ab98495db641eefee471eb03df71e394ee51fdfa9b4c0b9f6928eb \
--hash=sha256:fe8c1d2538867bf2753988a4a2d548bcb211fcbba125aa3e9092391b16f47b56
ecdsa==0.13 \ ecdsa==0.13 \
--hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \
--hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa
hidapi==0.7.99.post21 \ hidapi==0.7.99.post21 \
--hash=sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24 \ --hash=sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24 \
--hash=sha256:6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3 \
--hash=sha256:8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946 \ --hash=sha256:8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946 \
--hash=sha256:92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7 \
--hash=sha256:b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87 \ --hash=sha256:b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87 \
--hash=sha256:bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660 \ --hash=sha256:bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660 \
--hash=sha256:c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7 \ --hash=sha256:c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7 \
@ -58,30 +54,26 @@ hidapi==0.7.99.post21 \
--hash=sha256:d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b \ --hash=sha256:d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b \
--hash=sha256:e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97 \ --hash=sha256:e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97 \
--hash=sha256:edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922 --hash=sha256:edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922
idna==2.8 \ idna==2.7 \
--hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \ --hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
--hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c --hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16
keepkey==6.0.2 \ keepkey==4.0.2 \
--hash=sha256:3236dd701bde74768c41a92e724e322ea5e01b90985e2e6215eb85b77f9a0ae1 \ --hash=sha256:cddee60ae405841cdff789cbc54168ceaeb2282633420f2be155554c25c69138
--hash=sha256:677e07deacc2ff97bee313b8dd7ae55faebab02e7d17b9a8e49b889996a36010 \ libusb1==1.6.6 \
--hash=sha256:af107f610fb0e2417fc7a9d87a2fa22aac9b80b79559370d178be424bb85489a --hash=sha256:a49917a2262cf7134396f6720c8be011f14aabfc5cdc53f880cc672c0f39d271
libusb1==1.7 \
--hash=sha256:9d4f66d2ed699986b06bc3082cd262101cb26af7a76a34bd15b7eb56cba37e0f
mnemonic==0.18 \ mnemonic==0.18 \
--hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d --hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d
pbkdf2==1.3 \ pbkdf2==1.3 \
--hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979 --hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979
pip==19.0.1 \ pip==18.0 \
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \ --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5 --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
protobuf==3.6.1 \ protobuf==3.6.1 \
--hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \ --hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \
--hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \ --hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \
--hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \ --hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \
--hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \ --hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \
--hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \ --hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \
--hash=sha256:4b92e235a3afd42e7493b281c8b80c0c65cbef45de30f43d571d1ee40a1f77ef \
--hash=sha256:574085a33ca0d2c67433e5f3e9a0965c487410d6cb3406c83bdaf549bfc2992e \
--hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \ --hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \
--hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \ --hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \
--hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \ --hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \
@ -104,31 +96,27 @@ pyblake2==1.1.2 \
--hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \ --hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \
--hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \ --hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \
--hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358 --hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358
requests==2.21.0 \ requests==2.19.1 \
--hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \ --hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
--hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b --hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
safet==0.1.4 \ safet==0.1.4 \
--hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \ --hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \
--hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1 --hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1
setuptools==40.8.0 \ setuptools==40.2.0 \
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \ --hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab --hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
six==1.12.0 \ six==1.11.0 \
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
trezor==0.11.1 \ trezor==0.10.2 \
--hash=sha256:6043f321d856e1b45b9df0c37810264f08d065bb56cd999f61a05fe2906e9e18 \ --hash=sha256:4dba4d5c53d3ca22884d79fb4aa68905fb8353a5da5f96c734645d8cf537138d \
--hash=sha256:6119b30cf9a136667753935bd06c5f341e78950b35e8ccbadaecc65c12f1946d --hash=sha256:d2b32f25982ab403758d870df1d0de86d0751c106ef1cd1289f452880ce68b84
typing-extensions==3.7.2 \ urllib3==1.23 \
--hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \ --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
--hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \ --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
--hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71 websocket-client==0.52.0 \
urllib3==1.24.1 \ --hash=sha256:03763384c530b331ec3822d0b52ffdc28c3aeb8a900ac8c98b2ceea3128a7b4e \
--hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ --hash=sha256:3c9924675eaf0b27ae22feeeab4741bb4149b94820bd3a143eeaf8b62f64d821
--hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 wheel==0.31.1 \
websocket-client==0.54.0 \ --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
--hash=sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786 \ --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
--hash=sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849
wheel==0.32.3 \
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44

View File

@ -1,99 +1,96 @@
aiohttp==3.5.4 \ aiohttp==3.4.4 \
--hash=sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55 \ --hash=sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b \
--hash=sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed \ --hash=sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08 \
--hash=sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10 \ --hash=sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd \
--hash=sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5 \ --hash=sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac \
--hash=sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1 \ --hash=sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650 \
--hash=sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939 \ --hash=sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa \
--hash=sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390 \ --hash=sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95 \
--hash=sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa \ --hash=sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330 \
--hash=sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc \ --hash=sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc \
--hash=sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5 \ --hash=sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b \
--hash=sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d \ --hash=sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de \
--hash=sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf \ --hash=sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4 \
--hash=sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6 \ --hash=sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7 \
--hash=sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72 \ --hash=sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b \
--hash=sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12 \ --hash=sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8 \
--hash=sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366 \ --hash=sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd \
--hash=sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4 \ --hash=sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2 \
--hash=sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300 \ --hash=sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698 \
--hash=sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d \ --hash=sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95 \
--hash=sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303 \ --hash=sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6 \
--hash=sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6 \ --hash=sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0 \
--hash=sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889 --hash=sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07
aiohttp-socks==0.2.2 \ aiohttp_socks==0.1.6 \
--hash=sha256:e473ee222b001fe33798957b9ce3352b32c187cf41684f8e2259427925914993 \ --hash=sha256:943148a3797ba9ffb6df6ddb006ffdd40538885b410589d589bda42a8e8bcd5a
--hash=sha256:eebd8939a7c3c1e3e7e1b2552c60039b4c65ef6b8b2351efcbdd98290538e310 aiorpcX==0.7.3 \
aiorpcX==0.10.4 \ --hash=sha256:24dd4fe2f65f743cb74c8626570470e325bb777bb66d1932e7d2965ae71d1164 \
--hash=sha256:7130105d31230f069b0eea4e1893c7199cfe2d89a52a31aec718d37f4449935d \ --hash=sha256:5120ca40beef6b6a45d3a7055e343815401385dc607da2fd93baca2762c8a97d
--hash=sha256:e6dfd584f597ee3aa6a8d4cb5755c8ffbbe42754f32728561d9e5940379d5096 async_timeout==3.0.0 \
async_timeout==3.0.1 \ --hash=sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c \
--hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \ --hash=sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287
--hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3
attrs==18.2.0 \ attrs==18.2.0 \
--hash=sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69 \ --hash=sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69 \
--hash=sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb --hash=sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb
certifi==2018.11.29 \ certifi==2018.8.24 \
--hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \ --hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \
--hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033 --hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a
chardet==3.0.4 \ chardet==3.0.4 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
dnspython==1.16.0 \ dnspython==1.15.0 \
--hash=sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01 \ --hash=sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c \
--hash=sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d --hash=sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31
ecdsa==0.13 \ ecdsa==0.13 \
--hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \
--hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa
idna==2.8 \ idna==2.7 \
--hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \ --hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
--hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c --hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16
idna_ssl==1.1.0 \ idna_ssl==1.1.0 \
--hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c --hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c
jsonrpclib-pelix==0.4.0 \ jsonrpclib-pelix==0.3.1 \
--hash=sha256:19c558e169a51480b39548783067ca55046b62b2409ab4559931255e12f635de \ --hash=sha256:5417b1508d5a50ec64f6e5b88907f111155d52607b218ff3ba9a777afb2e49e3 \
--hash=sha256:a966d17f2f739ee89031cf5c807d85d92db6b2715fb2b2f8a88bbfc87f468b12 --hash=sha256:bd89a6093bc4d47dc8a096197aacb827359944a4533be5193f3845f57b9f91b4
multidict==4.5.2 \ multidict==4.4.0 \
--hash=sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f \ --hash=sha256:112eeeddd226af681dc82b756ed34aa7b6d98f9c4a15760050298c21d715473d \
--hash=sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3 \ --hash=sha256:13b64ecb692effcabc5e29569ba9b5eb69c35112f990a16d6833ec3a9d9f8ec0 \
--hash=sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef \ --hash=sha256:1725373fb8f18c2166f8e0e5789851ccf98453c849b403945fa4ef59a16ca44e \
--hash=sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b \ --hash=sha256:2061a50b7cae60a1f987503a995b2fc38e47027a937a355a124306ed9c629041 \
--hash=sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73 \ --hash=sha256:35b062288a9a478f627c520fd27983160fc97591017d170f966805b428d17e07 \
--hash=sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc \ --hash=sha256:467b134bcc227b91b8e2ef8d2931f28b50bf7eb7a04c0403d102ded22e66dbfc \
--hash=sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3 \ --hash=sha256:475a3ece8bb450e49385414ebfae7f8fdb33f62f1ac0c12935c1cfb1b7c1076a \
--hash=sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd \ --hash=sha256:49b885287e227a24545a1126d9ac17ae43138610713dc6219b781cc0ad5c6dfc \
--hash=sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351 \ --hash=sha256:4c95b2725592adb5c46642be2875c1234c32af841732c5504c17726b92082021 \
--hash=sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941 \ --hash=sha256:4ea7ed00f4be0f7335c9a2713a65ac3d986be789ce5ebc10821da9664cbe6b85 \
--hash=sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d \ --hash=sha256:5e2d5e1d999e941b4a626aea46bdc4206877cf727107fdaa9d46a8a773a6e49b \
--hash=sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1 \ --hash=sha256:8039c520ef7bb9ec7c3db3df14c570be6362f43c200ae9854d2422d4ffe175a4 \
--hash=sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b \ --hash=sha256:81459a0ebcca09c1fcb8fe887ed13cf267d9b60fe33718fc5fd1a2a1ab49470a \
--hash=sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a \ --hash=sha256:847c3b7b9ca3268e883685dc1347a4d09f84de7bd7597310044d847590447492 \
--hash=sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3 \ --hash=sha256:8551d1db45f0ca4e8ec99130767009a29a4e0dc6558a4a6808491bcd3472d325 \
--hash=sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7 \ --hash=sha256:8fa7679ffe615e0c1c7b80946ab4194669be74848719adf2d7867b5e861eb073 \
--hash=sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0 \ --hash=sha256:a42a36f09f0f907579ff0fde547f2fde8a739a69efe4a2728835979d2bb5e17b \
--hash=sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0 \ --hash=sha256:a5fcad0070685c5b2d04b468bf5f4c735f5c176432f495ad055fcc4bc0a79b23 \
--hash=sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014 \ --hash=sha256:ae22195b2a7494619b73c01129ddcddc0dfaa9e42727404b1d9a77253da3f420 \
--hash=sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5 \ --hash=sha256:b360e82bdbbd862e1ce2a41cc3bbd0ab614350e813ca74801b34aac0f73465aa \
--hash=sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036 \ --hash=sha256:b96417899344c5e96bef757f4963a72d02e52653a4e0f99bbea3a531cedac59f \
--hash=sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d \ --hash=sha256:b9e921140b797093edfc13ac08dc2a4fd016dd711dc42bb0e1aaf180e48425a7 \
--hash=sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a \ --hash=sha256:c5022b94fc330e6d177f3eb38097fb52c7df96ca0e04842c068cf0d9fc38b1e6 \
--hash=sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce \ --hash=sha256:cf2b117f2a8d951638efc7592fb72d3eeb2d38cc2194c26ba7f00e7190451d92 \
--hash=sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1 \ --hash=sha256:d79620b542d9d0e23ae9790ca2fe44f1af40ffad9936efa37bd14954bc3e2818 \
--hash=sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a \ --hash=sha256:e2860691c11d10dac7c91bddae44f6211b3da4122d9a2ebb509c2247674d6070 \
--hash=sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9 \ --hash=sha256:e3a293553715afecf7e10ea02da40593f9d7f48fe48a74fc5dd3ce08a0c46188 \
--hash=sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7 \ --hash=sha256:e465be3fe7e992e5a6e16731afa6f41cb6ca53afccb4f28ea2fa6457783edf15 \
--hash=sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b --hash=sha256:e6d27895ef922bc859d969452f247bfbe5345d9aba69b9c8dbe1ea7704f0c5d9
pip==19.0.1 \ pip==18.0 \
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \ --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5 --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
protobuf==3.6.1 \ protobuf==3.6.1 \
--hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \ --hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \
--hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \ --hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \
--hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \ --hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \
--hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \ --hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \
--hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \ --hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \
--hash=sha256:4b92e235a3afd42e7493b281c8b80c0c65cbef45de30f43d571d1ee40a1f77ef \
--hash=sha256:574085a33ca0d2c67433e5f3e9a0965c487410d6cb3406c83bdaf549bfc2992e \
--hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \ --hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \
--hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \ --hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \
--hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \ --hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \
@ -106,50 +103,43 @@ protobuf==3.6.1 \
--hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10 --hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10
pyaes==1.6.1 \ pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
PySocks==1.6.8 \
--hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672
QDarkStyle==2.5.4 \ QDarkStyle==2.5.4 \
--hash=sha256:3eb60922b8c4d9cedecb6897ca4c9f8a259d81bdefe5791976ccdf12432de1f0 \ --hash=sha256:3eb60922b8c4d9cedecb6897ca4c9f8a259d81bdefe5791976ccdf12432de1f0 \
--hash=sha256:51331fc6490b38c376e6ba8d8c814320c8d2d1c2663055bc396321a7c28fa8be --hash=sha256:51331fc6490b38c376e6ba8d8c814320c8d2d1c2663055bc396321a7c28fa8be
qrcode==6.1 \ qrcode==6.0 \
--hash=sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5 \ --hash=sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf \
--hash=sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369 --hash=sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3
setuptools==40.8.0 \ requests==2.19.1 \
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \ --hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab --hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
six==1.12.0 \ setuptools==40.2.0 \
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ --hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 --hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
typing-extensions==3.7.2 \ six==1.11.0 \
--hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \ --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
--hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \ --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
--hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71 typing==3.6.6 \
wheel==0.32.3 \ --hash=sha256:4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d \
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \ --hash=sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4 \
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44 --hash=sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a
yarl==1.3.0 \ urllib3==1.23 \
--hash=sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9 \ --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
--hash=sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f \ --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
--hash=sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb \ wheel==0.31.1 \
--hash=sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320 \ --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
--hash=sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842 \ --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
--hash=sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0 \ yarl==1.2.6 \
--hash=sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829 \ --hash=sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9 \
--hash=sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310 \ --hash=sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee \
--hash=sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4 \ --hash=sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308 \
--hash=sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8 \ --hash=sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357 \
--hash=sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1 --hash=sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78 \
colorama==0.4.1 \ --hash=sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8 \
--hash=sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d \ --hash=sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1 \
--hash=sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48 --hash=sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4 \
pylibscrypt==1.7.1 \ --hash=sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7
--hash=sha256:7aa9424e211a12106c67ea884ccfe609856289372b900d3702faaf66e87f79ac colorama==0.3.9 \
scrypt==0.8.6 \ --hash=sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda \
--hash=sha256:85919f023148cd9fb01d75ad4e3e061928c298fa6249a0cd6cd469c4b947595e \ --hash=sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1
--hash=sha256:4ad7188f2e42dbee2ff1cd72e3da40b170ba41847effbf0d726444f62ae60f3a \
--hash=sha256:bc131f74a688fa09993c518ca666a2ebd4268b207e039cbab03a034228140d3e \
--hash=sha256:232acdbc3434d2de55def8d5dbf1bc4b9bfc50da7c5741df2a6eebc4e18d3720 \
--hash=sha256:971db040d3963ebe4b919a203fe10d7d6659951d3644066314330983dc175ed4 \
--hash=sha256:475ac80239b3d788ae71a09c3019ca915e149aaa339adcdd1c9eef121293dc88 \
--hash=sha256:18ccbc63d87c6f89b753194194bb37aeaf1abc517e4b989461d115c1d93ce128 \
--hash=sha256:c23daecee405cb036845917295c76f8d747fc890158df40cb304b4b3c3640079 \
--hash=sha256:f8239b2d47fa1d40bc27efd231dc7083695d10c1c2ac51a99380360741e0362d

View File

@ -1,8 +1,6 @@
#!/bin/bash #!/bin/bash
# Run this after a new release to update dependencies # Run this after a new release to update dependencies
set -e
venv_dir=~/.electrum-venv venv_dir=~/.electrum-venv
contrib=$(dirname "$0") contrib=$(dirname "$0")

View File

@ -1,26 +1,7 @@
#!/bin/bash #!/bin/bash
set -e
CONTRIB="$(dirname "$(readlink -e "$0")")"
ROOT_FOLDER="$CONTRIB"/..
PACKAGES="$ROOT_FOLDER"/packages/
LOCALE="$ROOT_FOLDER"/electrum/locale/
if [ ! -d "$LOCALE" ]; then
echo "Run make_locale first!"
exit 1
fi
if [ ! -d "$PACKAGES" ]; then
echo "Run make_packages first!"
exit 1
fi
pushd ./electrum/gui/kivy/ pushd ./electrum/gui/kivy/
make theming
if [[ -n "$1" && "$1" == "release" ]] ; then if [[ -n "$1" && "$1" == "release" ]] ; then
echo -n Keystore Password: echo -n Keystore Password:
read -s password read -s password

View File

@ -24,7 +24,6 @@ string = string.replace("##VERSION_APK##", APK_VERSION)
files = { files = {
'tgz': "Electrum-%s.tar.gz" % version, 'tgz': "Electrum-%s.tar.gz" % version,
'appimage': "electrum-%s-x86_64.AppImage" % version,
'zip': "Electrum-%s.zip" % version, 'zip': "Electrum-%s.zip" % version,
'mac': "electrum-%s.dmg" % version_mac, 'mac': "electrum-%s.dmg" % version_mac,
'win': "electrum-%s.exe" % version_win, 'win': "electrum-%s.exe" % version_win,

View File

@ -3,12 +3,7 @@ import os
import subprocess import subprocess
import io import io
import zipfile import zipfile
import sys import requests
try:
import requests
except ImportError as e:
sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'")
os.chdir(os.path.dirname(os.path.realpath(__file__))) os.chdir(os.path.dirname(os.path.realpath(__file__)))
os.chdir('..') os.chdir('..')

View File

@ -1,10 +1,13 @@
#!/bin/bash #!/bin/bash
CONTRIB="$(dirname "$0")" contrib=$(dirname "$0")
test -n "$CONTRIB" -a -d "$CONTRIB" || exit test -n "$contrib" -a -d "$contrib" || exit
rm "$CONTRIB"/../packages/ -r whereis pip3
if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi
rm "$contrib"/../packages/ -r
#Install pure python modules in electrum directory #Install pure python modules in electrum directory
python3 -m pip install -r "$CONTRIB"/deterministic-build/requirements.txt -t "$CONTRIB"/../packages pip3 install -r $contrib/deterministic-build/requirements.txt -t $contrib/../packages

View File

@ -1,31 +1 @@
#!/bin/bash python3 setup.py sdist --format=zip,gztar
set -e
CONTRIB="$(dirname "$(readlink -e "$0")")"
ROOT_FOLDER="$CONTRIB"/..
PACKAGES="$ROOT_FOLDER"/packages/
LOCALE="$ROOT_FOLDER"/electrum/locale/
if [ ! -d "$LOCALE" ]; then
echo "Run make_locale first!"
exit 1
fi
if [ ! -d "$PACKAGES" ]; then
echo "Run make_packages first!"
exit 1
fi
(
cd "$ROOT_FOLDER"
echo "'git clean -fx' would delete the following files: >>>"
git clean -fx --dry-run
echo "<<<"
# we could build the kivy atlas potentially?
#(cd electrum/gui/kivy/; make theming) || echo "building kivy atlas failed! skipping."
python3 setup.py --quiet sdist --format=zip,gztar
)

@ -1 +0,0 @@
Subproject commit 59dfc03272751cd29ee311456fa34c40f7ebb7c0

View File

@ -1,66 +0,0 @@
Building Mac OS binaries
========================
This guide explains how to build Electrum binaries for macOS systems.
## 1. Building the binary
This needs to be done on a system running macOS or OS X. We use El Capitan (10.11.6) as building it
on High Sierra (or later)
makes the binaries [incompatible with older versions](https://github.com/pyinstaller/pyinstaller/issues/1191).
Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`).
#### 1.1a Get Xcode
Building the QR scanner (CalinsQRReader) requires full Xcode (not just command line tools).
The last Xcode version compatible with El Capitan is Xcode 8.2.1
Get it from [here](https://developer.apple.com/download/more/).
Unfortunately, you need an "Apple ID" account.
After downloading, uncompress it.
Make sure it is the "selected" xcode (e.g.):
sudo xcode-select -s $HOME/Downloads/Xcode.app/Contents/Developer/
#### 1.1b Build QR scanner separately on newer Mac
Alternatively, you can try building just the QR scanner on newer macOS.
On newer Mac, run:
pushd contrib/osx/CalinsQRReader; xcodebuild; popd
cp -r contrib/osx/CalinsQRReader/build prebuilt_qr
Move `prebuilt_qr` to El Capitan: `contrib/osx/CalinsQRReader/prebuilt_qr`.
#### 1.2 Build Electrum
cd electrum
./contrib/osx/make_osx
This creates both a folder named Electrum.app and the .dmg file.
## 2. Building the image deterministically (WIP)
The usual way to distribute macOS applications is to use image files containing the
application. Although these images can be created on a Mac with the built-in `hdiutil`,
they are not deterministic.
Instead, we use the toolchain that Bitcoin uses: genisoimage and libdmg-hfsplus.
These tools do not work on macOS, so you need a separate Linux machine (or VM).
Copy the Electrum.app directory over and install the dependencies, e.g.:
apt install libcap-dev cmake make gcc faketime
Then you can just invoke `package.sh` with the path to the app:
cd electrum
./contrib/osx/package.sh ~/Electrum.app/

View File

@ -1,23 +0,0 @@
#!/usr/bin/env bash
. $(dirname "$0")/../build_tools_util.sh
function DoCodeSignMaybe { # ARGS: infoName fileOrDirName codesignIdentity
infoName="$1"
file="$2"
identity="$3"
deep=""
if [ -z "$identity" ]; then
# we are ok with them not passing anything; master script calls us unconditionally even if no identity is specified
return
fi
if [ -d "$file" ]; then
deep="--deep"
fi
if [ -z "$infoName" ] || [ -z "$file" ] || [ -z "$identity" ] || [ ! -e "$file" ]; then
fail "Argument error to internal function DoCodeSignMaybe()"
fi
info "Code signing ${infoName}..."
codesign -f -v $deep -s "$identity" "$file" || fail "Could not code sign ${infoName}"
}

View File

@ -1,2 +1,2 @@
PyQt5 PyQt5<5.11
pycryptodomex pycryptodomex

View File

@ -1,8 +1,8 @@
Cython>=0.27 Cython>=0.27
trezor[hidapi]>=0.11.1 trezor[hidapi]>=0.9.0
safet[hidapi]>=0.1.0 safet[hidapi]>=0.1.0
keepkey keepkey
btchip-python>=0.1.26 btchip-python
ckcc-protocol>=0.7.2 ckcc-protocol>=0.7.2
websocket-client websocket-client
hidapi hidapi

View File

@ -1,12 +1,11 @@
pyaes>=0.1a1 pyaes>=0.1a1
ecdsa>=0.9 ecdsa>=0.9
requests
qrcode qrcode
protobuf protobuf
dnspython dnspython
jsonrpclib-pelix jsonrpclib-pelix
qdarkstyle<2.6 qdarkstyle<3.0
aiorpcx>=0.9,<0.11 aiorpcx>=0.7.1,<0.8
aiohttp>=3.3.0 aiohttp
aiohttp_socks aiohttp_socks
certifi
pylibscrypt==1.7.1

View File

@ -1,4 +0,0 @@
#!/bin/bash
version=`python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)"`
sig=`./run_electrum -w $SIGNING_WALLET signmessage $SIGNING_ADDRESS $version`
echo "{ \"version\":\"$version\", \"signatures\":{ \"$SIGNING_ADDRESS\":\"$sig\"}}"

View File

@ -17,7 +17,7 @@ if [ -e ./env/bin/activate ]; then
else else
virtualenv env -p `which python3` virtualenv env -p `which python3`
source ./env/bin/activate source ./env/bin/activate
python3 -m pip install .[fast] python3 setup.py install
fi fi
export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH" export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH"

16
electrum.conf.sample Normal file
View File

@ -0,0 +1,16 @@
# Configuration file for the Electrum client
# Settings defined here are shared across wallets
#
# copy this file to /etc/electrum.conf if you want read-only settings
[client]
server = electrum.novit.ro:50001:t
proxy = None
gap_limit = 5
# booleans use python syntax
use_change = True
gui = qt
num_zeros = 2
# default transaction fee is in Satoshis
fee = 10000
winpos-qt = [799, 226, 877, 435]

View File

@ -3,20 +3,19 @@
[Desktop Entry] [Desktop Entry]
Comment=Lightweight Bitcoin Client Comment=Lightweight Bitcoin Client
Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum %u" Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\" electrum %u"
GenericName[en_US]=Bitcoin Wallet GenericName[en_US]=Bitcoin Wallet
GenericName=Bitcoin Wallet GenericName=Bitcoin Wallet
Icon=electrum Icon=electrum
Name[en_US]=Electrum Bitcoin Wallet Name[en_US]=Electrum Bitcoin Wallet
Name=Electrum Bitcoin Wallet Name=Electrum Bitcoin Wallet
Categories=Finance;Network; Categories=Finance;Network;
StartupNotify=true StartupNotify=false
StartupWMClass=electrum
Terminal=false Terminal=false
Type=Application Type=Application
MimeType=x-scheme-handler/bitcoin; MimeType=x-scheme-handler/bitcoin;
Actions=Testnet; Actions=Testnet;
[Desktop Action Testnet] [Desktop Action Testnet]
Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum --testnet %u" Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\" electrum --testnet %u"
Name=Testnet mode Name=Testnet mode

BIN
electrum.icns Normal file

Binary file not shown.

View File

@ -12,6 +12,3 @@ from . import daemon
from .transaction import Transaction from .transaction import Transaction
from .plugin import BasePlugin from .plugin import BasePlugin
from .commands import Commands, known_commands from .commands import Commands, known_commands
__version__ = ELECTRUM_VERSION

View File

@ -25,22 +25,16 @@ import threading
import asyncio import asyncio
import itertools import itertools
from collections import defaultdict from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional
from . import bitcoin from . import bitcoin
from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY
from .util import PrintError, profiler, bfh, TxMinedInfo from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus, aiosafe, CustomTaskGroup
from .transaction import Transaction, TxOutput from .transaction import Transaction, TxOutput
from .synchronizer import Synchronizer from .synchronizer import Synchronizer
from .verifier import SPV from .verifier import SPV
from .blockchain import hash_header from .blockchain import hash_header
from .i18n import _ from .i18n import _
if TYPE_CHECKING:
from .storage import WalletStorage
from .network import Network
TX_HEIGHT_LOCAL = -2 TX_HEIGHT_LOCAL = -2
TX_HEIGHT_UNCONF_PARENT = -1 TX_HEIGHT_UNCONF_PARENT = -1
TX_HEIGHT_UNCONFIRMED = 0 TX_HEIGHT_UNCONFIRMED = 0
@ -59,27 +53,24 @@ class AddressSynchronizer(PrintError):
inherited by wallet inherited by wallet
""" """
def __init__(self, storage: 'WalletStorage'): def __init__(self, storage):
self.storage = storage self.storage = storage
self.network = None # type: Network self.network = None
# verifier (SPV) and synchronizer are started in start_network # verifier (SPV) and synchronizer are started in start_threads
self.synchronizer = None # type: Synchronizer self.synchronizer = None
self.verifier = None # type: SPV 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! # locks: if you need to take multiple ones, acquire them in the order they are defined here!
self.lock = threading.RLock() self.lock = threading.RLock()
self.transaction_lock = threading.RLock() self.transaction_lock = threading.RLock()
# address -> list(txid, height) # address -> list(txid, height)
self.history = storage.get('addr_history',{}) self.history = storage.get('addr_history',{})
# Verified transactions. txid -> TxMinedInfo. Access with self.lock. # Verified transactions. txid -> VerifiedTxInfo. Access with self.lock.
verified_tx = storage.get('verified_tx3', {}) verified_tx = storage.get('verified_tx3', {})
self.verified_tx = {} # type: Dict[str, TxMinedInfo] self.verified_tx = {}
for txid, (height, timestamp, txpos, header_hash, flodata) in verified_tx.items(): for txid, (height, timestamp, txpos, header_hash, tx_comment) in verified_tx.items():
self.verified_tx[txid] = TxMinedInfo(height=height, self.verified_tx[txid] = VerifiedTxInfo(height, timestamp, txpos, header_hash, tx_comment)
conf=None,
timestamp=timestamp,
txpos=txpos,
header_hash=header_hash,
flodata=flodata)
# Transactions pending verification. txid -> tx_height. Access with self.lock. # Transactions pending verification. txid -> tx_height. Access with self.lock.
self.unverified_tx = defaultdict(int) self.unverified_tx = defaultdict(int)
# true when synchronized # true when synchronized
@ -89,12 +80,6 @@ class AddressSynchronizer(PrintError):
self.load_and_cleanup() self.load_and_cleanup()
def with_transaction_lock(func):
def func_wrapper(self, *args, **kwargs):
with self.transaction_lock:
return func(self, *args, **kwargs)
return func_wrapper
def load_and_cleanup(self): def load_and_cleanup(self):
self.load_transactions() self.load_transactions()
self.load_local_history() self.load_local_history()
@ -152,25 +137,49 @@ class AddressSynchronizer(PrintError):
# add it in case it was previously unconfirmed # add it in case it was previously unconfirmed
self.add_unverified_tx(tx_hash, tx_height) self.add_unverified_tx(tx_hash, tx_height)
@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): def start_network(self, network):
self.network = network self.network = network
if self.network is not None: if self.network is not None:
self.synchronizer = Synchronizer(self) self.network.register_callback(self.on_default_server_changed, ['default_server_changed'])
self.verifier = SPV(self.network, self) asyncio.run_coroutine_threadsafe(self._start_threads(), network.asyncio_loop)
def stop_threads(self, write_to_disk=True): 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: if self.network:
if self.synchronizer: self.synchronizer = None
asyncio.run_coroutine_threadsafe(self.synchronizer.stop(), self.network.asyncio_loop) self.verifier = None
self.synchronizer = None if self.group:
if self.verifier: asyncio.run_coroutine_threadsafe(self.group.cancel_remaining(), self.network.asyncio_loop)
asyncio.run_coroutine_threadsafe(self.verifier.stop(), self.network.asyncio_loop) self.group = None
self.verifier = None
self.storage.put('stored_height', self.get_local_height()) self.storage.put('stored_height', self.get_local_height())
if write_to_disk: self.save_transactions()
self.save_transactions() self.save_verified_tx()
self.save_verified_tx() self.storage.write()
self.storage.write()
def add_address(self, address): def add_address(self, address):
if address not in self.history: if address not in self.history:
@ -179,7 +188,7 @@ class AddressSynchronizer(PrintError):
if self.synchronizer: if self.synchronizer:
self.synchronizer.add(address) self.synchronizer.add(address)
def get_conflicting_transactions(self, tx_hash, tx): def get_conflicting_transactions(self, tx):
"""Returns a set of transaction hashes from the wallet history that are """Returns a set of transaction hashes from the wallet history that are
directly conflicting with tx, i.e. they have common outpoints being directly conflicting with tx, i.e. they have common outpoints being
spent with tx. If the tx is already in wallet history, that will not be spent with tx. If the tx is already in wallet history, that will not be
@ -198,18 +207,18 @@ class AddressSynchronizer(PrintError):
# this outpoint has already been spent, by spending_tx # this outpoint has already been spent, by spending_tx
assert spending_tx_hash in self.transactions assert spending_tx_hash in self.transactions
conflicting_txns |= {spending_tx_hash} conflicting_txns |= {spending_tx_hash}
if tx_hash in conflicting_txns: txid = tx.txid()
if txid in conflicting_txns:
# this tx is already in history, so it conflicts with itself # this tx is already in history, so it conflicts with itself
if len(conflicting_txns) > 1: if len(conflicting_txns) > 1:
raise Exception('Found conflicting transactions already in wallet history.') raise Exception('Found conflicting transactions already in wallet history.')
conflicting_txns -= {tx_hash} conflicting_txns -= {txid}
return conflicting_txns return conflicting_txns
def add_transaction(self, tx_hash, tx, allow_unrelated=False): def add_transaction(self, tx_hash, tx, allow_unrelated=False):
assert tx_hash, tx_hash assert tx_hash, tx_hash
assert tx, tx assert tx, tx
assert tx.is_complete() assert tx.is_complete()
# assert tx_hash == tx.txid() # disabled as expensive; test done by Synchronizer.
# we need self.transaction_lock but get_tx_height will take self.lock # we need self.transaction_lock but get_tx_height will take self.lock
# so we need to take that too here, to enforce order of locks # so we need to take that too here, to enforce order of locks
with self.lock, self.transaction_lock: with self.lock, self.transaction_lock:
@ -234,7 +243,7 @@ class AddressSynchronizer(PrintError):
# When this method exits, there must NOT be any conflict, so # When this method exits, there must NOT be any conflict, so
# either keep this txn and remove all conflicting (along with dependencies) # either keep this txn and remove all conflicting (along with dependencies)
# or drop this txn # or drop this txn
conflicting_txns = self.get_conflicting_transactions(tx_hash, tx) conflicting_txns = self.get_conflicting_transactions(tx)
if conflicting_txns: if conflicting_txns:
existing_mempool_txn = any( existing_mempool_txn = any(
self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
@ -374,13 +383,11 @@ class AddressSynchronizer(PrintError):
@profiler @profiler
def load_transactions(self): def load_transactions(self):
# load txi, txo, tx_fees # load txi, txo, tx_fees
# bookkeeping data of is_mine inputs of transactions self.txi = self.storage.get('txi', {})
self.txi = self.storage.get('txi', {}) # txid -> address -> (prev_outpoint, value)
for txid, d in list(self.txi.items()): for txid, d in list(self.txi.items()):
for addr, lst in d.items(): for addr, lst in d.items():
self.txi[txid][addr] = set([tuple(x) for x in lst]) self.txi[txid][addr] = set([tuple(x) for x in lst])
# bookkeeping data of is_mine outputs of transactions self.txo = self.storage.get('txo', {})
self.txo = self.storage.get('txo', {}) # txid -> address -> (output_index, value, is_coinbase)
self.tx_fees = self.storage.get('tx_fees', {}) self.tx_fees = self.storage.get('tx_fees', {})
tx_list = self.storage.get('transactions', {}) tx_list = self.storage.get('transactions', {})
# load transactions # load transactions
@ -404,7 +411,6 @@ class AddressSynchronizer(PrintError):
@profiler @profiler
def load_local_history(self): def load_local_history(self):
self._history_local = {} # address -> set(txid) self._history_local = {} # address -> set(txid)
self._address_history_changed_events = defaultdict(asyncio.Event) # address -> Event
for txid in itertools.chain(self.txi, self.txo): for txid in itertools.chain(self.txi, self.txo):
self._add_tx_to_local_history(txid) self._add_tx_to_local_history(txid)
@ -452,11 +458,7 @@ class AddressSynchronizer(PrintError):
def save_verified_tx(self, write=False): def save_verified_tx(self, write=False):
with self.lock: with self.lock:
verified_tx_to_save = {} self.storage.put('verified_tx3', self.verified_tx)
for txid, tx_info in self.verified_tx.items():
verified_tx_to_save[txid] = (tx_info.height, tx_info.timestamp,
tx_info.txpos, tx_info.header_hash, tx_info.flodata)
self.storage.put('verified_tx3', verified_tx_to_save)
if write: if write:
self.storage.write() self.storage.write()
@ -469,7 +471,7 @@ class AddressSynchronizer(PrintError):
self.spent_outpoints = defaultdict(dict) self.spent_outpoints = defaultdict(dict)
self.history = {} self.history = {}
self.verified_tx = {} self.verified_tx = {}
self.transactions = {} # type: Dict[str, Transaction] self.transactions = {}
self.save_transactions() self.save_transactions()
def get_txpos(self, tx_hash): def get_txpos(self, tx_hash):
@ -519,7 +521,8 @@ class AddressSynchronizer(PrintError):
delta = tx_deltas[tx_hash] delta = tx_deltas[tx_hash]
tx_mined_status = self.get_tx_height(tx_hash) tx_mined_status = self.get_tx_height(tx_hash)
history.append((tx_hash, tx_mined_status, delta)) history.append((tx_hash, tx_mined_status, delta))
history.sort(key = lambda x: self.get_txpos(x[0]), reverse=True) history.sort(key = lambda x: self.get_txpos(x[0]))
history.reverse()
# 3. add balance # 3. add balance
c, u, x = self.get_balance(domain) c, u, x = self.get_balance(domain)
balance = c + u + x balance = c + u + x
@ -544,7 +547,6 @@ class AddressSynchronizer(PrintError):
cur_hist = self._history_local.get(addr, set()) cur_hist = self._history_local.get(addr, set())
cur_hist.add(txid) cur_hist.add(txid)
self._history_local[addr] = cur_hist self._history_local[addr] = cur_hist
self._mark_address_history_changed(addr)
def _remove_tx_from_local_history(self, txid): def _remove_tx_from_local_history(self, txid):
with self.transaction_lock: with self.transaction_lock:
@ -557,21 +559,6 @@ class AddressSynchronizer(PrintError):
else: else:
self._history_local[addr] = cur_hist self._history_local[addr] = cur_hist
def _mark_address_history_changed(self, addr: str) -> None:
# history for this address changed, wake up coroutines:
self._address_history_changed_events[addr].set()
# clear event immediately so that coroutines can wait() for the next change:
self._address_history_changed_events[addr].clear()
async def wait_for_address_history_to_change(self, addr: str) -> None:
"""Wait until the server tells us about a new transaction related to addr.
Unconfirmed and confirmed transactions are not distinguished, and so e.g. SPV
is not taken into account.
"""
assert self.is_mine(addr), "address needs to be is_mine to be watched"
await self._address_history_changed_events[addr].wait()
def add_unverified_tx(self, tx_hash, tx_height): def add_unverified_tx(self, tx_hash, tx_height):
if tx_hash in self.verified_tx: if tx_hash in self.verified_tx:
if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT): if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
@ -583,20 +570,17 @@ class AddressSynchronizer(PrintError):
with self.lock: with self.lock:
# tx will be verified only if height > 0 # tx will be verified only if height > 0
self.unverified_tx[tx_hash] = tx_height self.unverified_tx[tx_hash] = tx_height
# to remove pending proof requests:
if self.verifier:
self.verifier.remove_spv_proof_for_tx(tx_hash)
def remove_unverified_tx(self, tx_hash, tx_height): def add_verified_tx(self, tx_hash: str, info: VerifiedTxInfo):
with self.lock:
new_height = self.unverified_tx.get(tx_hash)
if new_height == tx_height:
self.unverified_tx.pop(tx_hash, None)
def add_verified_tx(self, tx_hash: str, info: TxMinedInfo):
# Remove from the unverified map and add to the verified map # Remove from the unverified map and add to the verified map
with self.lock: with self.lock:
self.unverified_tx.pop(tx_hash, None) self.unverified_tx.pop(tx_hash, None)
self.verified_tx[tx_hash] = info self.verified_tx[tx_hash] = info
tx_mined_status = self.get_tx_height(tx_hash) tx_mined_status = self.get_tx_height(tx_hash)
self.network.trigger_callback('verified', self, tx_hash, tx_mined_status) self.network.trigger_callback('verified', tx_hash, tx_mined_status)
def get_unverified_txs(self): def get_unverified_txs(self):
'''Returns a map from tx hash to transaction height''' '''Returns a map from tx hash to transaction height'''
@ -633,41 +617,40 @@ class AddressSynchronizer(PrintError):
return cached_local_height return cached_local_height
return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
def get_tx_height(self, tx_hash: str) -> TxMinedInfo: def get_tx_height(self, tx_hash: str) -> TxMinedStatus:
""" Given a transaction, returns (height, conf, timestamp, header_hash) """
with self.lock: with self.lock:
if tx_hash in self.verified_tx: if tx_hash in self.verified_tx:
info = self.verified_tx[tx_hash] info = self.verified_tx[tx_hash]
conf = max(self.get_local_height() - info.height + 1, 0) conf = max(self.get_local_height() - info.height + 1, 0)
return info._replace(conf=conf) return TxMinedStatus(info.height, conf, info.timestamp, info.header_hash)
elif tx_hash in self.unverified_tx: elif tx_hash in self.unverified_tx:
height = self.unverified_tx[tx_hash] height = self.unverified_tx[tx_hash]
return TxMinedInfo(height=height, conf=0) return TxMinedStatus(height, 0, None, None)
else: else:
# local transaction # local transaction
return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0) return TxMinedStatus(TX_HEIGHT_LOCAL, 0, None, None)
def get_flodata(self, tx_hash: str): def get_tx_comment(self, tx_hash: str):
""" Given a transaction, returns flodata """ """ Given a transaction, returns txcomment/floData """
with self.lock: with self.lock:
if tx_hash in self.verified_tx: if tx_hash in self.verified_tx:
info = self.verified_tx[tx_hash] info = self.verified_tx[tx_hash]
flodata = info[5] tx_comment = info[4]
return flodata return tx_comment
elif tx_hash in self.unverified_tx: elif tx_hash in self.unverified_tx:
tx = self.transactions.get(tx_hash) tx = self.transactions.get(tx_hash)
flodata = tx.flodata[5:] tx_comment = tx.txcomment[5:]
return flodata return tx_comment
else: else:
# local transaction # local transaction
tx = self.transactions.get(tx_hash) tx = self.transactions.get(tx_hash)
flodata = tx.flodata[5:] tx_comment = tx.txcomment[5:]
return flodata return tx_comment
def set_up_to_date(self, up_to_date): def set_up_to_date(self, up_to_date):
with self.lock: with self.lock:
self.up_to_date = up_to_date self.up_to_date = up_to_date
if self.network:
self.network.notify('status')
if up_to_date: if up_to_date:
self.save_transactions(write=True) self.save_transactions(write=True)
# if the verifier is also up to date, persist that too; # if the verifier is also up to date, persist that too;
@ -678,9 +661,8 @@ class AddressSynchronizer(PrintError):
def is_up_to_date(self): def is_up_to_date(self):
with self.lock: return self.up_to_date with self.lock: return self.up_to_date
@with_transaction_lock
def get_tx_delta(self, tx_hash, address): def get_tx_delta(self, tx_hash, address):
"""effect of tx on address""" "effect of tx on address"
delta = 0 delta = 0
# substract the value of coins sent from address # substract the value of coins sent from address
d = self.txi.get(tx_hash, {}).get(address, []) d = self.txi.get(tx_hash, {}).get(address, [])
@ -692,9 +674,8 @@ class AddressSynchronizer(PrintError):
delta += v delta += v
return delta return delta
@with_transaction_lock
def get_tx_value(self, txid): def get_tx_value(self, txid):
"""effect of tx on the entire domain""" " effect of tx on the entire domain"
delta = 0 delta = 0
for addr, d in self.txi.get(txid, {}).items(): for addr, d in self.txi.get(txid, {}).items():
for n, v in d: for n, v in d:
@ -704,7 +685,7 @@ class AddressSynchronizer(PrintError):
delta += v delta += v
return delta return delta
def get_wallet_delta(self, tx: Transaction): def get_wallet_delta(self, tx):
""" effect of tx on wallet """ """ effect of tx on wallet """
is_relevant = False # "related to wallet?" is_relevant = False # "related to wallet?"
is_mine = False is_mine = False
@ -731,10 +712,10 @@ class AddressSynchronizer(PrintError):
is_partial = True is_partial = True
if not is_mine: if not is_mine:
is_partial = False is_partial = False
for o in tx.outputs(): for addr, value in tx.get_outputs():
v_out += o.value v_out += value
if self.is_mine(o.address): if self.is_mine(addr):
v_out_mine += o.value v_out_mine += value
is_relevant = True is_relevant = True
if is_pruned: if is_pruned:
# some inputs are mine: # some inputs are mine:
@ -756,34 +737,18 @@ class AddressSynchronizer(PrintError):
fee = None fee = None
return is_relevant, is_mine, v, fee return is_relevant, is_mine, v, fee
def get_tx_fee(self, tx: Transaction) -> Optional[int]:
if not tx:
return None
if hasattr(tx, '_cached_fee'):
return tx._cached_fee
with self.lock, self.transaction_lock:
is_relevant, is_mine, v, fee = self.get_wallet_delta(tx)
if fee is None:
txid = tx.txid()
fee = self.tx_fees.get(txid)
# only cache non-None, as None can still change while syncing
if fee is not None:
tx._cached_fee = fee
return fee
def get_addr_io(self, address): def get_addr_io(self, address):
with self.lock, self.transaction_lock: h = self.get_address_history(address)
h = self.get_address_history(address) received = {}
received = {} sent = {}
sent = {} for tx_hash, height in h:
for tx_hash, height in h: l = self.txo.get(tx_hash, {}).get(address, [])
l = self.txo.get(tx_hash, {}).get(address, []) for n, v, is_cb in l:
for n, v, is_cb in l: received[tx_hash + ':%d'%n] = (height, v, is_cb)
received[tx_hash + ':%d'%n] = (height, v, is_cb) for tx_hash, height in h:
for tx_hash, height in h: l = self.txi.get(tx_hash, {}).get(address, [])
l = self.txi.get(tx_hash, {}).get(address, []) for txi, v in l:
for txi, v in l: sent[txi] = height
sent[txi] = height
return received, sent return received, sent
def get_addr_utxo(self, address): def get_addr_utxo(self, address):
@ -812,7 +777,7 @@ class AddressSynchronizer(PrintError):
@with_local_height_cached @with_local_height_cached
def get_addr_balance(self, address): def get_addr_balance(self, address):
"""Return the balance of a FLO address: """Return the balance of a bitcoin address:
confirmed and matured, unconfirmed, unmatured confirmed and matured, unconfirmed, unmatured
""" """
received, sent = self.get_addr_io(address) received, sent = self.get_addr_io(address)
@ -833,7 +798,7 @@ class AddressSynchronizer(PrintError):
return c, u, x return c, u, x
@with_local_height_cached @with_local_height_cached
def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False, nonlocal_only=False): def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False):
coins = [] coins = []
if domain is None: if domain is None:
domain = self.get_addresses() domain = self.get_addresses()
@ -845,8 +810,6 @@ class AddressSynchronizer(PrintError):
for x in utxos.values(): for x in utxos.values():
if confirmed_only and x['height'] <= 0: if confirmed_only and x['height'] <= 0:
continue continue
if nonlocal_only and x['height'] == TX_HEIGHT_LOCAL:
continue
if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height(): if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height():
continue continue
coins.append(x) coins.append(x)
@ -872,6 +835,3 @@ class AddressSynchronizer(PrintError):
def is_empty(self, address): def is_empty(self, address):
c, u, x = self.get_addr_balance(address) c, u, x = self.get_addr_balance(address)
return c+u+x == 0 return c+u+x == 0
def synchronize(self):
pass

View File

@ -28,10 +28,10 @@ import sys
import os import os
from .version import ELECTRUM_VERSION from .version import ELECTRUM_VERSION
from . import constants from .import constants
from .i18n import _ from .i18n import _
from .util import make_aiohttp_session
from .util import make_aiohttp_session
class BaseCrashReporter: class BaseCrashReporter:
report_server = "https://crashhub.electrum.org" report_server = "https://crashhub.electrum.org"

View File

@ -27,24 +27,14 @@ import os
import sys import sys
import traceback import traceback
from functools import partial from functools import partial
from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any
from . import bitcoin from . import bitcoin
from . import keystore from . import keystore
from .bip32 import is_bip32_derivation, xpub_type
from .keystore import bip44_derivation, purpose48_derivation from .keystore import bip44_derivation, purpose48_derivation
from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet
wallet_types, Wallet, Abstract_Wallet) from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
from .storage import (WalletStorage, STO_EV_USER_PW, STO_EV_XPUB_PW,
get_derivation_used_for_hw_device_encryption)
from .i18n import _ from .i18n import _
from .util import UserCancelled, InvalidPassword, WalletFileException from .util import UserCancelled, InvalidPassword, WalletFileException
from .simple_config import SimpleConfig
from .plugin import Plugins
if TYPE_CHECKING:
from .plugin import DeviceInfo
# hardware device setup purpose # hardware device setup purpose
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
@ -56,21 +46,15 @@ class ScriptTypeNotSupported(Exception): pass
class GoBack(Exception): pass class GoBack(Exception): pass
class WizardStackItem(NamedTuple):
action: Any
args: Any
storage_data: dict
class BaseWizard(object): class BaseWizard(object):
def __init__(self, config: SimpleConfig, plugins: Plugins, storage: WalletStorage): def __init__(self, config, plugins, storage):
super(BaseWizard, self).__init__() super(BaseWizard, self).__init__()
self.config = config self.config = config
self.plugins = plugins self.plugins = plugins
self.storage = storage self.storage = storage
self.wallet = None # type: Abstract_Wallet self.wallet = None
self._stack = [] # type: List[WizardStackItem] self.stack = []
self.plugin = None self.plugin = None
self.keystores = [] self.keystores = []
self.is_kivy = config.get('gui') == 'kivy' self.is_kivy = config.get('gui') == 'kivy'
@ -82,8 +66,7 @@ class BaseWizard(object):
def run(self, *args): def run(self, *args):
action = args[0] action = args[0]
args = args[1:] args = args[1:]
storage_data = self.storage.get_all_data() self.stack.append((action, args))
self._stack.append(WizardStackItem(action, args, storage_data))
if not action: if not action:
return return
if type(action) is tuple: if type(action) is tuple:
@ -98,23 +81,14 @@ class BaseWizard(object):
raise Exception("unknown action", action) raise Exception("unknown action", action)
def can_go_back(self): def can_go_back(self):
return len(self._stack) > 1 return len(self.stack)>1
def go_back(self): def go_back(self):
if not self.can_go_back(): if not self.can_go_back():
return return
# pop 'current' frame self.stack.pop()
self._stack.pop() action, args = self.stack.pop()
# pop 'previous' frame self.run(action, *args)
stack_item = self._stack.pop()
# try to undo side effects since we last entered 'previous' frame
# FIXME only self.storage is properly restored
self.storage.overwrite_all_data(stack_item.storage_data)
# rerun 'previous' frame
self.run(stack_item.action, *stack_item.args)
def reset_stack(self):
self._stack = []
def new(self): def new(self):
name = os.path.basename(self.storage.path) name = os.path.basename(self.storage.path)
@ -124,8 +98,9 @@ class BaseWizard(object):
]) ])
wallet_kinds = [ wallet_kinds = [
('standard', _("Standard wallet")), ('standard', _("Standard wallet")),
('2fa', _("Wallet with two-factor authentication")),
('multisig', _("Multi-signature wallet")), ('multisig', _("Multi-signature wallet")),
('imported', _("Import FLO addresses or private keys")), ('imported', _("Import Bitcoin addresses or private keys")),
] ]
choices = [pair for pair in wallet_kinds if pair[0] in wallet_types] choices = [pair for pair in wallet_kinds if pair[0] in wallet_types]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type) self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
@ -166,8 +141,8 @@ class BaseWizard(object):
def choose_multisig(self): def choose_multisig(self):
def on_multisig(m, n): def on_multisig(m, n):
multisig_type = "%dof%d" % (m, n) self.multisig_type = "%dof%d"%(m, n)
self.storage.put('wallet_type', multisig_type) self.storage.put('wallet_type', self.multisig_type)
self.n = n self.n = n
self.run('choose_keystore') self.run('choose_keystore')
self.multisig_dialog(run_next=on_multisig) self.multisig_dialog(run_next=on_multisig)
@ -198,8 +173,8 @@ class BaseWizard(object):
def import_addresses_or_keys(self): def import_addresses_or_keys(self):
v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x) v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x)
title = _("Import FLO Addresses") title = _("Import Bitcoin Addresses")
message = _("Enter a list of FLO addresses (this will create a watching-only wallet), or a list of private keys.") message = _("Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.")
self.add_xpub_dialog(title=title, message=message, run_next=self.on_import, self.add_xpub_dialog(title=title, message=message, run_next=self.on_import,
is_valid=v, allow_multi=True, show_wif_help=True) is_valid=v, allow_multi=True, show_wif_help=True)
@ -208,23 +183,17 @@ class BaseWizard(object):
# will be reflected on self.storage # will be reflected on self.storage
if keystore.is_address_list(text): if keystore.is_address_list(text):
w = Imported_Wallet(self.storage) w = Imported_Wallet(self.storage)
addresses = text.split() for x in text.split():
good_inputs, bad_inputs = w.import_addresses(addresses, write_to_disk=False) w.import_address(x)
elif keystore.is_private_key_list(text): elif keystore.is_private_key_list(text):
k = keystore.Imported_KeyStore({}) k = keystore.Imported_KeyStore({})
self.storage.put('keystore', k.dump()) self.storage.put('keystore', k.dump())
w = Imported_Wallet(self.storage) w = Imported_Wallet(self.storage)
keys = keystore.get_private_keys(text) for x in keystore.get_private_keys(text):
good_inputs, bad_inputs = w.import_private_keys(keys, None, write_to_disk=False) w.import_private_key(x, None)
self.keystores.append(w.keystore) self.keystores.append(w.keystore)
else: else:
return self.terminate() return self.terminate()
if bad_inputs:
msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10])
if len(bad_inputs) > 10: msg += '\n...'
self.show_error(_("The following inputs could not be imported")
+ f' ({len(bad_inputs)}):\n' + msg)
# FIXME what if len(good_inputs) == 0 ?
return self.run('create_wallet') return self.run('create_wallet')
def restore_from_key(self): def restore_from_key(self):
@ -247,48 +216,34 @@ class BaseWizard(object):
def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET): def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET):
title = _('Hardware Keystore') title = _('Hardware Keystore')
# check available plugins # check available plugins
supported_plugins = self.plugins.get_hardware_support() support = self.plugins.get_hardware_support()
devices = [] # type: List[Tuple[str, DeviceInfo]] if not support:
devmgr = self.plugins.device_manager msg = '\n'.join([
debug_msg = '' _('No hardware wallet support found on your system.'),
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
def failed_getting_device_infos(name, e): ])
nonlocal debug_msg self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
devmgr.print_error(f'error getting device infos for {name}: {e}') return
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n'
# scan devices # scan devices
devices = []
devmgr = self.plugins.device_manager
try: try:
scanned_devices = devmgr.scan_devices() scanned_devices = devmgr.scan_devices()
except BaseException as e: except BaseException as e:
devmgr.print_error('error scanning devices: {}'.format(repr(e))) devmgr.print_error('error scanning devices: {}'.format(e))
debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e) debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e)
else: else:
for splugin in supported_plugins: debug_msg = ''
name, plugin = splugin.name, splugin.plugin for name, description, plugin in support:
# plugin init errored?
if not plugin:
e = splugin.exception
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
debug_msg += f' {name}: (error during plugin init)\n'
debug_msg += ' {}\n'.format(_('You might have an incompatible library.'))
debug_msg += f'{indented_error_msg}\n'
continue
# see if plugin recognizes 'scanned_devices'
try: try:
# FIXME: side-effect: unpaired_device_info sets client.handler # FIXME: side-effect: unpaired_device_info sets client.handler
device_infos = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices, u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices)
include_failing_clients=True)
except BaseException as e: except BaseException as e:
traceback.print_exc() devmgr.print_error('error getting device infos for {}: {}'.format(name, e))
failed_getting_device_infos(name, e) indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
debug_msg += ' {}:\n{}\n'.format(plugin.name, indented_error_msg)
continue continue
device_infos_failing = list(filter(lambda di: di.exception is not None, device_infos)) devices += list(map(lambda x: (name, x), u))
for di in device_infos_failing:
failed_getting_device_infos(name, di.exception)
device_infos_working = list(filter(lambda di: di.exception is None, device_infos))
devices += list(map(lambda x: (name, x), device_infos_working))
if not debug_msg: if not debug_msg:
debug_msg = ' {}'.format(_('No exceptions encountered.')) debug_msg = ' {}'.format(_('No exceptions encountered.'))
if not devices: if not devices:
@ -308,9 +263,7 @@ class BaseWizard(object):
for name, info in devices: for name, info in devices:
state = _("initialized") if info.initialized else _("wiped") state = _("initialized") if info.initialized else _("wiped")
label = info.label or _("An unnamed {}").format(name) label = info.label or _("An unnamed {}").format(name)
try: transport_str = info.device.transport_ui_string[:20] descr = "%s [%s, %s]" % (label, name, state)
except: transport_str = 'unknown transport'
descr = f"{label} [{name}, {state}, {transport_str}]"
choices.append(((name, info), descr)) choices.append(((name, info), descr))
msg = _('Select a device') + ':' msg = _('Select a device') + ':'
self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose)) self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose))
@ -366,14 +319,12 @@ class BaseWizard(object):
# There is no general standard for HD multisig. # There is no general standard for HD multisig.
# For legacy, this is partially compatible with BIP45; assumes index=0 # For legacy, this is partially compatible with BIP45; assumes index=0
# For segwit, a custom path is used, as there is no standard at all. # For segwit, a custom path is used, as there is no standard at all.
default_choice_idx = 2
choices = [ choices = [
('standard', 'legacy multisig (p2sh)', "m/45'/0"), ('standard', 'legacy multisig (p2sh)', "m/45'/0"),
('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')),
('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')),
] ]
else: else:
default_choice_idx = 2
choices = [ choices = [
('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), ('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)),
('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), ('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)),
@ -383,8 +334,7 @@ class BaseWizard(object):
try: try:
self.choice_and_line_dialog( self.choice_and_line_dialog(
run_next=f, title=_('Script type and Derivation path'), message1=message1, run_next=f, title=_('Script type and Derivation path'), message1=message1,
message2=message2, choices=choices, test_text=is_bip32_derivation, message2=message2, choices=choices, test_text=bitcoin.is_bip32_derivation)
default_choice_idx=default_choice_idx)
return return
except ScriptTypeNotSupported as e: except ScriptTypeNotSupported as e:
self.show_error(e) self.show_error(e)
@ -442,7 +392,7 @@ class BaseWizard(object):
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
elif self.seed_type == 'old': elif self.seed_type == 'old':
self.run('create_keystore', seed, '') self.run('create_keystore', seed, '')
elif bitcoin.is_any_2fa_seed_type(self.seed_type): elif self.seed_type == '2fa':
self.load_2fa() self.load_2fa()
self.run('on_restore_seed', seed, is_ext) self.run('on_restore_seed', seed, is_ext)
else: else:
@ -464,6 +414,7 @@ class BaseWizard(object):
def on_keystore(self, k): def on_keystore(self, k):
has_xpub = isinstance(k, keystore.Xpub) has_xpub = isinstance(k, keystore.Xpub)
if has_xpub: if has_xpub:
from .bitcoin import xpub_type
t1 = xpub_type(k.xpub) t1 = xpub_type(k.xpub)
if self.wallet_type == 'standard': if self.wallet_type == 'standard':
if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
@ -491,7 +442,7 @@ class BaseWizard(object):
self.keystores.append(k) self.keystores.append(k)
if len(self.keystores) == 1: if len(self.keystores) == 1:
xpub = k.get_master_public_key() xpub = k.get_master_public_key()
self.reset_stack() self.stack = []
self.run('show_xpub_and_add_cosigners', xpub) self.run('show_xpub_and_add_cosigners', xpub)
elif len(self.keystores) < self.n: elif len(self.keystores) < self.n:
self.run('choose_keystore') self.run('choose_keystore')
@ -535,7 +486,6 @@ class BaseWizard(object):
def on_password(self, password, *, encrypt_storage, def on_password(self, password, *, encrypt_storage,
storage_enc_version=STO_EV_USER_PW, encrypt_keystore): storage_enc_version=STO_EV_USER_PW, encrypt_keystore):
assert not self.storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
self.storage.set_keystore_encryption(bool(password) and encrypt_keystore) self.storage.set_keystore_encryption(bool(password) and encrypt_keystore)
if encrypt_storage: if encrypt_storage:
self.storage.set_password(password, enc_version=storage_enc_version) self.storage.set_password(password, enc_version=storage_enc_version)
@ -565,16 +515,18 @@ class BaseWizard(object):
def show_xpub_and_add_cosigners(self, xpub): def show_xpub_and_add_cosigners(self, xpub):
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
def choose_seed_type(self, message=None, choices=None): def choose_seed_type(self):
title = _('Choose Seed type') title = _('Choose Seed type')
if message is None: message = ' '.join([
message = ' '.join([ _("The type of addresses used by your wallet will depend on your seed."),
_("The type of addresses used by your wallet will depend on your seed.") _("Segwit wallets use bech32 addresses, defined in BIP173."),
]) _("Please note that websites and other wallets may not support these addresses yet."),
if choices is None: _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.")
choices = [ ])
('create_standard_seed', _('Legacy')), choices = [
] ('create_standard_seed', _('Standard')),
('create_segwit_seed', _('Segwit')),
]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
def create_segwit_seed(self): self.create_seed('segwit') def create_segwit_seed(self): self.create_seed('segwit')

View File

@ -1,269 +0,0 @@
# Copyright (C) 2018 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
import hashlib
from typing import List
from .util import bfh, bh2u, BitcoinException, print_error
from . import constants
from . import ecc
from .crypto import hash_160, hmac_oneshot
from .bitcoin import rev_hex, int_to_hex, EncodeBase58Check, DecodeBase58Check
BIP32_PRIME = 0x80000000
def protect_against_invalid_ecpoint(func):
def func_wrapper(*args):
n = args[-1]
while True:
is_prime = n & BIP32_PRIME
try:
return func(*args[:-1], n=n)
except ecc.InvalidECPointException:
print_error('bip32 protect_against_invalid_ecpoint: skipping index')
n += 1
is_prime2 = n & BIP32_PRIME
if is_prime != is_prime2: raise OverflowError()
return func_wrapper
# Child private key derivation function (from master private key)
# k = master private key (32 bytes)
# c = master chain code (extra entropy for key derivation) (32 bytes)
# n = the index of the key we want to derive. (only 32 bits will be used)
# If n is hardened (i.e. the 32nd bit is set), the resulting private key's
# corresponding public key can NOT be determined without the master private key.
# However, if n is not hardened, the resulting private key's corresponding
# public key can be determined without the master private key.
@protect_against_invalid_ecpoint
def CKD_priv(k, c, n):
if n < 0: raise ValueError('the bip32 index needs to be non-negative')
is_prime = n & BIP32_PRIME
return _CKD_priv(k, c, bfh(rev_hex(int_to_hex(n,4))), is_prime)
def _CKD_priv(k, c, s, is_prime):
try:
keypair = ecc.ECPrivkey(k)
except ecc.InvalidECPointException as e:
raise BitcoinException('Impossible xprv (not within curve order)') from e
cK = keypair.get_public_key_bytes(compressed=True)
data = bytes([0]) + k + s if is_prime else cK + s
I = hmac_oneshot(c, data, hashlib.sha512)
I_left = ecc.string_to_number(I[0:32])
k_n = (I_left + ecc.string_to_number(k)) % ecc.CURVE_ORDER
if I_left >= ecc.CURVE_ORDER or k_n == 0:
raise ecc.InvalidECPointException()
k_n = ecc.number_to_string(k_n, ecc.CURVE_ORDER)
c_n = I[32:]
return k_n, c_n
# Child public key derivation function (from public key only)
# K = master public key
# c = master chain code
# n = index of key we want to derive
# This function allows us to find the nth public key, as long as n is
# not hardened. If n is hardened, we need the master private key to find it.
@protect_against_invalid_ecpoint
def CKD_pub(cK, c, n):
if n < 0: raise ValueError('the bip32 index needs to be non-negative')
if n & BIP32_PRIME: raise Exception()
return _CKD_pub(cK, c, bfh(rev_hex(int_to_hex(n,4))))
# helper function, callable with arbitrary string.
# note: 's' does not need to fit into 32 bits here! (c.f. trustedcoin billing)
def _CKD_pub(cK, c, s):
I = hmac_oneshot(c, cK + s, hashlib.sha512)
pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(cK)
if pubkey.is_at_infinity():
raise ecc.InvalidECPointException()
cK_n = pubkey.get_public_key_bytes(compressed=True)
c_n = I[32:]
return cK_n, c_n
def xprv_header(xtype, *, net=None):
if net is None:
net = constants.net
return bfh("%08x" % net.XPRV_HEADERS[xtype])
def xpub_header(xtype, *, net=None):
if net is None:
net = constants.net
return bfh("%08x" % net.XPUB_HEADERS[xtype])
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4,
child_number=b'\x00'*4, *, net=None):
if not ecc.is_secret_within_curve_range(k):
raise BitcoinException('Impossible xprv (not within curve order)')
xprv = xprv_header(xtype, net=net) \
+ bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
return EncodeBase58Check(xprv)
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
child_number=b'\x00'*4, *, net=None):
xpub = xpub_header(xtype, net=net) \
+ bytes([depth]) + fingerprint + child_number + c + cK
return EncodeBase58Check(xpub)
class InvalidMasterKeyVersionBytes(BitcoinException): pass
def deserialize_xkey(xkey, prv, *, net=None):
if net is None:
net = constants.net
xkey = DecodeBase58Check(xkey)
if len(xkey) != 78:
raise BitcoinException('Invalid length for extended key: {}'
.format(len(xkey)))
depth = xkey[4]
fingerprint = xkey[5:9]
child_number = xkey[9:13]
c = xkey[13:13+32]
header = int.from_bytes(xkey[0:4], byteorder='big')
headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
if header not in headers.values():
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:]
if prv and not ecc.is_secret_within_curve_range(K_or_k):
raise BitcoinException('Impossible xprv (not within curve order)')
return xtype, depth, fingerprint, child_number, c, K_or_k
def deserialize_xpub(xkey, *, net=None):
return deserialize_xkey(xkey, False, net=net)
def deserialize_xprv(xkey, *, net=None):
return deserialize_xkey(xkey, True, net=net)
def xpub_type(x):
return deserialize_xpub(x)[0]
def is_xpub(text):
try:
deserialize_xpub(text)
return True
except:
return False
def is_xprv(text):
try:
deserialize_xprv(text)
return True
except:
return False
def xpub_from_xprv(xprv):
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True)
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
def bip32_root(seed, xtype):
I = hmac_oneshot(b"Bitcoin seed", seed, hashlib.sha512)
master_k = I[0:32]
master_c = I[32:]
# create xprv first, as that will check if master_k is within curve order
xprv = serialize_xprv(xtype, master_c, master_k)
cK = ecc.ECPrivkey(master_k).get_public_key_bytes(compressed=True)
xpub = serialize_xpub(xtype, master_c, cK)
return xprv, xpub
def xpub_from_pubkey(xtype, cK):
if cK[0] not in (0x02, 0x03):
raise ValueError('Unexpected first byte: {}'.format(cK[0]))
return serialize_xpub(xtype, b'\x00'*32, cK)
def bip32_derivation(s: str) -> int:
if not s.startswith('m/'):
raise ValueError('invalid bip32 derivation path: {}'.format(s))
s = s[2:]
for n in s.split('/'):
if n == '': continue
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
yield i
def 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: str) -> bool:
try:
[ i for i in bip32_derivation(x)]
return True
except :
return False
def bip32_private_derivation(xprv, branch, sequence):
if not sequence.startswith(branch):
raise ValueError('incompatible branch ({}) and sequence ({})'
.format(branch, sequence))
if branch == sequence:
return xprv, xpub_from_xprv(xprv)
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
sequence = sequence[len(branch):]
for n in sequence.split('/'):
if n == '': continue
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
parent_k = k
k, c = CKD_priv(k, c, i)
depth += 1
parent_cK = ecc.ECPrivkey(parent_k).get_public_key_bytes(compressed=True)
fingerprint = hash_160(parent_cK)[0:4]
child_number = bfh("%08X"%i)
cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True)
xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
xprv = serialize_xprv(xtype, c, k, depth, fingerprint, child_number)
return xprv, xpub
def bip32_public_derivation(xpub, branch, sequence):
xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
if not sequence.startswith(branch):
raise ValueError('incompatible branch ({}) and sequence ({})'
.format(branch, sequence))
sequence = sequence[len(branch):]
for n in sequence.split('/'):
if n == '': continue
i = int(n)
parent_cK = cK
cK, c = CKD_pub(cK, c, i)
depth += 1
fingerprint = hash_160(parent_cK)[0:4]
child_number = bfh("%08X"%i)
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
def bip32_private_key(sequence, k, chain):
for i in sequence:
k, chain = CKD_priv(k, chain, i)
return k

View File

@ -24,17 +24,14 @@
# SOFTWARE. # SOFTWARE.
import hashlib import hashlib
from typing import List, Tuple, TYPE_CHECKING, Optional, Union from typing import List
from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict from .util import bfh, bh2u, BitcoinException, print_error, assert_bytes, to_bytes, inv_dict
from . import version from . import version
from . import segwit_addr from . import segwit_addr
from . import constants from . import constants
from . import ecc from . import ecc
from .crypto import sha256d, sha256, hash_160, hmac_oneshot from .crypto import Hash, sha256, hash_160, hmac_oneshot
if TYPE_CHECKING:
from .network import Network
################################## transactions ################################## transactions
@ -49,7 +46,7 @@ TYPE_PUBKEY = 1
TYPE_SCRIPT = 2 TYPE_SCRIPT = 2
def rev_hex(s: str) -> str: def rev_hex(s):
return bh2u(bfh(s)[::-1]) return bh2u(bfh(s)[::-1])
@ -60,7 +57,7 @@ def int_to_hex(i: int, length: int=1) -> str:
if not isinstance(i, int): if not isinstance(i, int):
raise TypeError('{} instead of int'.format(i)) raise TypeError('{} instead of int'.format(i))
range_size = pow(256, length) range_size = pow(256, length)
if i < -(range_size//2) or i >= range_size: if i < -range_size/2 or i >= range_size:
raise OverflowError('cannot convert int {} to hex ({} bytes)'.format(i, length)) raise OverflowError('cannot convert int {} to hex ({} bytes)'.format(i, length))
if i < 0: if i < 0:
# two's complement # two's complement
@ -150,37 +147,19 @@ def add_number_to_script(i: int) -> bytes:
return bfh(push_script(script_num_to_hex(i))) return bfh(push_script(script_num_to_hex(i)))
def relayfee(network: 'Network'=None) -> int: hash_encode = lambda x: bh2u(x[::-1])
from .simple_config import FEERATE_DEFAULT_RELAY hash_decode = lambda x: bfh(x)[::-1]
MAX_RELAY_FEE = 50000 hmac_sha_512 = lambda x, y: hmac_oneshot(x, y, hashlib.sha512)
f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY
return min(f, MAX_RELAY_FEE)
def dust_threshold(network: 'Network'=None) -> int: def is_new_seed(x, prefix=version.SEED_PREFIX):
# Change <= dust threshold is added to the tx fee
return 182 * 3 * relayfee(network) // 1000
def hash_encode(x: bytes) -> str:
return bh2u(x[::-1])
def hash_decode(x: str) -> bytes:
return bfh(x)[::-1]
################################## electrum seeds
def is_new_seed(x: str, prefix=version.SEED_PREFIX) -> bool:
from . import mnemonic from . import mnemonic
x = mnemonic.normalize_text(x) x = mnemonic.normalize_text(x)
s = bh2u(hmac_oneshot(b"Seed version", x.encode('utf8'), hashlib.sha512)) s = bh2u(hmac_sha_512(b"Seed version", x.encode('utf8')))
return s.startswith(prefix) return s.startswith(prefix)
def is_old_seed(seed: str) -> bool: def is_old_seed(seed):
from . import old_mnemonic, mnemonic from . import old_mnemonic, mnemonic
seed = mnemonic.normalize_text(seed) seed = mnemonic.normalize_text(seed)
words = seed.split() words = seed.split()
@ -198,7 +177,7 @@ def is_old_seed(seed: str) -> bool:
return is_hex or (uses_electrum_words and (len(words) == 12 or len(words) == 24)) return is_hex or (uses_electrum_words and (len(words) == 12 or len(words) == 24))
def seed_type(x: str) -> str: def seed_type(x):
if is_old_seed(x): if is_old_seed(x):
return 'old' return 'old'
elif is_new_seed(x): elif is_new_seed(x):
@ -207,130 +186,119 @@ def seed_type(x: str) -> str:
return 'segwit' return 'segwit'
elif is_new_seed(x, version.SEED_PREFIX_2FA): elif is_new_seed(x, version.SEED_PREFIX_2FA):
return '2fa' return '2fa'
elif is_new_seed(x, version.SEED_PREFIX_2FA_SW):
return '2fa_segwit'
return '' return ''
is_seed = lambda x: bool(seed_type(x))
def is_seed(x: str) -> bool:
return bool(seed_type(x))
def is_any_2fa_seed_type(seed_type):
return seed_type in ['2fa', '2fa_segwit']
############ functions from pywallet ##################### ############ functions from pywallet #####################
def hash160_to_b58_address(h160: bytes, addrtype: int) -> str: def hash160_to_b58_address(h160: bytes, addrtype):
s = bytes([addrtype]) + h160 s = bytes([addrtype])
s = s + sha256d(s)[0:4] s += h160
return base_encode(s, base=58) return base_encode(s+Hash(s)[0:4], base=58)
def b58_address_to_hash160(addr: str) -> Tuple[int, bytes]: def b58_address_to_hash160(addr):
addr = to_bytes(addr, 'ascii') addr = to_bytes(addr, 'ascii')
_bytes = base_decode(addr, 25, base=58) _bytes = base_decode(addr, 25, base=58)
return _bytes[0], _bytes[1:21] return _bytes[0], _bytes[1:21]
def hash160_to_p2pkh(h160: bytes, *, net=None) -> str: def hash160_to_p2pkh(h160, *, net=None):
if net is None: net = constants.net if net is None:
net = constants.net
return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH) return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH)
def hash160_to_p2sh(h160: bytes, *, net=None) -> str: def hash160_to_p2sh(h160, *, net=None):
if net is None: net = constants.net if net is None:
net = constants.net
return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH) return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH)
def public_key_to_p2pkh(public_key: bytes, *, net=None) -> str: def public_key_to_p2pkh(public_key: bytes) -> str:
if net is None: net = constants.net return hash160_to_p2pkh(hash_160(public_key))
return hash160_to_p2pkh(hash_160(public_key), net=net)
def hash_to_segwit_addr(h: bytes, witver: int, *, net=None) -> str: def hash_to_segwit_addr(h, witver, *, net=None):
if net is None: net = constants.net if net is None:
net = constants.net
return segwit_addr.encode(net.SEGWIT_HRP, witver, h) return segwit_addr.encode(net.SEGWIT_HRP, witver, h)
def public_key_to_p2wpkh(public_key: bytes, *, net=None) -> str: def public_key_to_p2wpkh(public_key):
if net is None: net = constants.net return hash_to_segwit_addr(hash_160(public_key), witver=0)
return hash_to_segwit_addr(hash_160(public_key), witver=0, net=net)
def script_to_p2wsh(script: str, *, net=None) -> str: def script_to_p2wsh(script):
if net is None: net = constants.net return hash_to_segwit_addr(sha256(bfh(script)), witver=0)
return hash_to_segwit_addr(sha256(bfh(script)), witver=0, net=net)
def p2wpkh_nested_script(pubkey: str) -> str: def p2wpkh_nested_script(pubkey):
pkh = bh2u(hash_160(bfh(pubkey))) pkh = bh2u(hash_160(bfh(pubkey)))
return '00' + push_script(pkh) return '00' + push_script(pkh)
def p2wsh_nested_script(witness_script: str) -> str: def p2wsh_nested_script(witness_script):
wsh = bh2u(sha256(bfh(witness_script))) wsh = bh2u(sha256(bfh(witness_script)))
return '00' + push_script(wsh) return '00' + push_script(wsh)
def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: def pubkey_to_address(txin_type, pubkey):
if net is None: net = constants.net
if txin_type == 'p2pkh': if txin_type == 'p2pkh':
return public_key_to_p2pkh(bfh(pubkey), net=net) return public_key_to_p2pkh(bfh(pubkey))
elif txin_type == 'p2wpkh': elif txin_type == 'p2wpkh':
return public_key_to_p2wpkh(bfh(pubkey), net=net) return public_key_to_p2wpkh(bfh(pubkey))
elif txin_type == 'p2wpkh-p2sh': elif txin_type == 'p2wpkh-p2sh':
scriptSig = p2wpkh_nested_script(pubkey) scriptSig = p2wpkh_nested_script(pubkey)
return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net) return hash160_to_p2sh(hash_160(bfh(scriptSig)))
else: else:
raise NotImplementedError(txin_type) raise NotImplementedError(txin_type)
def redeem_script_to_address(txin_type: str, redeem_script: str, *, net=None) -> str: def redeem_script_to_address(txin_type, redeem_script):
if net is None: net = constants.net
if txin_type == 'p2sh': if txin_type == 'p2sh':
return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net) return hash160_to_p2sh(hash_160(bfh(redeem_script)))
elif txin_type == 'p2wsh': elif txin_type == 'p2wsh':
return script_to_p2wsh(redeem_script, net=net) return script_to_p2wsh(redeem_script)
elif txin_type == 'p2wsh-p2sh': elif txin_type == 'p2wsh-p2sh':
scriptSig = p2wsh_nested_script(redeem_script) scriptSig = p2wsh_nested_script(redeem_script)
return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net) return hash160_to_p2sh(hash_160(bfh(scriptSig)))
else: else:
raise NotImplementedError(txin_type) raise NotImplementedError(txin_type)
def script_to_address(script: str, *, net=None) -> str: def script_to_address(script, *, net=None):
from .transaction import get_address_from_output_script from .transaction import get_address_from_output_script
t, addr = get_address_from_output_script(bfh(script), net=net) t, addr = get_address_from_output_script(bfh(script), net=net)
assert t == TYPE_ADDRESS assert t == TYPE_ADDRESS
return addr return addr
def address_to_script(addr: str, *, net=None) -> str: def address_to_script(addr, *, net=None):
if net is None: net = constants.net if net is None:
if not is_address(addr, net=net): net = constants.net
raise BitcoinException(f"invalid bitcoin address: {addr}")
witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr) witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr)
if witprog is not None: if witprog is not None:
if not (0 <= witver <= 16): if not (0 <= witver <= 16):
raise BitcoinException(f'impossible witness version: {witver}') raise BitcoinException('impossible witness version: {}'.format(witver))
OP_n = witver + 0x50 if witver > 0 else 0 OP_n = witver + 0x50 if witver > 0 else 0
script = bh2u(bytes([OP_n])) script = bh2u(bytes([OP_n]))
script += push_script(bh2u(bytes(witprog))) script += push_script(bh2u(bytes(witprog)))
return script return script
addrtype, hash_160_ = b58_address_to_hash160(addr) addrtype, hash_160 = b58_address_to_hash160(addr)
if addrtype == net.ADDRTYPE_P2PKH: if addrtype == net.ADDRTYPE_P2PKH:
script = '76a9' # op_dup, op_hash_160 script = '76a9' # op_dup, op_hash_160
script += push_script(bh2u(hash_160_)) script += push_script(bh2u(hash_160))
script += '88ac' # op_equalverify, op_checksig script += '88ac' # op_equalverify, op_checksig
elif addrtype == net.ADDRTYPE_P2SH: elif addrtype == net.ADDRTYPE_P2SH:
script = 'a9' # op_hash_160 script = 'a9' # op_hash_160
script += push_script(bh2u(hash_160_)) script += push_script(bh2u(hash_160))
script += '87' # op_equal script += '87' # op_equal
else: else:
raise BitcoinException(f'unknown address type: {addrtype}') raise BitcoinException('unknown address type: {}'.format(addrtype))
return script return script
def address_to_scripthash(addr: str) -> str: def address_to_scripthash(addr):
script = address_to_script(addr) script = address_to_script(addr)
return script_to_scripthash(script) return script_to_scripthash(script)
def script_to_scripthash(script: str) -> str: def script_to_scripthash(script):
h = sha256(bfh(script))[0:32] h = sha256(bytes.fromhex(script))[0:32]
return bh2u(bytes(reversed(h))) return bh2u(bytes(reversed(h)))
def public_key_to_p2pk_script(pubkey: str) -> str: def public_key_to_p2pk_script(pubkey):
script = push_script(pubkey) script = push_script(pubkey)
script += 'ac' # op_checksig script += 'ac' # op_checksig
return script return script
@ -372,7 +340,7 @@ def base_encode(v: bytes, base: int) -> str:
return result.decode('ascii') return result.decode('ascii')
def base_decode(v: Union[bytes, str], length: Optional[int], base: int) -> Optional[bytes]: def base_decode(v, length, base):
""" decode v into a string of len bytes.""" """ decode v into a string of len bytes."""
# assert_bytes(v) # assert_bytes(v)
v = to_bytes(v, 'ascii') v = to_bytes(v, 'ascii')
@ -410,20 +378,21 @@ class InvalidChecksum(Exception):
pass pass
def EncodeBase58Check(vchIn: bytes) -> str: def EncodeBase58Check(vchIn):
hash = sha256d(vchIn) hash = Hash(vchIn)
return base_encode(vchIn + hash[0:4], base=58) return base_encode(vchIn + hash[0:4], base=58)
def DecodeBase58Check(psz: Union[bytes, str]) -> bytes: def DecodeBase58Check(psz):
vchRet = base_decode(psz, None, base=58) vchRet = base_decode(psz, None, base=58)
payload = vchRet[0:-4] key = vchRet[0:-4]
csum_found = vchRet[-4:] csum = vchRet[-4:]
csum_calculated = sha256d(payload)[0:4] hash = Hash(key)
if csum_calculated != csum_found: cs32 = hash[0:4]
raise InvalidChecksum(f'calculated {bh2u(csum_calculated)}, found {bh2u(csum_found)}') if cs32 != csum:
raise InvalidChecksum('expected {}, actual {}'.format(bh2u(cs32), bh2u(csum)))
else: else:
return payload return key
# backwards compat # backwards compat
@ -440,6 +409,12 @@ WIF_SCRIPT_TYPES = {
WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES) WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES)
PURPOSE48_SCRIPT_TYPES = {
'p2wsh-p2sh': 1, # specifically multisig
'p2wsh': 2, # specifically multisig
}
PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES)
def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, def serialize_privkey(secret: bytes, compressed: bool, txin_type: str,
internal_use: bool=False) -> str: internal_use: bool=False) -> str:
@ -458,7 +433,7 @@ def serialize_privkey(secret: bytes, compressed: bool, txin_type: str,
return '{}:{}'.format(txin_type, base58_wif) return '{}:{}'.format(txin_type, base58_wif)
def deserialize_privkey(key: str) -> Tuple[str, bytes, bool]: def deserialize_privkey(key: str) -> (str, bytes, bool):
if is_minikey(key): if is_minikey(key):
return 'p2pkh', minikey_to_private_key(key), False return 'p2pkh', minikey_to_private_key(key), False
@ -495,40 +470,36 @@ def deserialize_privkey(key: str) -> Tuple[str, bytes, bool]:
return txin_type, secret_bytes, compressed return txin_type, secret_bytes, compressed
def is_compressed_privkey(sec: str) -> bool: def is_compressed(sec):
return deserialize_privkey(sec)[2] return deserialize_privkey(sec)[2]
def address_from_private_key(sec: str) -> str: def address_from_private_key(sec):
txin_type, privkey, compressed = deserialize_privkey(sec) txin_type, privkey, compressed = deserialize_privkey(sec)
public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
return pubkey_to_address(txin_type, public_key) return pubkey_to_address(txin_type, public_key)
def is_segwit_address(addr: str, *, net=None) -> bool: def is_segwit_address(addr):
if net is None: net = constants.net
try: try:
witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr) witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
except Exception as e: except Exception as e:
return False return False
return witprog is not None return witprog is not None
def is_b58_address(addr: str, *, net=None) -> bool: def is_b58_address(addr):
if net is None: net = constants.net
try: try:
addrtype, h = b58_address_to_hash160(addr) addrtype, h = b58_address_to_hash160(addr)
except Exception as e: except Exception as e:
return False return False
if addrtype not in [net.ADDRTYPE_P2PKH, net.ADDRTYPE_P2SH]: if addrtype not in [constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH]:
return False return False
return addr == hash160_to_b58_address(h, addrtype) return addr == hash160_to_b58_address(h, addrtype)
def is_address(addr: str, *, net=None) -> bool: def is_address(addr):
if net is None: net = constants.net return is_segwit_address(addr) or is_b58_address(addr)
return is_segwit_address(addr, net=net) \
or is_b58_address(addr, net=net)
def is_private_key(key: str) -> bool: def is_private_key(key):
try: try:
k = deserialize_privkey(key) k = deserialize_privkey(key)
return k is not False return k is not False
@ -538,7 +509,7 @@ def is_private_key(key: str) -> bool:
########### end pywallet functions ####################### ########### end pywallet functions #######################
def is_minikey(text: str) -> bool: def is_minikey(text):
# Minikeys are typically 22 or 30 characters, but this routine # Minikeys are typically 22 or 30 characters, but this routine
# permits any length of 20 or more provided the minikey is valid. # permits any length of 20 or more provided the minikey is valid.
# A valid minikey must begin with an 'S', be in base58, and when # A valid minikey must begin with an 'S', be in base58, and when
@ -548,5 +519,264 @@ def is_minikey(text: str) -> bool:
and all(ord(c) in __b58chars for c in text) and all(ord(c) in __b58chars for c in text)
and sha256(text + '?')[0] == 0x00) and sha256(text + '?')[0] == 0x00)
def minikey_to_private_key(text: str) -> bytes: def minikey_to_private_key(text):
return sha256(text) return sha256(text)
###################################### BIP32 ##############################
BIP32_PRIME = 0x80000000
def protect_against_invalid_ecpoint(func):
def func_wrapper(*args):
n = args[-1]
while True:
is_prime = n & BIP32_PRIME
try:
return func(*args[:-1], n=n)
except ecc.InvalidECPointException:
print_error('bip32 protect_against_invalid_ecpoint: skipping index')
n += 1
is_prime2 = n & BIP32_PRIME
if is_prime != is_prime2: raise OverflowError()
return func_wrapper
# Child private key derivation function (from master private key)
# k = master private key (32 bytes)
# c = master chain code (extra entropy for key derivation) (32 bytes)
# n = the index of the key we want to derive. (only 32 bits will be used)
# If n is hardened (i.e. the 32nd bit is set), the resulting private key's
# corresponding public key can NOT be determined without the master private key.
# However, if n is not hardened, the resulting private key's corresponding
# public key can be determined without the master private key.
@protect_against_invalid_ecpoint
def CKD_priv(k, c, n):
if n < 0: raise ValueError('the bip32 index needs to be non-negative')
is_prime = n & BIP32_PRIME
return _CKD_priv(k, c, bfh(rev_hex(int_to_hex(n,4))), is_prime)
def _CKD_priv(k, c, s, is_prime):
try:
keypair = ecc.ECPrivkey(k)
except ecc.InvalidECPointException as e:
raise BitcoinException('Impossible xprv (not within curve order)') from e
cK = keypair.get_public_key_bytes(compressed=True)
data = bytes([0]) + k + s if is_prime else cK + s
I = hmac_oneshot(c, data, hashlib.sha512)
I_left = ecc.string_to_number(I[0:32])
k_n = (I_left + ecc.string_to_number(k)) % ecc.CURVE_ORDER
if I_left >= ecc.CURVE_ORDER or k_n == 0:
raise ecc.InvalidECPointException()
k_n = ecc.number_to_string(k_n, ecc.CURVE_ORDER)
c_n = I[32:]
return k_n, c_n
# Child public key derivation function (from public key only)
# K = master public key
# c = master chain code
# n = index of key we want to derive
# This function allows us to find the nth public key, as long as n is
# not hardened. If n is hardened, we need the master private key to find it.
@protect_against_invalid_ecpoint
def CKD_pub(cK, c, n):
if n < 0: raise ValueError('the bip32 index needs to be non-negative')
if n & BIP32_PRIME: raise Exception()
return _CKD_pub(cK, c, bfh(rev_hex(int_to_hex(n,4))))
# helper function, callable with arbitrary string.
# note: 's' does not need to fit into 32 bits here! (c.f. trustedcoin billing)
def _CKD_pub(cK, c, s):
I = hmac_oneshot(c, cK + s, hashlib.sha512)
pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(cK)
if pubkey.is_at_infinity():
raise ecc.InvalidECPointException()
cK_n = pubkey.get_public_key_bytes(compressed=True)
c_n = I[32:]
return cK_n, c_n
def xprv_header(xtype, *, net=None):
if net is None:
net = constants.net
return bfh("%08x" % net.XPRV_HEADERS[xtype])
def xpub_header(xtype, *, net=None):
if net is None:
net = constants.net
return bfh("%08x" % net.XPUB_HEADERS[xtype])
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4,
child_number=b'\x00'*4, *, net=None):
if not ecc.is_secret_within_curve_range(k):
raise BitcoinException('Impossible xprv (not within curve order)')
xprv = xprv_header(xtype, net=net) \
+ bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
return EncodeBase58Check(xprv)
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
child_number=b'\x00'*4, *, net=None):
xpub = xpub_header(xtype, net=net) \
+ bytes([depth]) + fingerprint + child_number + c + cK
return EncodeBase58Check(xpub)
class InvalidMasterKeyVersionBytes(BitcoinException): pass
def deserialize_xkey(xkey, prv, *, net=None):
if net is None:
net = constants.net
xkey = DecodeBase58Check(xkey)
if len(xkey) != 78:
raise BitcoinException('Invalid length for extended key: {}'
.format(len(xkey)))
depth = xkey[4]
fingerprint = xkey[5:9]
child_number = xkey[9:13]
c = xkey[13:13+32]
header = int('0x' + bh2u(xkey[0:4]), 16)
headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
if header not in headers.values():
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:]
if prv and not ecc.is_secret_within_curve_range(K_or_k):
raise BitcoinException('Impossible xprv (not within curve order)')
return xtype, depth, fingerprint, child_number, c, K_or_k
def deserialize_xpub(xkey, *, net=None):
return deserialize_xkey(xkey, False, net=net)
def deserialize_xprv(xkey, *, net=None):
return deserialize_xkey(xkey, True, net=net)
def xpub_type(x):
return deserialize_xpub(x)[0]
def is_xpub(text):
try:
deserialize_xpub(text)
return True
except:
return False
def is_xprv(text):
try:
deserialize_xprv(text)
return True
except:
return False
def xpub_from_xprv(xprv):
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True)
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
def bip32_root(seed, xtype):
I = hmac_oneshot(b"Bitcoin seed", seed, hashlib.sha512)
master_k = I[0:32]
master_c = I[32:]
# create xprv first, as that will check if master_k is within curve order
xprv = serialize_xprv(xtype, master_c, master_k)
cK = ecc.ECPrivkey(master_k).get_public_key_bytes(compressed=True)
xpub = serialize_xpub(xtype, master_c, cK)
return xprv, xpub
def xpub_from_pubkey(xtype, cK):
if cK[0] not in (0x02, 0x03):
raise ValueError('Unexpected first byte: {}'.format(cK[0]))
return serialize_xpub(xtype, b'\x00'*32, cK)
def bip32_derivation(s):
if not s.startswith('m/'):
raise ValueError('invalid bip32 derivation path: {}'.format(s))
s = s[2:]
for n in s.split('/'):
if n == '': continue
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
yield i
def 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)]
return True
except :
return False
def bip32_private_derivation(xprv, branch, sequence):
if not sequence.startswith(branch):
raise ValueError('incompatible branch ({}) and sequence ({})'
.format(branch, sequence))
if branch == sequence:
return xprv, xpub_from_xprv(xprv)
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
sequence = sequence[len(branch):]
for n in sequence.split('/'):
if n == '': continue
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
parent_k = k
k, c = CKD_priv(k, c, i)
depth += 1
parent_cK = ecc.ECPrivkey(parent_k).get_public_key_bytes(compressed=True)
fingerprint = hash_160(parent_cK)[0:4]
child_number = bfh("%08X"%i)
cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True)
xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
xprv = serialize_xprv(xtype, c, k, depth, fingerprint, child_number)
return xprv, xpub
def bip32_public_derivation(xpub, branch, sequence):
xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
if not sequence.startswith(branch):
raise ValueError('incompatible branch ({}) and sequence ({})'
.format(branch, sequence))
sequence = sequence[len(branch):]
for n in sequence.split('/'):
if n == '': continue
i = int(n)
parent_cK = cK
cK, c = CKD_pub(cK, c, i)
depth += 1
fingerprint = hash_160(parent_cK)[0:4]
child_number = bfh("%08X"%i)
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
def bip32_private_key(sequence, k, chain):
for i in sequence:
k, chain = CKD_priv(k, chain, i)
return k

View File

@ -22,47 +22,45 @@
# SOFTWARE. # SOFTWARE.
import os import os
import threading import threading
from typing import Optional, Dict
from . import util from . import util
from .bitcoin import hash_encode, int_to_hex, rev_hex from .bitcoin import Hash, hash_encode, int_to_hex, rev_hex
from .crypto import sha256d
from . import constants from . import constants
from .util import bfh, bh2u from .util import bfh, bh2u
from .simple_config import SimpleConfig
try: try:
import pylibscrypt import scrypt
getPoWHash = lambda x: pylibscrypt.scrypt(password=x, salt=x, N=1024, r=1, p=1, olen=32) getPoWHash = lambda x: scrypt.hash(x, x, N=1024, r=1, p=1, buflen=32)
except ImportError: except ImportError:
util.print_msg("Warning: package pylibscrypt not available") util.print_msg("Warning: package scrypt not available; synchronization could be very slow")
from .scrypt import scrypt_1024_1_1_80 as getPoWHash
HEADER_SIZE = 80 # bytes
MAX_TARGET = 0x00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff MAX_TARGET = 0x00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
class MissingHeader(Exception): class MissingHeader(Exception):
pass pass
class InvalidHeader(Exception): class InvalidHeader(Exception):
pass pass
def serialize_header(header_dict: dict) -> str: def serialize_header(res):
s = int_to_hex(header_dict['version'], 4) \ s = int_to_hex(res.get('version'), 4) \
+ rev_hex(header_dict['prev_block_hash']) \ + rev_hex(res.get('prev_block_hash')) \
+ rev_hex(header_dict['merkle_root']) \ + rev_hex(res.get('merkle_root')) \
+ int_to_hex(int(header_dict['timestamp']), 4) \ + int_to_hex(int(res.get('timestamp')), 4) \
+ int_to_hex(int(header_dict['bits']), 4) \ + int_to_hex(int(res.get('bits')), 4) \
+ int_to_hex(int(header_dict['nonce']), 4) + int_to_hex(int(res.get('nonce')), 4)
return s return s
def deserialize_header(s: bytes, height: int) -> dict: def deserialize_header(s, height):
if not s: if not s:
raise InvalidHeader('Invalid header: {}'.format(s)) raise InvalidHeader('Invalid header: {}'.format(s))
if len(s) != HEADER_SIZE: if len(s) != 80:
raise InvalidHeader('Invalid header length: {}'.format(len(s))) raise InvalidHeader('Invalid header length: {}'.format(len(s)))
hex_to_int = lambda s: int.from_bytes(s, byteorder='little') hex_to_int = lambda s: int('0x' + bh2u(s[::-1]), 16)
h = {} h = {}
h['version'] = hex_to_int(s[0:4]) h['version'] = hex_to_int(s[0:4])
h['prev_block_hash'] = hash_encode(s[4:36]) h['prev_block_hash'] = hash_encode(s[4:36])
@ -73,97 +71,48 @@ def deserialize_header(s: bytes, height: int) -> dict:
h['block_height'] = height h['block_height'] = height
return h return h
def hash_header(header: dict) -> str: def hash_header(header):
if header is None: if header is None:
return '0' * 64 return '0' * 64
if header.get('prev_block_hash') is None: if header.get('prev_block_hash') is None:
header['prev_block_hash'] = '00'*32 header['prev_block_hash'] = '00'*32
return hash_raw_header(serialize_header(header)) return hash_encode(Hash(bfh(serialize_header(header))))
def hash_raw_header(header: str) -> str:
return hash_encode(sha256d(bfh(header)))
# key: blockhash hex at forkpoint
# the chain at some key is the best chain that includes the given hash
blockchains = {} # type: Dict[str, Blockchain]
blockchains_lock = threading.RLock()
def read_blockchains(config: 'SimpleConfig'):
best_chain = Blockchain(config=config,
forkpoint=0,
parent=None,
forkpoint_hash=constants.net.GENESIS,
prev_hash=None)
blockchains[constants.net.GENESIS] = best_chain
# consistency checks
if best_chain.height() > constants.net.max_checkpoint():
header_after_cp = best_chain.read_header(constants.net.max_checkpoint()+1)
if not header_after_cp or not best_chain.can_connect(header_after_cp, check_height=False):
util.print_error("[blockchain] deleting best chain. cannot connect header after last cp to last cp.")
os.unlink(best_chain.path())
best_chain.update_size()
# forks
fdir = os.path.join(util.get_headers_dir(config), 'forks')
util.make_dir(fdir)
# files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash}
l = filter(lambda x: x.startswith('fork2_') and '.' not in x, os.listdir(fdir))
l = sorted(l, key=lambda x: int(x.split('_')[1])) # sort by forkpoint
def delete_chain(filename, reason):
util.print_error(f"[blockchain] deleting chain {filename}: {reason}")
os.unlink(os.path.join(fdir, filename))
def instantiate_chain(filename):
__, forkpoint, prev_hash, first_hash = filename.split('_')
forkpoint = int(forkpoint)
prev_hash = (64-len(prev_hash)) * "0" + prev_hash # left-pad with zeroes
first_hash = (64-len(first_hash)) * "0" + first_hash
# forks below the max checkpoint are not allowed
if forkpoint <= constants.net.max_checkpoint():
delete_chain(filename, "deleting fork below max checkpoint")
return
# find parent (sorting by forkpoint guarantees it's already instantiated)
for parent in blockchains.values():
if parent.check_hash(forkpoint - 1, prev_hash):
break
else:
delete_chain(filename, "cannot find parent for chain")
return
b = Blockchain(config=config,
forkpoint=forkpoint,
parent=parent,
forkpoint_hash=first_hash,
prev_hash=prev_hash)
# consistency checks
h = b.read_header(b.forkpoint)
if first_hash != hash_header(h):
delete_chain(filename, "incorrect first hash for chain")
return
if not b.parent.can_connect(h, check_height=False):
delete_chain(filename, "cannot connect chain to parent")
return
chain_id = b.get_id()
assert first_hash == chain_id, (first_hash, chain_id)
blockchains[chain_id] = b
for filename in l:
instantiate_chain(filename)
def pow_hash_header(header): def pow_hash_header(header):
return hash_encode(getPoWHash(bfh(serialize_header(header)))) return hash_encode(getPoWHash(bfh(serialize_header(header))))
blockchains = {}
def get_best_chain() -> 'Blockchain': def read_blockchains(config):
return blockchains[constants.net.GENESIS] blockchains[0] = Blockchain(config, 0, None)
fdir = os.path.join(util.get_headers_dir(config), 'forks')
util.make_dir(fdir)
l = filter(lambda x: x.startswith('fork_'), os.listdir(fdir))
l = sorted(l, key = lambda x: int(x.split('_')[1]))
for filename in l:
forkpoint = int(filename.split('_')[2])
parent_id = int(filename.split('_')[1])
b = Blockchain(config, forkpoint, parent_id)
h = b.read_header(b.forkpoint)
if b.parent().can_connect(h, check_height=False):
blockchains[b.forkpoint] = b
else:
util.print_error("cannot connect", filename)
return blockchains
# block hash -> chain work; up to and including that block def check_header(header):
_CHAINWORK_CACHE = { if type(header) is not dict:
"0000000000000000000000000000000000000000000000000000000000000000": 0, # virtual block at height -1 return False
} # type: Dict[str, int] for b in blockchains.values():
if b.check_header(header):
return b
return False
def can_connect(header):
for b in blockchains.values():
if b.can_connect(header):
return b
return False
class Blockchain(util.PrintError): class Blockchain(util.PrintError):
@ -171,20 +120,16 @@ class Blockchain(util.PrintError):
Manages blockchain headers and their verification Manages blockchain headers and their verification
""" """
def __init__(self, config: SimpleConfig, forkpoint: int, parent: Optional['Blockchain'], def __init__(self, config, forkpoint, parent_id):
forkpoint_hash: str, prev_hash: Optional[str]):
assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash
assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash
# assert (parent is None) == (forkpoint == 0)
if 0 < forkpoint <= constants.net.max_checkpoint():
raise Exception(f"cannot fork below max checkpoint. forkpoint: {forkpoint}")
self.config = config self.config = config
self.forkpoint = forkpoint # height of first header self.catch_up = None # interface catching up
self.parent = parent self.forkpoint = forkpoint
self._forkpoint_hash = forkpoint_hash # blockhash at forkpoint. "first hash" self.checkpoints = constants.net.CHECKPOINTS
self._prev_hash = prev_hash # blockhash immediately before forkpoint self.parent_id = parent_id
assert parent_id != forkpoint
self.lock = threading.RLock() self.lock = threading.RLock()
self.update_size() with self.lock:
self.update_size()
def with_lock(func): def with_lock(func):
def func_wrapper(self, *args, **kwargs): def func_wrapper(self, *args, **kwargs):
@ -192,80 +137,50 @@ class Blockchain(util.PrintError):
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return func_wrapper return func_wrapper
@property def parent(self):
def checkpoints(self): return blockchains[self.parent_id]
return constants.net.CHECKPOINTS
def get_max_child(self) -> Optional[int]: def get_max_child(self):
with blockchains_lock: chains = list(blockchains.values()) children = list(filter(lambda y: y.parent_id==self.forkpoint, blockchains.values()))
children = list(filter(lambda y: y.parent==self, chains))
return max([x.forkpoint for x in children]) if children else None return max([x.forkpoint for x in children]) if children else None
def get_max_forkpoint(self) -> int: def get_forkpoint(self):
"""Returns the max height where there is a fork
related to this chain.
"""
mc = self.get_max_child() mc = self.get_max_child()
return mc if mc is not None else self.forkpoint return mc if mc is not None else self.forkpoint
@with_lock def get_branch_size(self):
def get_branch_size(self) -> int: return self.height() - self.get_forkpoint() + 1
return self.height() - self.get_max_forkpoint() + 1
def get_name(self) -> str: def get_name(self):
return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10] return self.get_hash(self.get_forkpoint()).lstrip('00')[0:10]
def check_header(self, header: dict) -> bool: def check_header(self, header):
header_hash = hash_header(header) header_hash = hash_header(header)
height = header.get('block_height') height = header.get('block_height')
return self.check_hash(height, header_hash)
def check_hash(self, height: int, header_hash: str) -> bool:
"""Returns whether the hash of the block at given height
is the given hash.
"""
assert isinstance(header_hash, str) and len(header_hash) == 64, header_hash # hex
try: try:
return header_hash == self.get_hash(height) return header_hash == self.get_hash(height)
except Exception: except MissingHeader:
return False return False
def fork(parent, header: dict) -> 'Blockchain': def fork(parent, header):
if not parent.can_connect(header, check_height=False):
raise Exception("forking header does not connect to parent chain")
forkpoint = header.get('block_height') forkpoint = header.get('block_height')
self = Blockchain(config=parent.config, self = Blockchain(parent.config, forkpoint, parent.forkpoint)
forkpoint=forkpoint,
parent=parent,
forkpoint_hash=hash_header(header),
prev_hash=parent.get_hash(forkpoint-1))
open(self.path(), 'w+').close() open(self.path(), 'w+').close()
self.save_header(header) self.save_header(header)
# put into global dict. note that in some cases
# save_header might have already put it there but that's OK
chain_id = self.get_id()
with blockchains_lock:
blockchains[chain_id] = self
return self return self
@with_lock def height(self):
def height(self) -> int:
return self.forkpoint + self.size() - 1 return self.forkpoint + self.size() - 1
@with_lock def size(self):
def size(self) -> int: with self.lock:
return self._size return self._size
@with_lock def update_size(self):
def update_size(self) -> None:
p = self.path() p = self.path()
self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0 self._size = os.path.getsize(p)//80 if os.path.exists(p) else 0
#def pow_hash_header(header): def verify_header(self, header, prev_hash, target, expected_header_hash=None):
# return hash_encode(getPoWHash(bfh(serialize_header(header))))
@classmethod
def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None:
_hash = hash_header(header) _hash = hash_header(header)
_powhash = pow_hash_header(header) _powhash = pow_hash_header(header)
if expected_header_hash and expected_header_hash != _hash: if expected_header_hash and expected_header_hash != _hash:
@ -274,155 +189,98 @@ class Blockchain(util.PrintError):
raise Exception("prev hash mismatch: %s vs %s" % (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: if constants.net.TESTNET:
return return
bits = cls.target_to_bits(target) #print("I'm inside verify_header")
#bits = self.target_to_bits(target)
bits = target bits = target
if bits != header.get('bits'): if bits != header.get('bits'):
raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits'))) raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits')))
block_hash_as_num = int.from_bytes(bfh(_hash), byteorder='big') block_hash = int('0x' + _hash, 16)
target_val = cls.bits_to_target(bits) target_val = self.bits_to_target(bits)
if int('0x' + _powhash, 16) > target_val: if int('0x' + _powhash, 16) > target_val:
raise Exception("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target_val)) raise Exception("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target_val))
#print("I passed verify_header(). Calc target values have been matched")
def verify_chunk(self, index: int, data: bytes) -> None: def verify_chunk(self, index, data):
num = len(data) // HEADER_SIZE num = len(data) // 80
current_header = index * 2016 current_header = (index * 2016)
print('chunk ' + str(index)) # last = (index * 2016 + 2015)
print(index*2016)
prev_hash = self.get_hash(current_header - 1) prev_hash = self.get_hash(current_header - 1)
headerLast = None
headerFirst = None
capture = None
lst = []
for i in range(num): for i in range(num):
averaging_interval = self.AveragingInterval(current_header) target = self.get_target(current_header - 1)
difficulty_interval = self.DifficultyAdjustmentInterval(current_header) try:
if current_header < 426000: expected_header_hash = self.get_hash(current_header)
target = self.get_target(current_header - 1, headerLast, headerFirst) except MissingHeader:
try: expected_header_hash = None
expected_header_hash = self.get_hash(current_header) raw_header = data[i*80:(i+1) * 80]
except MissingHeader: header = deserialize_header(raw_header, current_header)
expected_header_hash = None print(i)
raw_header = data[i*HEADER_SIZE : (i+1)*HEADER_SIZE] self.verify_header(header, prev_hash, target, expected_header_hash)
header = deserialize_header(raw_header, current_header) self.save_chunk_part(header)
self.verify_header(header, prev_hash, target, expected_header_hash) prev_hash = hash_header(header)
prev_hash = hash_header(header) current_header = current_header + 1
headerLast = header
if current_header == 0:
headerFirst = header
elif (current_header + averaging_interval + 1) % difficulty_interval == 0:
capture = header
if current_header != 0 and current_header % difficulty_interval == 0:
headerFirst = capture
if current_header >= 425993:
lst.append(headerLast)
current_header = current_header + 1
else:
if len(lst)>6:
headerFirst = lst[0]
target = self.get_target(current_header - 1, headerLast, headerFirst)
try:
expected_header_hash = self.get_hash(current_header)
except MissingHeader:
expected_header_hash = None
raw_header = data[i * HEADER_SIZE: (i + 1) * HEADER_SIZE]
header = deserialize_header(raw_header, current_header)
self.verify_header(header, prev_hash, target, expected_header_hash)
prev_hash = hash_header(header)
headerLast = header
lst.append(header)
if len(lst)>7:
lst.pop(0)
current_header = current_header + 1
@with_lock
def path(self): def path(self):
d = util.get_headers_dir(self.config) d = util.get_headers_dir(self.config)
if self.parent is None: filename = 'blockchain_headers' if self.parent_id is None else os.path.join('forks', 'fork_%d_%d'%(self.parent_id, self.forkpoint))
filename = 'blockchain_headers'
else:
assert self.forkpoint > 0, self.forkpoint
prev_hash = self._prev_hash.lstrip('0')
first_hash = self._forkpoint_hash.lstrip('0')
basename = f'fork2_{self.forkpoint}_{prev_hash}_{first_hash}'
filename = os.path.join('forks', basename)
return os.path.join(d, filename) return os.path.join(d, filename)
@with_lock @with_lock
def save_chunk(self, index: int, chunk: bytes): def save_chunk(self, index, chunk):
assert index >= 0, index #chunk_within_checkpoint_region = index < len(self.checkpoints)
chunk_within_checkpoint_region = index < len(self.checkpoints)
# chunks in checkpoint region are the responsibility of the 'main chain' # chunks in checkpoint region are the responsibility of the 'main chain'
if chunk_within_checkpoint_region and self.parent is not None: #if chunk_within_checkpoint_region and self.parent_id is not None:
main_chain = get_best_chain() # main_chain = blockchains[0]
main_chain.save_chunk(index, chunk) # main_chain.save_chunk(index, chunk)
return # return
delta_height = (index * 2016 - self.forkpoint) #delta_height = (index * 2016 - self.forkpoint)
delta_bytes = delta_height * HEADER_SIZE #delta_bytes = delta_height * 80
# if this chunk contains our forkpoint, only save the part after forkpoint # if this chunk contains our forkpoint, only save the part after forkpoint
# (the part before is the responsibility of the parent) # (the part before is the responsibility of the parent)
if delta_bytes < 0: #if delta_bytes < 0:
chunk = chunk[-delta_bytes:] # chunk = chunk[-delta_bytes:]
delta_bytes = 0 # delta_bytes = 0
truncate = not chunk_within_checkpoint_region #truncate = not chunk_within_checkpoint_region
self.write(chunk, delta_bytes, truncate) #self.write(chunk, delta_bytes, truncate)
self.swap_with_parent() self.swap_with_parent()
def swap_with_parent(self) -> None: @with_lock
parent_lock = self.parent.lock if self.parent is not None else threading.Lock() def swap_with_parent(self):
with parent_lock, self.lock, blockchains_lock: # this order should not deadlock if self.parent_id is None:
# do the swap; possibly multiple ones return
cnt = 0 parent_branch_size = self.parent().height() - self.forkpoint + 1
while self._swap_with_parent(): if parent_branch_size >= self.size():
cnt += 1 return
if cnt > len(blockchains): # make sure we are making progress self.print_error("swap", self.forkpoint, self.parent_id)
raise Exception(f'swapping fork with parent too many times: {cnt}') parent_id = self.parent_id
forkpoint = self.forkpoint
def _swap_with_parent(self) -> bool: parent = self.parent()
"""Check if this chain became stronger than its parent, and swap
the underlying files if so. The Blockchain instances will keep
'containing' the same headers, but their ids change and so
they will be stored in different files."""
if self.parent is None:
return False
if self.parent.get_chainwork() >= self.get_chainwork():
return False
self.print_error("swap", self.forkpoint, self.parent.forkpoint)
parent_branch_size = self.parent.height() - self.forkpoint + 1
forkpoint = self.forkpoint # type: Optional[int]
parent = self.parent # type: Optional[Blockchain]
child_old_id = self.get_id()
parent_old_id = parent.get_id()
# swap files
# child takes parent's name
# parent's new name will be something new (not child's old name)
self.assert_headers_file_available(self.path()) self.assert_headers_file_available(self.path())
child_old_name = self.path()
with open(self.path(), 'rb') as f: with open(self.path(), 'rb') as f:
my_data = f.read() my_data = f.read()
self.assert_headers_file_available(parent.path()) self.assert_headers_file_available(parent.path())
with open(parent.path(), 'rb') as f: with open(parent.path(), 'rb') as f:
f.seek((forkpoint - parent.forkpoint)*HEADER_SIZE) f.seek((forkpoint - parent.forkpoint)*80)
parent_data = f.read(parent_branch_size*HEADER_SIZE) parent_data = f.read(parent_branch_size*80)
self.write(parent_data, 0) self.write(parent_data, 0)
parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE) parent.write(my_data, (forkpoint - parent.forkpoint)*80)
# store file path
for b in blockchains.values():
b.old_path = b.path()
# swap parameters # swap parameters
self.parent, parent.parent = parent.parent, self # type: Optional[Blockchain], Optional[Blockchain] self.parent_id = parent.parent_id; parent.parent_id = parent_id
self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint self.forkpoint = parent.forkpoint; parent.forkpoint = forkpoint
self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE])) self._size = parent._size; parent._size = parent_branch_size
self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash # move files
# parent's new name for b in blockchains.values():
os.replace(child_old_name, parent.path()) if b in [self, parent]: continue
self.update_size() if b.old_path != b.path():
parent.update_size() self.print_error("renaming", b.old_path, b.path())
os.rename(b.old_path, b.path())
# update pointers # update pointers
blockchains.pop(child_old_id, None) blockchains[self.forkpoint] = self
blockchains.pop(parent_old_id, None) blockchains[parent.forkpoint] = parent
blockchains[self.get_id()] = self
blockchains[parent.get_id()] = parent
return True
def get_id(self) -> str:
return self._forkpoint_hash
def assert_headers_file_available(self, path): def assert_headers_file_available(self, path):
if os.path.exists(path): if os.path.exists(path):
@ -432,56 +290,51 @@ class Blockchain(util.PrintError):
else: else:
raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path)) raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path))
@with_lock def write(self, data, offset, truncate=True):
def write(self, data: bytes, offset: int, truncate: bool=True) -> None:
filename = self.path() filename = self.path()
self.assert_headers_file_available(filename) with self.lock:
with open(filename, 'rb+') as f: self.assert_headers_file_available(filename)
if truncate and offset != self._size * HEADER_SIZE: with open(filename, 'rb+') as f:
if truncate and offset != self._size*80:
f.seek(offset)
f.truncate()
f.seek(offset) f.seek(offset)
f.truncate() f.write(data)
f.seek(offset) f.flush()
f.write(data) os.fsync(f.fileno())
f.flush() self.update_size()
os.fsync(f.fileno())
self.update_size()
@with_lock @with_lock
def save_header(self, header: dict) -> None: def save_header(self, header):
delta = header.get('block_height') - self.forkpoint delta = header.get('block_height') - self.forkpoint
data = bfh(serialize_header(header)) data = bfh(serialize_header(header))
# headers are only _appended_ to the end: # headers are only _appended_ to the end:
assert delta == self.size(), (delta, self.size()) assert delta == self.size()
assert len(data) == HEADER_SIZE assert len(data) == 80
self.write(data, delta*HEADER_SIZE) self.write(data, delta*80)
self.swap_with_parent() self.swap_with_parent()
@with_lock def read_header(self, height):
def read_header(self, height: int) -> Optional[dict]: assert self.parent_id != self.forkpoint
if height < 0: if height < 0:
return return
if height < self.forkpoint: if height < self.forkpoint:
return self.parent.read_header(height) return self.parent().read_header(height)
if height > self.height(): if height > self.height():
return return
delta = height - self.forkpoint delta = height - self.forkpoint
name = self.path() name = self.path()
self.assert_headers_file_available(name) self.assert_headers_file_available(name)
with open(name, 'rb') as f: with open(name, 'rb') as f:
f.seek(delta * HEADER_SIZE) f.seek(delta * 80)
h = f.read(HEADER_SIZE) h = f.read(80)
if len(h) < HEADER_SIZE: if len(h) < 80:
raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h))) raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h)))
if h == bytes([0])*HEADER_SIZE: if h == bytes([0])*80:
return None return None
return deserialize_header(h, height) return deserialize_header(h, height)
def header_at_tip(self) -> Optional[dict]: def get_hash(self, height):
"""Return latest header."""
height = self.height()
return self.read_header(height)
def get_hash(self, height: int) -> str:
def is_height_checkpoint(): def is_height_checkpoint():
within_cp_range = height <= constants.net.max_checkpoint() within_cp_range = height <= constants.net.max_checkpoint()
at_chunk_boundary = (height+1) % 2016 == 0 at_chunk_boundary = (height+1) % 2016 == 0
@ -501,7 +354,7 @@ class Blockchain(util.PrintError):
raise MissingHeader(height) raise MissingHeader(height)
return hash_header(header) return hash_header(header)
def get_target(self, index: int, headerLast: dict=None, headerFirst: dict=None) -> int: def get_target(self, index):
# compute target from chunk x, used in chunk x+1 # compute target from chunk x, used in chunk x+1
if constants.net.TESTNET: if constants.net.TESTNET:
return 0 return 0
@ -512,21 +365,18 @@ class Blockchain(util.PrintError):
h, t = self.checkpoints[index] h, t = self.checkpoints[index]
return t return t
# new target # new target
if headerLast is None: headerLast = self.read_header(index)
headerLast = self.read_header(index)
height = headerLast["block_height"] height = headerLast["block_height"]
# check if the height passes is in range for retargeting # check if the height passes is in range for retargeting
if (height + 1) % self.DifficultyAdjustmentInterval(height + 1) != 0: if (height + 1) % self.DifficultyAdjustmentInterval(height + 1) != 0:
return int(headerLast["bits"]) return int(headerLast["bits"])
if headerFirst is None: averagingInterval = self.AveragingInterval(height + 1)
averagingInterval = self.AveragingInterval(height + 1) blockstogoback = averagingInterval - 1
blockstogoback = averagingInterval - 1 # print("Blocks to go back = " + str(blockstogoback))
# print("Blocks to go back = " + str(blockstogoback)) if (height + 1) != averagingInterval:
if (height + 1) != averagingInterval: blockstogoback = averagingInterval
blockstogoback = averagingInterval firstHeight = height - blockstogoback
firstHeight = height - blockstogoback headerFirst = self.read_header(int(firstHeight))
headerFirst = self.read_header(int(firstHeight))
firstBlockTime = headerFirst["timestamp"] firstBlockTime = headerFirst["timestamp"]
nMinActualTimespan = int(self.MinActualTimespan(int(headerLast["block_height"]) + 1)) nMinActualTimespan = int(self.MinActualTimespan(int(headerLast["block_height"]) + 1))
@ -556,62 +406,26 @@ class Blockchain(util.PrintError):
bnNew = self.target_to_bits(int(bnNew)) bnNew = self.target_to_bits(int(bnNew))
return bnNew return bnNew
@classmethod def bits_to_target(self, bits):
def bits_to_target(cls, bits: int) -> int:
bitsN = (bits >> 24) & 0xff bitsN = (bits >> 24) & 0xff
if not (0x03 <= bitsN <= 0x1e): if not (bitsN >= 0x03 and bitsN <= 0x1e):
raise Exception("First part of bits should be in [0x03, 0x1e]") raise BaseException("First part of bits should be in [0x03, 0x1e]")
bitsBase = bits & 0xffffff bitsBase = bits & 0xffffff
if not (0x8000 <= bitsBase <= 0x7fffff): if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff):
raise Exception("Second part of bits should be in [0x8000, 0x7fffff]") raise Exception("Second part of bits should be in [0x8000, 0x7fffff]")
return bitsBase << (8 * (bitsN - 3)) return bitsBase << (8 * (bitsN-3))
@classmethod def target_to_bits(self, target):
def target_to_bits(cls, target: int) -> int:
c = ("%064x" % target)[2:] c = ("%064x" % target)[2:]
while c[:2] == '00' and len(c) > 6: while c[:2] == '00' and len(c) > 6:
c = c[2:] c = c[2:]
bitsN, bitsBase = len(c) // 2, int.from_bytes(bfh(c[:6]), byteorder='big') bitsN, bitsBase = len(c) // 2, int('0x' + c[:6], 16)
if bitsBase >= 0x800000: if bitsBase >= 0x800000:
bitsN += 1 bitsN += 1
bitsBase >>= 8 bitsBase >>= 8
return bitsN << 24 | bitsBase return bitsN << 24 | bitsBase
def chainwork_of_header_at_height(self, height: int) -> int: def can_connect(self, header, check_height=True):
"""work done by single header at given height"""
chunk_idx = height // 2016 - 1
target = self.get_target(chunk_idx)
work = ((2 ** 256 - target - 1) // (target + 1)) + 1
return work
@with_lock
def get_chainwork(self, height=None) -> int:
if height is None:
height = max(0, self.height())
if constants.net.TESTNET:
# On testnet/regtest, difficulty works somewhat different.
# It's out of scope to properly implement that.
return height
last_retarget = height // 2016 * 2016 - 1
cached_height = last_retarget
while _CHAINWORK_CACHE.get(self.get_hash(cached_height)) is None:
if cached_height <= -1:
break
cached_height -= 2016
assert cached_height >= -1, cached_height
running_total = _CHAINWORK_CACHE[self.get_hash(cached_height)]
while cached_height < last_retarget:
cached_height += 2016
work_in_single_header = self.chainwork_of_header_at_height(cached_height)
work_in_chunk = 2016 * work_in_single_header
running_total += work_in_chunk
_CHAINWORK_CACHE[self.get_hash(cached_height)] = running_total
cached_height += 2016
work_in_single_header = self.chainwork_of_header_at_height(cached_height)
work_in_last_partial_chunk = (height % 2016 + 1) * work_in_single_header
return running_total + work_in_last_partial_chunk
def can_connect(self, header: dict, check_height: bool=True) -> bool:
if header is None: if header is None:
return False return False
height = header['block_height'] height = header['block_height']
@ -636,8 +450,7 @@ class Blockchain(util.PrintError):
return False return False
return True return True
def connect_chunk(self, idx: int, hexdata: str) -> bool: def connect_chunk(self, idx, hexdata):
assert idx >= 0, idx
try: try:
data = bfh(hexdata) data = bfh(hexdata)
self.verify_chunk(idx, data) self.verify_chunk(idx, data)
@ -645,7 +458,7 @@ class Blockchain(util.PrintError):
self.save_chunk(idx, data) self.save_chunk(idx, data)
return True return True
except BaseException as e: except BaseException as e:
self.print_error(f'verify_chunk idx {idx} failed: {repr(e)}') self.print_error('verify_chunk %d failed'%idx, str(e))
return False return False
def get_checkpoints(self): def get_checkpoints(self):
@ -713,20 +526,11 @@ class Blockchain(util.PrintError):
# V3 # V3
return constants.net.nInterval_Version3 return constants.net.nInterval_Version3
def save_chunk_part(self, header):
def check_header(header: dict) -> Optional[Blockchain]: filename = self.path()
if type(header) is not dict: delta = header.get('block_height')
return None data = bfh(serialize_header(header))
with blockchains_lock: chains = list(blockchains.values()) # assert delta == self.size()
for b in chains: assert len(data) == 80
if b.check_header(header): self.write(data, delta * 80)
return b # self.swap_with_parent()
return None
def can_connect(header: dict) -> Optional[Blockchain]:
with blockchains_lock: chains = list(blockchains.values())
for b in chains:
if b.can_connect(header):
return b
return None

View File

@ -22,9 +22,8 @@
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
from collections import defaultdict from collections import defaultdict, namedtuple
from math import floor, log10 from math import floor, log10
from typing import NamedTuple, List
from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
from .transaction import Transaction, TxOutput from .transaction import Transaction, TxOutput
@ -69,14 +68,13 @@ class PRNG:
x[i], x[j] = x[j], x[i] x[i], x[j] = x[j], x[i]
class Bucket(NamedTuple): Bucket = namedtuple('Bucket',
desc: str ['desc',
weight: int # as in BIP-141 'weight', # as in BIP-141
value: int # in satoshis 'value', # in satoshis
coins: List[dict] # UTXOs 'coins', # UTXOs
min_height: int # min block height where a coin was confirmed 'min_height', # min block height where a coin was confirmed
witness: bool # whether any coin uses segwit 'witness']) # whether any coin uses segwit
def strip_unneeded(bkts, sufficient_funds): def strip_unneeded(bkts, sufficient_funds):
'''Remove buckets that are unnecessary in achieving the spend amount''' '''Remove buckets that are unnecessary in achieving the spend amount'''
@ -84,8 +82,8 @@ def strip_unneeded(bkts, sufficient_funds):
for i in range(len(bkts)): for i in range(len(bkts)):
if not sufficient_funds(bkts[i + 1:]): if not sufficient_funds(bkts[i + 1:]):
return bkts[i:] return bkts[i:]
# none of the buckets are needed # Shouldn't get here
return [] return bkts
class CoinChooserBase(PrintError): class CoinChooserBase(PrintError):
@ -187,7 +185,7 @@ class CoinChooserBase(PrintError):
self.print_error('not keeping dust', dust) self.print_error('not keeping dust', dust)
return change return change
def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator, def make_tx(self, coins, outputs, change_addrs, fee_estimator,
dust_threshold): dust_threshold):
"""Select unspent coins to spend to pay outputs. If the change is """Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to greater than dust_threshold (after adding the change output to
@ -202,14 +200,11 @@ class CoinChooserBase(PrintError):
self.p = PRNG(''.join(sorted(utxos))) self.p = PRNG(''.join(sorted(utxos)))
# Copy the outputs so when adding change we don't modify "outputs" # Copy the outputs so when adding change we don't modify "outputs"
tx = Transaction.from_io(inputs[:], outputs[:]) tx = Transaction.from_io([], outputs[:])
input_value = tx.input_value()
# Weight of the transaction with no inputs and no change # Weight of the transaction with no inputs and no change
# Note: this will use legacy tx serialization as the need for "segwit" # Note: this will use legacy tx serialization as the need for "segwit"
# would be detected from inputs. The only side effect should be that the # would be detected from inputs. The only side effect should be that the
# marker and flag are excluded, which is compensated in get_tx_weight() # marker and flag are excluded, which is compensated in get_tx_weight()
# FIXME calculation will be off by this (2 wu) in case of RBF batching
base_weight = tx.estimated_weight() base_weight = tx.estimated_weight()
spent_amount = tx.output_value() spent_amount = tx.output_value()
@ -233,7 +228,7 @@ class CoinChooserBase(PrintError):
def sufficient_funds(buckets): def sufficient_funds(buckets):
'''Given a list of buckets, return True if it has enough '''Given a list of buckets, return True if it has enough
value to pay for the transaction''' value to pay for the transaction'''
total_input = input_value + sum(bucket.value for bucket in buckets) total_input = sum(bucket.value for bucket in buckets)
total_weight = get_tx_weight(buckets) total_weight = get_tx_weight(buckets)
return total_input >= spent_amount + fee_estimator_w(total_weight) return total_input >= spent_amount + fee_estimator_w(total_weight)

View File

@ -32,26 +32,15 @@ import ast
import base64 import base64
from functools import wraps from functools import wraps
from decimal import Decimal from decimal import Decimal
from typing import Optional, TYPE_CHECKING
from .import util, ecc from .import util, ecc
from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode
from . import bitcoin from . import bitcoin
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
from . import bip32
from .i18n import _ from .i18n import _
from .transaction import Transaction, multisig_script, TxOutput from .transaction import Transaction, multisig_script, TxOutput
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .synchronizer import Notifier from .plugin import run_hook
from .storage import WalletStorage
from . import keystore
from .wallet import Wallet, Imported_Wallet, Abstract_Wallet
from .mnemonic import Mnemonic
if TYPE_CHECKING:
from .network import Network
from .simple_config import SimpleConfig
known_commands = {} known_commands = {}
@ -102,8 +91,7 @@ def command(s):
class Commands: class Commands:
def __init__(self, config: 'SimpleConfig', wallet: Abstract_Wallet, def __init__(self, config, wallet, network, callback = None):
network: Optional['Network'], callback=None):
self.config = config self.config = config
self.wallet = wallet self.wallet = wallet
self.network = network self.network = network
@ -135,80 +123,17 @@ class Commands:
return ' '.join(sorted(known_commands.keys())) return ' '.join(sorted(known_commands.keys()))
@command('') @command('')
def create(self, passphrase=None, password=None, encrypt_file=True, segwit=False): def create(self, segwit=False):
"""Create a new wallet""" """Create a new wallet"""
storage = WalletStorage(self.config.get_wallet_path()) raise Exception('Not a JSON-RPC command')
if storage.file_exists():
raise Exception("Remove the existing wallet first!")
seed_type = 'segwit' if segwit else 'standard' @command('wn')
seed = Mnemonic('en').make_seed(seed_type) def restore(self, text):
k = keystore.from_seed(seed, passphrase)
storage.put('keystore', k.dump())
storage.put('wallet_type', 'standard')
wallet = Wallet(storage)
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
wallet.synchronize()
msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet."
wallet.storage.write()
return {'seed': seed, 'path': wallet.storage.path, 'msg': msg}
@command('')
def restore(self, text, passphrase=None, password=None, encrypt_file=True):
"""Restore a wallet from text. Text can be a seed phrase, a master """Restore a wallet from text. Text can be a seed phrase, a master
public key, a master private key, a list of bitcoin addresses public key, a master private key, a list of bitcoin addresses
or bitcoin private keys. If you want to be prompted for your or bitcoin private keys. If you want to be prompted for your
seed, type '?' or ':' (concealed) """ seed, type '?' or ':' (concealed) """
storage = WalletStorage(self.config.get_wallet_path()) raise Exception('Not a JSON-RPC command')
if storage.file_exists():
raise Exception("Remove the existing wallet first!")
text = text.strip()
if keystore.is_address_list(text):
wallet = Imported_Wallet(storage)
addresses = text.split()
good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
# FIXME tell user about bad_inputs
if not good_inputs:
raise Exception("None of the given addresses can be imported")
elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
k = keystore.Imported_KeyStore({})
storage.put('keystore', k.dump())
wallet = Imported_Wallet(storage)
keys = keystore.get_private_keys(text)
good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
# FIXME tell user about bad_inputs
if not good_inputs:
raise Exception("None of the given privkeys can be imported")
else:
if keystore.is_seed(text):
k = keystore.from_seed(text, passphrase)
elif keystore.is_master_key(text):
k = keystore.from_master_key(text)
else:
raise Exception("Seed or key not recognized")
storage.put('keystore', k.dump())
storage.put('wallet_type', 'standard')
wallet = Wallet(storage)
assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
wallet.synchronize()
if self.network:
wallet.start_network(self.network)
print_error("Recovering wallet...")
wallet.wait_until_synchronized()
wallet.stop_threads()
# note: we don't wait for SPV
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
else:
msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
"Start a daemon (not offline) to sync history.")
wallet.storage.write()
return {'path': wallet.storage.path, 'msg': msg}
@command('wp') @command('wp')
def password(self, password=None, new_password=None): def password(self, password=None, new_password=None):
@ -220,11 +145,6 @@ class Commands:
self.wallet.storage.write() self.wallet.storage.write()
return {'password':self.wallet.has_password()} return {'password':self.wallet.has_password()}
@command('w')
def get(self, key):
"""Return item from wallet storage"""
return self.wallet.storage.get(key)
@command('') @command('')
def getconfig(self, key): def getconfig(self, key):
"""Return a configuration variable. """ """Return a configuration variable. """
@ -261,7 +181,7 @@ class Commands:
walletless server query, results are not checked by SPV. walletless server query, results are not checked by SPV.
""" """
sh = bitcoin.address_to_scripthash(address) sh = bitcoin.address_to_scripthash(address)
return self.network.run_from_another_thread(self.network.get_history_for_scripthash(sh)) return self.network.get_history_for_scripthash(sh)
@command('w') @command('w')
def listunspent(self): def listunspent(self):
@ -279,7 +199,7 @@ class Commands:
is a walletless server query, results are not checked by SPV. is a walletless server query, results are not checked by SPV.
""" """
sh = bitcoin.address_to_scripthash(address) sh = bitcoin.address_to_scripthash(address)
return self.network.run_from_another_thread(self.network.listunspent_for_scripthash(sh)) return self.network.listunspent_for_scripthash(sh)
@command('') @command('')
def serialize(self, jsontx): def serialize(self, jsontx):
@ -329,14 +249,13 @@ class Commands:
def deserialize(self, tx): def deserialize(self, tx):
"""Deserialize a serialized transaction""" """Deserialize a serialized transaction"""
tx = Transaction(tx) tx = Transaction(tx)
return tx.deserialize(force_full_parse=True) return tx.deserialize()
@command('n') @command('n')
def broadcast(self, tx): def broadcast(self, tx):
"""Broadcast a transaction to the network. """ """Broadcast a transaction to the network. """
tx = Transaction(tx) tx = Transaction(tx)
self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) return self.network.broadcast_transaction_from_non_network_thread(tx)
return tx.txid()
@command('') @command('')
def createmultisig(self, num, pubkeys): def createmultisig(self, num, pubkeys):
@ -403,7 +322,7 @@ class Commands:
server query, results are not checked by SPV. server query, results are not checked by SPV.
""" """
sh = bitcoin.address_to_scripthash(address) sh = bitcoin.address_to_scripthash(address)
out = self.network.run_from_another_thread(self.network.get_balance_for_scripthash(sh)) out = self.network.get_balance_for_scripthash(sh)
out["confirmed"] = str(Decimal(out["confirmed"])/COIN) out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN) out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
return out return out
@ -412,7 +331,7 @@ class Commands:
def getmerkle(self, txid, height): def getmerkle(self, txid, height):
"""Get Merkle branch of a transaction included in a block. Electrum """Get Merkle branch of a transaction included in a block. Electrum
uses this to verify transactions (Simple Payment Verification).""" uses this to verify transactions (Simple Payment Verification)."""
return self.network.run_from_another_thread(self.network.get_merkle_for_transaction(txid, int(height))) return self.network.get_merkle_for_transaction(txid, int(height))
@command('n') @command('n')
def getservers(self): def getservers(self):
@ -435,16 +354,6 @@ class Commands:
"""Get master private key. Return your wallet\'s master private key""" """Get master private key. Return your wallet\'s master private key"""
return str(self.wallet.keystore.get_master_private_key(password)) return str(self.wallet.keystore.get_master_private_key(password))
@command('')
def convert_xkey(self, xkey, xtype):
"""Convert xtype of a master key. e.g. xpub -> ypub"""
is_xprv = bip32.is_xprv(xkey)
if not bip32.is_xpub(xkey) and not is_xprv:
raise Exception('xkey should be a master public/private key')
_, depth, fingerprint, child_number, c, cK = bip32.deserialize_xkey(xkey, is_xprv)
serialize = bip32.serialize_xprv if is_xprv else bip32.serialize_xpub
return serialize(xtype, c, cK, depth, fingerprint, child_number)
@command('wp') @command('wp')
def getseed(self, password=None): def getseed(self, password=None):
"""Get seed phrase. Print the generation seed of your wallet.""" """Get seed phrase. Print the generation seed of your wallet."""
@ -459,7 +368,7 @@ class Commands:
try: try:
addr = self.wallet.import_private_key(privkey, password) addr = self.wallet.import_private_key(privkey, password)
out = "Keypair imported: " + addr out = "Keypair imported: " + addr
except Exception as e: except BaseException as e:
out = "Error: " + str(e) out = "Error: " + str(e)
return out return out
@ -510,7 +419,7 @@ class Commands:
coins = self.wallet.get_spendable_coins(domain, self.config) coins = self.wallet.get_spendable_coins(domain, self.config)
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
if locktime != None: if locktime != None:
tx.locktime = locktime tx.locktime = locktime
if rbf is None: if rbf is None:
rbf = self.config.get('use_rbf', True) rbf = self.config.get('use_rbf', True)
@ -537,15 +446,9 @@ class Commands:
return tx.as_dict() return tx.as_dict()
@command('w') @command('w')
def history(self, year=None, show_addresses=False, show_fiat=False, show_fees=False, def history(self, year=None, show_addresses=False, show_fiat=False):
from_height=None, to_height=None):
"""Wallet history. Returns the transaction history of your wallet.""" """Wallet history. Returns the transaction history of your wallet."""
kwargs = { kwargs = {'show_addresses': show_addresses}
'show_addresses': show_addresses,
'show_fees': show_fees,
'from_height': from_height,
'to_height': to_height,
}
if year: if year:
import time import time
start_date = datetime.datetime(year, 1, 1) start_date = datetime.datetime(year, 1, 1)
@ -614,7 +517,7 @@ class Commands:
if self.wallet and txid in self.wallet.transactions: if self.wallet and txid in self.wallet.transactions:
tx = self.wallet.transactions[txid] tx = self.wallet.transactions[txid]
else: else:
raw = self.network.run_from_another_thread(self.network.get_transaction(txid)) raw = self.network.get_transaction(txid)
if raw: if raw:
tx = Transaction(raw) tx = Transaction(raw)
else: else:
@ -640,7 +543,7 @@ class Commands:
PR_PAID: 'Paid', PR_PAID: 'Paid',
PR_EXPIRED: 'Expired', PR_EXPIRED: 'Expired',
} }
out['amount (BTC)'] = format_satoshis(out.get('amount')) out['amount (FLO)'] = format_satoshis(out.get('amount'))
out['status'] = pr_str[out.get('status', PR_UNKNOWN)] out['status'] = pr_str[out.get('status', PR_UNKNOWN)]
return out return out
@ -732,11 +635,20 @@ class Commands:
self.wallet.remove_payment_request(k, self.config) self.wallet.remove_payment_request(k, self.config)
@command('n') @command('n')
def notify(self, address: str, URL: str): def notify(self, address, URL):
"""Watch an address. Every time the address changes, a http POST is sent to the URL.""" """Watch an address. Every time the address changes, a http POST is sent to the URL."""
if not hasattr(self, "_notifier"): def callback(x):
self._notifier = Notifier(self.network) import urllib.request
self.network.run_from_another_thread(self._notifier.start_watching_queue.put((address, URL))) headers = {'content-type':'application/json'}
data = {'address':address, 'status':x.get('result')}
serialized_data = util.to_bytes(json.dumps(data))
try:
req = urllib.request.Request(URL, serialized_data, headers)
response_stream = urllib.request.urlopen(req, timeout=5)
util.print_error('Got Response for %s' % address)
except BaseException as e:
util.print_error(str(e))
self.network.subscribe_to_addresses([address], callback)
return True return True
@command('wn') @command('wn')
@ -768,16 +680,6 @@ class Commands:
# for the python console # for the python console
return sorted(known_commands.keys()) return sorted(known_commands.keys())
def eval_bool(x: str) -> bool:
if x == 'false': return False
if x == 'true': return True
try:
return bool(ast.literal_eval(x))
except:
return bool(x)
param_descriptions = { param_descriptions = {
'privkey': 'Private key. Type \'?\' to get a prompt.', 'privkey': 'Private key. Type \'?\' to get a prompt.',
'destination': 'Bitcoin address, contact or alias', 'destination': 'Bitcoin address, contact or alias',
@ -791,8 +693,8 @@ param_descriptions = {
'pubkey': 'Public key', 'pubkey': 'Public key',
'message': 'Clear text message. Use quotes if it contains spaces.', 'message': 'Clear text message. Use quotes if it contains spaces.',
'encrypted': 'Encrypted message', 'encrypted': 'Encrypted message',
'amount': 'Amount to be sent (in BTC). Type \'!\' to send the maximum available.', 'amount': 'Amount to be sent (in FLO). Type \'!\' to send the maximum available.',
'requested_amount': 'Requested amount (in BTC).', 'requested_amount': 'Requested amount (in FLO).',
'outputs': 'list of ["address", amount]', 'outputs': 'list of ["address", amount]',
'redeem_script': 'redeem script (hexadecimal)', 'redeem_script': 'redeem script (hexadecimal)',
} }
@ -800,7 +702,6 @@ param_descriptions = {
command_options = { command_options = {
'password': ("-W", "Password"), 'password': ("-W", "Password"),
'new_password':(None, "New Password"), 'new_password':(None, "New Password"),
'encrypt_file':(None, "Whether the file on disk should be encrypted with the provided password"),
'receiving': (None, "Show only receiving addresses"), 'receiving': (None, "Show only receiving addresses"),
'change': (None, "Show only change addresses"), 'change': (None, "Show only change addresses"),
'frozen': (None, "Show only frozen addresses"), 'frozen': (None, "Show only frozen addresses"),
@ -810,13 +711,12 @@ command_options = {
'labels': ("-l", "Show the labels of listed addresses"), 'labels': ("-l", "Show the labels of listed addresses"),
'nocheck': (None, "Do not verify aliases"), 'nocheck': (None, "Do not verify aliases"),
'imax': (None, "Maximum number of inputs"), 'imax': (None, "Maximum number of inputs"),
'fee': ("-f", "Transaction fee (in BTC)"), 'fee': ("-f", "Transaction fee (in FLO)"),
'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."), 'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."),
'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"), 'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
'nbits': (None, "Number of bits of entropy"), 'nbits': (None, "Number of bits of entropy"),
'segwit': (None, "Create segwit seed"), 'segwit': (None, "Create segwit seed"),
'language': ("-L", "Default language for wordlist"), 'language': ("-L", "Default language for wordlist"),
'passphrase': (None, "Seed extension"),
'privkey': (None, "Private key. Set to '?' to get a prompt."), 'privkey': (None, "Private key. Set to '?' to get a prompt."),
'unsigned': ("-u", "Do not sign transaction"), 'unsigned': ("-u", "Do not sign transaction"),
'rbf': (None, "Replace-by-fee transaction"), 'rbf': (None, "Replace-by-fee transaction"),
@ -831,12 +731,9 @@ command_options = {
'paid': (None, "Show only paid requests."), 'paid': (None, "Show only paid requests."),
'show_addresses': (None, "Show input and output addresses"), 'show_addresses': (None, "Show input and output addresses"),
'show_fiat': (None, "Show fiat value of transactions"), 'show_fiat': (None, "Show fiat value of transactions"),
'show_fees': (None, "Show miner fees paid by transactions"),
'year': (None, "Show history for a given year"), 'year': (None, "Show history for a given year"),
'fee_method': (None, "Fee estimation method to use"), 'fee_method': (None, "Fee estimation method to use"),
'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position"), 'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position")
'from_height': (None, "Only show transactions that confirmed after given block height"),
'to_height': (None, "Only show transactions that confirmed before given block height"),
} }
@ -848,8 +745,6 @@ arg_types = {
'nbits': int, 'nbits': int,
'imax': int, 'imax': int,
'year': int, 'year': int,
'from_height': int,
'to_height': int,
'tx': tx_from_str, 'tx': tx_from_str,
'pubkeys': json_loads, 'pubkeys': json_loads,
'jsontx': json_loads, 'jsontx': json_loads,
@ -860,7 +755,6 @@ arg_types = {
'locktime': int, 'locktime': int,
'fee_method': str, 'fee_method': str,
'fee_level': json_loads, 'fee_level': json_loads,
'encrypt_file': eval_bool,
} }
config_variables = { config_variables = {
@ -933,7 +827,6 @@ def add_network_options(parser):
parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)") parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http") parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers") parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers")
parser.add_argument("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=False, help="Tolerate invalid merkle proofs from server")
def add_global_options(parser): def add_global_options(parser):
group = parser.add_argument_group('global options') group = parser.add_argument_group('global options')
@ -960,7 +853,6 @@ def get_parser():
parser_gui.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline") parser_gui.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup") parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI") parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
add_network_options(parser_gui) add_network_options(parser_gui)
add_global_options(parser_gui) add_global_options(parser_gui)
# daemon # daemon
@ -974,10 +866,12 @@ def get_parser():
cmd = known_commands[cmdname] cmd = known_commands[cmdname]
p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description) p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description)
add_global_options(p) add_global_options(p)
if cmdname == 'restore':
p.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
for optname, default in zip(cmd.options, cmd.defaults): for optname, default in zip(cmd.options, cmd.defaults):
a, help = command_options[optname] a, help = command_options[optname]
b = '--' + optname b = '--' + optname
action = "store_true" if default is False else 'store' action = "store_true" if type(default) is bool else 'store'
args = (a, b) if a else (b,) args = (a, b) if a else (b,)
if action == 'store': if action == 'store':
_type = arg_types.get(optname, str) _type = arg_types.get(optname, str)

View File

@ -49,8 +49,8 @@ class BitcoinMainnet(AbstractNet):
TESTNET = False TESTNET = False
WIF_PREFIX = 0xa3 WIF_PREFIX = 0xa3
ADDRTYPE_P2PKH = 35 ADDRTYPE_P2PKH = 35
ADDRTYPE_P2SH = 94 ADDRTYPE_P2SH = 8
SEGWIT_HRP = "flo" SEGWIT_HRP = "ltc"
GENESIS = "09c7781c9df90708e278c35d38ea5c9041d7ecfcdd1c56ba67274b7cff3e1cea" GENESIS = "09c7781c9df90708e278c35d38ea5c9041d7ecfcdd1c56ba67274b7cff3e1cea"
DEFAULT_PORTS = {'t': '50001', 's': '50002'} DEFAULT_PORTS = {'t': '50001', 's': '50002'}
DEFAULT_SERVERS = read_json('servers.json', {}) DEFAULT_SERVERS = read_json('servers.json', {})
@ -70,7 +70,7 @@ class BitcoinMainnet(AbstractNet):
'p2wpkh': 0x04b24746, # zpub 'p2wpkh': 0x04b24746, # zpub
'p2wsh': 0x02aa7ed3, # Zpub 'p2wsh': 0x02aa7ed3, # Zpub
} }
BIP44_COIN_TYPE = 216 BIP44_COIN_TYPE = 2
# FLO Network constants # FLO Network constants
fPowAllowMinDifficultyBlocks = False fPowAllowMinDifficultyBlocks = False
fPowNoRetargeting = False fPowNoRetargeting = False
@ -103,8 +103,8 @@ class BitcoinTestnet(AbstractNet):
TESTNET = True TESTNET = True
WIF_PREFIX = 0xef WIF_PREFIX = 0xef
ADDRTYPE_P2PKH = 115 ADDRTYPE_P2PKH = 115
ADDRTYPE_P2SH = 58 ADDRTYPE_P2SH = 198
SEGWIT_HRP = "tflo" SEGWIT_HRP = "tltc"
GENESIS = "9b7bc86236c34b5e3a39367c036b7fe8807a966c22a7a1f0da2a198a27e03731" GENESIS = "9b7bc86236c34b5e3a39367c036b7fe8807a966c22a7a1f0da2a198a27e03731"
DEFAULT_PORTS = {'t': '51001', 's': '51002'} DEFAULT_PORTS = {'t': '51001', 's': '51002'}
DEFAULT_SERVERS = read_json('servers_testnet.json', {}) DEFAULT_SERVERS = read_json('servers_testnet.json', {})

View File

@ -21,9 +21,11 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
import re import re
import dns import dns
from dns.exception import DNSException from dns.exception import DNSException
import json
import traceback
import sys
from . import bitcoin from . import bitcoin
from . import dnssec from . import dnssec
@ -65,9 +67,8 @@ class Contacts(dict):
def pop(self, key): def pop(self, key):
if key in self.keys(): if key in self.keys():
res = dict.pop(self, key) dict.pop(self, key)
self.save() self.save()
return res
def resolve(self, k): def resolve(self, k):
if bitcoin.is_address(k): if bitcoin.is_address(k):
@ -99,7 +100,7 @@ class Contacts(dict):
try: try:
records, validated = dnssec.query(url, dns.rdatatype.TXT) records, validated = dnssec.query(url, dns.rdatatype.TXT)
except DNSException as e: except DNSException as e:
print_error(f'Error resolving openalias: {repr(e)}') print_error('Error resolving openalias: ', str(e))
return None return None
prefix = 'btc' prefix = 'btc'
for record in records: for record in records:

View File

@ -27,12 +27,10 @@ import base64
import os import os
import hashlib import hashlib
import hmac import hmac
from typing import Union
import pyaes import pyaes
from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException from .util import assert_bytes, InvalidPassword, to_bytes, to_string
from .i18n import _
try: try:
@ -45,26 +43,26 @@ class InvalidPadding(Exception):
pass pass
def append_PKCS7_padding(data: bytes) -> bytes: def append_PKCS7_padding(data):
assert_bytes(data) assert_bytes(data)
padlen = 16 - (len(data) % 16) padlen = 16 - (len(data) % 16)
return data + bytes([padlen]) * padlen return data + bytes([padlen]) * padlen
def strip_PKCS7_padding(data: bytes) -> bytes: def strip_PKCS7_padding(data):
assert_bytes(data) assert_bytes(data)
if len(data) % 16 != 0 or len(data) == 0: if len(data) % 16 != 0 or len(data) == 0:
raise InvalidPadding("invalid length") raise InvalidPadding("invalid length")
padlen = data[-1] padlen = data[-1]
if not (0 < padlen <= 16): if padlen > 16:
raise InvalidPadding("invalid padding byte (out of range)") raise InvalidPadding("invalid padding byte (large)")
for i in data[-padlen:]: for i in data[-padlen:]:
if i != padlen: if i != padlen:
raise InvalidPadding("invalid padding byte (inconsistent)") raise InvalidPadding("invalid padding byte (inconsistent)")
return data[0:-padlen] return data[0:-padlen]
def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: def aes_encrypt_with_iv(key, iv, data):
assert_bytes(key, iv, data) assert_bytes(key, iv, data)
data = append_PKCS7_padding(data) data = append_PKCS7_padding(data)
if AES: if AES:
@ -76,7 +74,7 @@ def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
return e return e
def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: def aes_decrypt_with_iv(key, iv, data):
assert_bytes(key, iv, data) assert_bytes(key, iv, data)
if AES: if AES:
cipher = AES.new(key, AES.MODE_CBC, iv) cipher = AES.new(key, AES.MODE_CBC, iv)
@ -91,107 +89,44 @@ def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
raise InvalidPassword() raise InvalidPassword()
def EncodeAES_base64(secret: bytes, msg: bytes) -> bytes: def EncodeAES(secret, s):
"""Returns base64 encoded ciphertext.""" assert_bytes(s)
e = EncodeAES_bytes(secret, msg) iv = bytes(os.urandom(16))
ct = aes_encrypt_with_iv(secret, iv, s)
e = iv + ct
return base64.b64encode(e) return base64.b64encode(e)
def DecodeAES(secret, e):
def EncodeAES_bytes(secret: bytes, msg: bytes) -> bytes: e = bytes(base64.b64decode(e))
assert_bytes(msg) iv, e = e[:16], e[16:]
iv = bytes(os.urandom(16))
ct = aes_encrypt_with_iv(secret, iv, msg)
return iv + ct
def DecodeAES_base64(secret: bytes, ciphertext_b64: Union[bytes, str]) -> bytes:
ciphertext = bytes(base64.b64decode(ciphertext_b64))
return DecodeAES_bytes(secret, ciphertext)
def DecodeAES_bytes(secret: bytes, ciphertext: bytes) -> bytes:
assert_bytes(ciphertext)
iv, e = ciphertext[:16], ciphertext[16:]
s = aes_decrypt_with_iv(secret, iv, e) s = aes_decrypt_with_iv(secret, iv, e)
return s return s
def pw_encode(s, password):
PW_HASH_VERSION_LATEST = 1 if password:
KNOWN_PW_HASH_VERSIONS = (1, 2, ) secret = Hash(password)
SUPPORTED_PW_HASH_VERSIONS = (1, ) return EncodeAES(secret, to_bytes(s, "utf8")).decode('utf8')
assert PW_HASH_VERSION_LATEST in KNOWN_PW_HASH_VERSIONS
assert PW_HASH_VERSION_LATEST in SUPPORTED_PW_HASH_VERSIONS
class UnexpectedPasswordHashVersion(InvalidPassword, WalletFileException):
def __init__(self, version):
self.version = version
def __str__(self):
return "{unexpected}: {version}\n{instruction}".format(
unexpected=_("Unexpected password hash version"),
version=self.version,
instruction=_('You are most likely using an outdated version of Electrum. Please update.'))
class UnsupportedPasswordHashVersion(InvalidPassword, WalletFileException):
def __init__(self, version):
self.version = version
def __str__(self):
return "{unsupported}: {version}\n{instruction}".format(
unsupported=_("Unsupported password hash version"),
version=self.version,
instruction=f"To open this wallet, try 'git checkout password_v{self.version}'.\n"
"Alternatively, restore from seed.")
def _hash_password(password: Union[bytes, str], *, version: int) -> bytes:
pw = to_bytes(password, 'utf8')
if version not in SUPPORTED_PW_HASH_VERSIONS:
raise UnsupportedPasswordHashVersion(version)
if version == 1:
return sha256d(pw)
else: else:
assert version not in KNOWN_PW_HASH_VERSIONS return s
raise UnexpectedPasswordHashVersion(version)
def pw_decode(s, password):
if password is not None:
secret = Hash(password)
try:
d = to_string(DecodeAES(secret, s), "utf8")
except Exception:
raise InvalidPassword()
return d
else:
return s
def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str: def sha256(x: bytes) -> bytes:
if not password:
return data
if version not in KNOWN_PW_HASH_VERSIONS:
raise UnexpectedPasswordHashVersion(version)
# derive key from password
secret = _hash_password(password, version=version)
# encrypt given data
ciphertext = EncodeAES_bytes(secret, to_bytes(data, "utf8"))
ciphertext_b64 = base64.b64encode(ciphertext)
return ciphertext_b64.decode('utf8')
def pw_decode(data: str, password: Union[bytes, str, None], *, version: int) -> str:
if password is None:
return data
if version not in KNOWN_PW_HASH_VERSIONS:
raise UnexpectedPasswordHashVersion(version)
data_bytes = bytes(base64.b64decode(data))
# derive key from password
secret = _hash_password(password, version=version)
# decrypt given data
try:
d = to_string(DecodeAES_bytes(secret, data_bytes), "utf8")
except Exception as e:
raise InvalidPassword() from e
return d
def sha256(x: Union[bytes, str]) -> bytes:
x = to_bytes(x, 'utf8') x = to_bytes(x, 'utf8')
return bytes(hashlib.sha256(x).digest()) return bytes(hashlib.sha256(x).digest())
def sha256d(x: Union[bytes, str]) -> bytes: def Hash(x: bytes) -> bytes:
x = to_bytes(x, 'utf8') x = to_bytes(x, 'utf8')
out = bytes(sha256(sha256(x))) out = bytes(sha256(sha256(x)))
return out return out

View File

@ -1,44 +1,825 @@
{ {
"CoinMarketcap": [ "BitFlyer": [
"JPY"
],
"BitPay": [
"AED", "AED",
"AFN",
"ALL", "ALL",
"AMD",
"ANG",
"AOA",
"ARS", "ARS",
"AUD", "AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BCH",
"BDT",
"BGN",
"BHD", "BHD",
"BIF",
"BMD",
"BND",
"BOB", "BOB",
"BRL", "BRL",
"KHR", "BSD",
"BTC",
"BTN",
"BWP",
"BZD",
"CAD", "CAD",
"CDF",
"CHF",
"CLF",
"CLP", "CLP",
"CNY", "CNY",
"COP", "COP",
"CRC",
"CUP", "CUP",
"CVE",
"CZK", "CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP", "EGP",
"ETB",
"EUR", "EUR",
"FJD",
"FKP",
"FLO",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD", "HKD",
"HNL",
"HRK",
"HTG",
"HUF", "HUF",
"ISK",
"INR",
"IDR", "IDR",
"IQD",
"ILS", "ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JEP",
"JMD",
"JOD",
"JPY", "JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP", "LBP",
"MYR", "LKR",
"LRD",
"LSL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRU",
"MUR",
"MVR",
"MWK",
"MXN", "MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR", "NPR",
"NZD", "NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"STN",
"SVC",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMW",
"ZWL"
],
"BitStamp": [
"USD"
],
"Bitbank": [
"JPY"
],
"BitcoinAverage": [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BZD",
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNH",
"CNY",
"COP",
"CRC",
"CUC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GGP",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"IMP",
"INR",
"IQD",
"IRR",
"ISK",
"JEP",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SVC",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XCD",
"XDR",
"XOF",
"XPD",
"XPF",
"XPT",
"YER",
"ZAR",
"ZMW",
"ZWL"
],
"Bitcointoyou": [
"BRL"
],
"BitcoinVenezuela": [
"ARS",
"EUR",
"USD",
"VEF"
],
"Bitmarket": [
"PLN"
],
"Bitso": [
"MXN"
],
"Bitvalor": [
"BRL"
],
"Bittrex": [
"AUD",
"BRL",
"CAD",
"CHF",
"CLP",
"CNY",
"DKK",
"EUR",
"GBP",
"HKD",
"INR",
"ISK",
"JPY",
"KRW",
"NZD",
"PLN",
"RUB",
"SEK",
"SGD",
"THB",
"TWD",
"USD"
],
"CoinDesk": [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTC",
"BTN",
"BWP",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EEK",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FLO",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JEP",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LVL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MTL",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"STD",
"SVC",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XBT",
"XCD",
"XDR",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZMW",
"ZWL"
],
"Coinbase": [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BCH",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNH",
"CNY",
"COP",
"CRC",
"CUC",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EEK",
"EGP",
"ERN",
"ETB",
"ETH",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GGP",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"IMP",
"INR",
"IQD",
"ISK",
"JEP",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTC",
"LTL",
"LVL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MTL",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SVC",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XCD",
"XDR",
"XOF",
"XPD",
"XPF",
"XPT",
"YER",
"ZAR",
"ZMK",
"ZMW",
"ZWL"
],
"Foxbit": [
"BRL"
],
"Kraken": [
"CAD",
"EUR",
"GBP",
"JPY",
"USD"
],
"LocalBitcoins": [
"AED",
"ARS",
"AUD",
"BDT",
"BHD",
"BOB",
"BRL",
"BWP",
"BYN",
"CAD",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CZK",
"DKK",
"DOP",
"EGP",
"ETH",
"EUR",
"GBP",
"GEL",
"GHS",
"HKD",
"HRK",
"HUF",
"IDR",
"ILS",
"INR",
"IRR",
"JOD",
"JPY",
"KES",
"KRW",
"KZT",
"LKR",
"LTC",
"MAD",
"MXN",
"MYR",
"NGN", "NGN",
"NOK", "NOK",
"GBP", "NZD",
"QAR", "PAB",
"PEN",
"PHP",
"PKR",
"PLN",
"PYG",
"RON",
"RSD",
"RUB", "RUB",
"SGD", "SAR",
"SEK", "SEK",
"CHF", "SGD",
"SZL",
"THB", "THB",
"TRY",
"TWD",
"TZS",
"UAH",
"UGX",
"USD", "USD",
"VND" "UYU",
] "VEF",
} "VND",
"XAF",
"XRP",
"ZAR",
"ZMW"
],
"MercadoBitcoin": [
"BRL"
],
"NegocieCoins": [
"BRL"
],
"TheRockTrading": [
"EUR"
],
"WEX": [
"EUR",
"RUB",
"USD"
],
"itBit": []
}

View File

@ -28,17 +28,16 @@ import os
import time import time
import traceback import traceback
import sys import sys
import threading
from typing import Dict, Optional, Tuple
# from jsonrpc import JSONRPCResponseManager
import jsonrpclib import jsonrpclib
from .jsonrpc import VerifyingJSONRPCServer from .jsonrpc import VerifyingJSONRPCServer
from .version import ELECTRUM_VERSION from .version import ELECTRUM_VERSION
from .network import Network from .network import Network
from .util import (json_decode, DaemonThread, print_error, to_string, from .util import json_decode, DaemonThread
create_and_start_event_loop, profiler, standardize_path) from .util import print_error, to_string
from .wallet import Wallet, Abstract_Wallet from .wallet import Wallet
from .storage import WalletStorage from .storage import WalletStorage
from .commands import known_commands, Commands from .commands import known_commands, Commands
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
@ -46,7 +45,7 @@ from .exchange_rate import FxThread
from .plugin import run_hook from .plugin import run_hook
def get_lockfile(config: SimpleConfig): def get_lockfile(config):
return os.path.join(config.path, 'daemon') return os.path.join(config.path, 'daemon')
@ -54,7 +53,7 @@ def remove_lockfile(lockfile):
os.unlink(lockfile) os.unlink(lockfile)
def get_fd_or_server(config: SimpleConfig): def get_fd_or_server(config):
'''Tries to create the lockfile, using O_EXCL to '''Tries to create the lockfile, using O_EXCL to
prevent races. If it succeeds it returns the FD. prevent races. If it succeeds it returns the FD.
Otherwise try and connect to the server specified in the lockfile. Otherwise try and connect to the server specified in the lockfile.
@ -73,7 +72,7 @@ def get_fd_or_server(config: SimpleConfig):
remove_lockfile(lockfile) remove_lockfile(lockfile)
def get_server(config: SimpleConfig) -> Optional[jsonrpclib.Server]: def get_server(config):
lockfile = get_lockfile(config) lockfile = get_lockfile(config)
while True: while True:
create_time = None create_time = None
@ -92,14 +91,14 @@ def get_server(config: SimpleConfig) -> Optional[jsonrpclib.Server]:
server.ping() server.ping()
return server return server
except Exception as e: except Exception as e:
print_error(f"failed to connect to JSON-RPC server: {e}") print_error("[get_server]", e)
if not create_time or create_time < time.time() - 1.0: if not create_time or create_time < time.time() - 1.0:
return None return None
# Sleep a bit and try again; it might have just been started # Sleep a bit and try again; it might have just been started
time.sleep(1.0) time.sleep(1.0)
def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]: def get_rpc_credentials(config):
rpc_user = config.get('rpcuser', None) rpc_user = config.get('rpcuser', None)
rpc_password = config.get('rpcpassword', None) rpc_password = config.get('rpcpassword', None)
if rpc_user is None or rpc_password is None: if rpc_user is None or rpc_password is None:
@ -121,33 +120,25 @@ def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
class Daemon(DaemonThread): class Daemon(DaemonThread):
@profiler def __init__(self, config, fd, is_gui):
def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
DaemonThread.__init__(self) DaemonThread.__init__(self)
self.config = config self.config = config
if fd is None and listen_jsonrpc:
fd, server = get_fd_or_server(config)
if fd is None: raise Exception('failed to lock daemon; already running?')
self.asyncio_loop, self._stop_loop, self._loop_thread = create_and_start_event_loop()
if config.get('offline'): if config.get('offline'):
self.network = None self.network = None
else: else:
self.network = Network(config) self.network = Network(config)
self.network._loop_thread = self._loop_thread
self.fx = FxThread(config, self.network) self.fx = FxThread(config, self.network)
if self.network: if self.network:
self.network.start([self.fx.run]) self.network.start(self.fx.run())
self.gui = None self.gui = None
self.wallets = {} # type: Dict[str, Abstract_Wallet] self.wallets = {}
# Setup JSONRPC server # Setup JSONRPC server
self.server = None self.init_server(config, fd, is_gui)
if listen_jsonrpc:
self.init_server(config, fd)
self.start()
def init_server(self, config: SimpleConfig, fd): def init_server(self, config, fd, is_gui):
host = config.get('rpchost', '127.0.0.1') host = config.get('rpchost', '127.0.0.1')
port = config.get('rpcport', 0) port = config.get('rpcport', 0)
rpc_user, rpc_password = get_rpc_credentials(config) rpc_user, rpc_password = get_rpc_credentials(config)
try: try:
server = VerifyingJSONRPCServer((host, port), logRequests=False, server = VerifyingJSONRPCServer((host, port), logRequests=False,
@ -162,18 +153,19 @@ class Daemon(DaemonThread):
self.server = server self.server = server
server.timeout = 0.1 server.timeout = 0.1
server.register_function(self.ping, 'ping') server.register_function(self.ping, 'ping')
server.register_function(self.run_gui, 'gui') if is_gui:
server.register_function(self.run_daemon, 'daemon') server.register_function(self.run_gui, 'gui')
self.cmd_runner = Commands(self.config, None, self.network) else:
for cmdname in known_commands: server.register_function(self.run_daemon, 'daemon')
server.register_function(getattr(self.cmd_runner, cmdname), cmdname) self.cmd_runner = Commands(self.config, None, self.network)
server.register_function(self.run_cmdline, 'run_cmdline') for cmdname in known_commands:
server.register_function(getattr(self.cmd_runner, cmdname), cmdname)
server.register_function(self.run_cmdline, 'run_cmdline')
def ping(self): def ping(self):
return True return True
def run_daemon(self, config_options): def run_daemon(self, config_options):
asyncio.set_event_loop(self.asyncio_loop)
config = SimpleConfig(config_options) config = SimpleConfig(config_options)
sub = config.get('subcommand') sub = config.get('subcommand')
assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet'] assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet']
@ -195,18 +187,18 @@ class Daemon(DaemonThread):
response = False response = False
elif sub == 'status': elif sub == 'status':
if self.network: if self.network:
net_params = self.network.get_parameters() p = self.network.get_parameters()
current_wallet = self.cmd_runner.wallet current_wallet = self.cmd_runner.wallet
current_wallet_path = current_wallet.storage.path \ current_wallet_path = current_wallet.storage.path \
if current_wallet else None if current_wallet else None
response = { response = {
'path': self.network.config.path, 'path': self.network.config.path,
'server': net_params.host, 'server': p[0],
'blockchain_height': self.network.get_local_height(), 'blockchain_height': self.network.get_local_height(),
'server_height': self.network.get_server_height(), 'server_height': self.network.get_server_height(),
'spv_nodes': len(self.network.get_interfaces()), 'spv_nodes': len(self.network.get_interfaces()),
'connected': self.network.is_connected(), 'connected': self.network.is_connected(),
'auto_connect': net_params.auto_connect, 'auto_connect': p[4],
'version': ELECTRUM_VERSION, 'version': ELECTRUM_VERSION,
'wallets': {k: w.is_up_to_date() 'wallets': {k: w.is_up_to_date()
for k, w in self.wallets.items()}, for k, w in self.wallets.items()},
@ -223,19 +215,18 @@ class Daemon(DaemonThread):
def run_gui(self, config_options): def run_gui(self, config_options):
config = SimpleConfig(config_options) config = SimpleConfig(config_options)
if self.gui: if self.gui:
if hasattr(self.gui, 'new_window'): #if hasattr(self.gui, 'new_window'):
config.open_last_wallet() # path = config.get_wallet_path()
path = config.get_wallet_path() # self.gui.new_window(path, config.get('url'))
self.gui.new_window(path, config.get('url')) # response = "ok"
response = "ok" #else:
else: # response = "error: current GUI does not support multiple windows"
response = "error: current GUI does not support multiple windows" response = "error: Electrum GUI already running"
else: else:
response = "Error: Electrum is running in daemon mode. Please stop the daemon first." response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
return response return response
def load_wallet(self, path, password) -> Optional[Abstract_Wallet]: def load_wallet(self, path, password):
path = standardize_path(path)
# wizard will be launched if we return # wizard will be launched if we return
if path in self.wallets: if path in self.wallets:
wallet = self.wallets[path] wallet = self.wallets[path]
@ -256,27 +247,18 @@ class Daemon(DaemonThread):
self.wallets[path] = wallet self.wallets[path] = wallet
return wallet return wallet
def add_wallet(self, wallet: Abstract_Wallet): def add_wallet(self, wallet):
path = wallet.storage.path path = wallet.storage.path
self.wallets[path] = wallet self.wallets[path] = wallet
def get_wallet(self, path): def get_wallet(self, path):
return self.wallets.get(path) return self.wallets.get(path)
def delete_wallet(self, path):
self.stop_wallet(path)
if os.path.exists(path):
os.unlink(path)
return True
return False
def stop_wallet(self, path): def stop_wallet(self, path):
wallet = self.wallets.pop(path, None) wallet = self.wallets.pop(path)
if not wallet: return
wallet.stop_threads() wallet.stop_threads()
def run_cmdline(self, config_options): def run_cmdline(self, config_options):
asyncio.set_event_loop(self.asyncio_loop)
password = config_options.get('password') password = config_options.get('password')
new_password = config_options.get('new_password') new_password = config_options.get('new_password')
config = SimpleConfig(config_options) config = SimpleConfig(config_options)
@ -302,35 +284,26 @@ class Daemon(DaemonThread):
kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x)) kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x))
cmd_runner = Commands(config, wallet, self.network) cmd_runner = Commands(config, wallet, self.network)
func = getattr(cmd_runner, cmd.name) func = getattr(cmd_runner, cmd.name)
try: result = func(*args, **kwargs)
result = func(*args, **kwargs)
except TypeError as e:
raise Exception("Wrapping TypeError to prevent JSONRPC-Pelix from hiding traceback") from e
return result return result
def run(self): def run(self):
while self.is_running(): while self.is_running():
self.server.handle_request() if self.server else time.sleep(0.1) self.server.handle_request() if self.server else time.sleep(0.1)
# stop network/wallets
for k, wallet in self.wallets.items(): for k, wallet in self.wallets.items():
wallet.stop_threads() wallet.stop_threads()
if self.network: if self.network:
self.print_error("shutting down network") self.print_error("shutting down network")
self.network.stop() self.network.stop()
# stop event loop self.network.join()
self.asyncio_loop.call_soon_threadsafe(self._stop_loop.set_result, 1)
self._loop_thread.join(timeout=1)
self.on_stop() self.on_stop()
def stop(self): def stop(self):
if self.gui:
self.gui.stop()
self.print_error("stopping, removing lockfile") self.print_error("stopping, removing lockfile")
remove_lockfile(get_lockfile(self.config)) remove_lockfile(get_lockfile(self.config))
DaemonThread.stop(self) DaemonThread.stop(self)
def init_gui(self, config, plugins): def init_gui(self, config, plugins):
threading.current_thread().setName('GUI')
gui_name = config.get('gui', 'qt') gui_name = config.get('gui', 'qt')
if gui_name in ['lite', 'classic']: if gui_name in ['lite', 'classic']:
gui_name = 'qt' gui_name = 'qt'

View File

@ -110,9 +110,11 @@ def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
if rrsig.algorithm == ECDSAP256SHA256: if rrsig.algorithm == ECDSAP256SHA256:
curve = ecdsa.curves.NIST256p curve = ecdsa.curves.NIST256p
key_len = 32 key_len = 32
digest_len = 32
elif rrsig.algorithm == ECDSAP384SHA384: elif rrsig.algorithm == ECDSAP384SHA384:
curve = ecdsa.curves.NIST384p curve = ecdsa.curves.NIST384p
key_len = 48 key_len = 48
digest_len = 48
else: else:
# shouldn't happen # shouldn't happen
raise ValidationFailure('unknown ECDSA curve') raise ValidationFailure('unknown ECDSA curve')
@ -139,7 +141,7 @@ def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
rrnamebuf = rrname.to_digestable(origin) rrnamebuf = rrname.to_digestable(origin)
rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass, rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
rrsig.original_ttl) rrsig.original_ttl)
rrlist = sorted(rdataset) rrlist = sorted(rdataset);
for rr in rrlist: for rr in rrlist:
hash.update(rrnamebuf) hash.update(rrnamebuf)
hash.update(rrfixed) hash.update(rrfixed)

View File

@ -24,8 +24,10 @@
# SOFTWARE. # SOFTWARE.
import base64 import base64
import hmac
import hashlib import hashlib
from typing import Union, Tuple from typing import Union
import ecdsa import ecdsa
from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1 from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1
@ -34,10 +36,8 @@ from ecdsa.ellipticcurve import Point
from ecdsa.util import string_to_number, number_to_string from ecdsa.util import string_to_number, number_to_string
from .util import bfh, bh2u, assert_bytes, print_error, to_bytes, InvalidPassword, profiler from .util import bfh, bh2u, assert_bytes, print_error, to_bytes, InvalidPassword, profiler
from .crypto import (sha256d, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot) from .crypto import (Hash, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot)
from .ecc_fast import do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1 from .ecc_fast import do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1
from . import msqr
from . import constants
do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1() do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1()
@ -53,31 +53,31 @@ def point_at_infinity():
return ECPubkey(None) return ECPubkey(None)
def sig_string_from_der_sig(der_sig: bytes, order=CURVE_ORDER) -> bytes: def sig_string_from_der_sig(der_sig, order=CURVE_ORDER):
r, s = ecdsa.util.sigdecode_der(der_sig, order) r, s = ecdsa.util.sigdecode_der(der_sig, order)
return ecdsa.util.sigencode_string(r, s, order) return ecdsa.util.sigencode_string(r, s, order)
def der_sig_from_sig_string(sig_string: bytes, order=CURVE_ORDER) -> bytes: def der_sig_from_sig_string(sig_string, order=CURVE_ORDER):
r, s = ecdsa.util.sigdecode_string(sig_string, order) r, s = ecdsa.util.sigdecode_string(sig_string, order)
return ecdsa.util.sigencode_der_canonize(r, s, order) return ecdsa.util.sigencode_der_canonize(r, s, order)
def der_sig_from_r_and_s(r: int, s: int, order=CURVE_ORDER) -> bytes: def der_sig_from_r_and_s(r, s, order=CURVE_ORDER):
return ecdsa.util.sigencode_der_canonize(r, s, order) return ecdsa.util.sigencode_der_canonize(r, s, order)
def get_r_and_s_from_der_sig(der_sig: bytes, order=CURVE_ORDER) -> Tuple[int, int]: def get_r_and_s_from_der_sig(der_sig, order=CURVE_ORDER):
r, s = ecdsa.util.sigdecode_der(der_sig, order) r, s = ecdsa.util.sigdecode_der(der_sig, order)
return r, s return r, s
def get_r_and_s_from_sig_string(sig_string: bytes, order=CURVE_ORDER) -> Tuple[int, int]: def get_r_and_s_from_sig_string(sig_string, order=CURVE_ORDER):
r, s = ecdsa.util.sigdecode_string(sig_string, order) r, s = ecdsa.util.sigdecode_string(sig_string, order)
return r, s return r, s
def sig_string_from_r_and_s(r: int, s: int, order=CURVE_ORDER) -> bytes: def sig_string_from_r_and_s(r, s, order=CURVE_ORDER):
return ecdsa.util.sigencode_string_canonize(r, s, order) return ecdsa.util.sigencode_string_canonize(r, s, order)
@ -94,22 +94,23 @@ def point_to_ser(P, compressed=True) -> bytes:
return bfh('04'+('%064x' % x)+('%064x' % y)) return bfh('04'+('%064x' % x)+('%064x' % y))
def get_y_coord_from_x(x: int, odd: bool=True) -> int: def get_y_coord_from_x(x, odd=True):
curve = curve_secp256k1 curve = curve_secp256k1
_p = curve.p() _p = curve.p()
_a = curve.a() _a = curve.a()
_b = curve.b() _b = curve.b()
x = x % _p for offset in range(128):
y2 = (pow(x, 3, _p) + _a * x + _b) % _p Mx = x + offset
y = msqr.modular_sqrt(y2, _p) My2 = pow(Mx, 3, _p) + _a * pow(Mx, 2, _p) + _b % _p
if curve.contains_point(x, y): My = pow(My2, (_p + 1) // 4, _p)
if odd == bool(y & 1): if curve.contains_point(Mx, My):
return y if odd == bool(My & 1):
return _p - y return My
raise InvalidECPointException() return _p - My
raise Exception('ECC_YfromX: No Y found')
def ser_to_point(ser: bytes) -> Tuple[int, int]: def ser_to_point(ser: bytes) -> (int, int):
if ser[0] not in (0x02, 0x03, 0x04): if ser[0] not in (0x02, 0x03, 0x04):
raise ValueError('Unexpected first byte: {}'.format(ser[0])) raise ValueError('Unexpected first byte: {}'.format(ser[0]))
if ser[0] == 0x04: if ser[0] == 0x04:
@ -226,7 +227,7 @@ class ECPubkey(object):
def get_public_key_hex(self, compressed=True): def get_public_key_hex(self, compressed=True):
return bh2u(self.get_public_key_bytes(compressed)) return bh2u(self.get_public_key_bytes(compressed))
def point(self) -> Tuple[int, int]: def point(self) -> (int, int):
return self._pubkey.point.x(), self._pubkey.point.y() return self._pubkey.point.x(), self._pubkey.point.y()
def __mul__(self, other: int): def __mul__(self, other: int):
@ -253,7 +254,7 @@ class ECPubkey(object):
def verify_message_for_address(self, sig65: bytes, message: bytes) -> None: def verify_message_for_address(self, sig65: bytes, message: bytes) -> None:
assert_bytes(message) assert_bytes(message)
h = sha256d(msg_magic(message)) h = Hash(msg_magic(message))
public_key, compressed = self.from_signature65(sig65, h) public_key, compressed = self.from_signature65(sig65, h)
# check public key # check public key
if public_key != self: if public_key != self:
@ -295,14 +296,6 @@ class ECPubkey(object):
def is_at_infinity(self): def is_at_infinity(self):
return self == point_at_infinity() return self == point_at_infinity()
@classmethod
def is_pubkey_bytes(cls, b: bytes):
try:
ECPubkey(b)
return True
except:
return False
def msg_magic(message: bytes) -> bytes: def msg_magic(message: bytes) -> bytes:
from .bitcoin import var_int from .bitcoin import var_int
@ -310,17 +303,16 @@ def msg_magic(message: bytes) -> bytes:
return b"\x18Bitcoin Signed Message:\n" + length + message return b"\x18Bitcoin Signed Message:\n" + length + message
def verify_message_with_address(address: str, sig65: bytes, message: bytes, *, net=None): def verify_message_with_address(address: str, sig65: bytes, message: bytes):
from .bitcoin import pubkey_to_address from .bitcoin import pubkey_to_address
assert_bytes(sig65, message) assert_bytes(sig65, message)
if net is None: net = constants.net
try: try:
h = sha256d(msg_magic(message)) h = Hash(msg_magic(message))
public_key, compressed = ECPubkey.from_signature65(sig65, h) public_key, compressed = ECPubkey.from_signature65(sig65, h)
# check public key using the address # check public key using the address
pubkey_hex = public_key.get_public_key_hex(compressed) pubkey_hex = public_key.get_public_key_hex(compressed)
for txin_type in ['p2pkh','p2wpkh','p2wpkh-p2sh']: for txin_type in ['p2pkh','p2wpkh','p2wpkh-p2sh']:
addr = pubkey_to_address(txin_type, pubkey_hex, net=net) addr = pubkey_to_address(txin_type, pubkey_hex)
if address == addr: if address == addr:
break break
else: else:
@ -329,7 +321,7 @@ def verify_message_with_address(address: str, sig65: bytes, message: bytes, *, n
public_key.verify_message_hash(sig65[1:], h) public_key.verify_message_hash(sig65[1:], h)
return True return True
except Exception as e: except Exception as e:
print_error(f"Verification error: {repr(e)}") print_error("Verification error: {0}".format(e))
return False return False
@ -405,14 +397,14 @@ class ECPrivkey(ECPubkey):
raise Exception("error: cannot sign message. no recid fits..") raise Exception("error: cannot sign message. no recid fits..")
message = to_bytes(message, 'utf8') message = to_bytes(message, 'utf8')
msg_hash = sha256d(msg_magic(message)) msg_hash = Hash(msg_magic(message))
sig_string = self.sign(msg_hash, sig_string = self.sign(msg_hash,
sigencode=sig_string_from_r_and_s, sigencode=sig_string_from_r_and_s,
sigdecode=get_r_and_s_from_sig_string) sigdecode=get_r_and_s_from_sig_string)
sig65, recid = bruteforce_recid(sig_string) sig65, recid = bruteforce_recid(sig_string)
return sig65 return sig65
def decrypt_message(self, encrypted: Tuple[str, bytes], magic: bytes=b'BIE1') -> bytes: def decrypt_message(self, encrypted, magic=b'BIE1'):
encrypted = base64.b64decode(encrypted) encrypted = base64.b64decode(encrypted)
if len(encrypted) < 85: if len(encrypted) < 85:
raise Exception('invalid ciphertext: length') raise Exception('invalid ciphertext: length')
@ -437,6 +429,6 @@ class ECPrivkey(ECPubkey):
return aes_decrypt_with_iv(key_e, iv, ciphertext) return aes_decrypt_with_iv(key_e, iv, ciphertext)
def construct_sig65(sig_string: bytes, recid: int, is_compressed: bool) -> bytes: def construct_sig65(sig_string, recid, is_compressed):
comp = 4 if is_compressed else 0 comp = 4 if is_compressed else 0
return bytes([27 + recid + comp]) + sig_string return bytes([27 + recid + comp]) + sig_string

View File

@ -1,4 +1,6 @@
import asyncio import asyncio
import aiohttp
from aiohttp_socks import SocksConnector, SocksVer
from datetime import datetime from datetime import datetime
import inspect import inspect
import sys import sys
@ -10,15 +12,12 @@ import decimal
from decimal import Decimal from decimal import Decimal
import concurrent.futures import concurrent.futures
import traceback import traceback
from typing import Sequence
from .bitcoin import COIN from .bitcoin import COIN
from .i18n import _ from .i18n import _
from .util import (PrintError, ThreadJob, make_dir, log_exceptions, from .util import PrintError, ThreadJob, make_dir, aiosafe
make_aiohttp_session, resource_path) from .util import make_aiohttp_session
from .network import Network from .network import Network
from .simple_config import SimpleConfig
# See https://en.wikipedia.org/wiki/ISO_4217 # See https://en.wikipedia.org/wiki/ISO_4217
CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
@ -28,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, 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0} 'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
class ExchangeBase(PrintError): class ExchangeBase(PrintError):
def __init__(self, on_quotes, on_history): def __init__(self, on_quotes, on_history):
@ -49,8 +47,7 @@ class ExchangeBase(PrintError):
url = ''.join(['https://', site, get_string]) url = ''.join(['https://', site, get_string])
async with make_aiohttp_session(Network.get_instance().proxy) as session: async with make_aiohttp_session(Network.get_instance().proxy) as session:
async with session.get(url) as response: async with session.get(url) as response:
# set content_type to None to disable checking MIME type return await response.json()
return await response.json(content_type=None)
async def get_csv(self, site, get_string): async def get_csv(self, site, get_string):
raw = await self.get_raw(site, get_string) raw = await self.get_raw(site, get_string)
@ -60,14 +57,14 @@ class ExchangeBase(PrintError):
def name(self): def name(self):
return self.__class__.__name__ return self.__class__.__name__
@log_exceptions @aiosafe
async def update_safe(self, ccy): async def update_safe(self, ccy):
try: try:
self.print_error("getting fx quotes for", ccy) self.print_error("getting fx quotes for", ccy)
self.quotes = await self.get_rates(ccy) self.quotes = await self.get_rates(ccy)
self.print_error("received fx quotes") self.print_error("received fx quotes")
except BaseException as e: except BaseException as e:
self.print_error("failed fx quotes:", repr(e)) self.print_error("failed fx quotes:", e)
self.quotes = {} self.quotes = {}
self.on_quotes() self.on_quotes()
@ -91,7 +88,7 @@ class ExchangeBase(PrintError):
self.on_history() self.on_history()
return h return h
@log_exceptions @aiosafe
async def get_historical_rates_safe(self, ccy, cache_dir): async def get_historical_rates_safe(self, ccy, cache_dir):
try: try:
self.print_error("requesting fx history for", ccy) self.print_error("requesting fx history for", ccy)
@ -222,11 +219,13 @@ class Bitvalor(ExchangeBase):
return {'BRL': Decimal(json['ticker_1h']['total']['last'])} return {'BRL': Decimal(json['ticker_1h']['total']['last'])}
class BlockchainInfo(ExchangeBase): class Bittrex(ExchangeBase):
async def get_rates(self, ccy): async def get_rates(self, ccy):
json = await self.get_json('blockchain.info', '/ticker') json = await self.get_json('bittrex.com','/api/v1.1/public/getticker?market=BTC-FLO')
return dict([(r, Decimal(json[r]['15m'])) for r in json]) 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])
class BTCChina(ExchangeBase): class BTCChina(ExchangeBase):
@ -246,9 +245,10 @@ class BTCParalelo(ExchangeBase):
class Coinbase(ExchangeBase): class Coinbase(ExchangeBase):
async def get_rates(self, ccy): async def get_rates(self, ccy):
json = await self.get_json('api.coinbase.com', json = await self.get_json('coinbase.com',
'/v2/exchange-rates?currency=BTC') '/api/v1/currencies/exchange_rates')
return {ccy: Decimal(rate) for (ccy, rate) in json["data"]["rates"].items()} return dict([(r[7:].upper(), Decimal(json[r]))
for r in json if r.startswith('btc_to_')])
class CoinDesk(ExchangeBase): class CoinDesk(ExchangeBase):
@ -280,14 +280,6 @@ class CoinDesk(ExchangeBase):
return json['bpi'] return json['bpi']
class CoinMarketcap(ExchangeBase):
async def get_rates(self, ccy):
json = await self.get_json('pro-api.coinmarketcap.com','/v1/cryptocurrency/quotes/latest?symbol=FLO&CMC_PRO_API_KEY=194ee8a1-5a58-4f3e-ba07-a1d6bc633210&convert=%s' % ccy)
result = {ccy: Decimal(json['data']['FLO']['quote'][ccy]['price'])}
return result
class Coinsecure(ExchangeBase): class Coinsecure(ExchangeBase):
async def get_rates(self, ccy): async def get_rates(self, ccy):
@ -402,7 +394,7 @@ def dictinvert(d):
return inv return inv
def get_exchanges_and_currencies(): def get_exchanges_and_currencies():
path = resource_path('currencies.json') path = os.path.join(os.path.dirname(__file__), 'currencies.json')
try: try:
with open(path, 'r', encoding='utf-8') as f: with open(path, 'r', encoding='utf-8') as f:
return json.loads(f.read()) return json.loads(f.read())
@ -443,7 +435,7 @@ def get_exchanges_by_ccy(history=True):
class FxThread(ThreadJob): class FxThread(ThreadJob):
def __init__(self, config: SimpleConfig, network: Network): def __init__(self, config, network):
self.config = config self.config = config
self.network = network self.network = network
if self.network: if self.network:
@ -453,31 +445,25 @@ class FxThread(ThreadJob):
self.ccy_combo = None self.ccy_combo = None
self.hist_checkbox = None self.hist_checkbox = None
self.cache_dir = os.path.join(config.path, 'cache') self.cache_dir = os.path.join(config.path, 'cache')
self._trigger = asyncio.Event() self.trigger = asyncio.Event()
self._trigger.set() self.trigger.set()
self.set_exchange(self.config_exchange()) self.set_exchange(self.config_exchange())
make_dir(self.cache_dir) make_dir(self.cache_dir)
def set_proxy(self, trigger_name, *args): def set_proxy(self, trigger_name, *args):
self._trigger.set() self.trigger.set()
@staticmethod def get_currencies(self, h):
def get_currencies(history: bool) -> Sequence[str]: d = get_exchanges_by_ccy(h)
d = get_exchanges_by_ccy(history)
return sorted(d.keys()) return sorted(d.keys())
@staticmethod def get_exchanges_by_ccy(self, ccy, h):
def get_exchanges_by_ccy(ccy: str, history: bool) -> Sequence[str]: d = get_exchanges_by_ccy(h)
d = get_exchanges_by_ccy(history)
return d.get(ccy, []) return d.get(ccy, [])
@staticmethod
def remove_thousands_separator(text):
return text.replace(',', '') # FIXME use THOUSAND_SEPARATOR in util
def ccy_amount_str(self, amount, commas): def ccy_amount_str(self, amount, commas):
prec = CCY_PRECISIONS.get(self.ccy, 2) prec = CCY_PRECISIONS.get(self.ccy, 2)
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) # FIXME use util.THOUSAND_SEPARATOR and util.DECIMAL_POINT fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
try: try:
rounded_amount = round(amount, prec) rounded_amount = round(amount, prec)
except decimal.InvalidOperation: except decimal.InvalidOperation:
@ -487,11 +473,11 @@ class FxThread(ThreadJob):
async def run(self): async def run(self):
while True: while True:
try: try:
await asyncio.wait_for(self._trigger.wait(), 150) await asyncio.wait_for(self.trigger.wait(), 150)
except concurrent.futures.TimeoutError: except concurrent.futures.TimeoutError:
pass pass
else: else:
self._trigger.clear() self.trigger.clear()
if self.is_enabled(): if self.is_enabled():
if self.show_history(): if self.show_history():
self.exchange.get_historical_rates(self.ccy, self.cache_dir) self.exchange.get_historical_rates(self.ccy, self.cache_dir)
@ -503,7 +489,7 @@ class FxThread(ThreadJob):
def set_enabled(self, b): def set_enabled(self, b):
self.config.set_key('use_exchange_rate', bool(b)) self.config.set_key('use_exchange_rate', bool(b))
self.trigger_update() self.trigger.set()
def get_history_config(self): def get_history_config(self):
return bool(self.config.get('history_rates')) return bool(self.config.get('history_rates'))
@ -528,7 +514,7 @@ class FxThread(ThreadJob):
return self.config.get("currency", "EUR") return self.config.get("currency", "EUR")
def config_exchange(self): def config_exchange(self):
return self.config.get('use_exchange', 'CoinMarketcap') return self.config.get('use_exchange', 'BitcoinAverage')
def show_history(self): def show_history(self):
return self.is_enabled() and self.get_history_config() and self.ccy in self.exchange.history_ccys() return self.is_enabled() and self.get_history_config() and self.ccy in self.exchange.history_ccys()
@ -536,13 +522,9 @@ class FxThread(ThreadJob):
def set_currency(self, ccy): def set_currency(self, ccy):
self.ccy = ccy self.ccy = ccy
self.config.set_key('currency', ccy, True) self.config.set_key('currency', ccy, True)
self.trigger_update() self.trigger.set() # Because self.ccy changes
self.on_quotes() self.on_quotes()
def trigger_update(self):
if self.network:
self.network.asyncio_loop.call_soon_threadsafe(self._trigger.set)
def set_exchange(self, name): def set_exchange(self, name):
class_ = globals().get(name, BitcoinAverage) class_ = globals().get(name, BitcoinAverage)
self.print_error("using exchange", name) self.print_error("using exchange", name)
@ -551,7 +533,7 @@ class FxThread(ThreadJob):
self.exchange = class_(self.on_quotes, self.on_history) self.exchange = class_(self.on_quotes, self.on_history)
# A new exchange means new fx quotes, initially empty. Force # A new exchange means new fx quotes, initially empty. Force
# a quote refresh # a quote refresh
self.trigger_update() self.trigger.set()
self.exchange.read_historical_rates(self.ccy, self.cache_dir) self.exchange.read_historical_rates(self.ccy, self.cache_dir)
def on_quotes(self): def on_quotes(self):
@ -613,4 +595,4 @@ class FxThread(ThreadJob):
def timestamp_rate(self, timestamp): def timestamp_rate(self, timestamp):
from .util import timestamp_to_datetime from .util import timestamp_to_datetime
date = timestamp_to_datetime(timestamp) date = timestamp_to_datetime(timestamp)
return self.history_rate(date) return self.history_rate(date)

View File

@ -1,5 +1,5 @@
# To create a new GUI, please add its code to this directory. # To create a new GUI, please add its code to this directory.
# Three objects are passed to the ElectrumGui: config, daemon and plugins # Three objects are passed to the ElectrumGui: config, daemon and plugins
# The Wallet object is instantiated by the GUI # The Wallet object is instanciated by the GUI
# Notifications about network events are sent to the GUI by using network.register_callback() # Notifications about network events are sent to the GUI by using network.register_callback()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -11,7 +11,7 @@ prepare:
@cp tools/buildozer.spec ../../../buildozer.spec @cp tools/buildozer.spec ../../../buildozer.spec
# copy electrum to main.py # copy electrum to main.py
@cp ../../../run_electrum ../../../main.py @cp ../../../run_electrum ../../../main.py
@-if [ ! -d "../../../.buildozer" ];then \ @-if [ ! -d "../../.buildozer" ];then \
cd ../../..; buildozer android debug;\ cd ../../..; buildozer android debug;\
cp -f electrum/gui/kivy/tools/blacklist.txt .buildozer/android/platform/python-for-android/src/blacklist.txt;\ cp -f electrum/gui/kivy/tools/blacklist.txt .buildozer/android/platform/python-for-android/src/blacklist.txt;\
rm -rf ./.buildozer/android/platform/python-for-android/dist;\ rm -rf ./.buildozer/android/platform/python-for-android/dist;\

View File

@ -1,90 +1,130 @@
# Kivy GUI # Kivy GUI
The Kivy GUI is used with Electrum on Android devices. The Kivy GUI is used with Electrum on Android devices. To generate an APK file, follow these instructions.
To generate an APK file, follow these instructions.
## Android binary with Docker ## 1. Preliminaries
This assumes an Ubuntu host, but it should not be too hard to adapt to another Make sure the current user can write `/opt` (e.g. `sudo chown username: /opt`).
similar system. The docker commands should be executed in the project's root
folder.
1. Install Docker We assume that you already got Electrum to run from source on this machine,
hence have e.g. `git`, `python3-pip` and `python3-setuptools`.
``` ## 2. Install kivy
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get update
$ sudo apt-get install -y docker-ce
```
2. Build image Install kivy for python3 as described [here](https://kivy.org/docs/installation/installation-linux.html).
So for example:
``` ```sh
$ sudo docker build -t electrum-android-builder-img electrum/gui/kivy/tools sudo add-apt-repository ppa:kivy-team/kivy
``` sudo apt-get install python3-kivy
```
3. Build locale files
```
$ ./contrib/make_locale
```
4. Prepare pure python dependencies
```
$ ./contrib/make_packages
```
5. Build binaries
```
$ sudo docker run -it --rm \
--name electrum-android-builder-cont \
-v $PWD:/home/user/wspace/electrum \
-v ~/.keystore:/home/user/.keystore \
--workdir /home/user/wspace/electrum \
electrum-android-builder-img \
./contrib/make_apk
```
This mounts the project dir inside the container,
and so the modifications will affect it, e.g. `.buildozer` folder
will be created.
5. The generated binary is in `./bin`.
## 3. Install python-for-android (p4a)
p4a is used to package Electrum, Python, SDL and a bootstrap Java app into an APK file.
We patched p4a to add some functionality we need for Electrum. Until those changes are
merged into p4a, you need to merge them locally (into the master branch):
## FAQ 3.1 [kivy/python-for-android#1217](https://github.com/kivy/python-for-android/pull/1217)
### I changed something but I don't see any differences on the phone. What did I do wrong? Something like this should work:
```sh
cd /opt
git clone https://github.com/kivy/python-for-android
cd python-for-android
git remote add agilewalker https://github.com/agilewalker/python-for-android
git remote add sombernight https://github.com/SomberNight/python-for-android
git fetch --all
git checkout 93759f36ba45c7bbe0456a4b3e6788622924cbac
git cherry-pick a2fb5ecbc09c4847adbcfd03c6b1ca62b3d09b8d # openssl-fix
git cherry-pick a0ef2007bc60ed642fbd8b61937995dbed0ddd24 # disable backups
```
## 4. Install buildozer
4.1 Buildozer is a frontend to p4a. Luckily we don't need to patch it:
```sh
cd /opt
git clone https://github.com/kivy/buildozer
cd buildozer
sudo python3 setup.py install
```
4.2 Install additional dependencies:
```sh
sudo apt-get install python-pip
```
and the ones listed
[here](https://buildozer.readthedocs.io/en/latest/installation.html#targeting-android).
You will also need
```sh
python3 -m pip install colorama appdirs sh jinja2
```
4.3 Download the [Crystax NDK](https://www.crystax.net/en/download) manually.
Extract into `/opt/crystax-ndk-10.3.2`
## 5. Create the UI Atlas
In the `gui/kivy` directory of Electrum, run `make theming`.
## 6. Download Electrum dependencies
```sh
sudo contrib/make_packages
```
## 7. Try building the APK and fail
```sh
contrib/make_apk
```
During this build attempt, buildozer downloaded some tools,
e.g. those needed in the next step.
## 8. Update the Android SDK build tools
### Method 1: Using the GUI
Start the Android SDK manager in GUI mode:
~/.buildozer/android/platform/android-sdk-20/tools/android
Check the latest SDK available and install it
("Android SDK Tools" and "Android SDK Platform-tools").
Close the SDK manager. Repeat until there is no newer version.
Reopen the SDK manager, and install the latest build tools
("Android SDK Build-tools"), 27.0.3 at the time of writing.
Install "Android Support Repository" from the SDK manager (under "Extras").
### Method 2: Using the command line:
Repeat the following command until there is nothing to install:
~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t tools,platform-tools
Install Build Tools, android API 19 and Android Support Library:
~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t build-tools-27.0.3,android-19,extra-android-m2repository
## 9. Build the APK
```sh
contrib/make_apk
```
# FAQ
## Why do I get errors like `package me.dm7.barcodescanner.zxing does not exist` while compiling?
Update your Android build tools to version 27 like described above.
## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling?
Make sure that your p4a installation includes commit a3cc78a6d1a107cd3b6bd28db8b80f89e3ecddd2.
Also make sure you have recent SDK tools and platform-tools
## I changed something but I don't see any differences on the phone. What did I do wrong?
You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}`
### How do I deploy on connected phone for quick testing?
Assuming `adb` is installed:
```
$ adb -d install -r bin/Electrum-*-debug.apk
$ adb shell monkey -p org.electrum.electrum 1
```
### How do I get an interactive shell inside docker?
```
$ sudo docker run -it --rm \
-v $PWD:/home/user/wspace/electrum \
--workdir /home/user/wspace/electrum \
electrum-android-builder-img
```
### How do I get more verbose logs?
See `log_level` in `buildozer.spec`
### Kivy can be run directly on Linux Desktop. How?
Install Kivy.
Build atlas: `(cd electrum/gui/kivy/; make theming)`
Run electrum with the `-g` switch: `electrum -g kivy`

View File

@ -4,9 +4,6 @@ import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.content.Intent; import android.content.Intent;
import android.support.v4.app.ActivityCompat;
import android.Manifest;
import android.content.pm.PackageManager;
import java.util.Arrays; import java.util.Arrays;
@ -16,35 +13,28 @@ import com.google.zxing.Result;
import com.google.zxing.BarcodeFormat; import com.google.zxing.BarcodeFormat;
public class SimpleScannerActivity extends Activity implements ZXingScannerView.ResultHandler { public class SimpleScannerActivity extends Activity implements ZXingScannerView.ResultHandler {
private static final int MY_PERMISSIONS_CAMERA = 1002; private ZXingScannerView mScannerView;
private ZXingScannerView mScannerView = null;
final String TAG = "org.electrum.SimpleScannerActivity"; final String TAG = "org.electrum.SimpleScannerActivity";
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view
mScannerView.setFormats(Arrays.asList(BarcodeFormat.QR_CODE));
setContentView(mScannerView); // Set the scanner view as the content view
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
if (this.hasPermission()) { mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results.
this.startCamera(); mScannerView.startCamera(); // Start camera on resume
} else {
this.requestPermission();
}
} }
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (null != mScannerView) { mScannerView.stopCamera(); // Stop camera on pause
mScannerView.stopCamera(); // Stop camera on pause
}
}
private void startCamera() {
mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view
mScannerView.setFormats(Arrays.asList(BarcodeFormat.QR_CODE));
setContentView(mScannerView); // Set the scanner view as the content view
mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results.
mScannerView.startCamera(); // Start camera on resume
} }
@Override @Override
@ -55,35 +45,4 @@ public class SimpleScannerActivity extends Activity implements ZXingScannerView.
setResult(Activity.RESULT_OK, resultIntent); setResult(Activity.RESULT_OK, resultIntent);
this.finish(); this.finish();
} }
private boolean hasPermission() {
return (ActivityCompat.checkSelfPermission(this,
Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED);
}
private void requestPermission() {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
MY_PERMISSIONS_CAMERA);
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_CAMERA: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay!
this.startCamera();
} else {
// permission denied
this.finish();
}
return;
}
}
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -151,20 +151,6 @@
data: '' data: ''
text: ' '.join(map(''.join, zip(*[iter(self.data)]*4))) if self.data else '' text: ' '.join(map(''.join, zip(*[iter(self.data)]*4))) if self.data else ''
<FLODataLabel>
font_size: '6pt'
name: ''
data: ''
text: self.data
touched: False
padding: '10dp', '10dp'
canvas.before:
Color:
rgb: .3, .3, .3
Rectangle:
size: self.size
pos: self.pos
<InfoBubble> <InfoBubble>
size_hint: None, None size_hint: None, None
width: '270dp' if root.fs else min(self.width, dp(270)) width: '270dp' if root.fs else min(self.width, dp(270))
@ -207,18 +193,6 @@
size: self.size size: self.size
pos: self.pos pos: self.pos
<FLODataBlueBottom@BoxLayout>
item_height: dp(60)
foreground_color: .843, .914, .972, 1
padding: '12dp', 0
canvas.before:
Color:
rgba: 0.192, .498, 0.745, 1
BorderImage:
source: 'atlas://electrum/gui/kivy/theming/light/card_bottom'
size: self.size
pos: self.pos
<AddressFilter@GridLayout> <AddressFilter@GridLayout>
item_height: dp(42) item_height: dp(42)
@ -264,7 +238,7 @@
padding: dp(12) padding: dp(12)
spacing: dp(5) spacing: dp(5)
screen: None screen: None
on_state: on_release:
self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu() self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu()
canvas.before: canvas.before:
Color: Color:
@ -273,36 +247,6 @@
size: self.size size: self.size
pos: self.pos pos: self.pos
<FLODataButton@Button>:
background_color: 1, .585, .878, 0
halign: 'left'
text_size: (self.width-10, None)
size_hint: 1, 1
default_text: ''
text: self.default_text
text_color: self.foreground_color
disabled_color: 1, 1, 1, 1
foreground_color: 1, 1, 1, 1
canvas.before:
Color:
rgba: (0.9, .498, 0.745, 1) if self.state == 'down' else self.background_color
Rectangle:
size: self.size
pos: self.pos
<ClickableLabel@ButtonBehavior+Label>:
padding: '10dp', '10dp'
text_color: self.foreground_color
disabled_color: 1, 1, 1, 1
foreground_color: 1, 1, 1, 1
canvas.before:
Color:
rgba: (0.9, .498, 0.745, 1) if self.state == 'down' else self.background_color
Rectangle:
size: self.size
pos: self.pos
<BlueButton@Button>: <BlueButton@Button>:
background_color: 1, .585, .878, 0 background_color: 1, .585, .878, 0
halign: 'left' halign: 'left'
@ -322,17 +266,6 @@
size: self.size size: self.size
pos: self.pos pos: self.pos
<ClickableInput@ButtonBehavior+TextInput>:
readonly: True
keyboard_mode: 'managed'
canvas:
Color:
rgba: 1, 0, 0, .3
Rectangle:
size: self.size
pos: self.pos
<AddressButton@Button>: <AddressButton@Button>:
background_color: 1, .585, .878, 0 background_color: 1, .585, .878, 0
halign: 'center' halign: 'center'
@ -374,6 +307,8 @@
carousel: carousel carousel: carousel
do_default_tab: False do_default_tab: False
Carousel: Carousel:
scroll_timeout: 250
scroll_distance: '100dp'
anim_type: 'out_quart' anim_type: 'out_quart'
min_move: .05 min_move: .05
anim_move_duration: .1 anim_move_duration: .1
@ -500,7 +435,7 @@ BoxLayout:
size_hint: 1, 1 size_hint: 1, 1
bold: True bold: True
color: 0.7, 0.7, 0.7, 1 color: 0.7, 0.7, 0.7, 1
text: app.wallet_name text: app.status
font_size: '22dp' font_size: '22dp'
#minimum_width: '1dp' #minimum_width: '1dp'
on_release: app.popup_dialog('status') on_release: app.popup_dialog('status')
@ -516,7 +451,6 @@ BoxLayout:
ActionOvrButton: ActionOvrButton:
name: 'network' name: 'network'
text: _('Network') text: _('Network')
text: _('Network')
ActionOvrButton: ActionOvrButton:
name: 'settings' name: 'settings'
text: _('Settings') text: _('Settings')

View File

@ -9,14 +9,13 @@ import threading
from electrum.bitcoin import TYPE_ADDRESS from electrum.bitcoin import TYPE_ADDRESS
from electrum.storage import WalletStorage from electrum.storage import WalletStorage
from electrum.wallet import Wallet, InternalAddressCorruption from electrum.wallet import Wallet
from electrum.paymentrequest import InvoiceStore from electrum.paymentrequest import InvoiceStore
from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter from electrum.util import profiler, InvalidPassword
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.util import format_satoshis, format_satoshis_plain from electrum.util import format_satoshis, format_satoshis_plain
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
from electrum import blockchain from electrum import blockchain
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from .i18n import _ from .i18n import _
from kivy.app import App from kivy.app import App
@ -40,7 +39,7 @@ from kivy.lang import Builder
from .uix.dialogs.installwizard import InstallWizard from .uix.dialogs.installwizard import InstallWizard
from .uix.dialogs import InfoBubble, crash_reporter from .uix.dialogs import InfoBubble, crash_reporter
from .uix.dialogs import OutputList, OutputItem from .uix.dialogs import OutputList, OutputItem
from .uix.dialogs import TopLabel, RefLabel, FLODataLabel from .uix.dialogs import TopLabel, RefLabel
#from kivy.core.window import Window #from kivy.core.window import Window
#Window.softinput_mode = 'below_target' #Window.softinput_mode = 'below_target'
@ -97,25 +96,10 @@ class ElectrumWindow(App):
def on_auto_connect(self, instance, x): def on_auto_connect(self, instance, x):
net_params = self.network.get_parameters() net_params = self.network.get_parameters()
net_params = net_params._replace(auto_connect=self.auto_connect) net_params = net_params._replace(auto_connect=self.auto_connect)
self.network.run_from_another_thread(self.network.set_parameters(net_params)) self.network.set_parameters(net_params)
def toggle_auto_connect(self, x): def toggle_auto_connect(self, x):
self.auto_connect = not self.auto_connect self.auto_connect = not self.auto_connect
oneserver = BooleanProperty(False)
def on_oneserver(self, instance, x):
net_params = self.network.get_parameters()
net_params = net_params._replace(oneserver=self.oneserver)
self.network.run_from_another_thread(self.network.set_parameters(net_params))
def toggle_oneserver(self, x):
self.oneserver = not self.oneserver
proxy_str = StringProperty('')
def update_proxy_str(self, proxy: dict):
mode = proxy.get('mode')
host = proxy.get('host')
port = proxy.get('port')
self.proxy_str = (host + ':' + port) if mode else _('None')
def choose_server_dialog(self, popup): def choose_server_dialog(self, popup):
from .uix.dialogs.choice_dialog import ChoiceDialog from .uix.dialogs.choice_dialog import ChoiceDialog
protocol = 's' protocol = 's'
@ -132,13 +116,10 @@ class ElectrumWindow(App):
from .uix.dialogs.choice_dialog import ChoiceDialog from .uix.dialogs.choice_dialog import ChoiceDialog
chains = self.network.get_blockchains() chains = self.network.get_blockchains()
def cb(name): def cb(name):
with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items()) for index, b in blockchain.blockchains.items():
for chain_id, b in blockchain_items:
if name == b.get_name(): if name == b.get_name():
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id)) self.network.follow_chain(index)
chain_objects = [blockchain.blockchains.get(chain_id) for chain_id in chains] names = [blockchain.blockchains[b].get_name() for b in chains]
chain_objects = filter(lambda b: b is not None, chain_objects)
names = [b.get_name() for b in chain_objects]
if len(names) > 1: if len(names) > 1:
cur_chain = self.network.blockchain().get_name() cur_chain = self.network.blockchain().get_name()
ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open() ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()
@ -160,7 +141,7 @@ class ElectrumWindow(App):
self.send_screen.set_URI(uri) self.send_screen.set_URI(uri)
def on_new_intent(self, intent): def on_new_intent(self, intent):
if intent.getScheme() != 'flo': if intent.getScheme() != 'bitcoin':
return return
uri = intent.getDataString() uri = intent.getDataString()
self.set_URI(uri) self.set_URI(uri)
@ -180,7 +161,6 @@ class ElectrumWindow(App):
def on_history(self, d): def on_history(self, d):
Logger.info("on_history") Logger.info("on_history")
self.wallet.clear_coin_price_cache()
self._trigger_update_history() self._trigger_update_history()
def on_fee_histogram(self, *args): def on_fee_histogram(self, *args):
@ -200,8 +180,8 @@ class ElectrumWindow(App):
self._trigger_update_status() self._trigger_update_status()
self._trigger_update_history() self._trigger_update_history()
wallet_name = StringProperty(_('No Wallet'))
base_unit = AliasProperty(_get_bu, _set_bu) base_unit = AliasProperty(_get_bu, _set_bu)
status = StringProperty('')
fiat_unit = StringProperty('') fiat_unit = StringProperty('')
def on_fiat_unit(self, a, b): def on_fiat_unit(self, a, b):
@ -285,7 +265,7 @@ class ElectrumWindow(App):
title = _('Electrum App') title = _('Electrum App')
self.electrum_config = config = kwargs.get('config', None) self.electrum_config = config = kwargs.get('config', None)
self.language = config.get('language', 'en') self.language = config.get('language', 'en')
self.network = network = kwargs.get('network', None) # type: Network self.network = network = kwargs.get('network', None)
if self.network: if self.network:
self.num_blocks = self.network.get_local_height() self.num_blocks = self.network.get_local_height()
self.num_nodes = len(self.network.get_interfaces()) self.num_nodes = len(self.network.get_interfaces())
@ -293,9 +273,7 @@ class ElectrumWindow(App):
self.server_host = net_params.host self.server_host = net_params.host
self.server_port = net_params.port self.server_port = net_params.port
self.auto_connect = net_params.auto_connect self.auto_connect = net_params.auto_connect
self.oneserver = net_params.oneserver
self.proxy_config = net_params.proxy if net_params.proxy else {} self.proxy_config = net_params.proxy if net_params.proxy else {}
self.update_proxy_str(self.proxy_config)
self.plugins = kwargs.get('plugins', []) self.plugins = kwargs.get('plugins', [])
self.gui_object = kwargs.get('gui_object', None) self.gui_object = kwargs.get('gui_object', None)
@ -316,6 +294,9 @@ class ElectrumWindow(App):
self._password_dialog = None self._password_dialog = None
self.fee_status = self.electrum_config.get_fee_status() self.fee_status = self.electrum_config.get_fee_status()
def wallet_name(self):
return os.path.basename(self.wallet.storage.path) if self.wallet else ' '
def on_pr(self, pr): def on_pr(self, pr):
if not self.wallet: if not self.wallet:
self.show_error(_('No wallet loaded.')) self.show_error(_('No wallet loaded.'))
@ -509,8 +490,7 @@ class ElectrumWindow(App):
activity.bind(on_new_intent=self.on_new_intent) activity.bind(on_new_intent=self.on_new_intent)
# connect callbacks # connect callbacks
if self.network: if self.network:
interests = ['wallet_updated', 'network_updated', 'blockchain_updated', interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces']
'status', 'new_transaction', 'verified']
self.network.register_callback(self.on_network_event, interests) self.network.register_callback(self.on_network_event, interests)
self.network.register_callback(self.on_fee, ['fee']) self.network.register_callback(self.on_fee, ['fee'])
self.network.register_callback(self.on_fee_histogram, ['fee_histogram']) self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
@ -552,6 +532,8 @@ class ElectrumWindow(App):
else: else:
self.load_wallet(wallet) self.load_wallet(wallet)
else: else:
Logger.debug('Electrum: Wallet not found or action needed. Launching install wizard')
def launch_wizard(): def launch_wizard():
storage = WalletStorage(path, manual_upgrades=True) storage = WalletStorage(path, manual_upgrades=True)
wizard = Factory.InstallWizard(self.electrum_config, self.plugins, storage) wizard = Factory.InstallWizard(self.electrum_config, self.plugins, storage)
@ -562,6 +544,7 @@ class ElectrumWindow(App):
launch_wizard() launch_wizard()
else: else:
from .uix.dialogs.question import Question from .uix.dialogs.question import Question
def handle_answer(b: bool): def handle_answer(b: bool):
if b: if b:
launch_wizard() launch_wizard()
@ -671,35 +654,26 @@ class ElectrumWindow(App):
self.receive_screen = None self.receive_screen = None
self.requests_screen = None self.requests_screen = None
self.address_screen = None self.address_screen = None
self.icon = "electrum/gui/icons/electrum.png" self.icon = "icons/electrum.png"
self.tabs = self.root.ids['tabs'] self.tabs = self.root.ids['tabs']
def update_interfaces(self, dt): def update_interfaces(self, dt):
net_params = self.network.get_parameters()
self.num_nodes = len(self.network.get_interfaces()) self.num_nodes = len(self.network.get_interfaces())
self.num_chains = len(self.network.get_blockchains()) self.num_chains = len(self.network.get_blockchains())
chain = self.network.blockchain() chain = self.network.blockchain()
self.blockchain_forkpoint = chain.get_max_forkpoint() self.blockchain_forkpoint = chain.get_forkpoint()
self.blockchain_name = chain.get_name() self.blockchain_name = chain.get_name()
interface = self.network.interface interface = self.network.interface
if interface: if interface:
self.server_host = interface.host self.server_host = interface.host
else:
self.server_host = str(net_params.host) + ' (connecting...)'
self.proxy_config = net_params.proxy or {}
self.update_proxy_str(self.proxy_config)
def on_network_event(self, event, *args): def on_network_event(self, event, *args):
Logger.info('network event: '+ event) Logger.info('network event: '+ event)
if event == 'network_updated': if event == 'interfaces':
self._trigger_update_interfaces() self._trigger_update_interfaces()
self._trigger_update_status() elif event == 'updated':
elif event == 'wallet_updated':
self._trigger_update_wallet() self._trigger_update_wallet()
self._trigger_update_status() self._trigger_update_status()
elif event == 'blockchain_updated':
# to update number of confirmations in history
self._trigger_update_wallet()
elif event == 'status': elif event == 'status':
self._trigger_update_status() self._trigger_update_status()
elif event == 'new_transaction': elif event == 'new_transaction':
@ -712,7 +686,6 @@ class ElectrumWindow(App):
if self.wallet: if self.wallet:
self.stop_wallet() self.stop_wallet()
self.wallet = wallet self.wallet = wallet
self.wallet_name = wallet.basename()
self.update_wallet() self.update_wallet()
# Once GUI has been initialized check if we want to announce something # Once GUI has been initialized check if we want to announce something
# since the callback has been called before the GUI was initialized # since the callback has been called before the GUI was initialized
@ -720,21 +693,17 @@ class ElectrumWindow(App):
self.receive_screen.clear() self.receive_screen.clear()
self.update_tabs() self.update_tabs()
run_hook('load_wallet', wallet, self) run_hook('load_wallet', wallet, self)
try:
wallet.try_detecting_internal_addresses_corruption()
except InternalAddressCorruption as e:
self.show_error(str(e))
send_exception_to_crash_reporter(e)
def update_status(self, *dt): def update_status(self, *dt):
self.num_blocks = self.network.get_local_height()
if not self.wallet: if not self.wallet:
self.status = _("No Wallet")
return return
if self.network is None or not self.network.is_connected(): if self.network is None or not self.network.is_connected():
status = _("Offline") status = _("Offline")
elif self.network.is_connected(): elif self.network.is_connected():
self.num_blocks = self.network.get_local_height()
server_height = self.network.get_server_height() server_height = self.network.get_server_height()
server_lag = self.num_blocks - server_height server_lag = self.network.get_local_height() - server_height
if not self.wallet.up_to_date or server_height == 0: if not self.wallet.up_to_date or server_height == 0:
status = _("Synchronizing...") status = _("Synchronizing...")
elif server_lag > 1: elif server_lag > 1:
@ -743,14 +712,12 @@ class ElectrumWindow(App):
status = '' status = ''
else: else:
status = _("Disconnected") status = _("Disconnected")
if status: self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '')
self.balance = status # balance
self.fiat_balance = status c, u, x = self.wallet.get_balance()
else: text = self.format_amount(c+x+u)
c, u, x = self.wallet.get_balance() self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
text = self.format_amount(c+x+u) self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy
self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy
def get_max_amount(self): def get_max_amount(self):
from electrum.transaction import TxOutput from electrum.transaction import TxOutput
@ -768,10 +735,6 @@ class ElectrumWindow(App):
return '' return ''
except NotEnoughFunds: except NotEnoughFunds:
return '' return ''
except InternalAddressCorruption as e:
self.show_error(str(e))
send_exception_to_crash_reporter(e)
return ''
amount = tx.output_value() amount = tx.output_value()
__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0) __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
amount_after_all_fees = amount - x_fee_amount amount_after_all_fees = amount - x_fee_amount
@ -799,7 +762,7 @@ class ElectrumWindow(App):
notification.notify('Electrum', message, notification.notify('Electrum', message,
app_icon=icon, app_name='Electrum') app_icon=icon, app_name='Electrum')
except ImportError: except ImportError:
Logger.Error('Notification: needs plyer; `sudo python3 -m pip install plyer`') Logger.Error('Notification: needs plyer; `sudo pip install plyer`')
def on_pause(self): def on_pause(self):
self.pause_time = time.time() self.pause_time = time.time()
@ -829,6 +792,9 @@ class ElectrumWindow(App):
self._clipboard.copy(label.data) self._clipboard.copy(label.data)
Clock.schedule_once(lambda dt: self.show_info(_('Text copied to clipboard.\nTap again to display it as QR code.'))) Clock.schedule_once(lambda dt: self.show_info(_('Text copied to clipboard.\nTap again to display it as QR code.')))
def set_send(self, address, amount, label, message):
self.send_payment(address, amount=amount, label=label, message=message)
def show_error(self, error, width='200dp', pos=None, arrow_pos=None, def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
exit=False, icon='atlas://electrum/gui/kivy/theming/light/error', duration=0, exit=False, icon='atlas://electrum/gui/kivy/theming/light/error', duration=0,
modal=False): modal=False):
@ -914,16 +880,8 @@ class ElectrumWindow(App):
Clock.schedule_once(lambda dt: on_success(tx)) Clock.schedule_once(lambda dt: on_success(tx))
def _broadcast_thread(self, tx, on_complete): def _broadcast_thread(self, tx, on_complete):
status = False ok, txid = self.network.broadcast_transaction_from_non_network_thread(tx)
try: Clock.schedule_once(lambda dt: on_complete(ok, txid))
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
except TxBroadcastError as e:
msg = e.get_message_for_gui()
except BestEffortRequestFailed as e:
msg = repr(e)
else:
status, msg = True, tx.txid()
Clock.schedule_once(lambda dt: on_complete(status, msg))
def broadcast(self, tx, pr=None): def broadcast(self, tx, pr=None):
def on_complete(ok, msg): def on_complete(ok, msg):
@ -936,7 +894,7 @@ class ElectrumWindow(App):
self.wallet.invoices.save() self.wallet.invoices.save()
self.update_tab('invoices') self.update_tab('invoices')
else: else:
msg = msg or '' msg = msg[:500] if msg else _('There was an error broadcasting the transaction.')
self.show_error(msg) self.show_error(msg)
if self.network and self.network.is_connected(): if self.network and self.network.is_connected():
@ -954,13 +912,11 @@ class ElectrumWindow(App):
d.open() d.open()
def flodata_dialog(self, screen): def flodata_dialog(self, screen):
from .uix.dialogs.flodata_dialog import FLODataDialog from .uix.dialogs.label_dialog import LabelDialog
text = screen.flodata text = screen.flodata
def callback(text): def callback(text):
screen.flodata = text screen.flodata = text
d = LabelDialog(_('Enter FLO data'), text, callback)
d = FLODataDialog(_('Enter FLO data'), text, callback)
d.open() d.open()
def amount_dialog(self, screen, show_max): def amount_dialog(self, screen, show_max):
@ -1012,7 +968,6 @@ class ElectrumWindow(App):
def on_fee(self, event, *arg): def on_fee(self, event, *arg):
self.fee_status = self.electrum_config.get_fee_status() self.fee_status = self.electrum_config.get_fee_status()
def protected(self, msg, f, args): def protected(self, msg, f, args):
if self.wallet.has_password(): if self.wallet.has_password():
on_success = lambda pw: f(*(args + (pw,))) on_success = lambda pw: f(*(args + (pw,)))

View File

@ -1,7 +1,3 @@
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.core import core_select_lib
__all__ = ('NFCBase', 'NFCScanner') __all__ = ('NFCBase', 'NFCScanner')
class NFCBase(Widget): class NFCBase(Widget):

View File

@ -117,8 +117,8 @@ class ScannerAndroid(NFCBase):
recTypes = [] recTypes = []
for record in ndefrecords: for record in ndefrecords:
recTypes.append({ recTypes.append({
'type': ''.join(map(chr, record.getType())), 'type': ''.join(map(unichr, record.getType())),
'payload': ''.join(map(chr, record.getPayload())) 'payload': ''.join(map(unichr, record.getPayload()))
}) })
details['recTypes'] = recTypes details['recTypes'] = recTypes
@ -165,20 +165,20 @@ class ScannerAndroid(NFCBase):
def create_AAR(self): def create_AAR(self):
'''Create the record responsible for linking our application to the tag. '''Create the record responsible for linking our application to the tag.
''' '''
return NdefRecord.createApplicationRecord(JString("org.electrum_flo.kivy")) return NdefRecord.createApplicationRecord(JString("org.electrum.kivy"))
def create_TNF_EXTERNAL(self, data): def create_TNF_EXTERNAL(self, data):
'''Create our actual payload record. '''Create our actual payload record.
''' '''
if BUILDVERSION >= 14: if BUILDVERSION >= 14:
domain = "org.electrum_flo" domain = "org.electrum"
stype = "externalType" stype = "externalType"
extRecord = NdefRecord.createExternal(domain, stype, data) extRecord = NdefRecord.createExternal(domain, stype, data)
else: else:
# Creating the NdefRecord manually: # Creating the NdefRecord manually:
extRecord = NdefRecord( extRecord = NdefRecord(
NdefRecord.TNF_EXTERNAL_TYPE, NdefRecord.TNF_EXTERNAL_TYPE,
"org.electrum_flo:externalType", "org.electrum:externalType",
'', '',
data) data)
return extRecord return extRecord
@ -213,7 +213,7 @@ class ScannerAndroid(NFCBase):
# Create record # Create record
ndef_record = NdefRecord( ndef_record = NdefRecord(
NdefRecord.TNF_MIME_MEDIA, NdefRecord.TNF_MIME_MEDIA,
'org.electrum_flo.kivy', '', data) 'org.electrum.kivy', '', data)
# Create message # Create message
ndef_message = NdefMessage([ndef_record]) ndef_message = NdefMessage([ndef_record])

View File

@ -3,7 +3,6 @@
from . import NFCBase from . import NFCBase
from kivy.clock import Clock from kivy.clock import Clock
from kivy.logger import Logger from kivy.logger import Logger
from kivy.app import App
class ScannerDummy(NFCBase): class ScannerDummy(NFCBase):
'''This is the dummy interface that gets selected in case any other '''This is the dummy interface that gets selected in case any other

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Some files were not shown because too many files have changed in this diff Show More