Compare commits
1 Commits
master
...
olderVersi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e423e34c72 |
4
.gitignore
vendored
@ -6,7 +6,7 @@ dist/
|
||||
*.egg/
|
||||
contrib/pyinstaller/
|
||||
Electrum.egg-info/
|
||||
electrum/locale/
|
||||
locale/
|
||||
.devlocaltmp/
|
||||
*_trial_temp
|
||||
packages
|
||||
@ -14,9 +14,11 @@ env/
|
||||
.buildozer/
|
||||
bin/
|
||||
/app.fil
|
||||
|
||||
.idea
|
||||
|
||||
# icons
|
||||
electrum/gui/qt/icons_rc.py
|
||||
electrum/gui/kivy/theming/light-0.png
|
||||
electrum/gui/kivy/theming/light.atlas
|
||||
|
||||
|
||||
6
.gitmodules
vendored
@ -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"]
|
||||
path = contrib/deterministic-build/electrum-locale
|
||||
url = https://github.com/spesmilo/electrum-locale
|
||||
[submodule "contrib/CalinsQRReader"]
|
||||
path = contrib/osx/CalinsQRReader
|
||||
url = https://github.com/spesmilo/CalinsQRReader
|
||||
|
||||
47
.travis.yml
@ -4,16 +4,12 @@ language: python
|
||||
python:
|
||||
- 3.6
|
||||
- 3.7
|
||||
git:
|
||||
depth: false
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:tah83/secp256k1'
|
||||
packages:
|
||||
- libsecp256k1-0
|
||||
before_install:
|
||||
- git tag
|
||||
install:
|
||||
- pip install -r contrib/requirements/requirements-travis.txt
|
||||
cache:
|
||||
@ -23,12 +19,11 @@ cache:
|
||||
script:
|
||||
- tox
|
||||
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
|
||||
jobs:
|
||||
include:
|
||||
- stage: binary builds
|
||||
name: "Windows build"
|
||||
sudo: true
|
||||
language: c
|
||||
python: false
|
||||
@ -41,53 +36,21 @@ jobs:
|
||||
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
|
||||
after_success: true
|
||||
- name: "Android build"
|
||||
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
|
||||
- os: osx
|
||||
language: c
|
||||
env:
|
||||
- TARGET_OS=macOS
|
||||
python: false
|
||||
install:
|
||||
- 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_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
|
||||
install:
|
||||
- git fetch --all --tags
|
||||
- git fetch origin --unshallow
|
||||
script:
|
||||
- ./contrib/deterministic-build/check_submodules.sh
|
||||
after_success: true
|
||||
|
||||
11
AUTHORS
@ -1,5 +1,3 @@
|
||||
Electrum-BTC
|
||||
------------
|
||||
ThomasV - Creator and maintainer.
|
||||
Animazing / Tachikoma - Styled the new GUI. Mac version.
|
||||
Azelphur - GUI stuff.
|
||||
@ -12,12 +10,3 @@ Slush - Work on the server. Designed the original Stratum spec.
|
||||
Julian Toash (Tuxavant) - Various fixes to the client.
|
||||
rdymac - Website and translations.
|
||||
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
@ -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>
|
||||
@ -1,5 +1,6 @@
|
||||
include LICENCE RELEASE-NOTES AUTHORS
|
||||
include README.rst
|
||||
include electrum.conf.sample
|
||||
include electrum.desktop
|
||||
include *.py
|
||||
include run_electrum
|
||||
@ -7,11 +8,11 @@ include contrib/requirements/requirements.txt
|
||||
include contrib/requirements/requirements-hw.txt
|
||||
recursive-include packages *.py
|
||||
recursive-include packages cacert.pem
|
||||
include icons.qrc
|
||||
graft icons
|
||||
|
||||
graft electrum
|
||||
prune electrum/tests
|
||||
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co~]
|
||||
global-exclude *.py.orig
|
||||
global-exclude *.py.rej
|
||||
global-exclude *.py[co]
|
||||
|
||||
32
README.rst
@ -32,7 +32,7 @@ Qt interface, install the Qt dependencies::
|
||||
sudo apt-get install python3-pyqt5
|
||||
|
||||
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'
|
||||
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::
|
||||
|
||||
sudo apt-get install python3-setuptools
|
||||
python3 -m pip install .[fast]
|
||||
pip3 install .[fast]
|
||||
|
||||
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
|
||||
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)::
|
||||
|
||||
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::
|
||||
|
||||
@ -83,25 +91,25 @@ Create translations (optional)::
|
||||
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
|
||||
----------------
|
||||
|
||||
See :code:`contrib/osx/README.md`.
|
||||
--------
|
||||
|
||||
See `contrib/build-osx/`.
|
||||
|
||||
Windows
|
||||
-------
|
||||
|
||||
See :code:`contrib/build-wine/docker/README.md`.
|
||||
See `contrib/build-wine/`.
|
||||
|
||||
|
||||
Android
|
||||
-------
|
||||
|
||||
See :code:`electrum/gui/kivy/Readme.md`.
|
||||
See `electrum/gui/kivy/Readme.md` 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)
|
||||
|
||||
* 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
|
||||
* Windows: if using the installer binary, there is now a separate shortcut
|
||||
for "Electrum Testnet"
|
||||
* Digital Bitbox: added support for p2sh-segwit
|
||||
* Digital Bitbox: added suport for p2sh-segwit
|
||||
* OS notifications for incoming transactions
|
||||
* better transaction size estimation:
|
||||
- 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
|
||||
* 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
|
||||
* 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:
|
||||
- create and manage payment requests, with QR Codes
|
||||
- the former 'Receive' tab was renamed to 'Addresses'
|
||||
- the former Point of Sale plugin is replaced by a resizable
|
||||
- the former Point of Sale plugin is replaced by a resizeable
|
||||
window that pops up if you click on the QR code
|
||||
|
||||
* The 'Send' tab in the Qt GUI supports transactions with multiple
|
||||
@ -911,7 +837,7 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||
|
||||
* 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:
|
||||
encrypt <pubkey> <message>
|
||||
decrypt <pubkey> <message>
|
||||
@ -984,7 +910,7 @@ bugfixes: connection problems, transactions staying unverified
|
||||
|
||||
# Release 1.8.1
|
||||
|
||||
* Notification option when receiving new transactions
|
||||
* Notification option when receiving new tranactions
|
||||
* Confirm dialogue before sending large amounts
|
||||
* Alternative datafile location for non-windows systems
|
||||
* Fix offline wallet creation
|
||||
|
||||
@ -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
|
||||
```
|
||||
@ -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
|
||||
@ -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`
|
||||
@ -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" "$@"
|
||||
@ -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"/*
|
||||
36
contrib/build-osx/README.md
Normal 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
@ -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
|
||||
}
|
||||
@ -5,7 +5,7 @@ PYTHON_VERSION=3.6.4
|
||||
BUILDDIR=/tmp/electrum-build
|
||||
PACKAGE=Electrum
|
||||
GIT_REPO=https://github.com/spesmilo/electrum
|
||||
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
|
||||
LIBSECP_VERSION=452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4
|
||||
|
||||
. $(dirname "$0")/base.sh
|
||||
|
||||
@ -16,25 +16,6 @@ export PYTHONHASHSEED=22
|
||||
VERSION=`git describe --tags --dirty --always`
|
||||
|
||||
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"
|
||||
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"
|
||||
|
||||
|
||||
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"
|
||||
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:"
|
||||
sw_vers
|
||||
@ -72,14 +47,13 @@ rm -rf $BUILDDIR > /dev/null 2>&1
|
||||
mkdir $BUILDDIR
|
||||
|
||||
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..."
|
||||
curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \
|
||||
tar xz --directory $BUILDDIR
|
||||
cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/osx
|
||||
echo "82c368dfd4da017ceb32b12ca885576f325503428a4966cc09302cbd62702493 contrib/osx/libusb-1.0.dylib" | \
|
||||
shasum -a 256 -c || fail "libusb checksum mismatched"
|
||||
cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/build-osx
|
||||
|
||||
info "Building libsecp256k1"
|
||||
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
|
||||
make
|
||||
popd
|
||||
cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/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
|
||||
cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/build-osx
|
||||
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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..."
|
||||
for d in ~/Library/Python/ ~/.pyenv .; do
|
||||
@ -122,7 +88,7 @@ for d in ~/Library/Python/ ~/.pyenv .; do
|
||||
done
|
||||
|
||||
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"
|
||||
plutil -insert 'CFBundleURLTypes' \
|
||||
@ -130,14 +96,5 @@ plutil -insert 'CFBundleURLTypes' \
|
||||
-- dist/$PACKAGE.app/Contents/Info.plist \
|
||||
|| 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"
|
||||
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
|
||||
@ -2,50 +2,13 @@
|
||||
|
||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
|
||||
|
||||
import sys, os
|
||||
import sys
|
||||
import os
|
||||
|
||||
PACKAGE='Electrum'
|
||||
PYPKG='electrum'
|
||||
MAIN_SCRIPT='run_electrum'
|
||||
ICONS_FILE=PYPKG + '/gui/icons/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
|
||||
|
||||
ICONS_FILE='electrum.icns'
|
||||
|
||||
for i, x in enumerate(sys.argv):
|
||||
if x == '--name':
|
||||
@ -66,17 +29,11 @@ hiddenimports += collect_submodules('keepkeylib')
|
||||
hiddenimports += collect_submodules('websocket')
|
||||
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 = [
|
||||
(electrum + PYPKG + '/*.json', PYPKG),
|
||||
(electrum + PYPKG + '/wordlist/english.txt', PYPKG + '/wordlist'),
|
||||
(electrum + PYPKG + '/locale', PYPKG + '/locale'),
|
||||
(electrum + PYPKG + '/plugins', PYPKG + '/plugins'),
|
||||
(electrum + PYPKG + '/gui/icons', PYPKG + '/gui/icons'),
|
||||
]
|
||||
datas += collect_data_files('trezorlib')
|
||||
datas += collect_data_files('safetlib')
|
||||
@ -84,12 +41,9 @@ datas += collect_data_files('btchip')
|
||||
datas += collect_data_files('keepkeylib')
|
||||
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
|
||||
binaries = [(electrum + "contrib/osx/libusb-1.0.dylib", ".")]
|
||||
binaries += [(electrum + "contrib/osx/libsecp256k1.0.dylib", ".")]
|
||||
binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
|
||||
binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")]
|
||||
|
||||
# Workaround for "Retro Look":
|
||||
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/plugins/cosigner_pool/qt.py',
|
||||
electrum+'electrum/plugins/email_requests/qt.py',
|
||||
electrum+'electrum/plugins/trezor/client.py',
|
||||
electrum+'electrum/plugins/trezor/qt.py',
|
||||
electrum+'electrum/plugins/safe_t/client.py',
|
||||
electrum+'electrum/plugins/safe_t/qt.py',
|
||||
@ -124,19 +79,6 @@ for d in a.datas:
|
||||
a.datas.remove(d)
|
||||
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)
|
||||
|
||||
exe = EXE(pyz,
|
||||
@ -85,4 +85,4 @@ dmg dmg Electrum_uncompressed.dmg electrum-$VERSION.dmg || fail "Unable to creat
|
||||
rm Electrum_uncompressed.dmg
|
||||
|
||||
echo "Done."
|
||||
sha256sum electrum-$VERSION.dmg
|
||||
md5sum electrum-$VERSION.dmg
|
||||
@ -1,13 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
NAME_ROOT=electrum
|
||||
PYTHON_VERSION=3.6.6
|
||||
|
||||
# These settings probably don't need any change
|
||||
export WINEPREFIX=/opt/wine64
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONHASHSEED=22
|
||||
|
||||
PYHOME=c:/python3
|
||||
PYHOME=c:/python$PYTHON_VERSION
|
||||
PYTHON="wine $PYHOME/python.exe -OO -B"
|
||||
|
||||
|
||||
@ -20,7 +21,7 @@ cd tmp
|
||||
|
||||
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 update
|
||||
|
||||
@ -33,7 +34,7 @@ if ! which msgfmt > /dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true
|
||||
done
|
||||
@ -43,6 +44,7 @@ find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
|
||||
popd
|
||||
|
||||
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
|
||||
$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
|
||||
|
||||
pushd $WINEPREFIX/drive_c/electrum
|
||||
$PYTHON -m pip install .
|
||||
$PYTHON setup.py install
|
||||
popd
|
||||
|
||||
cd ..
|
||||
@ -58,7 +60,7 @@ cd ..
|
||||
rm -rf dist/
|
||||
|
||||
# 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
|
||||
pushd dist
|
||||
@ -74,4 +76,4 @@ mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe
|
||||
cd ..
|
||||
|
||||
echo "Done."
|
||||
sha256sum dist/electrum*exe
|
||||
md5sum dist/electrum*exe
|
||||
|
||||
@ -29,8 +29,7 @@ else
|
||||
git pull
|
||||
fi
|
||||
|
||||
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
|
||||
git reset --hard "$LIBSECP_VERSION"
|
||||
git reset --hard 452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4
|
||||
git clean -f -x -q
|
||||
|
||||
build_dll i686-w64-mingw32 # 64-bit would be: x86_64-w64-mingw32
|
||||
|
||||
@ -10,7 +10,8 @@ for i, x in enumerate(sys.argv):
|
||||
else:
|
||||
raise Exception('no name')
|
||||
|
||||
PYHOME = 'c:/python3'
|
||||
PYTHON_VERSION = '3.6.6'
|
||||
PYHOME = 'c:/python' + PYTHON_VERSION
|
||||
|
||||
home = 'C:\\electrum\\'
|
||||
|
||||
@ -23,11 +24,6 @@ hiddenimports += collect_submodules('keepkeylib')
|
||||
hiddenimports += collect_submodules('websocket')
|
||||
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
|
||||
binaries = [(PYHOME+"/libusb-1.0.dll", ".")]
|
||||
|
||||
@ -42,7 +38,6 @@ datas = [
|
||||
(home+'electrum/locale', 'electrum/locale'),
|
||||
(home+'electrum/plugins', 'electrum/plugins'),
|
||||
('C:\\Program Files (x86)\\ZBar\\bin\\', '.'),
|
||||
(home+'electrum/gui/icons', 'electrum/gui/icons'),
|
||||
]
|
||||
datas += collect_data_files('trezorlib')
|
||||
datas += collect_data_files('safetlib')
|
||||
@ -62,6 +57,7 @@ a = Analysis([home+'run_electrum',
|
||||
home+'electrum/commands.py',
|
||||
home+'electrum/plugins/cosigner_pool/qt.py',
|
||||
home+'electrum/plugins/email_requests/qt.py',
|
||||
home+'electrum/plugins/trezor/client.py',
|
||||
home+'electrum/plugins/trezor/qt.py',
|
||||
home+'electrum/plugins/safe_t/client.py',
|
||||
home+'electrum/plugins/safe_t/qt.py',
|
||||
@ -83,24 +79,6 @@ for d in a.datas:
|
||||
a.datas.remove(d)
|
||||
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)
|
||||
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,
|
||||
strip=None,
|
||||
upx=False,
|
||||
icon=home+'electrum/gui/icons/electrum.ico',
|
||||
icon=home+'icons/electrum.ico',
|
||||
console=False)
|
||||
# console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used
|
||||
|
||||
@ -132,7 +110,7 @@ exe_portable = EXE(
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
icon=home+'electrum/gui/icons/electrum.ico',
|
||||
icon=home+'icons/electrum.ico',
|
||||
console=False)
|
||||
|
||||
#####
|
||||
@ -146,7 +124,7 @@ exe_dependent = EXE(
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
icon=home+'electrum/gui/icons/electrum.ico',
|
||||
icon=home+'icons/electrum.ico',
|
||||
console=False)
|
||||
|
||||
coll = COLLECT(
|
||||
@ -157,6 +135,6 @@ coll = COLLECT(
|
||||
strip=None,
|
||||
upx=True,
|
||||
debug=False,
|
||||
icon=home+'electrum/gui/icons/electrum.ico',
|
||||
icon=home+'icons/electrum.ico',
|
||||
console=False,
|
||||
name=os.path.join('dist', 'electrum'))
|
||||
|
||||
@ -6,36 +6,29 @@ RUN dpkg --add-architecture i386 && \
|
||||
apt-get update -q && \
|
||||
apt-get install -qy \
|
||||
wget=1.19.4-1ubuntu2.1 \
|
||||
gnupg2=2.2.4-1ubuntu1.2 \
|
||||
dirmngr=2.2.4-1ubuntu1.2 \
|
||||
gnupg2=2.2.4-1ubuntu1.1 \
|
||||
dirmngr=2.2.4-1ubuntu1.1 \
|
||||
python3-software-properties=0.96.24.32.1 \
|
||||
software-properties-common=0.96.24.32.1
|
||||
|
||||
RUN apt-get update -q && \
|
||||
software-properties-common=0.96.24.32.1 \
|
||||
&& \
|
||||
wget -nc https://dl.winehq.org/wine-builds/Release.key && \
|
||||
apt-key add Release.key && \
|
||||
apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/ && \
|
||||
apt-get update -q && \
|
||||
apt-get install -qy \
|
||||
git=1:2.17.1-1ubuntu0.4 \
|
||||
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 \
|
||||
make=4.1-9.1ubuntu1 \
|
||||
mingw-w64=5.0.3-1 \
|
||||
autotools-dev=20180224.1 \
|
||||
autoconf=2.69-11 \
|
||||
libtool=2.4.6-2 \
|
||||
gettext=0.19.8.1-6
|
||||
|
||||
RUN wget -nc https://dl.winehq.org/wine-builds/Release.key && \
|
||||
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/* && \
|
||||
gettext=0.19.8.1-6 \
|
||||
&& \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean
|
||||
|
||||
@ -20,29 +20,16 @@ folder.
|
||||
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
|
||||
|
||||
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
|
||||
$ sudo docker run -it \
|
||||
$ sudo docker run \
|
||||
--name electrum-wine-builder-cont \
|
||||
-v $PWD:/opt/wine64/drive_c/electrum \
|
||||
--rm \
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
VIAddVersionKey ProductName "${PRODUCT_NAME} Installer"
|
||||
VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}"
|
||||
VIAddVersionKey CompanyName "${PRODUCT_NAME}"
|
||||
VIAddVersionKey LegalCopyright "2013-2018 ${PRODUCT_PUBLISHER}"
|
||||
VIAddVersionKey LegalCopyright "2013-2016 ${PRODUCT_PUBLISHER}"
|
||||
VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer"
|
||||
VIAddVersionKey FileVersion ${PRODUCT_VERSION}
|
||||
VIAddVersionKey ProductVersion ${PRODUCT_VERSION}
|
||||
@ -72,7 +72,7 @@
|
||||
!define MUI_ABORTWARNING
|
||||
!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
|
||||
@ -111,7 +111,7 @@ Section
|
||||
|
||||
;Files to pack into the installer
|
||||
File /r "dist\electrum\*.*"
|
||||
File "c:\electrum\electrum\gui\icons\electrum.ico"
|
||||
File "c:\electrum\icons\electrum.ico"
|
||||
|
||||
;Store installation folder
|
||||
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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_SHA256=4e1db5a7400e348b1b46a4a11b6d9557fd84368e4ad3d4bc4c1be636c89638aa
|
||||
NSIS_SHA256=bd3b15ab62ec6b0c7a00f46022d441af03277be893326f6fea8e212dc2d77743
|
||||
|
||||
ZBAR_FILENAME=zbarw-20121031-setup.exe
|
||||
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_SHA256=671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b
|
||||
|
||||
PYTHON_VERSION=3.6.8
|
||||
PYTHON_VERSION=3.6.6
|
||||
|
||||
## These settings probably don't need change
|
||||
export WINEPREFIX=/opt/wine64
|
||||
#export WINEARCH='win32'
|
||||
|
||||
PYTHON_FOLDER="python3"
|
||||
PYHOME="c:/$PYTHON_FOLDER"
|
||||
PYHOME=c:/python$PYTHON_VERSION
|
||||
PYTHON="wine $PYHOME/python.exe -OO -B"
|
||||
|
||||
|
||||
# Let's begin!
|
||||
here="$(dirname "$(readlink -e "$0")")"
|
||||
set -e
|
||||
# based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg
|
||||
verify_signature() {
|
||||
local file=$1 keyring=$2 out=
|
||||
if out=$(gpg --no-default-keyring --keyring "$keyring" --status-fd 1 --verify "$file" 2>/dev/null) &&
|
||||
echo "$out" | grep -qs "^\[GNUPG:\] VALIDSIG "; then
|
||||
return 0
|
||||
else
|
||||
echo "$out" >&2
|
||||
exit 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'
|
||||
|
||||
# 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
|
||||
|
||||
@ -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.asc"
|
||||
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
|
||||
|
||||
# Install dependencies specific to binaries
|
||||
# note that this also installs pinned versions of both pip and setuptools
|
||||
$PYTHON -m pip install -r "$here"/../deterministic-build/requirements-binaries.txt
|
||||
# upgrade pip
|
||||
$PYTHON -m pip install pip --upgrade
|
||||
|
||||
# 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
|
||||
$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
|
||||
download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL"
|
||||
verify_hash $ZBAR_FILENAME "$ZBAR_SHA256"
|
||||
wine "$PWD/$ZBAR_FILENAME" /S
|
||||
|
||||
# Upgrade setuptools (so Electrum can be installed later)
|
||||
$PYTHON -m pip install setuptools --upgrade
|
||||
|
||||
# Install NSIS installer
|
||||
download_if_not_exist $NSIS_FILENAME "$NSIS_URL"
|
||||
verify_hash $NSIS_FILENAME "$NSIS_SHA256"
|
||||
@ -76,7 +139,10 @@ download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL"
|
||||
verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256"
|
||||
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
|
||||
cp secp256k1/libsecp256k1.dll $WINEPREFIX/drive_c/tmp/
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -18,6 +18,13 @@ function get_git_mtime {
|
||||
|
||||
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
|
||||
echo "Last update from electrum-locale is older than 2 weeks."\
|
||||
|
||||
1
contrib/deterministic-build/electrum-icons
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 0b8cbcca428ceb791527bcbb2ef2b36b4ab29c73
|
||||
@ -1 +1 @@
|
||||
Subproject commit ff5ad3a4436dddcc82799f8a91793013240c3b7b
|
||||
Subproject commit 27e36687f4b0fbd126628bdde80758b63ade7347
|
||||
@ -1,10 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError as e:
|
||||
sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'")
|
||||
import requests
|
||||
|
||||
|
||||
def check_restriction(p, r):
|
||||
|
||||
@ -1,56 +1,58 @@
|
||||
pip==19.0.1 \
|
||||
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \
|
||||
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5
|
||||
pycryptodomex==3.7.3 \
|
||||
--hash=sha256:0bda549e20db1eb8e29fb365d10acf84b224d813b1131c828fc830b2ce313dcd \
|
||||
--hash=sha256:1210c0818e5334237b16d99b5785aa0cee815d9997ee258bd5e2936af8e8aa50 \
|
||||
--hash=sha256:2090dc8cd7843eae75bd504b9be86792baa171fc5a758ea3f60188ab67ca95cf \
|
||||
--hash=sha256:22e6784b65dfdd357bf9a8a842db445192b227103e2c3137a28c489c46742135 \
|
||||
--hash=sha256:2edb8c3965a77e3092b5c5c1233ffd32de083f335202013f52d662404191ac79 \
|
||||
--hash=sha256:310fe269ac870135ff610d272e88dcb594ee58f40ac237a688d7c972cbca43e8 \
|
||||
--hash=sha256:456136b7d459f000794a67b23558351c72e21f0c2d4fcaa09fc99dae7844b0ef \
|
||||
--hash=sha256:463e49a9c5f1fa7bd36aff8debae0b5c487868c1fb66704529f2ad7e92f0cc9f \
|
||||
--hash=sha256:4a33b2828799ef8be789a462e6645ea6fe2c42b0df03e6763ccbfd1789c453e6 \
|
||||
--hash=sha256:5ff02dff1b03929e6339226b318aa59bd0b5c362f96e3e0eb7f3401d30594ed3 \
|
||||
--hash=sha256:6b1db8234b8ee2b30435d9e991389c2eeae4d45e09e471ffe757ba1dfae682bb \
|
||||
--hash=sha256:6eb67ee02de143cd19e36a52bd3869a9dc53e9184cd6bed5c39ff71dee2f6a45 \
|
||||
--hash=sha256:6f42eea5afc7eee29494fdfddc6bb7173953d4197d9200e4f67096c2a24bc21b \
|
||||
--hash=sha256:87bc8082e2de2247df7d0b161234f8edb1384294362cc0c8db9324463097578b \
|
||||
--hash=sha256:8df93d34bc0e3a28a27652070164683a07d8a50c628119d6e0f7710f4d01b42f \
|
||||
--hash=sha256:989952c39e8fef1c959f0a0f85656e29c41c01162e33a3f5fd8ce71e47262ae9 \
|
||||
--hash=sha256:a4a203077e2f312ec8677dde80a5c4e6fe5a82a46173a8edc8da668602a3e073 \
|
||||
--hash=sha256:a793c1242dffd39f585ae356344e8935d30f01f6be7d4c62ffc87af376a2f5f9 \
|
||||
--hash=sha256:b70fe991564e178af02ccf89435a8f9e8d052707a7c4b95bf6027cb785da3175 \
|
||||
--hash=sha256:b83594196e3661cb78c97b80a62fbfbba2add459dfd532b58e7a7c62dd06aab4 \
|
||||
--hash=sha256:ba27725237d0a3ea66ec2b6b387259471840908836711a3b215160808dffed0f \
|
||||
--hash=sha256:d1ab8ad1113cdc553ca50c4d5f0142198c317497364c0c70443d69f7ad1c9288 \
|
||||
--hash=sha256:dce039a8a8a318d7af83cae3fd08d58cefd2120075dfac0ae14d706974040f63 \
|
||||
--hash=sha256:e3213037ea33c85ab705579268cbc8a4433357e9fb99ec7ce9fdcc4d4eec1d50 \
|
||||
--hash=sha256:ec8d8023d31ef72026d46e9fb301ff8759eff5336bcf3d1510836375f53f96a9 \
|
||||
--hash=sha256:ece65730d50aa57a1330d86d81582a2d1587b2ca51cb34f586da8551ddc68fee \
|
||||
--hash=sha256:ed21fc515e224727793e4cc3fb3d00f33f59e3a167d3ad6ac1475ab3b05c2f9e \
|
||||
--hash=sha256:eec1132d878153d61a05424f35f089f951bd6095a4f6c60bdd2ef8919d44425e
|
||||
PyQt5==5.11.3 \
|
||||
--hash=sha256:517e4339135c4874b799af0d484bc2e8c27b54850113a68eec40a0b56534f450 \
|
||||
--hash=sha256:ac1eb5a114b6e7788e8be378be41c5e54b17d5158994504e85e43b5fca006a39 \
|
||||
--hash=sha256:d2309296a5a79d0a1c0e6c387c30f0398b65523a6dcc8a19cc172e46b949e00d \
|
||||
--hash=sha256:e85936bae1581bcb908847d2038e5b34237a5e6acc03130099a78930770e7ead
|
||||
PyQt5-sip==4.19.13 \
|
||||
--hash=sha256:125f77c087572c9272219cda030a63c2f996b8507592b2a54d7ef9b75f9f054d \
|
||||
--hash=sha256:14c37b06e3fb7c2234cb208fa461ec4e62b4ba6d8b32ca3753c0b2cfd61b00e3 \
|
||||
--hash=sha256:1cb2cf52979f9085fc0eab7e0b2438eb4430d4aea8edec89762527e17317175b \
|
||||
--hash=sha256:4babef08bccbf223ec34464e1ed0a23caeaeea390ca9a3529227d9a57f0d6ee4 \
|
||||
--hash=sha256:53cb9c1208511cda0b9ed11cffee992a5a2f5d96eb88722569b2ce65ecf6b960 \
|
||||
--hash=sha256:549449d9461d6c665cbe8af4a3808805c5e6e037cd2ce4fd93308d44a049bfac \
|
||||
--hash=sha256:5f5b3089b200ff33de3f636b398e7199b57a6b5c1bb724bdb884580a072a14b5 \
|
||||
--hash=sha256:a4d9bf6e1fa2dd6e73f1873f1a47cee11a6ba0cf9ba8cf7002b28c76823600d0 \
|
||||
--hash=sha256:a4ee6026216f1fbe25c8847f9e0fbce907df5b908f84816e21af16ec7666e6fe \
|
||||
--hash=sha256:a91a308a5e0cc99de1e97afd8f09f46dd7ca20cfaa5890ef254113eebaa1adff \
|
||||
--hash=sha256:b0342540da479d2713edc68fb21f307473f68da896ad5c04215dae97630e0069 \
|
||||
--hash=sha256:f997e21b4e26a3397cb7b255b8d1db5b9772c8e0c94b6d870a5a0ab5c27eacaa
|
||||
setuptools==40.8.0 \
|
||||
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \
|
||||
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab
|
||||
wheel==0.32.3 \
|
||||
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \
|
||||
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44
|
||||
pip==18.0 \
|
||||
--hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
|
||||
--hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
|
||||
pycryptodomex==3.6.6 \
|
||||
--hash=sha256:0cf562fc5e5ddbe935bb6162d84a7e46e19edba8ac6609587ab9e78dc7c527d4 \
|
||||
--hash=sha256:13b77b7a177a2fd0beb42db84b21d7d4ab646dfd223989a3b5fa6a6901075ae8 \
|
||||
--hash=sha256:1baf0d485853cfa8f87c969148dc040ae81a8de3dcaaeb07b3f40a90ab2ac637 \
|
||||
--hash=sha256:1dac67655206c92eabf827cc3c9f9efd35d699057f12185f01245b22e02db5f8 \
|
||||
--hash=sha256:1dc3abcb853e8b2188d0c8d624dbdc575a1f23ae82feb9a99e4a69b7064df81a \
|
||||
--hash=sha256:24bf22fffba3a7c1132a039d95256ca9e5016798111c7bf7d0091bfddb0e0245 \
|
||||
--hash=sha256:26fde532deaed9781643be654b25fc899b97866b68e50bc015aadd442387c42b \
|
||||
--hash=sha256:2958eceb671795ec4e5f2164e4ec5ddc1788fdf4f72b18390f548de5e11c9dcc \
|
||||
--hash=sha256:34b3218fcdcd59c26de75c4ff8a15e6472e7b88f9e5b3b193cb1c001e7a0e815 \
|
||||
--hash=sha256:3f4d96cfa963960d190d3d6ef5e8f7c2c14815ff816999dff5efaa6bf7cd1134 \
|
||||
--hash=sha256:40063250911af6ec5b3ccc80108e7a3be9d1695dd3d17104a1930861cbdb78b7 \
|
||||
--hash=sha256:4b8669f4b3fa386696b9b0366e69898dd626b119eaead50ee3b2c59c56222272 \
|
||||
--hash=sha256:5144ad0df1009cce6d03b71c2e5843ed96be91ab746e58e8e64ef9bca5209bfe \
|
||||
--hash=sha256:549d0a0884e60d876b975e18e791cf2aece077cb174f0d7d042982b5d36a6279 \
|
||||
--hash=sha256:5b365bc3da05fbc15acd6600d41f84fe8ed903400566bc0d1a0fadc862b41c8e \
|
||||
--hash=sha256:6ab64112924be1905a22656166ec0eeeba275fb15c15adba6470bd030362e160 \
|
||||
--hash=sha256:a06e0e0965145cc0d625e1a031b49705aa2c6b4e16aa0044d4719fb1d639977f \
|
||||
--hash=sha256:a472d2271cfb37940ad80c4698affeaf39552e4c19ceeac4d5290fe71519e3f4 \
|
||||
--hash=sha256:bfc315eef8cdae280e404df64cf71f8673abcc81e646699b15722f41b82a4ed7 \
|
||||
--hash=sha256:c1d4ea2382c1726ff65548f41dc79bd9d0345f3a47ba128768242eea470c6515 \
|
||||
--hash=sha256:c54d3d2e3b099dae61124d7476311ab594df7eeec305f612a5eb79226cf36b53 \
|
||||
--hash=sha256:d0d448484e161786922b41e112b5d7cf76ef5f0c725ea5107ef866bb14a38b12 \
|
||||
--hash=sha256:ddf10a2a071ae749158d8fdad0cbc78f6197219721afa91e0752dd999fddf422 \
|
||||
--hash=sha256:e0cc6ec20e6dfce1a6715ce60d71df3e9491461d525980821f71223026950b0e \
|
||||
--hash=sha256:e5fcb23f936912b6cfb475c7b115aa2e31b7fed409bc947c6190503d9f7d0338 \
|
||||
--hash=sha256:e76d0148ffa5b86f1bcb111bd3950bc61fd50bed8b6b1cf9f5e56784378b55b6 \
|
||||
--hash=sha256:ea09a059e4cf3d99a446f0a80ac9be54802542226cdbf8ffeea79a6ddc77f310 \
|
||||
--hash=sha256:f5844faf085ad3d92fb94d07347c69f193c0bd1f32843329c2a422addeb042c9 \
|
||||
--hash=sha256:f8478a443bb747c2ef1d508a09eeb57ff8c2ebbd8ea0e6e2351d483c23e725b3 \
|
||||
--hash=sha256:ff31cb6cd69c6683bd44e99bc4f36cb74a338cee58a3a9c5c19a8ee4bdd2f3b2
|
||||
PyQt5==5.10.1 \
|
||||
--hash=sha256:1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac \
|
||||
--hash=sha256:4db7113f464c733a99fcb66c4c093a47cf7204ad3f8b3bda502efcc0839ac14b \
|
||||
--hash=sha256:9c17ab3974c1fc7bbb04cc1c9dae780522c0ebc158613f3025fccae82227b5f7 \
|
||||
--hash=sha256:f6035baa009acf45e5f460cf88f73580ad5dc0e72330029acd99e477f20a5d61
|
||||
setuptools==40.2.0 \
|
||||
--hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
|
||||
--hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
|
||||
SIP==4.19.8 \
|
||||
--hash=sha256:09f9a4e6c28afd0bafedb26ffba43375b97fe7207bd1a0d3513f79b7d168b331 \
|
||||
--hash=sha256:105edaaa1c8aa486662226360bd3999b4b89dd56de3e314d82b83ed0587d8783 \
|
||||
--hash=sha256:1bb10aac55bd5ab0e2ee74b3047aa2016cfa7932077c73f602a6f6541af8cd51 \
|
||||
--hash=sha256:265ddf69235dd70571b7d4da20849303b436192e875ce7226be7144ca702a45c \
|
||||
--hash=sha256:52074f7cb5488e8b75b52f34ec2230bc75d22986c7fe5cd3f2d266c23f3349a7 \
|
||||
--hash=sha256:5ff887a33839de8fc77d7f69aed0259b67a384dc91a1dc7588e328b0b980bde2 \
|
||||
--hash=sha256:74da4ddd20c5b35c19cda753ce1e8e1f71616931391caeac2de7a1715945c679 \
|
||||
--hash=sha256:7d69e9cf4f8253a3c0dfc5ba6bb9ac8087b8239851f22998e98cb35cfe497b68 \
|
||||
--hash=sha256:97bb93ee0ef01ba90f57be2b606e08002660affd5bc380776dd8b0fcaa9e093a \
|
||||
--hash=sha256:cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85 \
|
||||
--hash=sha256:d9023422127b94d11c1a84bfa94933e959c484f2c79553c1ef23c69fe00d25f8 \
|
||||
--hash=sha256:e72955e12f4fccf27aa421be383453d697b8a44bde2cc26b08d876fd492d0174
|
||||
wheel==0.31.1 \
|
||||
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
|
||||
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
|
||||
|
||||
@ -1,56 +1,52 @@
|
||||
btchip-python==0.1.28 \
|
||||
--hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83
|
||||
certifi==2018.11.29 \
|
||||
--hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \
|
||||
--hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033
|
||||
certifi==2018.8.24 \
|
||||
--hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \
|
||||
--hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a
|
||||
chardet==3.0.4 \
|
||||
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
|
||||
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
|
||||
ckcc-protocol==0.7.2 \
|
||||
--hash=sha256:31ee5178cfba8895eb2a6b8d06dc7830b51461a0ff767a670a64707c63e6b264 \
|
||||
--hash=sha256:498db4ccdda018cd9f40210f5bd02ddcc98e7df583170b2eab4035c86c3cc03b
|
||||
click==7.0 \
|
||||
--hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \
|
||||
--hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7
|
||||
construct==2.9.45 \
|
||||
--hash=sha256:2271a0efd0798679dea825ff47e22a4c550456a5db0ba8baa82f7eae0af0118c
|
||||
Cython==0.29.4 \
|
||||
--hash=sha256:004eeb2fc64e9db4a3bc0d65583d69769c7242d29d9335121cbab776688dc122 \
|
||||
--hash=sha256:028ee8571884a129e0d5c4d48296f6b3ea679668c096bb65fe8b2ff7ac29d707 \
|
||||
--hash=sha256:162b8b794ca9210c7039d54b6d96cd342e0404e41e7e467baae69f0252d7e52a \
|
||||
--hash=sha256:1aba4cf581d203e8fa3b6a7b432b09416e4f93c0d1f7744834acacfe3e9db424 \
|
||||
--hash=sha256:1be8f08c87b92a880f2fd19f93293e738ca8647834ad05625635320cec9ecad4 \
|
||||
--hash=sha256:21c707a811912aeb65abe8a66e5adebc759889661c8f4cf677523cd33c609084 \
|
||||
--hash=sha256:234de250ef09ba667fc6a8f6ba07712d3fe5bb8d92d70d2b958d4c56e3172c4a \
|
||||
--hash=sha256:33dad82003df518e1242ac3b0592fc63c49d65d0d37b696cb43b7d35085e6bd5 \
|
||||
--hash=sha256:54ee6cbc1397b27670e598ae15cab36e826a01605f63bf267a5fd2642bd8a147 \
|
||||
--hash=sha256:6058c57657d2704c9fad8a56458173d2f525dce4083ca46e9b99b1b35da2b27f \
|
||||
--hash=sha256:6d3065f39ea1354eba4807e2752e97d57f26d6f68bc4a4c561264ca4300c46cb \
|
||||
--hash=sha256:7059e5acac1d7a82e75e553924d9ea59b0e79203adf903cb999287fbcc8f50f1 \
|
||||
--hash=sha256:71c31e01f20a3a7273f6f38760d29170ee89e895be540481130cb173ef6b7246 \
|
||||
--hash=sha256:89225447801e8bd0f6d8e2c0807ded83af8ad7bf4086b5ecf1f22c5a68d1b3e3 \
|
||||
--hash=sha256:9783f11fe4a4af66b0aa0da68fda833c10b95edd9099a6dbe710d03bcb96adf2 \
|
||||
--hash=sha256:9a0be0aac30d71fe490a2b0377fca6e13a5242ecc01d09c7a358f1f2fcb07a80 \
|
||||
--hash=sha256:9a2cccc26dcf2df1e0048cdf63bd714f1d5dfad457f03b9938c5cc3eef74c9ab \
|
||||
--hash=sha256:b0889310f8558eb406a4a853d63553b90c621476f1b5b80b46b1ff57eef198cf \
|
||||
--hash=sha256:c46ef7b771c88512435399e5ffbc3a70079d4945123d6fbfc6211b4cfdc4e546 \
|
||||
--hash=sha256:c71a77c1047d65e5b4e614053cbb7b567c36359b2bc1d27fba23b984ab6dddd0 \
|
||||
--hash=sha256:c9361811a1a49db11efce54fedd01a5544af8db074fce471c720bdb85ec9c7a8 \
|
||||
--hash=sha256:d021a8326a1d2cdb182b0dd7f49bb42d8a4e6ddfb3c8d388ee5be26d57d49f3b \
|
||||
--hash=sha256:d1ee3d39c73a094ae5b6e2f9263ae0dc61af1b549a0869ade8c3c30325ed9f26 \
|
||||
--hash=sha256:d49d7cf82192edc6e386262a07ceb3515028afbd9009dd8ec669d2c0a9f20128 \
|
||||
--hash=sha256:dc5fc1fa072a98f152e46465aaf3e02b3ea36a9d3b8c79bfabd47b0e3ad9226c \
|
||||
--hash=sha256:e290fed7fe73860657af564e596fff87e75cfda861c067e89212970a47826cc6 \
|
||||
--hash=sha256:fcf9a9a566ab98495db641eefee471eb03df71e394ee51fdfa9b4c0b9f6928eb \
|
||||
--hash=sha256:fe8c1d2538867bf2753988a4a2d548bcb211fcbba125aa3e9092391b16f47b56
|
||||
click==6.7 \
|
||||
--hash=sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d \
|
||||
--hash=sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b
|
||||
Cython==0.28.5 \
|
||||
--hash=sha256:022592d419fc754509d0e0461eb2958dbaa45fb60d51c8a61778c58994edbe36 \
|
||||
--hash=sha256:07659f4c57582104d9486c071de512fbd7e087a3a630535298442cc0e20a3f5a \
|
||||
--hash=sha256:13c73e2ffa93a615851e03fad97591954d143b5b62361b9adef81f46a31cd8ef \
|
||||
--hash=sha256:13eab5a2835a84ff62db343035603044c908d2b3b6eec09d67fdf9970acf7ac9 \
|
||||
--hash=sha256:183b35a48f58862c4ec1e821f07bb7b1156c8c8559c85c32ae086f28947474eb \
|
||||
--hash=sha256:2f526b0887128bf20ab2acc905a975f62b5a04ab2f63ecbe5a30fc28285d0e0c \
|
||||
--hash=sha256:32de8637f5e6c5a76667bc7c8fc644bd9314dc19af36db8ce30a0b92ada0f642 \
|
||||
--hash=sha256:4172c183ef4fb2ace6a29cdf7fc9200c5a471a7f775ff691975b774bd9ed3ad2 \
|
||||
--hash=sha256:553956ec06ecbd731ef0c538eb28a5b46bedea7ab89b18237ff28b4b99d65eee \
|
||||
--hash=sha256:660eeb6870687fd3eda91e00ba4e72220545c254c8c4d967fd0c910f4fbb8cbc \
|
||||
--hash=sha256:693a8619ef066ece055ed065a15cf440f9d3ebd1bca60e87ea19144833756433 \
|
||||
--hash=sha256:759c799e9ef418f163b5412e295e14c0a48fe3b4dcba9ab8aab69e9f511cfefd \
|
||||
--hash=sha256:827d3a91b7a7c31ce69e5974496fd9a8ba28eb498b988affb66d0d30de11d934 \
|
||||
--hash=sha256:87e57b5d730cfab225d95e7b23abbc0c6f77598bd66639e93c73ce8afbae6f38 \
|
||||
--hash=sha256:9400e5db8383346b0694a3e794d8bded18a27b21123516dcdf4b79d7ec28e98b \
|
||||
--hash=sha256:9ec27681c5b1b457aacb1cbda5db04aa28b76da2af6e1e1fd15f233eafe6a0b0 \
|
||||
--hash=sha256:ae4784f040a3313c8bd00c8d04934b7ade63dc59692d8f00a5235be8ed72a445 \
|
||||
--hash=sha256:b2ba8310ebd3c0e0b884d5e95bbd99d467d6af922acd1e44fe4b819839b2150e \
|
||||
--hash=sha256:b64575241f64f6ec005a4d4137339fb0ba5e156e826db2fdb5f458060d9979e0 \
|
||||
--hash=sha256:c78ad0df75a9fc03ab28ca1b950c893a208c451a18f76796c3e25817d6994001 \
|
||||
--hash=sha256:cdbb917e41220bd3812234dbe59d15391adbc2c5d91ae11a5273aab9e32ba7ec \
|
||||
--hash=sha256:d2223a80c623e2a8e97953ab945dfaa9385750a494438dcb55562eb1ddd9565a \
|
||||
--hash=sha256:e22f21cf92a9f8f007a280e3b3462c886d9068132a6c698dec10ad6125e3ca1e \
|
||||
--hash=sha256:ea5c16c48e561f4a6f6b8c24807494b77a79e156b8133521c400f22ca712101b \
|
||||
--hash=sha256:ee7a9614d51fe16e32ca5befe72e0808baff481791728449d0b17c8b0fe29eb9 \
|
||||
--hash=sha256:ef86de9299e4ab2ebb129fb84b886bf40b9aced9807c6d6d5f28b46fb905f82c \
|
||||
--hash=sha256:f3e4860f5458a9875caa3de65e255720c0ed2ce71f0bcdab02497b32104f9db8 \
|
||||
--hash=sha256:fc6c20a8ac22202a779ad4c59756647be0826993d2151a03c015e76d2368ae5f
|
||||
ecdsa==0.13 \
|
||||
--hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \
|
||||
--hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa
|
||||
hidapi==0.7.99.post21 \
|
||||
--hash=sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24 \
|
||||
--hash=sha256:6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3 \
|
||||
--hash=sha256:8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946 \
|
||||
--hash=sha256:92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7 \
|
||||
--hash=sha256:b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87 \
|
||||
--hash=sha256:bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660 \
|
||||
--hash=sha256:c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7 \
|
||||
@ -58,30 +54,26 @@ hidapi==0.7.99.post21 \
|
||||
--hash=sha256:d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b \
|
||||
--hash=sha256:e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97 \
|
||||
--hash=sha256:edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922
|
||||
idna==2.8 \
|
||||
--hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \
|
||||
--hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c
|
||||
keepkey==6.0.2 \
|
||||
--hash=sha256:3236dd701bde74768c41a92e724e322ea5e01b90985e2e6215eb85b77f9a0ae1 \
|
||||
--hash=sha256:677e07deacc2ff97bee313b8dd7ae55faebab02e7d17b9a8e49b889996a36010 \
|
||||
--hash=sha256:af107f610fb0e2417fc7a9d87a2fa22aac9b80b79559370d178be424bb85489a
|
||||
libusb1==1.7 \
|
||||
--hash=sha256:9d4f66d2ed699986b06bc3082cd262101cb26af7a76a34bd15b7eb56cba37e0f
|
||||
idna==2.7 \
|
||||
--hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
|
||||
--hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16
|
||||
keepkey==4.0.2 \
|
||||
--hash=sha256:cddee60ae405841cdff789cbc54168ceaeb2282633420f2be155554c25c69138
|
||||
libusb1==1.6.6 \
|
||||
--hash=sha256:a49917a2262cf7134396f6720c8be011f14aabfc5cdc53f880cc672c0f39d271
|
||||
mnemonic==0.18 \
|
||||
--hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d
|
||||
pbkdf2==1.3 \
|
||||
--hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979
|
||||
pip==19.0.1 \
|
||||
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \
|
||||
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5
|
||||
pip==18.0 \
|
||||
--hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
|
||||
--hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
|
||||
protobuf==3.6.1 \
|
||||
--hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \
|
||||
--hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \
|
||||
--hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \
|
||||
--hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \
|
||||
--hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \
|
||||
--hash=sha256:4b92e235a3afd42e7493b281c8b80c0c65cbef45de30f43d571d1ee40a1f77ef \
|
||||
--hash=sha256:574085a33ca0d2c67433e5f3e9a0965c487410d6cb3406c83bdaf549bfc2992e \
|
||||
--hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \
|
||||
--hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \
|
||||
--hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \
|
||||
@ -104,31 +96,27 @@ pyblake2==1.1.2 \
|
||||
--hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \
|
||||
--hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \
|
||||
--hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358
|
||||
requests==2.21.0 \
|
||||
--hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \
|
||||
--hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b
|
||||
requests==2.19.1 \
|
||||
--hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
|
||||
--hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
|
||||
safet==0.1.4 \
|
||||
--hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \
|
||||
--hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1
|
||||
setuptools==40.8.0 \
|
||||
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \
|
||||
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab
|
||||
six==1.12.0 \
|
||||
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
|
||||
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
|
||||
trezor==0.11.1 \
|
||||
--hash=sha256:6043f321d856e1b45b9df0c37810264f08d065bb56cd999f61a05fe2906e9e18 \
|
||||
--hash=sha256:6119b30cf9a136667753935bd06c5f341e78950b35e8ccbadaecc65c12f1946d
|
||||
typing-extensions==3.7.2 \
|
||||
--hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \
|
||||
--hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \
|
||||
--hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71
|
||||
urllib3==1.24.1 \
|
||||
--hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \
|
||||
--hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22
|
||||
websocket-client==0.54.0 \
|
||||
--hash=sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786 \
|
||||
--hash=sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849
|
||||
wheel==0.32.3 \
|
||||
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \
|
||||
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44
|
||||
setuptools==40.2.0 \
|
||||
--hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
|
||||
--hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
|
||||
six==1.11.0 \
|
||||
--hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
|
||||
--hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
|
||||
trezor==0.10.2 \
|
||||
--hash=sha256:4dba4d5c53d3ca22884d79fb4aa68905fb8353a5da5f96c734645d8cf537138d \
|
||||
--hash=sha256:d2b32f25982ab403758d870df1d0de86d0751c106ef1cd1289f452880ce68b84
|
||||
urllib3==1.23 \
|
||||
--hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
|
||||
--hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
|
||||
websocket-client==0.52.0 \
|
||||
--hash=sha256:03763384c530b331ec3822d0b52ffdc28c3aeb8a900ac8c98b2ceea3128a7b4e \
|
||||
--hash=sha256:3c9924675eaf0b27ae22feeeab4741bb4149b94820bd3a143eeaf8b62f64d821
|
||||
wheel==0.31.1 \
|
||||
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
|
||||
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
|
||||
|
||||
@ -1,99 +1,96 @@
|
||||
aiohttp==3.5.4 \
|
||||
--hash=sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55 \
|
||||
--hash=sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed \
|
||||
--hash=sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10 \
|
||||
--hash=sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5 \
|
||||
--hash=sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1 \
|
||||
--hash=sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939 \
|
||||
--hash=sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390 \
|
||||
--hash=sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa \
|
||||
--hash=sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc \
|
||||
--hash=sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5 \
|
||||
--hash=sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d \
|
||||
--hash=sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf \
|
||||
--hash=sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6 \
|
||||
--hash=sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72 \
|
||||
--hash=sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12 \
|
||||
--hash=sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366 \
|
||||
--hash=sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4 \
|
||||
--hash=sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300 \
|
||||
--hash=sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d \
|
||||
--hash=sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303 \
|
||||
--hash=sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6 \
|
||||
--hash=sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889
|
||||
aiohttp-socks==0.2.2 \
|
||||
--hash=sha256:e473ee222b001fe33798957b9ce3352b32c187cf41684f8e2259427925914993 \
|
||||
--hash=sha256:eebd8939a7c3c1e3e7e1b2552c60039b4c65ef6b8b2351efcbdd98290538e310
|
||||
aiorpcX==0.10.4 \
|
||||
--hash=sha256:7130105d31230f069b0eea4e1893c7199cfe2d89a52a31aec718d37f4449935d \
|
||||
--hash=sha256:e6dfd584f597ee3aa6a8d4cb5755c8ffbbe42754f32728561d9e5940379d5096
|
||||
async_timeout==3.0.1 \
|
||||
--hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \
|
||||
--hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3
|
||||
aiohttp==3.4.4 \
|
||||
--hash=sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b \
|
||||
--hash=sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08 \
|
||||
--hash=sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd \
|
||||
--hash=sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac \
|
||||
--hash=sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650 \
|
||||
--hash=sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa \
|
||||
--hash=sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95 \
|
||||
--hash=sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330 \
|
||||
--hash=sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc \
|
||||
--hash=sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b \
|
||||
--hash=sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de \
|
||||
--hash=sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4 \
|
||||
--hash=sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7 \
|
||||
--hash=sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b \
|
||||
--hash=sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8 \
|
||||
--hash=sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd \
|
||||
--hash=sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2 \
|
||||
--hash=sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698 \
|
||||
--hash=sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95 \
|
||||
--hash=sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6 \
|
||||
--hash=sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0 \
|
||||
--hash=sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07
|
||||
aiohttp_socks==0.1.6 \
|
||||
--hash=sha256:943148a3797ba9ffb6df6ddb006ffdd40538885b410589d589bda42a8e8bcd5a
|
||||
aiorpcX==0.7.3 \
|
||||
--hash=sha256:24dd4fe2f65f743cb74c8626570470e325bb777bb66d1932e7d2965ae71d1164 \
|
||||
--hash=sha256:5120ca40beef6b6a45d3a7055e343815401385dc607da2fd93baca2762c8a97d
|
||||
async_timeout==3.0.0 \
|
||||
--hash=sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c \
|
||||
--hash=sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287
|
||||
attrs==18.2.0 \
|
||||
--hash=sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69 \
|
||||
--hash=sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb
|
||||
certifi==2018.11.29 \
|
||||
--hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \
|
||||
--hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033
|
||||
certifi==2018.8.24 \
|
||||
--hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \
|
||||
--hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a
|
||||
chardet==3.0.4 \
|
||||
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
|
||||
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
|
||||
dnspython==1.16.0 \
|
||||
--hash=sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01 \
|
||||
--hash=sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d
|
||||
dnspython==1.15.0 \
|
||||
--hash=sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c \
|
||||
--hash=sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31
|
||||
ecdsa==0.13 \
|
||||
--hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \
|
||||
--hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa
|
||||
idna==2.8 \
|
||||
--hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \
|
||||
--hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c
|
||||
idna==2.7 \
|
||||
--hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
|
||||
--hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16
|
||||
idna_ssl==1.1.0 \
|
||||
--hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c
|
||||
jsonrpclib-pelix==0.4.0 \
|
||||
--hash=sha256:19c558e169a51480b39548783067ca55046b62b2409ab4559931255e12f635de \
|
||||
--hash=sha256:a966d17f2f739ee89031cf5c807d85d92db6b2715fb2b2f8a88bbfc87f468b12
|
||||
multidict==4.5.2 \
|
||||
--hash=sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f \
|
||||
--hash=sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3 \
|
||||
--hash=sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef \
|
||||
--hash=sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b \
|
||||
--hash=sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73 \
|
||||
--hash=sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc \
|
||||
--hash=sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3 \
|
||||
--hash=sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd \
|
||||
--hash=sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351 \
|
||||
--hash=sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941 \
|
||||
--hash=sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d \
|
||||
--hash=sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1 \
|
||||
--hash=sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b \
|
||||
--hash=sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a \
|
||||
--hash=sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3 \
|
||||
--hash=sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7 \
|
||||
--hash=sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0 \
|
||||
--hash=sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0 \
|
||||
--hash=sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014 \
|
||||
--hash=sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5 \
|
||||
--hash=sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036 \
|
||||
--hash=sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d \
|
||||
--hash=sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a \
|
||||
--hash=sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce \
|
||||
--hash=sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1 \
|
||||
--hash=sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a \
|
||||
--hash=sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9 \
|
||||
--hash=sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7 \
|
||||
--hash=sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b
|
||||
pip==19.0.1 \
|
||||
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \
|
||||
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5
|
||||
jsonrpclib-pelix==0.3.1 \
|
||||
--hash=sha256:5417b1508d5a50ec64f6e5b88907f111155d52607b218ff3ba9a777afb2e49e3 \
|
||||
--hash=sha256:bd89a6093bc4d47dc8a096197aacb827359944a4533be5193f3845f57b9f91b4
|
||||
multidict==4.4.0 \
|
||||
--hash=sha256:112eeeddd226af681dc82b756ed34aa7b6d98f9c4a15760050298c21d715473d \
|
||||
--hash=sha256:13b64ecb692effcabc5e29569ba9b5eb69c35112f990a16d6833ec3a9d9f8ec0 \
|
||||
--hash=sha256:1725373fb8f18c2166f8e0e5789851ccf98453c849b403945fa4ef59a16ca44e \
|
||||
--hash=sha256:2061a50b7cae60a1f987503a995b2fc38e47027a937a355a124306ed9c629041 \
|
||||
--hash=sha256:35b062288a9a478f627c520fd27983160fc97591017d170f966805b428d17e07 \
|
||||
--hash=sha256:467b134bcc227b91b8e2ef8d2931f28b50bf7eb7a04c0403d102ded22e66dbfc \
|
||||
--hash=sha256:475a3ece8bb450e49385414ebfae7f8fdb33f62f1ac0c12935c1cfb1b7c1076a \
|
||||
--hash=sha256:49b885287e227a24545a1126d9ac17ae43138610713dc6219b781cc0ad5c6dfc \
|
||||
--hash=sha256:4c95b2725592adb5c46642be2875c1234c32af841732c5504c17726b92082021 \
|
||||
--hash=sha256:4ea7ed00f4be0f7335c9a2713a65ac3d986be789ce5ebc10821da9664cbe6b85 \
|
||||
--hash=sha256:5e2d5e1d999e941b4a626aea46bdc4206877cf727107fdaa9d46a8a773a6e49b \
|
||||
--hash=sha256:8039c520ef7bb9ec7c3db3df14c570be6362f43c200ae9854d2422d4ffe175a4 \
|
||||
--hash=sha256:81459a0ebcca09c1fcb8fe887ed13cf267d9b60fe33718fc5fd1a2a1ab49470a \
|
||||
--hash=sha256:847c3b7b9ca3268e883685dc1347a4d09f84de7bd7597310044d847590447492 \
|
||||
--hash=sha256:8551d1db45f0ca4e8ec99130767009a29a4e0dc6558a4a6808491bcd3472d325 \
|
||||
--hash=sha256:8fa7679ffe615e0c1c7b80946ab4194669be74848719adf2d7867b5e861eb073 \
|
||||
--hash=sha256:a42a36f09f0f907579ff0fde547f2fde8a739a69efe4a2728835979d2bb5e17b \
|
||||
--hash=sha256:a5fcad0070685c5b2d04b468bf5f4c735f5c176432f495ad055fcc4bc0a79b23 \
|
||||
--hash=sha256:ae22195b2a7494619b73c01129ddcddc0dfaa9e42727404b1d9a77253da3f420 \
|
||||
--hash=sha256:b360e82bdbbd862e1ce2a41cc3bbd0ab614350e813ca74801b34aac0f73465aa \
|
||||
--hash=sha256:b96417899344c5e96bef757f4963a72d02e52653a4e0f99bbea3a531cedac59f \
|
||||
--hash=sha256:b9e921140b797093edfc13ac08dc2a4fd016dd711dc42bb0e1aaf180e48425a7 \
|
||||
--hash=sha256:c5022b94fc330e6d177f3eb38097fb52c7df96ca0e04842c068cf0d9fc38b1e6 \
|
||||
--hash=sha256:cf2b117f2a8d951638efc7592fb72d3eeb2d38cc2194c26ba7f00e7190451d92 \
|
||||
--hash=sha256:d79620b542d9d0e23ae9790ca2fe44f1af40ffad9936efa37bd14954bc3e2818 \
|
||||
--hash=sha256:e2860691c11d10dac7c91bddae44f6211b3da4122d9a2ebb509c2247674d6070 \
|
||||
--hash=sha256:e3a293553715afecf7e10ea02da40593f9d7f48fe48a74fc5dd3ce08a0c46188 \
|
||||
--hash=sha256:e465be3fe7e992e5a6e16731afa6f41cb6ca53afccb4f28ea2fa6457783edf15 \
|
||||
--hash=sha256:e6d27895ef922bc859d969452f247bfbe5345d9aba69b9c8dbe1ea7704f0c5d9
|
||||
pip==18.0 \
|
||||
--hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \
|
||||
--hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76
|
||||
protobuf==3.6.1 \
|
||||
--hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \
|
||||
--hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \
|
||||
--hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \
|
||||
--hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \
|
||||
--hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \
|
||||
--hash=sha256:4b92e235a3afd42e7493b281c8b80c0c65cbef45de30f43d571d1ee40a1f77ef \
|
||||
--hash=sha256:574085a33ca0d2c67433e5f3e9a0965c487410d6cb3406c83bdaf549bfc2992e \
|
||||
--hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \
|
||||
--hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \
|
||||
--hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \
|
||||
@ -106,50 +103,43 @@ protobuf==3.6.1 \
|
||||
--hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10
|
||||
pyaes==1.6.1 \
|
||||
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
|
||||
PySocks==1.6.8 \
|
||||
--hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672
|
||||
QDarkStyle==2.5.4 \
|
||||
--hash=sha256:3eb60922b8c4d9cedecb6897ca4c9f8a259d81bdefe5791976ccdf12432de1f0 \
|
||||
--hash=sha256:51331fc6490b38c376e6ba8d8c814320c8d2d1c2663055bc396321a7c28fa8be
|
||||
qrcode==6.1 \
|
||||
--hash=sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5 \
|
||||
--hash=sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369
|
||||
setuptools==40.8.0 \
|
||||
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \
|
||||
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab
|
||||
six==1.12.0 \
|
||||
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
|
||||
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
|
||||
typing-extensions==3.7.2 \
|
||||
--hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \
|
||||
--hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \
|
||||
--hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71
|
||||
wheel==0.32.3 \
|
||||
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \
|
||||
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44
|
||||
yarl==1.3.0 \
|
||||
--hash=sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9 \
|
||||
--hash=sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f \
|
||||
--hash=sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb \
|
||||
--hash=sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320 \
|
||||
--hash=sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842 \
|
||||
--hash=sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0 \
|
||||
--hash=sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829 \
|
||||
--hash=sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310 \
|
||||
--hash=sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4 \
|
||||
--hash=sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8 \
|
||||
--hash=sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1
|
||||
colorama==0.4.1 \
|
||||
--hash=sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d \
|
||||
--hash=sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48
|
||||
pylibscrypt==1.7.1 \
|
||||
--hash=sha256:7aa9424e211a12106c67ea884ccfe609856289372b900d3702faaf66e87f79ac
|
||||
scrypt==0.8.6 \
|
||||
--hash=sha256:85919f023148cd9fb01d75ad4e3e061928c298fa6249a0cd6cd469c4b947595e \
|
||||
--hash=sha256:4ad7188f2e42dbee2ff1cd72e3da40b170ba41847effbf0d726444f62ae60f3a \
|
||||
--hash=sha256:bc131f74a688fa09993c518ca666a2ebd4268b207e039cbab03a034228140d3e \
|
||||
--hash=sha256:232acdbc3434d2de55def8d5dbf1bc4b9bfc50da7c5741df2a6eebc4e18d3720 \
|
||||
--hash=sha256:971db040d3963ebe4b919a203fe10d7d6659951d3644066314330983dc175ed4 \
|
||||
--hash=sha256:475ac80239b3d788ae71a09c3019ca915e149aaa339adcdd1c9eef121293dc88 \
|
||||
--hash=sha256:18ccbc63d87c6f89b753194194bb37aeaf1abc517e4b989461d115c1d93ce128 \
|
||||
--hash=sha256:c23daecee405cb036845917295c76f8d747fc890158df40cb304b4b3c3640079 \
|
||||
--hash=sha256:f8239b2d47fa1d40bc27efd231dc7083695d10c1c2ac51a99380360741e0362d
|
||||
|
||||
qrcode==6.0 \
|
||||
--hash=sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf \
|
||||
--hash=sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3
|
||||
requests==2.19.1 \
|
||||
--hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
|
||||
--hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
|
||||
setuptools==40.2.0 \
|
||||
--hash=sha256:47881d54ede4da9c15273bac65f9340f8929d4f0213193fa7894be384f2dcfa6 \
|
||||
--hash=sha256:ea3796a48a207b46ea36a9d26de4d0cc87c953a683a7b314ea65d666930ea8e6
|
||||
six==1.11.0 \
|
||||
--hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
|
||||
--hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
|
||||
typing==3.6.6 \
|
||||
--hash=sha256:4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d \
|
||||
--hash=sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4 \
|
||||
--hash=sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a
|
||||
urllib3==1.23 \
|
||||
--hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
|
||||
--hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
|
||||
wheel==0.31.1 \
|
||||
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
|
||||
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
|
||||
yarl==1.2.6 \
|
||||
--hash=sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9 \
|
||||
--hash=sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee \
|
||||
--hash=sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308 \
|
||||
--hash=sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357 \
|
||||
--hash=sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78 \
|
||||
--hash=sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8 \
|
||||
--hash=sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1 \
|
||||
--hash=sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4 \
|
||||
--hash=sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7
|
||||
colorama==0.3.9 \
|
||||
--hash=sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda \
|
||||
--hash=sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Run this after a new release to update dependencies
|
||||
|
||||
set -e
|
||||
|
||||
venv_dir=~/.electrum-venv
|
||||
contrib=$(dirname "$0")
|
||||
|
||||
|
||||
@ -1,26 +1,7 @@
|
||||
#!/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/
|
||||
|
||||
make theming
|
||||
|
||||
if [[ -n "$1" && "$1" == "release" ]] ; then
|
||||
echo -n Keystore Password:
|
||||
read -s password
|
||||
|
||||
@ -24,7 +24,6 @@ string = string.replace("##VERSION_APK##", APK_VERSION)
|
||||
|
||||
files = {
|
||||
'tgz': "Electrum-%s.tar.gz" % version,
|
||||
'appimage': "electrum-%s-x86_64.AppImage" % version,
|
||||
'zip': "Electrum-%s.zip" % version,
|
||||
'mac': "electrum-%s.dmg" % version_mac,
|
||||
'win': "electrum-%s.exe" % version_win,
|
||||
|
||||
@ -3,12 +3,7 @@ import os
|
||||
import subprocess
|
||||
import io
|
||||
import zipfile
|
||||
import sys
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError as e:
|
||||
sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'")
|
||||
import requests
|
||||
|
||||
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||
os.chdir('..')
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
CONTRIB="$(dirname "$0")"
|
||||
test -n "$CONTRIB" -a -d "$CONTRIB" || exit
|
||||
contrib=$(dirname "$0")
|
||||
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
|
||||
python3 -m pip install -r "$CONTRIB"/deterministic-build/requirements.txt -t "$CONTRIB"/../packages
|
||||
pip3 install -r $contrib/deterministic-build/requirements.txt -t $contrib/../packages
|
||||
|
||||
|
||||
@ -1,31 +1 @@
|
||||
#!/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
|
||||
|
||||
(
|
||||
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
|
||||
)
|
||||
python3 setup.py sdist --format=zip,gztar
|
||||
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 59dfc03272751cd29ee311456fa34c40f7ebb7c0
|
||||
@ -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/
|
||||
@ -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}"
|
||||
}
|
||||
@ -1,2 +1,2 @@
|
||||
PyQt5
|
||||
PyQt5<5.11
|
||||
pycryptodomex
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
Cython>=0.27
|
||||
trezor[hidapi]>=0.11.1
|
||||
trezor[hidapi]>=0.9.0
|
||||
safet[hidapi]>=0.1.0
|
||||
keepkey
|
||||
btchip-python>=0.1.26
|
||||
btchip-python
|
||||
ckcc-protocol>=0.7.2
|
||||
websocket-client
|
||||
hidapi
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
pyaes>=0.1a1
|
||||
ecdsa>=0.9
|
||||
requests
|
||||
qrcode
|
||||
protobuf
|
||||
dnspython
|
||||
jsonrpclib-pelix
|
||||
qdarkstyle<2.6
|
||||
aiorpcx>=0.9,<0.11
|
||||
aiohttp>=3.3.0
|
||||
qdarkstyle<3.0
|
||||
aiorpcx>=0.7.1,<0.8
|
||||
aiohttp
|
||||
aiohttp_socks
|
||||
certifi
|
||||
pylibscrypt==1.7.1
|
||||
|
||||
@ -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\"}}"
|
||||
@ -17,7 +17,7 @@ if [ -e ./env/bin/activate ]; then
|
||||
else
|
||||
virtualenv env -p `which python3`
|
||||
source ./env/bin/activate
|
||||
python3 -m pip install .[fast]
|
||||
python3 setup.py install
|
||||
fi
|
||||
|
||||
export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH"
|
||||
|
||||
16
electrum.conf.sample
Normal 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]
|
||||
@ -3,20 +3,19 @@
|
||||
|
||||
[Desktop Entry]
|
||||
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=Bitcoin Wallet
|
||||
Icon=electrum
|
||||
Name[en_US]=Electrum Bitcoin Wallet
|
||||
Name=Electrum Bitcoin Wallet
|
||||
Categories=Finance;Network;
|
||||
StartupNotify=true
|
||||
StartupWMClass=electrum
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/bitcoin;
|
||||
Actions=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
|
||||
|
||||
BIN
electrum.icns
Normal file
@ -12,6 +12,3 @@ from . import daemon
|
||||
from .transaction import Transaction
|
||||
from .plugin import BasePlugin
|
||||
from .commands import Commands, known_commands
|
||||
|
||||
|
||||
__version__ = ELECTRUM_VERSION
|
||||
|
||||
@ -25,22 +25,16 @@ import threading
|
||||
import asyncio
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from . import bitcoin
|
||||
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 .synchronizer import Synchronizer
|
||||
from .verifier import SPV
|
||||
from .blockchain import hash_header
|
||||
from .i18n import _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .storage import WalletStorage
|
||||
from .network import Network
|
||||
|
||||
|
||||
TX_HEIGHT_LOCAL = -2
|
||||
TX_HEIGHT_UNCONF_PARENT = -1
|
||||
TX_HEIGHT_UNCONFIRMED = 0
|
||||
@ -59,27 +53,24 @@ class AddressSynchronizer(PrintError):
|
||||
inherited by wallet
|
||||
"""
|
||||
|
||||
def __init__(self, storage: 'WalletStorage'):
|
||||
def __init__(self, storage):
|
||||
self.storage = storage
|
||||
self.network = None # type: Network
|
||||
# verifier (SPV) and synchronizer are started in start_network
|
||||
self.synchronizer = None # type: Synchronizer
|
||||
self.verifier = None # type: SPV
|
||||
self.network = None
|
||||
# verifier (SPV) and synchronizer are started in start_threads
|
||||
self.synchronizer = None
|
||||
self.verifier = None
|
||||
self.sync_restart_lock = asyncio.Lock()
|
||||
self.group = None
|
||||
# locks: if you need to take multiple ones, acquire them in the order they are defined here!
|
||||
self.lock = threading.RLock()
|
||||
self.transaction_lock = threading.RLock()
|
||||
# address -> list(txid, height)
|
||||
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', {})
|
||||
self.verified_tx = {} # type: Dict[str, TxMinedInfo]
|
||||
for txid, (height, timestamp, txpos, header_hash, flodata) in verified_tx.items():
|
||||
self.verified_tx[txid] = TxMinedInfo(height=height,
|
||||
conf=None,
|
||||
timestamp=timestamp,
|
||||
txpos=txpos,
|
||||
header_hash=header_hash,
|
||||
flodata=flodata)
|
||||
self.verified_tx = {}
|
||||
for txid, (height, timestamp, txpos, header_hash, tx_comment) in verified_tx.items():
|
||||
self.verified_tx[txid] = VerifiedTxInfo(height, timestamp, txpos, header_hash, tx_comment)
|
||||
# Transactions pending verification. txid -> tx_height. Access with self.lock.
|
||||
self.unverified_tx = defaultdict(int)
|
||||
# true when synchronized
|
||||
@ -89,12 +80,6 @@ class AddressSynchronizer(PrintError):
|
||||
|
||||
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):
|
||||
self.load_transactions()
|
||||
self.load_local_history()
|
||||
@ -152,22 +137,46 @@ class AddressSynchronizer(PrintError):
|
||||
# add it in case it was previously unconfirmed
|
||||
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):
|
||||
self.network = network
|
||||
if self.network is not None:
|
||||
self.synchronizer = Synchronizer(self)
|
||||
self.verifier = SPV(self.network, self)
|
||||
self.network.register_callback(self.on_default_server_changed, ['default_server_changed'])
|
||||
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.synchronizer:
|
||||
asyncio.run_coroutine_threadsafe(self.synchronizer.stop(), self.network.asyncio_loop)
|
||||
self.synchronizer = None
|
||||
if self.verifier:
|
||||
asyncio.run_coroutine_threadsafe(self.verifier.stop(), self.network.asyncio_loop)
|
||||
self.verifier = None
|
||||
if self.group:
|
||||
asyncio.run_coroutine_threadsafe(self.group.cancel_remaining(), self.network.asyncio_loop)
|
||||
self.group = None
|
||||
self.storage.put('stored_height', self.get_local_height())
|
||||
if write_to_disk:
|
||||
self.save_transactions()
|
||||
self.save_verified_tx()
|
||||
self.storage.write()
|
||||
@ -179,7 +188,7 @@ class AddressSynchronizer(PrintError):
|
||||
if self.synchronizer:
|
||||
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
|
||||
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
|
||||
@ -198,18 +207,18 @@ class AddressSynchronizer(PrintError):
|
||||
# this outpoint has already been spent, by spending_tx
|
||||
assert spending_tx_hash in self.transactions
|
||||
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
|
||||
if len(conflicting_txns) > 1:
|
||||
raise Exception('Found conflicting transactions already in wallet history.')
|
||||
conflicting_txns -= {tx_hash}
|
||||
conflicting_txns -= {txid}
|
||||
return conflicting_txns
|
||||
|
||||
def add_transaction(self, tx_hash, tx, allow_unrelated=False):
|
||||
assert tx_hash, tx_hash
|
||||
assert tx, tx
|
||||
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
|
||||
# so we need to take that too here, to enforce order of locks
|
||||
with self.lock, self.transaction_lock:
|
||||
@ -234,7 +243,7 @@ class AddressSynchronizer(PrintError):
|
||||
# When this method exits, there must NOT be any conflict, so
|
||||
# either keep this txn and remove all conflicting (along with dependencies)
|
||||
# or drop this txn
|
||||
conflicting_txns = self.get_conflicting_transactions(tx_hash, tx)
|
||||
conflicting_txns = self.get_conflicting_transactions(tx)
|
||||
if conflicting_txns:
|
||||
existing_mempool_txn = any(
|
||||
self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
|
||||
@ -374,13 +383,11 @@ class AddressSynchronizer(PrintError):
|
||||
@profiler
|
||||
def load_transactions(self):
|
||||
# load txi, txo, tx_fees
|
||||
# bookkeeping data of is_mine inputs of transactions
|
||||
self.txi = self.storage.get('txi', {}) # txid -> address -> (prev_outpoint, value)
|
||||
self.txi = self.storage.get('txi', {})
|
||||
for txid, d in list(self.txi.items()):
|
||||
for addr, lst in d.items():
|
||||
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', {}) # txid -> address -> (output_index, value, is_coinbase)
|
||||
self.txo = self.storage.get('txo', {})
|
||||
self.tx_fees = self.storage.get('tx_fees', {})
|
||||
tx_list = self.storage.get('transactions', {})
|
||||
# load transactions
|
||||
@ -404,7 +411,6 @@ class AddressSynchronizer(PrintError):
|
||||
@profiler
|
||||
def load_local_history(self):
|
||||
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):
|
||||
self._add_tx_to_local_history(txid)
|
||||
|
||||
@ -452,11 +458,7 @@ class AddressSynchronizer(PrintError):
|
||||
|
||||
def save_verified_tx(self, write=False):
|
||||
with self.lock:
|
||||
verified_tx_to_save = {}
|
||||
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)
|
||||
self.storage.put('verified_tx3', self.verified_tx)
|
||||
if write:
|
||||
self.storage.write()
|
||||
|
||||
@ -469,7 +471,7 @@ class AddressSynchronizer(PrintError):
|
||||
self.spent_outpoints = defaultdict(dict)
|
||||
self.history = {}
|
||||
self.verified_tx = {}
|
||||
self.transactions = {} # type: Dict[str, Transaction]
|
||||
self.transactions = {}
|
||||
self.save_transactions()
|
||||
|
||||
def get_txpos(self, tx_hash):
|
||||
@ -519,7 +521,8 @@ class AddressSynchronizer(PrintError):
|
||||
delta = tx_deltas[tx_hash]
|
||||
tx_mined_status = self.get_tx_height(tx_hash)
|
||||
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
|
||||
c, u, x = self.get_balance(domain)
|
||||
balance = c + u + x
|
||||
@ -544,7 +547,6 @@ class AddressSynchronizer(PrintError):
|
||||
cur_hist = self._history_local.get(addr, set())
|
||||
cur_hist.add(txid)
|
||||
self._history_local[addr] = cur_hist
|
||||
self._mark_address_history_changed(addr)
|
||||
|
||||
def _remove_tx_from_local_history(self, txid):
|
||||
with self.transaction_lock:
|
||||
@ -557,21 +559,6 @@ class AddressSynchronizer(PrintError):
|
||||
else:
|
||||
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):
|
||||
if tx_hash in self.verified_tx:
|
||||
if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
|
||||
@ -583,20 +570,17 @@ class AddressSynchronizer(PrintError):
|
||||
with self.lock:
|
||||
# tx will be verified only if height > 0
|
||||
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):
|
||||
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):
|
||||
def add_verified_tx(self, tx_hash: str, info: VerifiedTxInfo):
|
||||
# Remove from the unverified map and add to the verified map
|
||||
with self.lock:
|
||||
self.unverified_tx.pop(tx_hash, None)
|
||||
self.verified_tx[tx_hash] = info
|
||||
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):
|
||||
'''Returns a map from tx hash to transaction height'''
|
||||
@ -633,41 +617,40 @@ class AddressSynchronizer(PrintError):
|
||||
return cached_local_height
|
||||
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:
|
||||
if tx_hash in self.verified_tx:
|
||||
info = self.verified_tx[tx_hash]
|
||||
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:
|
||||
height = self.unverified_tx[tx_hash]
|
||||
return TxMinedInfo(height=height, conf=0)
|
||||
return TxMinedStatus(height, 0, None, None)
|
||||
else:
|
||||
# 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):
|
||||
""" Given a transaction, returns flodata """
|
||||
def get_tx_comment(self, tx_hash: str):
|
||||
""" Given a transaction, returns txcomment/floData """
|
||||
with self.lock:
|
||||
if tx_hash in self.verified_tx:
|
||||
info = self.verified_tx[tx_hash]
|
||||
flodata = info[5]
|
||||
return flodata
|
||||
tx_comment = info[4]
|
||||
return tx_comment
|
||||
elif tx_hash in self.unverified_tx:
|
||||
tx = self.transactions.get(tx_hash)
|
||||
flodata = tx.flodata[5:]
|
||||
return flodata
|
||||
tx_comment = tx.txcomment[5:]
|
||||
return tx_comment
|
||||
else:
|
||||
# local transaction
|
||||
tx = self.transactions.get(tx_hash)
|
||||
flodata = tx.flodata[5:]
|
||||
return flodata
|
||||
tx_comment = tx.txcomment[5:]
|
||||
return tx_comment
|
||||
|
||||
def set_up_to_date(self, up_to_date):
|
||||
with self.lock:
|
||||
self.up_to_date = up_to_date
|
||||
if self.network:
|
||||
self.network.notify('status')
|
||||
if up_to_date:
|
||||
self.save_transactions(write=True)
|
||||
# if the verifier is also up to date, persist that too;
|
||||
@ -678,9 +661,8 @@ class AddressSynchronizer(PrintError):
|
||||
def is_up_to_date(self):
|
||||
with self.lock: return self.up_to_date
|
||||
|
||||
@with_transaction_lock
|
||||
def get_tx_delta(self, tx_hash, address):
|
||||
"""effect of tx on address"""
|
||||
"effect of tx on address"
|
||||
delta = 0
|
||||
# substract the value of coins sent from address
|
||||
d = self.txi.get(tx_hash, {}).get(address, [])
|
||||
@ -692,9 +674,8 @@ class AddressSynchronizer(PrintError):
|
||||
delta += v
|
||||
return delta
|
||||
|
||||
@with_transaction_lock
|
||||
def get_tx_value(self, txid):
|
||||
"""effect of tx on the entire domain"""
|
||||
" effect of tx on the entire domain"
|
||||
delta = 0
|
||||
for addr, d in self.txi.get(txid, {}).items():
|
||||
for n, v in d:
|
||||
@ -704,7 +685,7 @@ class AddressSynchronizer(PrintError):
|
||||
delta += v
|
||||
return delta
|
||||
|
||||
def get_wallet_delta(self, tx: Transaction):
|
||||
def get_wallet_delta(self, tx):
|
||||
""" effect of tx on wallet """
|
||||
is_relevant = False # "related to wallet?"
|
||||
is_mine = False
|
||||
@ -731,10 +712,10 @@ class AddressSynchronizer(PrintError):
|
||||
is_partial = True
|
||||
if not is_mine:
|
||||
is_partial = False
|
||||
for o in tx.outputs():
|
||||
v_out += o.value
|
||||
if self.is_mine(o.address):
|
||||
v_out_mine += o.value
|
||||
for addr, value in tx.get_outputs():
|
||||
v_out += value
|
||||
if self.is_mine(addr):
|
||||
v_out_mine += value
|
||||
is_relevant = True
|
||||
if is_pruned:
|
||||
# some inputs are mine:
|
||||
@ -756,23 +737,7 @@ class AddressSynchronizer(PrintError):
|
||||
fee = None
|
||||
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):
|
||||
with self.lock, self.transaction_lock:
|
||||
h = self.get_address_history(address)
|
||||
received = {}
|
||||
sent = {}
|
||||
@ -812,7 +777,7 @@ class AddressSynchronizer(PrintError):
|
||||
|
||||
@with_local_height_cached
|
||||
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
|
||||
"""
|
||||
received, sent = self.get_addr_io(address)
|
||||
@ -833,7 +798,7 @@ class AddressSynchronizer(PrintError):
|
||||
return c, u, x
|
||||
|
||||
@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 = []
|
||||
if domain is None:
|
||||
domain = self.get_addresses()
|
||||
@ -845,8 +810,6 @@ class AddressSynchronizer(PrintError):
|
||||
for x in utxos.values():
|
||||
if confirmed_only and x['height'] <= 0:
|
||||
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():
|
||||
continue
|
||||
coins.append(x)
|
||||
@ -872,6 +835,3 @@ class AddressSynchronizer(PrintError):
|
||||
def is_empty(self, address):
|
||||
c, u, x = self.get_addr_balance(address)
|
||||
return c+u+x == 0
|
||||
|
||||
def synchronize(self):
|
||||
pass
|
||||
|
||||
@ -28,10 +28,10 @@ import sys
|
||||
import os
|
||||
|
||||
from .version import ELECTRUM_VERSION
|
||||
from . import constants
|
||||
from .import constants
|
||||
from .i18n import _
|
||||
from .util import make_aiohttp_session
|
||||
|
||||
from .util import make_aiohttp_session
|
||||
|
||||
class BaseCrashReporter:
|
||||
report_server = "https://crashhub.electrum.org"
|
||||
|
||||
@ -27,24 +27,14 @@ import os
|
||||
import sys
|
||||
import traceback
|
||||
from functools import partial
|
||||
from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any
|
||||
|
||||
from . import bitcoin
|
||||
from . import keystore
|
||||
from .bip32 import is_bip32_derivation, xpub_type
|
||||
from .keystore import bip44_derivation, purpose48_derivation
|
||||
from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet,
|
||||
wallet_types, Wallet, Abstract_Wallet)
|
||||
from .storage import (WalletStorage, STO_EV_USER_PW, STO_EV_XPUB_PW,
|
||||
get_derivation_used_for_hw_device_encryption)
|
||||
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet
|
||||
from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
|
||||
from .i18n import _
|
||||
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
|
||||
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
|
||||
@ -56,21 +46,15 @@ class ScriptTypeNotSupported(Exception): pass
|
||||
class GoBack(Exception): pass
|
||||
|
||||
|
||||
class WizardStackItem(NamedTuple):
|
||||
action: Any
|
||||
args: Any
|
||||
storage_data: dict
|
||||
|
||||
|
||||
class BaseWizard(object):
|
||||
|
||||
def __init__(self, config: SimpleConfig, plugins: Plugins, storage: WalletStorage):
|
||||
def __init__(self, config, plugins, storage):
|
||||
super(BaseWizard, self).__init__()
|
||||
self.config = config
|
||||
self.plugins = plugins
|
||||
self.storage = storage
|
||||
self.wallet = None # type: Abstract_Wallet
|
||||
self._stack = [] # type: List[WizardStackItem]
|
||||
self.wallet = None
|
||||
self.stack = []
|
||||
self.plugin = None
|
||||
self.keystores = []
|
||||
self.is_kivy = config.get('gui') == 'kivy'
|
||||
@ -82,8 +66,7 @@ class BaseWizard(object):
|
||||
def run(self, *args):
|
||||
action = args[0]
|
||||
args = args[1:]
|
||||
storage_data = self.storage.get_all_data()
|
||||
self._stack.append(WizardStackItem(action, args, storage_data))
|
||||
self.stack.append((action, args))
|
||||
if not action:
|
||||
return
|
||||
if type(action) is tuple:
|
||||
@ -98,23 +81,14 @@ class BaseWizard(object):
|
||||
raise Exception("unknown action", action)
|
||||
|
||||
def can_go_back(self):
|
||||
return len(self._stack) > 1
|
||||
return len(self.stack)>1
|
||||
|
||||
def go_back(self):
|
||||
if not self.can_go_back():
|
||||
return
|
||||
# pop 'current' frame
|
||||
self._stack.pop()
|
||||
# pop 'previous' frame
|
||||
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 = []
|
||||
self.stack.pop()
|
||||
action, args = self.stack.pop()
|
||||
self.run(action, *args)
|
||||
|
||||
def new(self):
|
||||
name = os.path.basename(self.storage.path)
|
||||
@ -124,8 +98,9 @@ class BaseWizard(object):
|
||||
])
|
||||
wallet_kinds = [
|
||||
('standard', _("Standard wallet")),
|
||||
('2fa', _("Wallet with two-factor authentication")),
|
||||
('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]
|
||||
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 on_multisig(m, n):
|
||||
multisig_type = "%dof%d" % (m, n)
|
||||
self.storage.put('wallet_type', multisig_type)
|
||||
self.multisig_type = "%dof%d"%(m, n)
|
||||
self.storage.put('wallet_type', self.multisig_type)
|
||||
self.n = n
|
||||
self.run('choose_keystore')
|
||||
self.multisig_dialog(run_next=on_multisig)
|
||||
@ -198,8 +173,8 @@ class BaseWizard(object):
|
||||
|
||||
def import_addresses_or_keys(self):
|
||||
v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x)
|
||||
title = _("Import FLO Addresses")
|
||||
message = _("Enter a list of FLO addresses (this will create a watching-only wallet), or a list of private keys.")
|
||||
title = _("Import Bitcoin Addresses")
|
||||
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,
|
||||
is_valid=v, allow_multi=True, show_wif_help=True)
|
||||
|
||||
@ -208,23 +183,17 @@ class BaseWizard(object):
|
||||
# will be reflected on self.storage
|
||||
if keystore.is_address_list(text):
|
||||
w = Imported_Wallet(self.storage)
|
||||
addresses = text.split()
|
||||
good_inputs, bad_inputs = w.import_addresses(addresses, write_to_disk=False)
|
||||
for x in text.split():
|
||||
w.import_address(x)
|
||||
elif keystore.is_private_key_list(text):
|
||||
k = keystore.Imported_KeyStore({})
|
||||
self.storage.put('keystore', k.dump())
|
||||
w = Imported_Wallet(self.storage)
|
||||
keys = keystore.get_private_keys(text)
|
||||
good_inputs, bad_inputs = w.import_private_keys(keys, None, write_to_disk=False)
|
||||
for x in keystore.get_private_keys(text):
|
||||
w.import_private_key(x, None)
|
||||
self.keystores.append(w.keystore)
|
||||
else:
|
||||
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')
|
||||
|
||||
def restore_from_key(self):
|
||||
@ -247,48 +216,34 @@ class BaseWizard(object):
|
||||
def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET):
|
||||
title = _('Hardware Keystore')
|
||||
# check available plugins
|
||||
supported_plugins = self.plugins.get_hardware_support()
|
||||
devices = [] # type: List[Tuple[str, DeviceInfo]]
|
||||
devmgr = self.plugins.device_manager
|
||||
debug_msg = ''
|
||||
|
||||
def failed_getting_device_infos(name, e):
|
||||
nonlocal debug_msg
|
||||
devmgr.print_error(f'error getting device infos for {name}: {e}')
|
||||
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
|
||||
debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n'
|
||||
|
||||
support = self.plugins.get_hardware_support()
|
||||
if not support:
|
||||
msg = '\n'.join([
|
||||
_('No hardware wallet support found on your system.'),
|
||||
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
|
||||
])
|
||||
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
|
||||
return
|
||||
# scan devices
|
||||
devices = []
|
||||
devmgr = self.plugins.device_manager
|
||||
try:
|
||||
scanned_devices = devmgr.scan_devices()
|
||||
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)
|
||||
else:
|
||||
for splugin in supported_plugins:
|
||||
name, plugin = splugin.name, splugin.plugin
|
||||
# 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'
|
||||
debug_msg = ''
|
||||
for name, description, plugin in support:
|
||||
try:
|
||||
# FIXME: side-effect: unpaired_device_info sets client.handler
|
||||
device_infos = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices,
|
||||
include_failing_clients=True)
|
||||
u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices)
|
||||
except BaseException as e:
|
||||
traceback.print_exc()
|
||||
failed_getting_device_infos(name, e)
|
||||
devmgr.print_error('error getting device infos for {}: {}'.format(name, e))
|
||||
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
|
||||
debug_msg += ' {}:\n{}\n'.format(plugin.name, indented_error_msg)
|
||||
continue
|
||||
device_infos_failing = list(filter(lambda di: di.exception is not None, device_infos))
|
||||
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))
|
||||
devices += list(map(lambda x: (name, x), u))
|
||||
if not debug_msg:
|
||||
debug_msg = ' {}'.format(_('No exceptions encountered.'))
|
||||
if not devices:
|
||||
@ -308,9 +263,7 @@ class BaseWizard(object):
|
||||
for name, info in devices:
|
||||
state = _("initialized") if info.initialized else _("wiped")
|
||||
label = info.label or _("An unnamed {}").format(name)
|
||||
try: transport_str = info.device.transport_ui_string[:20]
|
||||
except: transport_str = 'unknown transport'
|
||||
descr = f"{label} [{name}, {state}, {transport_str}]"
|
||||
descr = "%s [%s, %s]" % (label, name, state)
|
||||
choices.append(((name, info), descr))
|
||||
msg = _('Select a device') + ':'
|
||||
self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose))
|
||||
@ -366,14 +319,12 @@ class BaseWizard(object):
|
||||
# There is no general standard for HD multisig.
|
||||
# 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.
|
||||
default_choice_idx = 2
|
||||
choices = [
|
||||
('standard', 'legacy multisig (p2sh)', "m/45'/0"),
|
||||
('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')),
|
||||
('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')),
|
||||
]
|
||||
else:
|
||||
default_choice_idx = 2
|
||||
choices = [
|
||||
('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)),
|
||||
('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)),
|
||||
@ -383,8 +334,7 @@ class BaseWizard(object):
|
||||
try:
|
||||
self.choice_and_line_dialog(
|
||||
run_next=f, title=_('Script type and Derivation path'), message1=message1,
|
||||
message2=message2, choices=choices, test_text=is_bip32_derivation,
|
||||
default_choice_idx=default_choice_idx)
|
||||
message2=message2, choices=choices, test_text=bitcoin.is_bip32_derivation)
|
||||
return
|
||||
except ScriptTypeNotSupported as 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('')
|
||||
elif self.seed_type == 'old':
|
||||
self.run('create_keystore', seed, '')
|
||||
elif bitcoin.is_any_2fa_seed_type(self.seed_type):
|
||||
elif self.seed_type == '2fa':
|
||||
self.load_2fa()
|
||||
self.run('on_restore_seed', seed, is_ext)
|
||||
else:
|
||||
@ -464,6 +414,7 @@ class BaseWizard(object):
|
||||
def on_keystore(self, k):
|
||||
has_xpub = isinstance(k, keystore.Xpub)
|
||||
if has_xpub:
|
||||
from .bitcoin import xpub_type
|
||||
t1 = xpub_type(k.xpub)
|
||||
if self.wallet_type == 'standard':
|
||||
if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
|
||||
@ -491,7 +442,7 @@ class BaseWizard(object):
|
||||
self.keystores.append(k)
|
||||
if len(self.keystores) == 1:
|
||||
xpub = k.get_master_public_key()
|
||||
self.reset_stack()
|
||||
self.stack = []
|
||||
self.run('show_xpub_and_add_cosigners', xpub)
|
||||
elif len(self.keystores) < self.n:
|
||||
self.run('choose_keystore')
|
||||
@ -535,7 +486,6 @@ class BaseWizard(object):
|
||||
|
||||
def on_password(self, password, *, encrypt_storage,
|
||||
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)
|
||||
if encrypt_storage:
|
||||
self.storage.set_password(password, enc_version=storage_enc_version)
|
||||
@ -565,15 +515,17 @@ class BaseWizard(object):
|
||||
def show_xpub_and_add_cosigners(self, xpub):
|
||||
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')
|
||||
if message is None:
|
||||
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."),
|
||||
_("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.")
|
||||
])
|
||||
if choices is None:
|
||||
choices = [
|
||||
('create_standard_seed', _('Legacy')),
|
||||
('create_standard_seed', _('Standard')),
|
||||
('create_segwit_seed', _('Segwit')),
|
||||
]
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
||||
|
||||
|
||||
@ -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
|
||||
@ -24,17 +24,14 @@
|
||||
# SOFTWARE.
|
||||
|
||||
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 segwit_addr
|
||||
from . import constants
|
||||
from . import ecc
|
||||
from .crypto import sha256d, sha256, hash_160, hmac_oneshot
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .network import Network
|
||||
from .crypto import Hash, sha256, hash_160, hmac_oneshot
|
||||
|
||||
|
||||
################################## transactions
|
||||
@ -49,7 +46,7 @@ TYPE_PUBKEY = 1
|
||||
TYPE_SCRIPT = 2
|
||||
|
||||
|
||||
def rev_hex(s: str) -> str:
|
||||
def rev_hex(s):
|
||||
return bh2u(bfh(s)[::-1])
|
||||
|
||||
|
||||
@ -60,7 +57,7 @@ def int_to_hex(i: int, length: int=1) -> str:
|
||||
if not isinstance(i, int):
|
||||
raise TypeError('{} instead of int'.format(i))
|
||||
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))
|
||||
if i < 0:
|
||||
# two's complement
|
||||
@ -150,37 +147,19 @@ def add_number_to_script(i: int) -> bytes:
|
||||
return bfh(push_script(script_num_to_hex(i)))
|
||||
|
||||
|
||||
def relayfee(network: 'Network'=None) -> int:
|
||||
from .simple_config import FEERATE_DEFAULT_RELAY
|
||||
MAX_RELAY_FEE = 50000
|
||||
f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY
|
||||
return min(f, MAX_RELAY_FEE)
|
||||
hash_encode = lambda x: bh2u(x[::-1])
|
||||
hash_decode = lambda x: bfh(x)[::-1]
|
||||
hmac_sha_512 = lambda x, y: hmac_oneshot(x, y, hashlib.sha512)
|
||||
|
||||
|
||||
def dust_threshold(network: 'Network'=None) -> int:
|
||||
# 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:
|
||||
def is_new_seed(x, prefix=version.SEED_PREFIX):
|
||||
from . import mnemonic
|
||||
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)
|
||||
|
||||
|
||||
def is_old_seed(seed: str) -> bool:
|
||||
def is_old_seed(seed):
|
||||
from . import old_mnemonic, mnemonic
|
||||
seed = mnemonic.normalize_text(seed)
|
||||
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))
|
||||
|
||||
|
||||
def seed_type(x: str) -> str:
|
||||
def seed_type(x):
|
||||
if is_old_seed(x):
|
||||
return 'old'
|
||||
elif is_new_seed(x):
|
||||
@ -207,130 +186,119 @@ def seed_type(x: str) -> str:
|
||||
return 'segwit'
|
||||
elif is_new_seed(x, version.SEED_PREFIX_2FA):
|
||||
return '2fa'
|
||||
elif is_new_seed(x, version.SEED_PREFIX_2FA_SW):
|
||||
return '2fa_segwit'
|
||||
return ''
|
||||
|
||||
|
||||
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']
|
||||
is_seed = lambda x: bool(seed_type(x))
|
||||
|
||||
|
||||
############ functions from pywallet #####################
|
||||
|
||||
def hash160_to_b58_address(h160: bytes, addrtype: int) -> str:
|
||||
s = bytes([addrtype]) + h160
|
||||
s = s + sha256d(s)[0:4]
|
||||
return base_encode(s, base=58)
|
||||
def hash160_to_b58_address(h160: bytes, addrtype):
|
||||
s = bytes([addrtype])
|
||||
s += h160
|
||||
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')
|
||||
_bytes = base_decode(addr, 25, base=58)
|
||||
return _bytes[0], _bytes[1:21]
|
||||
|
||||
|
||||
def hash160_to_p2pkh(h160: bytes, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
def hash160_to_p2pkh(h160, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH)
|
||||
|
||||
def hash160_to_p2sh(h160: bytes, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
def hash160_to_p2sh(h160, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH)
|
||||
|
||||
def public_key_to_p2pkh(public_key: bytes, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return hash160_to_p2pkh(hash_160(public_key), net=net)
|
||||
def public_key_to_p2pkh(public_key: bytes) -> str:
|
||||
return hash160_to_p2pkh(hash_160(public_key))
|
||||
|
||||
def hash_to_segwit_addr(h: bytes, witver: int, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
def hash_to_segwit_addr(h, witver, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
return segwit_addr.encode(net.SEGWIT_HRP, witver, h)
|
||||
|
||||
def public_key_to_p2wpkh(public_key: bytes, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return hash_to_segwit_addr(hash_160(public_key), witver=0, net=net)
|
||||
def public_key_to_p2wpkh(public_key):
|
||||
return hash_to_segwit_addr(hash_160(public_key), witver=0)
|
||||
|
||||
def script_to_p2wsh(script: str, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return hash_to_segwit_addr(sha256(bfh(script)), witver=0, net=net)
|
||||
def script_to_p2wsh(script):
|
||||
return hash_to_segwit_addr(sha256(bfh(script)), witver=0)
|
||||
|
||||
def p2wpkh_nested_script(pubkey: str) -> str:
|
||||
def p2wpkh_nested_script(pubkey):
|
||||
pkh = bh2u(hash_160(bfh(pubkey)))
|
||||
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)))
|
||||
return '00' + push_script(wsh)
|
||||
|
||||
def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
def pubkey_to_address(txin_type, pubkey):
|
||||
if txin_type == 'p2pkh':
|
||||
return public_key_to_p2pkh(bfh(pubkey), net=net)
|
||||
return public_key_to_p2pkh(bfh(pubkey))
|
||||
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':
|
||||
scriptSig = p2wpkh_nested_script(pubkey)
|
||||
return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net)
|
||||
return hash160_to_p2sh(hash_160(bfh(scriptSig)))
|
||||
else:
|
||||
raise NotImplementedError(txin_type)
|
||||
|
||||
def redeem_script_to_address(txin_type: str, redeem_script: str, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
def redeem_script_to_address(txin_type, redeem_script):
|
||||
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':
|
||||
return script_to_p2wsh(redeem_script, net=net)
|
||||
return script_to_p2wsh(redeem_script)
|
||||
elif txin_type == 'p2wsh-p2sh':
|
||||
scriptSig = p2wsh_nested_script(redeem_script)
|
||||
return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net)
|
||||
return hash160_to_p2sh(hash_160(bfh(scriptSig)))
|
||||
else:
|
||||
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
|
||||
t, addr = get_address_from_output_script(bfh(script), net=net)
|
||||
assert t == TYPE_ADDRESS
|
||||
return addr
|
||||
|
||||
def address_to_script(addr: str, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
if not is_address(addr, net=net):
|
||||
raise BitcoinException(f"invalid bitcoin address: {addr}")
|
||||
def address_to_script(addr, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr)
|
||||
if witprog is not None:
|
||||
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
|
||||
script = bh2u(bytes([OP_n]))
|
||||
script += push_script(bh2u(bytes(witprog)))
|
||||
return script
|
||||
addrtype, hash_160_ = b58_address_to_hash160(addr)
|
||||
addrtype, hash_160 = b58_address_to_hash160(addr)
|
||||
if addrtype == net.ADDRTYPE_P2PKH:
|
||||
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
|
||||
elif addrtype == net.ADDRTYPE_P2SH:
|
||||
script = 'a9' # op_hash_160
|
||||
script += push_script(bh2u(hash_160_))
|
||||
script += push_script(bh2u(hash_160))
|
||||
script += '87' # op_equal
|
||||
else:
|
||||
raise BitcoinException(f'unknown address type: {addrtype}')
|
||||
raise BitcoinException('unknown address type: {}'.format(addrtype))
|
||||
return script
|
||||
|
||||
def address_to_scripthash(addr: str) -> str:
|
||||
def address_to_scripthash(addr):
|
||||
script = address_to_script(addr)
|
||||
return script_to_scripthash(script)
|
||||
|
||||
def script_to_scripthash(script: str) -> str:
|
||||
h = sha256(bfh(script))[0:32]
|
||||
def script_to_scripthash(script):
|
||||
h = sha256(bytes.fromhex(script))[0:32]
|
||||
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 += 'ac' # op_checksig
|
||||
return script
|
||||
@ -372,7 +340,7 @@ def base_encode(v: bytes, base: int) -> str:
|
||||
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."""
|
||||
# assert_bytes(v)
|
||||
v = to_bytes(v, 'ascii')
|
||||
@ -410,20 +378,21 @@ class InvalidChecksum(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def EncodeBase58Check(vchIn: bytes) -> str:
|
||||
hash = sha256d(vchIn)
|
||||
def EncodeBase58Check(vchIn):
|
||||
hash = Hash(vchIn)
|
||||
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)
|
||||
payload = vchRet[0:-4]
|
||||
csum_found = vchRet[-4:]
|
||||
csum_calculated = sha256d(payload)[0:4]
|
||||
if csum_calculated != csum_found:
|
||||
raise InvalidChecksum(f'calculated {bh2u(csum_calculated)}, found {bh2u(csum_found)}')
|
||||
key = vchRet[0:-4]
|
||||
csum = vchRet[-4:]
|
||||
hash = Hash(key)
|
||||
cs32 = hash[0:4]
|
||||
if cs32 != csum:
|
||||
raise InvalidChecksum('expected {}, actual {}'.format(bh2u(cs32), bh2u(csum)))
|
||||
else:
|
||||
return payload
|
||||
return key
|
||||
|
||||
|
||||
# backwards compat
|
||||
@ -440,6 +409,12 @@ 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,
|
||||
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)
|
||||
|
||||
|
||||
def deserialize_privkey(key: str) -> Tuple[str, bytes, bool]:
|
||||
def deserialize_privkey(key: str) -> (str, bytes, bool):
|
||||
if is_minikey(key):
|
||||
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
|
||||
|
||||
|
||||
def is_compressed_privkey(sec: str) -> bool:
|
||||
def is_compressed(sec):
|
||||
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)
|
||||
public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
|
||||
return pubkey_to_address(txin_type, public_key)
|
||||
|
||||
def is_segwit_address(addr: str, *, net=None) -> bool:
|
||||
if net is None: net = constants.net
|
||||
def is_segwit_address(addr):
|
||||
try:
|
||||
witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr)
|
||||
witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
|
||||
except Exception as e:
|
||||
return False
|
||||
return witprog is not None
|
||||
|
||||
def is_b58_address(addr: str, *, net=None) -> bool:
|
||||
if net is None: net = constants.net
|
||||
def is_b58_address(addr):
|
||||
try:
|
||||
addrtype, h = b58_address_to_hash160(addr)
|
||||
except Exception as e:
|
||||
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 addr == hash160_to_b58_address(h, addrtype)
|
||||
|
||||
def is_address(addr: str, *, net=None) -> bool:
|
||||
if net is None: net = constants.net
|
||||
return is_segwit_address(addr, net=net) \
|
||||
or is_b58_address(addr, net=net)
|
||||
def is_address(addr):
|
||||
return is_segwit_address(addr) or is_b58_address(addr)
|
||||
|
||||
|
||||
def is_private_key(key: str) -> bool:
|
||||
def is_private_key(key):
|
||||
try:
|
||||
k = deserialize_privkey(key)
|
||||
return k is not False
|
||||
@ -538,7 +509,7 @@ def is_private_key(key: str) -> bool:
|
||||
|
||||
########### end pywallet functions #######################
|
||||
|
||||
def is_minikey(text: str) -> bool:
|
||||
def is_minikey(text):
|
||||
# Minikeys are typically 22 or 30 characters, but this routine
|
||||
# permits any length of 20 or more provided the minikey is valid.
|
||||
# A valid minikey must begin with an 'S', be in base58, and when
|
||||
@ -548,5 +519,264 @@ def is_minikey(text: str) -> bool:
|
||||
and all(ord(c) in __b58chars for c in text)
|
||||
and sha256(text + '?')[0] == 0x00)
|
||||
|
||||
def minikey_to_private_key(text: str) -> bytes:
|
||||
def minikey_to_private_key(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
|
||||
|
||||
@ -22,47 +22,45 @@
|
||||
# SOFTWARE.
|
||||
import os
|
||||
import threading
|
||||
from typing import Optional, Dict
|
||||
|
||||
from . import util
|
||||
from .bitcoin import hash_encode, int_to_hex, rev_hex
|
||||
from .crypto import sha256d
|
||||
from .bitcoin import Hash, hash_encode, int_to_hex, rev_hex
|
||||
from . import constants
|
||||
from .util import bfh, bh2u
|
||||
from .simple_config import SimpleConfig
|
||||
|
||||
|
||||
try:
|
||||
import pylibscrypt
|
||||
getPoWHash = lambda x: pylibscrypt.scrypt(password=x, salt=x, N=1024, r=1, p=1, olen=32)
|
||||
import scrypt
|
||||
getPoWHash = lambda x: scrypt.hash(x, x, N=1024, r=1, p=1, buflen=32)
|
||||
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
|
||||
|
||||
|
||||
|
||||
class MissingHeader(Exception):
|
||||
pass
|
||||
|
||||
class InvalidHeader(Exception):
|
||||
pass
|
||||
|
||||
def serialize_header(header_dict: dict) -> str:
|
||||
s = int_to_hex(header_dict['version'], 4) \
|
||||
+ rev_hex(header_dict['prev_block_hash']) \
|
||||
+ rev_hex(header_dict['merkle_root']) \
|
||||
+ int_to_hex(int(header_dict['timestamp']), 4) \
|
||||
+ int_to_hex(int(header_dict['bits']), 4) \
|
||||
+ int_to_hex(int(header_dict['nonce']), 4)
|
||||
def serialize_header(res):
|
||||
s = int_to_hex(res.get('version'), 4) \
|
||||
+ rev_hex(res.get('prev_block_hash')) \
|
||||
+ rev_hex(res.get('merkle_root')) \
|
||||
+ int_to_hex(int(res.get('timestamp')), 4) \
|
||||
+ int_to_hex(int(res.get('bits')), 4) \
|
||||
+ int_to_hex(int(res.get('nonce')), 4)
|
||||
return s
|
||||
|
||||
def deserialize_header(s: bytes, height: int) -> dict:
|
||||
def deserialize_header(s, height):
|
||||
if not s:
|
||||
raise InvalidHeader('Invalid header: {}'.format(s))
|
||||
if len(s) != HEADER_SIZE:
|
||||
if len(s) != 80:
|
||||
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['version'] = hex_to_int(s[0:4])
|
||||
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
|
||||
return h
|
||||
|
||||
def hash_header(header: dict) -> str:
|
||||
def hash_header(header):
|
||||
if header is None:
|
||||
return '0' * 64
|
||||
if header.get('prev_block_hash') is None:
|
||||
header['prev_block_hash'] = '00'*32
|
||||
return hash_raw_header(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)
|
||||
|
||||
return hash_encode(Hash(bfh(serialize_header(header))))
|
||||
|
||||
def pow_hash_header(header):
|
||||
return hash_encode(getPoWHash(bfh(serialize_header(header))))
|
||||
|
||||
blockchains = {}
|
||||
|
||||
def get_best_chain() -> 'Blockchain':
|
||||
return blockchains[constants.net.GENESIS]
|
||||
def read_blockchains(config):
|
||||
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
|
||||
_CHAINWORK_CACHE = {
|
||||
"0000000000000000000000000000000000000000000000000000000000000000": 0, # virtual block at height -1
|
||||
} # type: Dict[str, int]
|
||||
def check_header(header):
|
||||
if type(header) is not dict:
|
||||
return False
|
||||
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):
|
||||
@ -171,19 +120,15 @@ class Blockchain(util.PrintError):
|
||||
Manages blockchain headers and their verification
|
||||
"""
|
||||
|
||||
def __init__(self, config: SimpleConfig, forkpoint: int, parent: Optional['Blockchain'],
|
||||
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}")
|
||||
def __init__(self, config, forkpoint, parent_id):
|
||||
self.config = config
|
||||
self.forkpoint = forkpoint # height of first header
|
||||
self.parent = parent
|
||||
self._forkpoint_hash = forkpoint_hash # blockhash at forkpoint. "first hash"
|
||||
self._prev_hash = prev_hash # blockhash immediately before forkpoint
|
||||
self.catch_up = None # interface catching up
|
||||
self.forkpoint = forkpoint
|
||||
self.checkpoints = constants.net.CHECKPOINTS
|
||||
self.parent_id = parent_id
|
||||
assert parent_id != forkpoint
|
||||
self.lock = threading.RLock()
|
||||
with self.lock:
|
||||
self.update_size()
|
||||
|
||||
def with_lock(func):
|
||||
@ -192,80 +137,50 @@ class Blockchain(util.PrintError):
|
||||
return func(self, *args, **kwargs)
|
||||
return func_wrapper
|
||||
|
||||
@property
|
||||
def checkpoints(self):
|
||||
return constants.net.CHECKPOINTS
|
||||
def parent(self):
|
||||
return blockchains[self.parent_id]
|
||||
|
||||
def get_max_child(self) -> Optional[int]:
|
||||
with blockchains_lock: chains = list(blockchains.values())
|
||||
children = list(filter(lambda y: y.parent==self, chains))
|
||||
def get_max_child(self):
|
||||
children = list(filter(lambda y: y.parent_id==self.forkpoint, blockchains.values()))
|
||||
return max([x.forkpoint for x in children]) if children else None
|
||||
|
||||
def get_max_forkpoint(self) -> int:
|
||||
"""Returns the max height where there is a fork
|
||||
related to this chain.
|
||||
"""
|
||||
def get_forkpoint(self):
|
||||
mc = self.get_max_child()
|
||||
return mc if mc is not None else self.forkpoint
|
||||
|
||||
@with_lock
|
||||
def get_branch_size(self) -> int:
|
||||
return self.height() - self.get_max_forkpoint() + 1
|
||||
def get_branch_size(self):
|
||||
return self.height() - self.get_forkpoint() + 1
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10]
|
||||
def get_name(self):
|
||||
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)
|
||||
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:
|
||||
return header_hash == self.get_hash(height)
|
||||
except Exception:
|
||||
except MissingHeader:
|
||||
return False
|
||||
|
||||
def fork(parent, header: dict) -> 'Blockchain':
|
||||
if not parent.can_connect(header, check_height=False):
|
||||
raise Exception("forking header does not connect to parent chain")
|
||||
def fork(parent, header):
|
||||
forkpoint = header.get('block_height')
|
||||
self = Blockchain(config=parent.config,
|
||||
forkpoint=forkpoint,
|
||||
parent=parent,
|
||||
forkpoint_hash=hash_header(header),
|
||||
prev_hash=parent.get_hash(forkpoint-1))
|
||||
self = Blockchain(parent.config, forkpoint, parent.forkpoint)
|
||||
open(self.path(), 'w+').close()
|
||||
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
|
||||
|
||||
@with_lock
|
||||
def height(self) -> int:
|
||||
def height(self):
|
||||
return self.forkpoint + self.size() - 1
|
||||
|
||||
@with_lock
|
||||
def size(self) -> int:
|
||||
def size(self):
|
||||
with self.lock:
|
||||
return self._size
|
||||
|
||||
@with_lock
|
||||
def update_size(self) -> None:
|
||||
def update_size(self):
|
||||
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):
|
||||
# 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:
|
||||
def verify_header(self, header, prev_hash, target, expected_header_hash=None):
|
||||
_hash = hash_header(header)
|
||||
_powhash = pow_hash_header(header)
|
||||
if expected_header_hash and expected_header_hash != _hash:
|
||||
@ -274,155 +189,98 @@ class Blockchain(util.PrintError):
|
||||
raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
|
||||
if constants.net.TESTNET:
|
||||
return
|
||||
bits = cls.target_to_bits(target)
|
||||
#print("I'm inside verify_header")
|
||||
#bits = self.target_to_bits(target)
|
||||
bits = target
|
||||
if 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')
|
||||
target_val = cls.bits_to_target(bits)
|
||||
block_hash = int('0x' + _hash, 16)
|
||||
target_val = self.bits_to_target(bits)
|
||||
if int('0x' + _powhash, 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:
|
||||
num = len(data) // HEADER_SIZE
|
||||
current_header = index * 2016
|
||||
print('chunk ' + str(index))
|
||||
def verify_chunk(self, index, data):
|
||||
num = len(data) // 80
|
||||
current_header = (index * 2016)
|
||||
# last = (index * 2016 + 2015)
|
||||
print(index*2016)
|
||||
prev_hash = self.get_hash(current_header - 1)
|
||||
headerLast = None
|
||||
headerFirst = None
|
||||
capture = None
|
||||
lst = []
|
||||
for i in range(num):
|
||||
averaging_interval = self.AveragingInterval(current_header)
|
||||
difficulty_interval = self.DifficultyAdjustmentInterval(current_header)
|
||||
if current_header < 426000:
|
||||
target = self.get_target(current_header - 1, headerLast, headerFirst)
|
||||
target = self.get_target(current_header - 1)
|
||||
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]
|
||||
raw_header = data[i*80:(i+1) * 80]
|
||||
header = deserialize_header(raw_header, current_header)
|
||||
print(i)
|
||||
self.verify_header(header, prev_hash, target, expected_header_hash)
|
||||
self.save_chunk_part(header)
|
||||
prev_hash = hash_header(header)
|
||||
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):
|
||||
d = util.get_headers_dir(self.config)
|
||||
if self.parent is None:
|
||||
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)
|
||||
filename = 'blockchain_headers' if self.parent_id is None else os.path.join('forks', 'fork_%d_%d'%(self.parent_id, self.forkpoint))
|
||||
return os.path.join(d, filename)
|
||||
|
||||
@with_lock
|
||||
def save_chunk(self, index: int, chunk: bytes):
|
||||
assert index >= 0, index
|
||||
chunk_within_checkpoint_region = index < len(self.checkpoints)
|
||||
def save_chunk(self, index, chunk):
|
||||
#chunk_within_checkpoint_region = index < len(self.checkpoints)
|
||||
# chunks in checkpoint region are the responsibility of the 'main chain'
|
||||
if chunk_within_checkpoint_region and self.parent is not None:
|
||||
main_chain = get_best_chain()
|
||||
main_chain.save_chunk(index, chunk)
|
||||
return
|
||||
#if chunk_within_checkpoint_region and self.parent_id is not None:
|
||||
# main_chain = blockchains[0]
|
||||
# main_chain.save_chunk(index, chunk)
|
||||
# return
|
||||
|
||||
delta_height = (index * 2016 - self.forkpoint)
|
||||
delta_bytes = delta_height * HEADER_SIZE
|
||||
#delta_height = (index * 2016 - self.forkpoint)
|
||||
#delta_bytes = delta_height * 80
|
||||
# if this chunk contains our forkpoint, only save the part after forkpoint
|
||||
# (the part before is the responsibility of the parent)
|
||||
if delta_bytes < 0:
|
||||
chunk = chunk[-delta_bytes:]
|
||||
delta_bytes = 0
|
||||
truncate = not chunk_within_checkpoint_region
|
||||
self.write(chunk, delta_bytes, truncate)
|
||||
#if delta_bytes < 0:
|
||||
# chunk = chunk[-delta_bytes:]
|
||||
# delta_bytes = 0
|
||||
#truncate = not chunk_within_checkpoint_region
|
||||
#self.write(chunk, delta_bytes, truncate)
|
||||
self.swap_with_parent()
|
||||
|
||||
def swap_with_parent(self) -> None:
|
||||
parent_lock = self.parent.lock if self.parent is not None else threading.Lock()
|
||||
with parent_lock, self.lock, blockchains_lock: # this order should not deadlock
|
||||
# do the swap; possibly multiple ones
|
||||
cnt = 0
|
||||
while self._swap_with_parent():
|
||||
cnt += 1
|
||||
if cnt > len(blockchains): # make sure we are making progress
|
||||
raise Exception(f'swapping fork with parent too many times: {cnt}')
|
||||
|
||||
def _swap_with_parent(self) -> bool:
|
||||
"""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)
|
||||
@with_lock
|
||||
def swap_with_parent(self):
|
||||
if self.parent_id is None:
|
||||
return
|
||||
parent_branch_size = self.parent().height() - self.forkpoint + 1
|
||||
if parent_branch_size >= self.size():
|
||||
return
|
||||
self.print_error("swap", self.forkpoint, self.parent_id)
|
||||
parent_id = self.parent_id
|
||||
forkpoint = self.forkpoint
|
||||
parent = self.parent()
|
||||
self.assert_headers_file_available(self.path())
|
||||
child_old_name = self.path()
|
||||
with open(self.path(), 'rb') as f:
|
||||
my_data = f.read()
|
||||
self.assert_headers_file_available(parent.path())
|
||||
with open(parent.path(), 'rb') as f:
|
||||
f.seek((forkpoint - parent.forkpoint)*HEADER_SIZE)
|
||||
parent_data = f.read(parent_branch_size*HEADER_SIZE)
|
||||
f.seek((forkpoint - parent.forkpoint)*80)
|
||||
parent_data = f.read(parent_branch_size*80)
|
||||
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
|
||||
self.parent, parent.parent = parent.parent, self # type: Optional[Blockchain], Optional[Blockchain]
|
||||
self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint
|
||||
self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE]))
|
||||
self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash
|
||||
# parent's new name
|
||||
os.replace(child_old_name, parent.path())
|
||||
self.update_size()
|
||||
parent.update_size()
|
||||
self.parent_id = parent.parent_id; parent.parent_id = parent_id
|
||||
self.forkpoint = parent.forkpoint; parent.forkpoint = forkpoint
|
||||
self._size = parent._size; parent._size = parent_branch_size
|
||||
# move files
|
||||
for b in blockchains.values():
|
||||
if b in [self, parent]: continue
|
||||
if b.old_path != b.path():
|
||||
self.print_error("renaming", b.old_path, b.path())
|
||||
os.rename(b.old_path, b.path())
|
||||
# update pointers
|
||||
blockchains.pop(child_old_id, None)
|
||||
blockchains.pop(parent_old_id, None)
|
||||
blockchains[self.get_id()] = self
|
||||
blockchains[parent.get_id()] = parent
|
||||
return True
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self._forkpoint_hash
|
||||
blockchains[self.forkpoint] = self
|
||||
blockchains[parent.forkpoint] = parent
|
||||
|
||||
def assert_headers_file_available(self, path):
|
||||
if os.path.exists(path):
|
||||
@ -432,12 +290,12 @@ class Blockchain(util.PrintError):
|
||||
else:
|
||||
raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path))
|
||||
|
||||
@with_lock
|
||||
def write(self, data: bytes, offset: int, truncate: bool=True) -> None:
|
||||
def write(self, data, offset, truncate=True):
|
||||
filename = self.path()
|
||||
with self.lock:
|
||||
self.assert_headers_file_available(filename)
|
||||
with open(filename, 'rb+') as f:
|
||||
if truncate and offset != self._size * HEADER_SIZE:
|
||||
if truncate and offset != self._size*80:
|
||||
f.seek(offset)
|
||||
f.truncate()
|
||||
f.seek(offset)
|
||||
@ -447,41 +305,36 @@ class Blockchain(util.PrintError):
|
||||
self.update_size()
|
||||
|
||||
@with_lock
|
||||
def save_header(self, header: dict) -> None:
|
||||
def save_header(self, header):
|
||||
delta = header.get('block_height') - self.forkpoint
|
||||
data = bfh(serialize_header(header))
|
||||
# headers are only _appended_ to the end:
|
||||
assert delta == self.size(), (delta, self.size())
|
||||
assert len(data) == HEADER_SIZE
|
||||
self.write(data, delta*HEADER_SIZE)
|
||||
assert delta == self.size()
|
||||
assert len(data) == 80
|
||||
self.write(data, delta*80)
|
||||
self.swap_with_parent()
|
||||
|
||||
@with_lock
|
||||
def read_header(self, height: int) -> Optional[dict]:
|
||||
def read_header(self, height):
|
||||
assert self.parent_id != self.forkpoint
|
||||
if height < 0:
|
||||
return
|
||||
if height < self.forkpoint:
|
||||
return self.parent.read_header(height)
|
||||
return self.parent().read_header(height)
|
||||
if height > self.height():
|
||||
return
|
||||
delta = height - self.forkpoint
|
||||
name = self.path()
|
||||
self.assert_headers_file_available(name)
|
||||
with open(name, 'rb') as f:
|
||||
f.seek(delta * HEADER_SIZE)
|
||||
h = f.read(HEADER_SIZE)
|
||||
if len(h) < HEADER_SIZE:
|
||||
f.seek(delta * 80)
|
||||
h = f.read(80)
|
||||
if len(h) < 80:
|
||||
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 deserialize_header(h, height)
|
||||
|
||||
def header_at_tip(self) -> Optional[dict]:
|
||||
"""Return latest header."""
|
||||
height = self.height()
|
||||
return self.read_header(height)
|
||||
|
||||
def get_hash(self, height: int) -> str:
|
||||
def get_hash(self, height):
|
||||
def is_height_checkpoint():
|
||||
within_cp_range = height <= constants.net.max_checkpoint()
|
||||
at_chunk_boundary = (height+1) % 2016 == 0
|
||||
@ -501,7 +354,7 @@ class Blockchain(util.PrintError):
|
||||
raise MissingHeader(height)
|
||||
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
|
||||
if constants.net.TESTNET:
|
||||
return 0
|
||||
@ -512,13 +365,11 @@ class Blockchain(util.PrintError):
|
||||
h, t = self.checkpoints[index]
|
||||
return t
|
||||
# new target
|
||||
if headerLast is None:
|
||||
headerLast = self.read_header(index)
|
||||
height = headerLast["block_height"]
|
||||
# check if the height passes is in range for retargeting
|
||||
if (height + 1) % self.DifficultyAdjustmentInterval(height + 1) != 0:
|
||||
return int(headerLast["bits"])
|
||||
if headerFirst is None:
|
||||
averagingInterval = self.AveragingInterval(height + 1)
|
||||
blockstogoback = averagingInterval - 1
|
||||
# print("Blocks to go back = " + str(blockstogoback))
|
||||
@ -526,7 +377,6 @@ class Blockchain(util.PrintError):
|
||||
blockstogoback = averagingInterval
|
||||
firstHeight = height - blockstogoback
|
||||
headerFirst = self.read_header(int(firstHeight))
|
||||
|
||||
firstBlockTime = headerFirst["timestamp"]
|
||||
nMinActualTimespan = int(self.MinActualTimespan(int(headerLast["block_height"]) + 1))
|
||||
|
||||
@ -556,62 +406,26 @@ class Blockchain(util.PrintError):
|
||||
bnNew = self.target_to_bits(int(bnNew))
|
||||
return bnNew
|
||||
|
||||
@classmethod
|
||||
def bits_to_target(cls, bits: int) -> int:
|
||||
def bits_to_target(self, bits):
|
||||
bitsN = (bits >> 24) & 0xff
|
||||
if not (0x03 <= bitsN <= 0x1e):
|
||||
raise Exception("First part of bits should be in [0x03, 0x1e]")
|
||||
if not (bitsN >= 0x03 and bitsN <= 0x1e):
|
||||
raise BaseException("First part of bits should be in [0x03, 0x1e]")
|
||||
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]")
|
||||
return bitsBase << (8 * (bitsN - 3))
|
||||
return bitsBase << (8 * (bitsN-3))
|
||||
|
||||
@classmethod
|
||||
def target_to_bits(cls, target: int) -> int:
|
||||
def target_to_bits(self, target):
|
||||
c = ("%064x" % target)[2:]
|
||||
while c[:2] == '00' and len(c) > 6:
|
||||
c = c[2:]
|
||||
bitsN, bitsBase = len(c) // 2, int.from_bytes(bfh(c[:6]), byteorder='big')
|
||||
bitsN, bitsBase = len(c) // 2, int('0x' + c[:6], 16)
|
||||
if bitsBase >= 0x800000:
|
||||
bitsN += 1
|
||||
bitsBase >>= 8
|
||||
return bitsN << 24 | bitsBase
|
||||
|
||||
def chainwork_of_header_at_height(self, height: int) -> int:
|
||||
"""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:
|
||||
def can_connect(self, header, check_height=True):
|
||||
if header is None:
|
||||
return False
|
||||
height = header['block_height']
|
||||
@ -636,8 +450,7 @@ class Blockchain(util.PrintError):
|
||||
return False
|
||||
return True
|
||||
|
||||
def connect_chunk(self, idx: int, hexdata: str) -> bool:
|
||||
assert idx >= 0, idx
|
||||
def connect_chunk(self, idx, hexdata):
|
||||
try:
|
||||
data = bfh(hexdata)
|
||||
self.verify_chunk(idx, data)
|
||||
@ -645,7 +458,7 @@ class Blockchain(util.PrintError):
|
||||
self.save_chunk(idx, data)
|
||||
return True
|
||||
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
|
||||
|
||||
def get_checkpoints(self):
|
||||
@ -713,20 +526,11 @@ class Blockchain(util.PrintError):
|
||||
# V3
|
||||
return constants.net.nInterval_Version3
|
||||
|
||||
|
||||
def check_header(header: dict) -> Optional[Blockchain]:
|
||||
if type(header) is not dict:
|
||||
return None
|
||||
with blockchains_lock: chains = list(blockchains.values())
|
||||
for b in chains:
|
||||
if b.check_header(header):
|
||||
return b
|
||||
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
|
||||
def save_chunk_part(self, header):
|
||||
filename = self.path()
|
||||
delta = header.get('block_height')
|
||||
data = bfh(serialize_header(header))
|
||||
# assert delta == self.size()
|
||||
assert len(data) == 80
|
||||
self.write(data, delta * 80)
|
||||
# self.swap_with_parent()
|
||||
|
||||
@ -22,9 +22,8 @@
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from collections import defaultdict
|
||||
from collections import defaultdict, namedtuple
|
||||
from math import floor, log10
|
||||
from typing import NamedTuple, List
|
||||
|
||||
from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
|
||||
from .transaction import Transaction, TxOutput
|
||||
@ -69,14 +68,13 @@ class PRNG:
|
||||
x[i], x[j] = x[j], x[i]
|
||||
|
||||
|
||||
class Bucket(NamedTuple):
|
||||
desc: str
|
||||
weight: int # as in BIP-141
|
||||
value: int # in satoshis
|
||||
coins: List[dict] # UTXOs
|
||||
min_height: int # min block height where a coin was confirmed
|
||||
witness: bool # whether any coin uses segwit
|
||||
|
||||
Bucket = namedtuple('Bucket',
|
||||
['desc',
|
||||
'weight', # as in BIP-141
|
||||
'value', # in satoshis
|
||||
'coins', # UTXOs
|
||||
'min_height', # min block height where a coin was confirmed
|
||||
'witness']) # whether any coin uses segwit
|
||||
|
||||
def strip_unneeded(bkts, sufficient_funds):
|
||||
'''Remove buckets that are unnecessary in achieving the spend amount'''
|
||||
@ -84,8 +82,8 @@ def strip_unneeded(bkts, sufficient_funds):
|
||||
for i in range(len(bkts)):
|
||||
if not sufficient_funds(bkts[i + 1:]):
|
||||
return bkts[i:]
|
||||
# none of the buckets are needed
|
||||
return []
|
||||
# Shouldn't get here
|
||||
return bkts
|
||||
|
||||
class CoinChooserBase(PrintError):
|
||||
|
||||
@ -187,7 +185,7 @@ class CoinChooserBase(PrintError):
|
||||
self.print_error('not keeping dust', dust)
|
||||
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):
|
||||
"""Select unspent coins to spend to pay outputs. If the change is
|
||||
greater than dust_threshold (after adding the change output to
|
||||
@ -202,14 +200,11 @@ class CoinChooserBase(PrintError):
|
||||
self.p = PRNG(''.join(sorted(utxos)))
|
||||
|
||||
# Copy the outputs so when adding change we don't modify "outputs"
|
||||
tx = Transaction.from_io(inputs[:], outputs[:])
|
||||
input_value = tx.input_value()
|
||||
|
||||
tx = Transaction.from_io([], outputs[:])
|
||||
# Weight of the transaction with no inputs and no change
|
||||
# Note: this will use legacy tx serialization as the need for "segwit"
|
||||
# would be detected from inputs. The only side effect should be that the
|
||||
# marker and flag are excluded, which is compensated in get_tx_weight()
|
||||
# FIXME calculation will be off by this (2 wu) in case of RBF batching
|
||||
base_weight = tx.estimated_weight()
|
||||
spent_amount = tx.output_value()
|
||||
|
||||
@ -233,7 +228,7 @@ class CoinChooserBase(PrintError):
|
||||
def sufficient_funds(buckets):
|
||||
'''Given a list of buckets, return True if it has enough
|
||||
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)
|
||||
return total_input >= spent_amount + fee_estimator_w(total_weight)
|
||||
|
||||
|
||||
@ -32,26 +32,15 @@ import ast
|
||||
import base64
|
||||
from functools import wraps
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from .import util, ecc
|
||||
from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode
|
||||
from . import bitcoin
|
||||
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
|
||||
from . import bip32
|
||||
from .i18n import _
|
||||
from .transaction import Transaction, multisig_script, TxOutput
|
||||
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
|
||||
from .synchronizer import Notifier
|
||||
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
|
||||
|
||||
from .plugin import run_hook
|
||||
|
||||
known_commands = {}
|
||||
|
||||
@ -102,8 +91,7 @@ def command(s):
|
||||
|
||||
class Commands:
|
||||
|
||||
def __init__(self, config: 'SimpleConfig', wallet: Abstract_Wallet,
|
||||
network: Optional['Network'], callback=None):
|
||||
def __init__(self, config, wallet, network, callback = None):
|
||||
self.config = config
|
||||
self.wallet = wallet
|
||||
self.network = network
|
||||
@ -135,80 +123,17 @@ class Commands:
|
||||
return ' '.join(sorted(known_commands.keys()))
|
||||
|
||||
@command('')
|
||||
def create(self, passphrase=None, password=None, encrypt_file=True, segwit=False):
|
||||
def create(self, segwit=False):
|
||||
"""Create a new wallet"""
|
||||
storage = WalletStorage(self.config.get_wallet_path())
|
||||
if storage.file_exists():
|
||||
raise Exception("Remove the existing wallet first!")
|
||||
raise Exception('Not a JSON-RPC command')
|
||||
|
||||
seed_type = 'segwit' if segwit else 'standard'
|
||||
seed = Mnemonic('en').make_seed(seed_type)
|
||||
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):
|
||||
@command('wn')
|
||||
def restore(self, text):
|
||||
"""Restore a wallet from text. Text can be a seed phrase, a master
|
||||
public key, a master private key, a list of bitcoin addresses
|
||||
or bitcoin private keys. If you want to be prompted for your
|
||||
seed, type '?' or ':' (concealed) """
|
||||
storage = WalletStorage(self.config.get_wallet_path())
|
||||
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}
|
||||
raise Exception('Not a JSON-RPC command')
|
||||
|
||||
@command('wp')
|
||||
def password(self, password=None, new_password=None):
|
||||
@ -220,11 +145,6 @@ class Commands:
|
||||
self.wallet.storage.write()
|
||||
return {'password':self.wallet.has_password()}
|
||||
|
||||
@command('w')
|
||||
def get(self, key):
|
||||
"""Return item from wallet storage"""
|
||||
return self.wallet.storage.get(key)
|
||||
|
||||
@command('')
|
||||
def getconfig(self, key):
|
||||
"""Return a configuration variable. """
|
||||
@ -261,7 +181,7 @@ class Commands:
|
||||
walletless server query, results are not checked by SPV.
|
||||
"""
|
||||
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')
|
||||
def listunspent(self):
|
||||
@ -279,7 +199,7 @@ class Commands:
|
||||
is a walletless server query, results are not checked by SPV.
|
||||
"""
|
||||
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('')
|
||||
def serialize(self, jsontx):
|
||||
@ -329,14 +249,13 @@ class Commands:
|
||||
def deserialize(self, tx):
|
||||
"""Deserialize a serialized transaction"""
|
||||
tx = Transaction(tx)
|
||||
return tx.deserialize(force_full_parse=True)
|
||||
return tx.deserialize()
|
||||
|
||||
@command('n')
|
||||
def broadcast(self, tx):
|
||||
"""Broadcast a transaction to the network. """
|
||||
tx = Transaction(tx)
|
||||
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
||||
return tx.txid()
|
||||
return self.network.broadcast_transaction_from_non_network_thread(tx)
|
||||
|
||||
@command('')
|
||||
def createmultisig(self, num, pubkeys):
|
||||
@ -403,7 +322,7 @@ class Commands:
|
||||
server query, results are not checked by SPV.
|
||||
"""
|
||||
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["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
|
||||
return out
|
||||
@ -412,7 +331,7 @@ class Commands:
|
||||
def getmerkle(self, txid, height):
|
||||
"""Get Merkle branch of a transaction included in a block. Electrum
|
||||
uses this to verify transactions (Simple Payment Verification)."""
|
||||
return self.network.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')
|
||||
def getservers(self):
|
||||
@ -435,16 +354,6 @@ class Commands:
|
||||
"""Get master private key. Return your wallet\'s master private key"""
|
||||
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')
|
||||
def getseed(self, password=None):
|
||||
"""Get seed phrase. Print the generation seed of your wallet."""
|
||||
@ -459,7 +368,7 @@ class Commands:
|
||||
try:
|
||||
addr = self.wallet.import_private_key(privkey, password)
|
||||
out = "Keypair imported: " + addr
|
||||
except Exception as e:
|
||||
except BaseException as e:
|
||||
out = "Error: " + str(e)
|
||||
return out
|
||||
|
||||
@ -537,15 +446,9 @@ class Commands:
|
||||
return tx.as_dict()
|
||||
|
||||
@command('w')
|
||||
def history(self, year=None, show_addresses=False, show_fiat=False, show_fees=False,
|
||||
from_height=None, to_height=None):
|
||||
def history(self, year=None, show_addresses=False, show_fiat=False):
|
||||
"""Wallet history. Returns the transaction history of your wallet."""
|
||||
kwargs = {
|
||||
'show_addresses': show_addresses,
|
||||
'show_fees': show_fees,
|
||||
'from_height': from_height,
|
||||
'to_height': to_height,
|
||||
}
|
||||
kwargs = {'show_addresses': show_addresses}
|
||||
if year:
|
||||
import time
|
||||
start_date = datetime.datetime(year, 1, 1)
|
||||
@ -614,7 +517,7 @@ class Commands:
|
||||
if self.wallet and txid in self.wallet.transactions:
|
||||
tx = self.wallet.transactions[txid]
|
||||
else:
|
||||
raw = self.network.run_from_another_thread(self.network.get_transaction(txid))
|
||||
raw = self.network.get_transaction(txid)
|
||||
if raw:
|
||||
tx = Transaction(raw)
|
||||
else:
|
||||
@ -640,7 +543,7 @@ class Commands:
|
||||
PR_PAID: 'Paid',
|
||||
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)]
|
||||
return out
|
||||
|
||||
@ -732,11 +635,20 @@ class Commands:
|
||||
self.wallet.remove_payment_request(k, self.config)
|
||||
|
||||
@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."""
|
||||
if not hasattr(self, "_notifier"):
|
||||
self._notifier = Notifier(self.network)
|
||||
self.network.run_from_another_thread(self._notifier.start_watching_queue.put((address, URL)))
|
||||
def callback(x):
|
||||
import urllib.request
|
||||
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
|
||||
|
||||
@command('wn')
|
||||
@ -768,16 +680,6 @@ class Commands:
|
||||
# for the python console
|
||||
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 = {
|
||||
'privkey': 'Private key. Type \'?\' to get a prompt.',
|
||||
'destination': 'Bitcoin address, contact or alias',
|
||||
@ -791,8 +693,8 @@ param_descriptions = {
|
||||
'pubkey': 'Public key',
|
||||
'message': 'Clear text message. Use quotes if it contains spaces.',
|
||||
'encrypted': 'Encrypted message',
|
||||
'amount': 'Amount to be sent (in BTC). Type \'!\' to send the maximum available.',
|
||||
'requested_amount': 'Requested amount (in BTC).',
|
||||
'amount': 'Amount to be sent (in FLO). Type \'!\' to send the maximum available.',
|
||||
'requested_amount': 'Requested amount (in FLO).',
|
||||
'outputs': 'list of ["address", amount]',
|
||||
'redeem_script': 'redeem script (hexadecimal)',
|
||||
}
|
||||
@ -800,7 +702,6 @@ param_descriptions = {
|
||||
command_options = {
|
||||
'password': ("-W", "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"),
|
||||
'change': (None, "Show only change addresses"),
|
||||
'frozen': (None, "Show only frozen addresses"),
|
||||
@ -810,13 +711,12 @@ command_options = {
|
||||
'labels': ("-l", "Show the labels of listed addresses"),
|
||||
'nocheck': (None, "Do not verify aliases"),
|
||||
'imax': (None, "Maximum number of inputs"),
|
||||
'fee': ("-f", "Transaction fee (in 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)."),
|
||||
'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"),
|
||||
'segwit': (None, "Create segwit seed"),
|
||||
'language': ("-L", "Default language for wordlist"),
|
||||
'passphrase': (None, "Seed extension"),
|
||||
'privkey': (None, "Private key. Set to '?' to get a prompt."),
|
||||
'unsigned': ("-u", "Do not sign transaction"),
|
||||
'rbf': (None, "Replace-by-fee transaction"),
|
||||
@ -831,12 +731,9 @@ command_options = {
|
||||
'paid': (None, "Show only paid requests."),
|
||||
'show_addresses': (None, "Show input and output addresses"),
|
||||
'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"),
|
||||
'fee_method': (None, "Fee estimation method to use"),
|
||||
'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"),
|
||||
'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position")
|
||||
}
|
||||
|
||||
|
||||
@ -848,8 +745,6 @@ arg_types = {
|
||||
'nbits': int,
|
||||
'imax': int,
|
||||
'year': int,
|
||||
'from_height': int,
|
||||
'to_height': int,
|
||||
'tx': tx_from_str,
|
||||
'pubkeys': json_loads,
|
||||
'jsontx': json_loads,
|
||||
@ -860,7 +755,6 @@ arg_types = {
|
||||
'locktime': int,
|
||||
'fee_method': str,
|
||||
'fee_level': json_loads,
|
||||
'encrypt_file': eval_bool,
|
||||
}
|
||||
|
||||
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("-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("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=False, help="Tolerate invalid merkle proofs from server")
|
||||
|
||||
def add_global_options(parser):
|
||||
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("-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("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
|
||||
add_network_options(parser_gui)
|
||||
add_global_options(parser_gui)
|
||||
# daemon
|
||||
@ -974,10 +866,12 @@ def get_parser():
|
||||
cmd = known_commands[cmdname]
|
||||
p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description)
|
||||
add_global_options(p)
|
||||
if cmdname == 'restore':
|
||||
p.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
|
||||
for optname, default in zip(cmd.options, cmd.defaults):
|
||||
a, help = command_options[optname]
|
||||
b = '--' + optname
|
||||
action = "store_true" if default is False else 'store'
|
||||
action = "store_true" if type(default) is bool else 'store'
|
||||
args = (a, b) if a else (b,)
|
||||
if action == 'store':
|
||||
_type = arg_types.get(optname, str)
|
||||
|
||||
@ -49,8 +49,8 @@ class BitcoinMainnet(AbstractNet):
|
||||
TESTNET = False
|
||||
WIF_PREFIX = 0xa3
|
||||
ADDRTYPE_P2PKH = 35
|
||||
ADDRTYPE_P2SH = 94
|
||||
SEGWIT_HRP = "flo"
|
||||
ADDRTYPE_P2SH = 8
|
||||
SEGWIT_HRP = "ltc"
|
||||
GENESIS = "09c7781c9df90708e278c35d38ea5c9041d7ecfcdd1c56ba67274b7cff3e1cea"
|
||||
DEFAULT_PORTS = {'t': '50001', 's': '50002'}
|
||||
DEFAULT_SERVERS = read_json('servers.json', {})
|
||||
@ -70,7 +70,7 @@ class BitcoinMainnet(AbstractNet):
|
||||
'p2wpkh': 0x04b24746, # zpub
|
||||
'p2wsh': 0x02aa7ed3, # Zpub
|
||||
}
|
||||
BIP44_COIN_TYPE = 216
|
||||
BIP44_COIN_TYPE = 2
|
||||
# FLO Network constants
|
||||
fPowAllowMinDifficultyBlocks = False
|
||||
fPowNoRetargeting = False
|
||||
@ -103,8 +103,8 @@ class BitcoinTestnet(AbstractNet):
|
||||
TESTNET = True
|
||||
WIF_PREFIX = 0xef
|
||||
ADDRTYPE_P2PKH = 115
|
||||
ADDRTYPE_P2SH = 58
|
||||
SEGWIT_HRP = "tflo"
|
||||
ADDRTYPE_P2SH = 198
|
||||
SEGWIT_HRP = "tltc"
|
||||
GENESIS = "9b7bc86236c34b5e3a39367c036b7fe8807a966c22a7a1f0da2a198a27e03731"
|
||||
DEFAULT_PORTS = {'t': '51001', 's': '51002'}
|
||||
DEFAULT_SERVERS = read_json('servers_testnet.json', {})
|
||||
|
||||
@ -21,9 +21,11 @@
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import re
|
||||
|
||||
import dns
|
||||
from dns.exception import DNSException
|
||||
import json
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
from . import bitcoin
|
||||
from . import dnssec
|
||||
@ -65,9 +67,8 @@ class Contacts(dict):
|
||||
|
||||
def pop(self, key):
|
||||
if key in self.keys():
|
||||
res = dict.pop(self, key)
|
||||
dict.pop(self, key)
|
||||
self.save()
|
||||
return res
|
||||
|
||||
def resolve(self, k):
|
||||
if bitcoin.is_address(k):
|
||||
@ -99,7 +100,7 @@ class Contacts(dict):
|
||||
try:
|
||||
records, validated = dnssec.query(url, dns.rdatatype.TXT)
|
||||
except DNSException as e:
|
||||
print_error(f'Error resolving openalias: {repr(e)}')
|
||||
print_error('Error resolving openalias: ', str(e))
|
||||
return None
|
||||
prefix = 'btc'
|
||||
for record in records:
|
||||
|
||||
@ -27,12 +27,10 @@ import base64
|
||||
import os
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import Union
|
||||
|
||||
import pyaes
|
||||
|
||||
from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException
|
||||
from .i18n import _
|
||||
from .util import assert_bytes, InvalidPassword, to_bytes, to_string
|
||||
|
||||
|
||||
try:
|
||||
@ -45,26 +43,26 @@ class InvalidPadding(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def append_PKCS7_padding(data: bytes) -> bytes:
|
||||
def append_PKCS7_padding(data):
|
||||
assert_bytes(data)
|
||||
padlen = 16 - (len(data) % 16)
|
||||
return data + bytes([padlen]) * padlen
|
||||
|
||||
|
||||
def strip_PKCS7_padding(data: bytes) -> bytes:
|
||||
def strip_PKCS7_padding(data):
|
||||
assert_bytes(data)
|
||||
if len(data) % 16 != 0 or len(data) == 0:
|
||||
raise InvalidPadding("invalid length")
|
||||
padlen = data[-1]
|
||||
if not (0 < padlen <= 16):
|
||||
raise InvalidPadding("invalid padding byte (out of range)")
|
||||
if padlen > 16:
|
||||
raise InvalidPadding("invalid padding byte (large)")
|
||||
for i in data[-padlen:]:
|
||||
if i != padlen:
|
||||
raise InvalidPadding("invalid padding byte (inconsistent)")
|
||||
return data[0:-padlen]
|
||||
|
||||
|
||||
def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
|
||||
def aes_encrypt_with_iv(key, iv, data):
|
||||
assert_bytes(key, iv, data)
|
||||
data = append_PKCS7_padding(data)
|
||||
if AES:
|
||||
@ -76,7 +74,7 @@ def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
|
||||
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)
|
||||
if AES:
|
||||
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()
|
||||
|
||||
|
||||
def EncodeAES_base64(secret: bytes, msg: bytes) -> bytes:
|
||||
"""Returns base64 encoded ciphertext."""
|
||||
e = EncodeAES_bytes(secret, msg)
|
||||
def EncodeAES(secret, s):
|
||||
assert_bytes(s)
|
||||
iv = bytes(os.urandom(16))
|
||||
ct = aes_encrypt_with_iv(secret, iv, s)
|
||||
e = iv + ct
|
||||
return base64.b64encode(e)
|
||||
|
||||
|
||||
def EncodeAES_bytes(secret: bytes, msg: bytes) -> bytes:
|
||||
assert_bytes(msg)
|
||||
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:]
|
||||
def DecodeAES(secret, e):
|
||||
e = bytes(base64.b64decode(e))
|
||||
iv, e = e[:16], e[16:]
|
||||
s = aes_decrypt_with_iv(secret, iv, e)
|
||||
return s
|
||||
|
||||
|
||||
PW_HASH_VERSION_LATEST = 1
|
||||
KNOWN_PW_HASH_VERSIONS = (1, 2, )
|
||||
SUPPORTED_PW_HASH_VERSIONS = (1, )
|
||||
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)
|
||||
def pw_encode(s, password):
|
||||
if password:
|
||||
secret = Hash(password)
|
||||
return EncodeAES(secret, to_bytes(s, "utf8")).decode('utf8')
|
||||
else:
|
||||
assert version not in KNOWN_PW_HASH_VERSIONS
|
||||
raise UnexpectedPasswordHashVersion(version)
|
||||
return s
|
||||
|
||||
|
||||
def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str:
|
||||
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
|
||||
def pw_decode(s, password):
|
||||
if password is not None:
|
||||
secret = Hash(password)
|
||||
try:
|
||||
d = to_string(DecodeAES_bytes(secret, data_bytes), "utf8")
|
||||
except Exception as e:
|
||||
raise InvalidPassword() from e
|
||||
d = to_string(DecodeAES(secret, s), "utf8")
|
||||
except Exception:
|
||||
raise InvalidPassword()
|
||||
return d
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def sha256(x: Union[bytes, str]) -> bytes:
|
||||
def sha256(x: bytes) -> bytes:
|
||||
x = to_bytes(x, 'utf8')
|
||||
return bytes(hashlib.sha256(x).digest())
|
||||
|
||||
|
||||
def sha256d(x: Union[bytes, str]) -> bytes:
|
||||
def Hash(x: bytes) -> bytes:
|
||||
x = to_bytes(x, 'utf8')
|
||||
out = bytes(sha256(sha256(x)))
|
||||
return out
|
||||
|
||||
@ -1,44 +1,825 @@
|
||||
{
|
||||
"CoinMarketcap": [
|
||||
"BitFlyer": [
|
||||
"JPY"
|
||||
],
|
||||
"BitPay": [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
"AMD",
|
||||
"ANG",
|
||||
"AOA",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"AWG",
|
||||
"AZN",
|
||||
"BAM",
|
||||
"BBD",
|
||||
"BCH",
|
||||
"BDT",
|
||||
"BGN",
|
||||
"BHD",
|
||||
"BIF",
|
||||
"BMD",
|
||||
"BND",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"KHR",
|
||||
"BSD",
|
||||
"BTC",
|
||||
"BTN",
|
||||
"BWP",
|
||||
"BZD",
|
||||
"CAD",
|
||||
"CDF",
|
||||
"CHF",
|
||||
"CLF",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CRC",
|
||||
"CUP",
|
||||
"CVE",
|
||||
"CZK",
|
||||
"DJF",
|
||||
"DKK",
|
||||
"DOP",
|
||||
"DZD",
|
||||
"EGP",
|
||||
"ETB",
|
||||
"EUR",
|
||||
"FJD",
|
||||
"FKP",
|
||||
"FLO",
|
||||
"GBP",
|
||||
"GEL",
|
||||
"GHS",
|
||||
"GIP",
|
||||
"GMD",
|
||||
"GNF",
|
||||
"GTQ",
|
||||
"GYD",
|
||||
"HKD",
|
||||
"HNL",
|
||||
"HRK",
|
||||
"HTG",
|
||||
"HUF",
|
||||
"ISK",
|
||||
"INR",
|
||||
"IDR",
|
||||
"IQD",
|
||||
"ILS",
|
||||
"INR",
|
||||
"IQD",
|
||||
"IRR",
|
||||
"ISK",
|
||||
"JEP",
|
||||
"JMD",
|
||||
"JOD",
|
||||
"JPY",
|
||||
"KES",
|
||||
"KGS",
|
||||
"KHR",
|
||||
"KMF",
|
||||
"KPW",
|
||||
"KRW",
|
||||
"KWD",
|
||||
"KYD",
|
||||
"KZT",
|
||||
"LAK",
|
||||
"LBP",
|
||||
"MYR",
|
||||
"LKR",
|
||||
"LRD",
|
||||
"LSL",
|
||||
"LYD",
|
||||
"MAD",
|
||||
"MDL",
|
||||
"MGA",
|
||||
"MKD",
|
||||
"MMK",
|
||||
"MNT",
|
||||
"MOP",
|
||||
"MRU",
|
||||
"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",
|
||||
"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",
|
||||
"NOK",
|
||||
"GBP",
|
||||
"QAR",
|
||||
"NZD",
|
||||
"PAB",
|
||||
"PEN",
|
||||
"PHP",
|
||||
"PKR",
|
||||
"PLN",
|
||||
"PYG",
|
||||
"RON",
|
||||
"RSD",
|
||||
"RUB",
|
||||
"SGD",
|
||||
"SAR",
|
||||
"SEK",
|
||||
"CHF",
|
||||
"SGD",
|
||||
"SZL",
|
||||
"THB",
|
||||
"TRY",
|
||||
"TWD",
|
||||
"TZS",
|
||||
"UAH",
|
||||
"UGX",
|
||||
"USD",
|
||||
"VND"
|
||||
]
|
||||
"UYU",
|
||||
"VEF",
|
||||
"VND",
|
||||
"XAF",
|
||||
"XRP",
|
||||
"ZAR",
|
||||
"ZMW"
|
||||
],
|
||||
"MercadoBitcoin": [
|
||||
"BRL"
|
||||
],
|
||||
"NegocieCoins": [
|
||||
"BRL"
|
||||
],
|
||||
"TheRockTrading": [
|
||||
"EUR"
|
||||
],
|
||||
"WEX": [
|
||||
"EUR",
|
||||
"RUB",
|
||||
"USD"
|
||||
],
|
||||
"itBit": []
|
||||
}
|
||||
@ -28,17 +28,16 @@ import os
|
||||
import time
|
||||
import traceback
|
||||
import sys
|
||||
import threading
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
# from jsonrpc import JSONRPCResponseManager
|
||||
import jsonrpclib
|
||||
|
||||
from .jsonrpc import VerifyingJSONRPCServer
|
||||
|
||||
from .version import ELECTRUM_VERSION
|
||||
from .network import Network
|
||||
from .util import (json_decode, DaemonThread, print_error, to_string,
|
||||
create_and_start_event_loop, profiler, standardize_path)
|
||||
from .wallet import Wallet, Abstract_Wallet
|
||||
from .util import json_decode, DaemonThread
|
||||
from .util import print_error, to_string
|
||||
from .wallet import Wallet
|
||||
from .storage import WalletStorage
|
||||
from .commands import known_commands, Commands
|
||||
from .simple_config import SimpleConfig
|
||||
@ -46,7 +45,7 @@ from .exchange_rate import FxThread
|
||||
from .plugin import run_hook
|
||||
|
||||
|
||||
def get_lockfile(config: SimpleConfig):
|
||||
def get_lockfile(config):
|
||||
return os.path.join(config.path, 'daemon')
|
||||
|
||||
|
||||
@ -54,7 +53,7 @@ def remove_lockfile(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
|
||||
prevent races. If it succeeds it returns the FD.
|
||||
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)
|
||||
|
||||
|
||||
def get_server(config: SimpleConfig) -> Optional[jsonrpclib.Server]:
|
||||
def get_server(config):
|
||||
lockfile = get_lockfile(config)
|
||||
while True:
|
||||
create_time = None
|
||||
@ -92,14 +91,14 @@ def get_server(config: SimpleConfig) -> Optional[jsonrpclib.Server]:
|
||||
server.ping()
|
||||
return server
|
||||
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:
|
||||
return None
|
||||
# Sleep a bit and try again; it might have just been started
|
||||
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_password = config.get('rpcpassword', 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):
|
||||
|
||||
@profiler
|
||||
def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
|
||||
def __init__(self, config, fd, is_gui):
|
||||
DaemonThread.__init__(self)
|
||||
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'):
|
||||
self.network = None
|
||||
else:
|
||||
self.network = Network(config)
|
||||
self.network._loop_thread = self._loop_thread
|
||||
self.fx = FxThread(config, self.network)
|
||||
if self.network:
|
||||
self.network.start([self.fx.run])
|
||||
self.network.start(self.fx.run())
|
||||
self.gui = None
|
||||
self.wallets = {} # type: Dict[str, Abstract_Wallet]
|
||||
self.wallets = {}
|
||||
# Setup JSONRPC server
|
||||
self.server = None
|
||||
if listen_jsonrpc:
|
||||
self.init_server(config, fd)
|
||||
self.start()
|
||||
self.init_server(config, fd, is_gui)
|
||||
|
||||
def init_server(self, config: SimpleConfig, fd):
|
||||
def init_server(self, config, fd, is_gui):
|
||||
host = config.get('rpchost', '127.0.0.1')
|
||||
port = config.get('rpcport', 0)
|
||||
|
||||
rpc_user, rpc_password = get_rpc_credentials(config)
|
||||
try:
|
||||
server = VerifyingJSONRPCServer((host, port), logRequests=False,
|
||||
@ -162,7 +153,9 @@ class Daemon(DaemonThread):
|
||||
self.server = server
|
||||
server.timeout = 0.1
|
||||
server.register_function(self.ping, 'ping')
|
||||
if is_gui:
|
||||
server.register_function(self.run_gui, 'gui')
|
||||
else:
|
||||
server.register_function(self.run_daemon, 'daemon')
|
||||
self.cmd_runner = Commands(self.config, None, self.network)
|
||||
for cmdname in known_commands:
|
||||
@ -173,7 +166,6 @@ class Daemon(DaemonThread):
|
||||
return True
|
||||
|
||||
def run_daemon(self, config_options):
|
||||
asyncio.set_event_loop(self.asyncio_loop)
|
||||
config = SimpleConfig(config_options)
|
||||
sub = config.get('subcommand')
|
||||
assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet']
|
||||
@ -195,18 +187,18 @@ class Daemon(DaemonThread):
|
||||
response = False
|
||||
elif sub == 'status':
|
||||
if self.network:
|
||||
net_params = self.network.get_parameters()
|
||||
p = self.network.get_parameters()
|
||||
current_wallet = self.cmd_runner.wallet
|
||||
current_wallet_path = current_wallet.storage.path \
|
||||
if current_wallet else None
|
||||
response = {
|
||||
'path': self.network.config.path,
|
||||
'server': net_params.host,
|
||||
'server': p[0],
|
||||
'blockchain_height': self.network.get_local_height(),
|
||||
'server_height': self.network.get_server_height(),
|
||||
'spv_nodes': len(self.network.get_interfaces()),
|
||||
'connected': self.network.is_connected(),
|
||||
'auto_connect': net_params.auto_connect,
|
||||
'auto_connect': p[4],
|
||||
'version': ELECTRUM_VERSION,
|
||||
'wallets': {k: w.is_up_to_date()
|
||||
for k, w in self.wallets.items()},
|
||||
@ -223,19 +215,18 @@ class Daemon(DaemonThread):
|
||||
def run_gui(self, config_options):
|
||||
config = SimpleConfig(config_options)
|
||||
if self.gui:
|
||||
if hasattr(self.gui, 'new_window'):
|
||||
config.open_last_wallet()
|
||||
path = config.get_wallet_path()
|
||||
self.gui.new_window(path, config.get('url'))
|
||||
response = "ok"
|
||||
else:
|
||||
response = "error: current GUI does not support multiple windows"
|
||||
#if hasattr(self.gui, 'new_window'):
|
||||
# path = config.get_wallet_path()
|
||||
# self.gui.new_window(path, config.get('url'))
|
||||
# response = "ok"
|
||||
#else:
|
||||
# response = "error: current GUI does not support multiple windows"
|
||||
response = "error: Electrum GUI already running"
|
||||
else:
|
||||
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
||||
return response
|
||||
|
||||
def load_wallet(self, path, password) -> Optional[Abstract_Wallet]:
|
||||
path = standardize_path(path)
|
||||
def load_wallet(self, path, password):
|
||||
# wizard will be launched if we return
|
||||
if path in self.wallets:
|
||||
wallet = self.wallets[path]
|
||||
@ -256,27 +247,18 @@ class Daemon(DaemonThread):
|
||||
self.wallets[path] = wallet
|
||||
return wallet
|
||||
|
||||
def add_wallet(self, wallet: Abstract_Wallet):
|
||||
def add_wallet(self, wallet):
|
||||
path = wallet.storage.path
|
||||
self.wallets[path] = wallet
|
||||
|
||||
def get_wallet(self, path):
|
||||
return self.wallets.get(path)
|
||||
|
||||
def 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):
|
||||
wallet = self.wallets.pop(path, None)
|
||||
if not wallet: return
|
||||
wallet = self.wallets.pop(path)
|
||||
wallet.stop_threads()
|
||||
|
||||
def run_cmdline(self, config_options):
|
||||
asyncio.set_event_loop(self.asyncio_loop)
|
||||
password = config_options.get('password')
|
||||
new_password = config_options.get('new_password')
|
||||
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))
|
||||
cmd_runner = Commands(config, wallet, self.network)
|
||||
func = getattr(cmd_runner, cmd.name)
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
raise Exception("Wrapping TypeError to prevent JSONRPC-Pelix from hiding traceback") from e
|
||||
return result
|
||||
|
||||
def run(self):
|
||||
while self.is_running():
|
||||
self.server.handle_request() if self.server else time.sleep(0.1)
|
||||
# stop network/wallets
|
||||
for k, wallet in self.wallets.items():
|
||||
wallet.stop_threads()
|
||||
if self.network:
|
||||
self.print_error("shutting down network")
|
||||
self.network.stop()
|
||||
# stop event loop
|
||||
self.asyncio_loop.call_soon_threadsafe(self._stop_loop.set_result, 1)
|
||||
self._loop_thread.join(timeout=1)
|
||||
self.network.join()
|
||||
self.on_stop()
|
||||
|
||||
def stop(self):
|
||||
if self.gui:
|
||||
self.gui.stop()
|
||||
self.print_error("stopping, removing lockfile")
|
||||
remove_lockfile(get_lockfile(self.config))
|
||||
DaemonThread.stop(self)
|
||||
|
||||
def init_gui(self, config, plugins):
|
||||
threading.current_thread().setName('GUI')
|
||||
gui_name = config.get('gui', 'qt')
|
||||
if gui_name in ['lite', 'classic']:
|
||||
gui_name = 'qt'
|
||||
|
||||
@ -110,9 +110,11 @@ def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
|
||||
if rrsig.algorithm == ECDSAP256SHA256:
|
||||
curve = ecdsa.curves.NIST256p
|
||||
key_len = 32
|
||||
digest_len = 32
|
||||
elif rrsig.algorithm == ECDSAP384SHA384:
|
||||
curve = ecdsa.curves.NIST384p
|
||||
key_len = 48
|
||||
digest_len = 48
|
||||
else:
|
||||
# shouldn't happen
|
||||
raise ValidationFailure('unknown ECDSA curve')
|
||||
@ -139,7 +141,7 @@ def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
|
||||
rrnamebuf = rrname.to_digestable(origin)
|
||||
rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
|
||||
rrsig.original_ttl)
|
||||
rrlist = sorted(rdataset)
|
||||
rrlist = sorted(rdataset);
|
||||
for rr in rrlist:
|
||||
hash.update(rrnamebuf)
|
||||
hash.update(rrfixed)
|
||||
|
||||
@ -24,8 +24,10 @@
|
||||
# SOFTWARE.
|
||||
|
||||
import base64
|
||||
import hmac
|
||||
import hashlib
|
||||
from typing import Union, Tuple
|
||||
from typing import Union
|
||||
|
||||
|
||||
import ecdsa
|
||||
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 .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 . import msqr
|
||||
from . import constants
|
||||
|
||||
|
||||
do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1()
|
||||
@ -53,31 +53,31 @@ def point_at_infinity():
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@ -94,22 +94,23 @@ def point_to_ser(P, compressed=True) -> bytes:
|
||||
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
|
||||
_p = curve.p()
|
||||
_a = curve.a()
|
||||
_b = curve.b()
|
||||
x = x % _p
|
||||
y2 = (pow(x, 3, _p) + _a * x + _b) % _p
|
||||
y = msqr.modular_sqrt(y2, _p)
|
||||
if curve.contains_point(x, y):
|
||||
if odd == bool(y & 1):
|
||||
return y
|
||||
return _p - y
|
||||
raise InvalidECPointException()
|
||||
for offset in range(128):
|
||||
Mx = x + offset
|
||||
My2 = pow(Mx, 3, _p) + _a * pow(Mx, 2, _p) + _b % _p
|
||||
My = pow(My2, (_p + 1) // 4, _p)
|
||||
if curve.contains_point(Mx, My):
|
||||
if odd == bool(My & 1):
|
||||
return My
|
||||
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):
|
||||
raise ValueError('Unexpected first byte: {}'.format(ser[0]))
|
||||
if ser[0] == 0x04:
|
||||
@ -226,7 +227,7 @@ class ECPubkey(object):
|
||||
def get_public_key_hex(self, compressed=True):
|
||||
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()
|
||||
|
||||
def __mul__(self, other: int):
|
||||
@ -253,7 +254,7 @@ class ECPubkey(object):
|
||||
|
||||
def verify_message_for_address(self, sig65: bytes, message: bytes) -> None:
|
||||
assert_bytes(message)
|
||||
h = sha256d(msg_magic(message))
|
||||
h = Hash(msg_magic(message))
|
||||
public_key, compressed = self.from_signature65(sig65, h)
|
||||
# check public key
|
||||
if public_key != self:
|
||||
@ -295,14 +296,6 @@ class ECPubkey(object):
|
||||
def is_at_infinity(self):
|
||||
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:
|
||||
from .bitcoin import var_int
|
||||
@ -310,17 +303,16 @@ def msg_magic(message: bytes) -> bytes:
|
||||
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
|
||||
assert_bytes(sig65, message)
|
||||
if net is None: net = constants.net
|
||||
try:
|
||||
h = sha256d(msg_magic(message))
|
||||
h = Hash(msg_magic(message))
|
||||
public_key, compressed = ECPubkey.from_signature65(sig65, h)
|
||||
# check public key using the address
|
||||
pubkey_hex = public_key.get_public_key_hex(compressed)
|
||||
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:
|
||||
break
|
||||
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)
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"Verification error: {repr(e)}")
|
||||
print_error("Verification error: {0}".format(e))
|
||||
return False
|
||||
|
||||
|
||||
@ -405,14 +397,14 @@ class ECPrivkey(ECPubkey):
|
||||
raise Exception("error: cannot sign message. no recid fits..")
|
||||
|
||||
message = to_bytes(message, 'utf8')
|
||||
msg_hash = sha256d(msg_magic(message))
|
||||
msg_hash = Hash(msg_magic(message))
|
||||
sig_string = self.sign(msg_hash,
|
||||
sigencode=sig_string_from_r_and_s,
|
||||
sigdecode=get_r_and_s_from_sig_string)
|
||||
sig65, recid = bruteforce_recid(sig_string)
|
||||
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)
|
||||
if len(encrypted) < 85:
|
||||
raise Exception('invalid ciphertext: length')
|
||||
@ -437,6 +429,6 @@ class ECPrivkey(ECPubkey):
|
||||
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
|
||||
return bytes([27 + recid + comp]) + sig_string
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from aiohttp_socks import SocksConnector, SocksVer
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
import sys
|
||||
@ -10,15 +12,12 @@ import decimal
|
||||
from decimal import Decimal
|
||||
import concurrent.futures
|
||||
import traceback
|
||||
from typing import Sequence
|
||||
|
||||
from .bitcoin import COIN
|
||||
from .i18n import _
|
||||
from .util import (PrintError, ThreadJob, make_dir, log_exceptions,
|
||||
make_aiohttp_session, resource_path)
|
||||
from .util import PrintError, ThreadJob, make_dir, aiosafe
|
||||
from .util import make_aiohttp_session
|
||||
from .network import Network
|
||||
from .simple_config import SimpleConfig
|
||||
|
||||
|
||||
# See https://en.wikipedia.org/wiki/ISO_4217
|
||||
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,
|
||||
'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
|
||||
|
||||
|
||||
class ExchangeBase(PrintError):
|
||||
|
||||
def __init__(self, on_quotes, on_history):
|
||||
@ -49,8 +47,7 @@ class ExchangeBase(PrintError):
|
||||
url = ''.join(['https://', site, get_string])
|
||||
async with make_aiohttp_session(Network.get_instance().proxy) as session:
|
||||
async with session.get(url) as response:
|
||||
# set content_type to None to disable checking MIME type
|
||||
return await response.json(content_type=None)
|
||||
return await response.json()
|
||||
|
||||
async def get_csv(self, site, get_string):
|
||||
raw = await self.get_raw(site, get_string)
|
||||
@ -60,14 +57,14 @@ class ExchangeBase(PrintError):
|
||||
def name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@log_exceptions
|
||||
@aiosafe
|
||||
async def update_safe(self, ccy):
|
||||
try:
|
||||
self.print_error("getting fx quotes for", ccy)
|
||||
self.quotes = await self.get_rates(ccy)
|
||||
self.print_error("received fx quotes")
|
||||
except BaseException as e:
|
||||
self.print_error("failed fx quotes:", repr(e))
|
||||
self.print_error("failed fx quotes:", e)
|
||||
self.quotes = {}
|
||||
self.on_quotes()
|
||||
|
||||
@ -91,7 +88,7 @@ class ExchangeBase(PrintError):
|
||||
self.on_history()
|
||||
return h
|
||||
|
||||
@log_exceptions
|
||||
@aiosafe
|
||||
async def get_historical_rates_safe(self, ccy, cache_dir):
|
||||
try:
|
||||
self.print_error("requesting fx history for", ccy)
|
||||
@ -222,11 +219,13 @@ class Bitvalor(ExchangeBase):
|
||||
return {'BRL': Decimal(json['ticker_1h']['total']['last'])}
|
||||
|
||||
|
||||
class BlockchainInfo(ExchangeBase):
|
||||
class Bittrex(ExchangeBase):
|
||||
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('blockchain.info', '/ticker')
|
||||
return dict([(r, Decimal(json[r]['15m'])) for r in json])
|
||||
json = await self.get_json('bittrex.com','/api/v1.1/public/getticker?market=BTC-FLO')
|
||||
floPrice_inBTC = json['result']['Last']
|
||||
json = self.get_json('blockchain.info', '/ticker')
|
||||
return dict([(r, Decimal(json[r]['15m'] * floPrice_inBTC)) for r in json])
|
||||
|
||||
|
||||
class BTCChina(ExchangeBase):
|
||||
@ -246,9 +245,10 @@ class BTCParalelo(ExchangeBase):
|
||||
class Coinbase(ExchangeBase):
|
||||
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.coinbase.com',
|
||||
'/v2/exchange-rates?currency=BTC')
|
||||
return {ccy: Decimal(rate) for (ccy, rate) in json["data"]["rates"].items()}
|
||||
json = await self.get_json('coinbase.com',
|
||||
'/api/v1/currencies/exchange_rates')
|
||||
return dict([(r[7:].upper(), Decimal(json[r]))
|
||||
for r in json if r.startswith('btc_to_')])
|
||||
|
||||
|
||||
class CoinDesk(ExchangeBase):
|
||||
@ -280,14 +280,6 @@ class CoinDesk(ExchangeBase):
|
||||
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):
|
||||
|
||||
async def get_rates(self, ccy):
|
||||
@ -402,7 +394,7 @@ def dictinvert(d):
|
||||
return inv
|
||||
|
||||
def get_exchanges_and_currencies():
|
||||
path = resource_path('currencies.json')
|
||||
path = os.path.join(os.path.dirname(__file__), 'currencies.json')
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.loads(f.read())
|
||||
@ -443,7 +435,7 @@ def get_exchanges_by_ccy(history=True):
|
||||
|
||||
class FxThread(ThreadJob):
|
||||
|
||||
def __init__(self, config: SimpleConfig, network: Network):
|
||||
def __init__(self, config, network):
|
||||
self.config = config
|
||||
self.network = network
|
||||
if self.network:
|
||||
@ -453,31 +445,25 @@ class FxThread(ThreadJob):
|
||||
self.ccy_combo = None
|
||||
self.hist_checkbox = None
|
||||
self.cache_dir = os.path.join(config.path, 'cache')
|
||||
self._trigger = asyncio.Event()
|
||||
self._trigger.set()
|
||||
self.trigger = asyncio.Event()
|
||||
self.trigger.set()
|
||||
self.set_exchange(self.config_exchange())
|
||||
make_dir(self.cache_dir)
|
||||
|
||||
def set_proxy(self, trigger_name, *args):
|
||||
self._trigger.set()
|
||||
self.trigger.set()
|
||||
|
||||
@staticmethod
|
||||
def get_currencies(history: bool) -> Sequence[str]:
|
||||
d = get_exchanges_by_ccy(history)
|
||||
def get_currencies(self, h):
|
||||
d = get_exchanges_by_ccy(h)
|
||||
return sorted(d.keys())
|
||||
|
||||
@staticmethod
|
||||
def get_exchanges_by_ccy(ccy: str, history: bool) -> Sequence[str]:
|
||||
d = get_exchanges_by_ccy(history)
|
||||
def get_exchanges_by_ccy(self, ccy, h):
|
||||
d = get_exchanges_by_ccy(h)
|
||||
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):
|
||||
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:
|
||||
rounded_amount = round(amount, prec)
|
||||
except decimal.InvalidOperation:
|
||||
@ -487,11 +473,11 @@ class FxThread(ThreadJob):
|
||||
async def run(self):
|
||||
while True:
|
||||
try:
|
||||
await asyncio.wait_for(self._trigger.wait(), 150)
|
||||
await asyncio.wait_for(self.trigger.wait(), 150)
|
||||
except concurrent.futures.TimeoutError:
|
||||
pass
|
||||
else:
|
||||
self._trigger.clear()
|
||||
self.trigger.clear()
|
||||
if self.is_enabled():
|
||||
if self.show_history():
|
||||
self.exchange.get_historical_rates(self.ccy, self.cache_dir)
|
||||
@ -503,7 +489,7 @@ class FxThread(ThreadJob):
|
||||
|
||||
def set_enabled(self, b):
|
||||
self.config.set_key('use_exchange_rate', bool(b))
|
||||
self.trigger_update()
|
||||
self.trigger.set()
|
||||
|
||||
def get_history_config(self):
|
||||
return bool(self.config.get('history_rates'))
|
||||
@ -528,7 +514,7 @@ class FxThread(ThreadJob):
|
||||
return self.config.get("currency", "EUR")
|
||||
|
||||
def config_exchange(self):
|
||||
return self.config.get('use_exchange', 'CoinMarketcap')
|
||||
return self.config.get('use_exchange', 'BitcoinAverage')
|
||||
|
||||
def show_history(self):
|
||||
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):
|
||||
self.ccy = ccy
|
||||
self.config.set_key('currency', ccy, True)
|
||||
self.trigger_update()
|
||||
self.trigger.set() # Because self.ccy changes
|
||||
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):
|
||||
class_ = globals().get(name, BitcoinAverage)
|
||||
self.print_error("using exchange", name)
|
||||
@ -551,7 +533,7 @@ class FxThread(ThreadJob):
|
||||
self.exchange = class_(self.on_quotes, self.on_history)
|
||||
# A new exchange means new fx quotes, initially empty. Force
|
||||
# a quote refresh
|
||||
self.trigger_update()
|
||||
self.trigger.set()
|
||||
self.exchange.read_historical_rates(self.ccy, self.cache_dir)
|
||||
|
||||
def on_quotes(self):
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# To create a new GUI, please add its code to this directory.
|
||||
# Three objects are passed to the ElectrumGui: config, daemon and plugins
|
||||
# The Wallet object is 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()
|
||||
|
||||
|
Before Width: | Height: | Size: 687 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
@ -11,7 +11,7 @@ prepare:
|
||||
@cp tools/buildozer.spec ../../../buildozer.spec
|
||||
# copy electrum to main.py
|
||||
@cp ../../../run_electrum ../../../main.py
|
||||
@-if [ ! -d "../../../.buildozer" ];then \
|
||||
@-if [ ! -d "../../.buildozer" ];then \
|
||||
cd ../../..; buildozer android debug;\
|
||||
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;\
|
||||
|
||||
@ -1,90 +1,130 @@
|
||||
# Kivy GUI
|
||||
|
||||
The Kivy GUI is used with Electrum on Android devices.
|
||||
To generate an APK file, follow these instructions.
|
||||
The Kivy GUI is used with Electrum on Android devices. 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
|
||||
similar system. The docker commands should be executed in the project's root
|
||||
folder.
|
||||
Make sure the current user can write `/opt` (e.g. `sudo chown username: /opt`).
|
||||
|
||||
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`.
|
||||
|
||||
```
|
||||
$ 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. Install kivy
|
||||
|
||||
2. Build image
|
||||
|
||||
```
|
||||
$ sudo docker build -t electrum-android-builder-img electrum/gui/kivy/tools
|
||||
```
|
||||
|
||||
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`.
|
||||
Install kivy for python3 as described [here](https://kivy.org/docs/installation/installation-linux.html).
|
||||
So for example:
|
||||
```sh
|
||||
sudo add-apt-repository ppa:kivy-team/kivy
|
||||
sudo apt-get install python3-kivy
|
||||
```
|
||||
|
||||
|
||||
## 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}`
|
||||
|
||||
|
||||
### 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`
|
||||
|
||||
@ -4,9 +4,6 @@ import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -16,36 +13,29 @@ import com.google.zxing.Result;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
public class SimpleScannerActivity extends Activity implements ZXingScannerView.ResultHandler {
|
||||
private static final int MY_PERMISSIONS_CAMERA = 1002;
|
||||
|
||||
private ZXingScannerView mScannerView = null;
|
||||
private ZXingScannerView mScannerView;
|
||||
final String TAG = "org.electrum.SimpleScannerActivity";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view
|
||||
mScannerView.setFormats(Arrays.asList(BarcodeFormat.QR_CODE));
|
||||
setContentView(mScannerView); // Set the scanner view as the content view
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (this.hasPermission()) {
|
||||
this.startCamera();
|
||||
} else {
|
||||
this.requestPermission();
|
||||
}
|
||||
mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results.
|
||||
mScannerView.startCamera(); // Start camera on resume
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (null != mScannerView) {
|
||||
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
|
||||
public void handleResult(Result rawResult) {
|
||||
@ -55,35 +45,4 @@ public class SimpleScannerActivity extends Activity implements ZXingScannerView.
|
||||
setResult(Activity.RESULT_OK, resultIntent);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.5 KiB |
@ -151,20 +151,6 @@
|
||||
data: ''
|
||||
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>
|
||||
size_hint: None, None
|
||||
width: '270dp' if root.fs else min(self.width, dp(270))
|
||||
@ -207,18 +193,6 @@
|
||||
size: self.size
|
||||
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>
|
||||
item_height: dp(42)
|
||||
@ -264,7 +238,7 @@
|
||||
padding: dp(12)
|
||||
spacing: dp(5)
|
||||
screen: None
|
||||
on_state:
|
||||
on_release:
|
||||
self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu()
|
||||
canvas.before:
|
||||
Color:
|
||||
@ -273,36 +247,6 @@
|
||||
size: self.size
|
||||
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>:
|
||||
background_color: 1, .585, .878, 0
|
||||
halign: 'left'
|
||||
@ -322,17 +266,6 @@
|
||||
size: self.size
|
||||
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>:
|
||||
background_color: 1, .585, .878, 0
|
||||
halign: 'center'
|
||||
@ -374,6 +307,8 @@
|
||||
carousel: carousel
|
||||
do_default_tab: False
|
||||
Carousel:
|
||||
scroll_timeout: 250
|
||||
scroll_distance: '100dp'
|
||||
anim_type: 'out_quart'
|
||||
min_move: .05
|
||||
anim_move_duration: .1
|
||||
@ -500,7 +435,7 @@ BoxLayout:
|
||||
size_hint: 1, 1
|
||||
bold: True
|
||||
color: 0.7, 0.7, 0.7, 1
|
||||
text: app.wallet_name
|
||||
text: app.status
|
||||
font_size: '22dp'
|
||||
#minimum_width: '1dp'
|
||||
on_release: app.popup_dialog('status')
|
||||
@ -516,7 +451,6 @@ BoxLayout:
|
||||
ActionOvrButton:
|
||||
name: 'network'
|
||||
text: _('Network')
|
||||
text: _('Network')
|
||||
ActionOvrButton:
|
||||
name: 'settings'
|
||||
text: _('Settings')
|
||||
|
||||
@ -9,14 +9,13 @@ import threading
|
||||
|
||||
from electrum.bitcoin import TYPE_ADDRESS
|
||||
from electrum.storage import WalletStorage
|
||||
from electrum.wallet import Wallet, InternalAddressCorruption
|
||||
from electrum.wallet import Wallet
|
||||
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.util import format_satoshis, format_satoshis_plain
|
||||
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
||||
from electrum import blockchain
|
||||
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
|
||||
from .i18n import _
|
||||
|
||||
from kivy.app import App
|
||||
@ -40,7 +39,7 @@ from kivy.lang import Builder
|
||||
from .uix.dialogs.installwizard import InstallWizard
|
||||
from .uix.dialogs import InfoBubble, crash_reporter
|
||||
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
|
||||
#Window.softinput_mode = 'below_target'
|
||||
@ -97,25 +96,10 @@ class ElectrumWindow(App):
|
||||
def on_auto_connect(self, instance, x):
|
||||
net_params = self.network.get_parameters()
|
||||
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):
|
||||
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):
|
||||
from .uix.dialogs.choice_dialog import ChoiceDialog
|
||||
protocol = 's'
|
||||
@ -132,13 +116,10 @@ class ElectrumWindow(App):
|
||||
from .uix.dialogs.choice_dialog import ChoiceDialog
|
||||
chains = self.network.get_blockchains()
|
||||
def cb(name):
|
||||
with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items())
|
||||
for chain_id, b in blockchain_items:
|
||||
for index, b in blockchain.blockchains.items():
|
||||
if name == b.get_name():
|
||||
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
|
||||
chain_objects = [blockchain.blockchains.get(chain_id) for chain_id in chains]
|
||||
chain_objects = filter(lambda b: b is not None, chain_objects)
|
||||
names = [b.get_name() for b in chain_objects]
|
||||
self.network.follow_chain(index)
|
||||
names = [blockchain.blockchains[b].get_name() for b in chains]
|
||||
if len(names) > 1:
|
||||
cur_chain = self.network.blockchain().get_name()
|
||||
ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()
|
||||
@ -160,7 +141,7 @@ class ElectrumWindow(App):
|
||||
self.send_screen.set_URI(uri)
|
||||
|
||||
def on_new_intent(self, intent):
|
||||
if intent.getScheme() != 'flo':
|
||||
if intent.getScheme() != 'bitcoin':
|
||||
return
|
||||
uri = intent.getDataString()
|
||||
self.set_URI(uri)
|
||||
@ -180,7 +161,6 @@ class ElectrumWindow(App):
|
||||
|
||||
def on_history(self, d):
|
||||
Logger.info("on_history")
|
||||
self.wallet.clear_coin_price_cache()
|
||||
self._trigger_update_history()
|
||||
|
||||
def on_fee_histogram(self, *args):
|
||||
@ -200,8 +180,8 @@ class ElectrumWindow(App):
|
||||
self._trigger_update_status()
|
||||
self._trigger_update_history()
|
||||
|
||||
wallet_name = StringProperty(_('No Wallet'))
|
||||
base_unit = AliasProperty(_get_bu, _set_bu)
|
||||
status = StringProperty('')
|
||||
fiat_unit = StringProperty('')
|
||||
|
||||
def on_fiat_unit(self, a, b):
|
||||
@ -285,7 +265,7 @@ class ElectrumWindow(App):
|
||||
title = _('Electrum App')
|
||||
self.electrum_config = config = kwargs.get('config', None)
|
||||
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:
|
||||
self.num_blocks = self.network.get_local_height()
|
||||
self.num_nodes = len(self.network.get_interfaces())
|
||||
@ -293,9 +273,7 @@ class ElectrumWindow(App):
|
||||
self.server_host = net_params.host
|
||||
self.server_port = net_params.port
|
||||
self.auto_connect = net_params.auto_connect
|
||||
self.oneserver = net_params.oneserver
|
||||
self.proxy_config = net_params.proxy if net_params.proxy else {}
|
||||
self.update_proxy_str(self.proxy_config)
|
||||
|
||||
self.plugins = kwargs.get('plugins', [])
|
||||
self.gui_object = kwargs.get('gui_object', None)
|
||||
@ -316,6 +294,9 @@ class ElectrumWindow(App):
|
||||
self._password_dialog = None
|
||||
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):
|
||||
if not self.wallet:
|
||||
self.show_error(_('No wallet loaded.'))
|
||||
@ -509,8 +490,7 @@ class ElectrumWindow(App):
|
||||
activity.bind(on_new_intent=self.on_new_intent)
|
||||
# connect callbacks
|
||||
if self.network:
|
||||
interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
|
||||
'status', 'new_transaction', 'verified']
|
||||
interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces']
|
||||
self.network.register_callback(self.on_network_event, interests)
|
||||
self.network.register_callback(self.on_fee, ['fee'])
|
||||
self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
|
||||
@ -552,6 +532,8 @@ class ElectrumWindow(App):
|
||||
else:
|
||||
self.load_wallet(wallet)
|
||||
else:
|
||||
Logger.debug('Electrum: Wallet not found or action needed. Launching install wizard')
|
||||
|
||||
def launch_wizard():
|
||||
storage = WalletStorage(path, manual_upgrades=True)
|
||||
wizard = Factory.InstallWizard(self.electrum_config, self.plugins, storage)
|
||||
@ -562,6 +544,7 @@ class ElectrumWindow(App):
|
||||
launch_wizard()
|
||||
else:
|
||||
from .uix.dialogs.question import Question
|
||||
|
||||
def handle_answer(b: bool):
|
||||
if b:
|
||||
launch_wizard()
|
||||
@ -671,35 +654,26 @@ class ElectrumWindow(App):
|
||||
self.receive_screen = None
|
||||
self.requests_screen = None
|
||||
self.address_screen = None
|
||||
self.icon = "electrum/gui/icons/electrum.png"
|
||||
self.icon = "icons/electrum.png"
|
||||
self.tabs = self.root.ids['tabs']
|
||||
|
||||
def update_interfaces(self, dt):
|
||||
net_params = self.network.get_parameters()
|
||||
self.num_nodes = len(self.network.get_interfaces())
|
||||
self.num_chains = len(self.network.get_blockchains())
|
||||
chain = self.network.blockchain()
|
||||
self.blockchain_forkpoint = chain.get_max_forkpoint()
|
||||
self.blockchain_forkpoint = chain.get_forkpoint()
|
||||
self.blockchain_name = chain.get_name()
|
||||
interface = self.network.interface
|
||||
if interface:
|
||||
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):
|
||||
Logger.info('network event: '+ event)
|
||||
if event == 'network_updated':
|
||||
if event == 'interfaces':
|
||||
self._trigger_update_interfaces()
|
||||
self._trigger_update_status()
|
||||
elif event == 'wallet_updated':
|
||||
elif event == 'updated':
|
||||
self._trigger_update_wallet()
|
||||
self._trigger_update_status()
|
||||
elif event == 'blockchain_updated':
|
||||
# to update number of confirmations in history
|
||||
self._trigger_update_wallet()
|
||||
elif event == 'status':
|
||||
self._trigger_update_status()
|
||||
elif event == 'new_transaction':
|
||||
@ -712,7 +686,6 @@ class ElectrumWindow(App):
|
||||
if self.wallet:
|
||||
self.stop_wallet()
|
||||
self.wallet = wallet
|
||||
self.wallet_name = wallet.basename()
|
||||
self.update_wallet()
|
||||
# Once GUI has been initialized check if we want to announce something
|
||||
# since the callback has been called before the GUI was initialized
|
||||
@ -720,21 +693,17 @@ class ElectrumWindow(App):
|
||||
self.receive_screen.clear()
|
||||
self.update_tabs()
|
||||
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):
|
||||
self.num_blocks = self.network.get_local_height()
|
||||
if not self.wallet:
|
||||
self.status = _("No Wallet")
|
||||
return
|
||||
if self.network is None or not self.network.is_connected():
|
||||
status = _("Offline")
|
||||
elif self.network.is_connected():
|
||||
self.num_blocks = self.network.get_local_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:
|
||||
status = _("Synchronizing...")
|
||||
elif server_lag > 1:
|
||||
@ -743,10 +712,8 @@ class ElectrumWindow(App):
|
||||
status = ''
|
||||
else:
|
||||
status = _("Disconnected")
|
||||
if status:
|
||||
self.balance = status
|
||||
self.fiat_balance = status
|
||||
else:
|
||||
self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '')
|
||||
# balance
|
||||
c, u, x = self.wallet.get_balance()
|
||||
text = self.format_amount(c+x+u)
|
||||
self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
|
||||
@ -768,10 +735,6 @@ class ElectrumWindow(App):
|
||||
return ''
|
||||
except NotEnoughFunds:
|
||||
return ''
|
||||
except InternalAddressCorruption as e:
|
||||
self.show_error(str(e))
|
||||
send_exception_to_crash_reporter(e)
|
||||
return ''
|
||||
amount = tx.output_value()
|
||||
__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
|
||||
amount_after_all_fees = amount - x_fee_amount
|
||||
@ -799,7 +762,7 @@ class ElectrumWindow(App):
|
||||
notification.notify('Electrum', message,
|
||||
app_icon=icon, app_name='Electrum')
|
||||
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):
|
||||
self.pause_time = time.time()
|
||||
@ -829,6 +792,9 @@ class ElectrumWindow(App):
|
||||
self._clipboard.copy(label.data)
|
||||
Clock.schedule_once(lambda dt: self.show_info(_('Text copied to clipboard.\nTap again to display it as QR code.')))
|
||||
|
||||
def set_send(self, address, amount, label, message):
|
||||
self.send_payment(address, amount=amount, label=label, message=message)
|
||||
|
||||
def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
|
||||
exit=False, icon='atlas://electrum/gui/kivy/theming/light/error', duration=0,
|
||||
modal=False):
|
||||
@ -914,16 +880,8 @@ class ElectrumWindow(App):
|
||||
Clock.schedule_once(lambda dt: on_success(tx))
|
||||
|
||||
def _broadcast_thread(self, tx, on_complete):
|
||||
status = False
|
||||
try:
|
||||
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))
|
||||
ok, txid = self.network.broadcast_transaction_from_non_network_thread(tx)
|
||||
Clock.schedule_once(lambda dt: on_complete(ok, txid))
|
||||
|
||||
def broadcast(self, tx, pr=None):
|
||||
def on_complete(ok, msg):
|
||||
@ -936,7 +894,7 @@ class ElectrumWindow(App):
|
||||
self.wallet.invoices.save()
|
||||
self.update_tab('invoices')
|
||||
else:
|
||||
msg = msg or ''
|
||||
msg = msg[:500] if msg else _('There was an error broadcasting the transaction.')
|
||||
self.show_error(msg)
|
||||
|
||||
if self.network and self.network.is_connected():
|
||||
@ -954,13 +912,11 @@ class ElectrumWindow(App):
|
||||
d.open()
|
||||
|
||||
def flodata_dialog(self, screen):
|
||||
from .uix.dialogs.flodata_dialog import FLODataDialog
|
||||
from .uix.dialogs.label_dialog import LabelDialog
|
||||
text = screen.flodata
|
||||
|
||||
def callback(text):
|
||||
screen.flodata = text
|
||||
|
||||
d = FLODataDialog(_('Enter FLO data'), text, callback)
|
||||
d = LabelDialog(_('Enter FLO data'), text, callback)
|
||||
d.open()
|
||||
|
||||
def amount_dialog(self, screen, show_max):
|
||||
@ -1012,7 +968,6 @@ class ElectrumWindow(App):
|
||||
def on_fee(self, event, *arg):
|
||||
self.fee_status = self.electrum_config.get_fee_status()
|
||||
|
||||
|
||||
def protected(self, msg, f, args):
|
||||
if self.wallet.has_password():
|
||||
on_success = lambda pw: f(*(args + (pw,)))
|
||||
|
||||
@ -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')
|
||||
|
||||
class NFCBase(Widget):
|
||||
|
||||
@ -117,8 +117,8 @@ class ScannerAndroid(NFCBase):
|
||||
recTypes = []
|
||||
for record in ndefrecords:
|
||||
recTypes.append({
|
||||
'type': ''.join(map(chr, record.getType())),
|
||||
'payload': ''.join(map(chr, record.getPayload()))
|
||||
'type': ''.join(map(unichr, record.getType())),
|
||||
'payload': ''.join(map(unichr, record.getPayload()))
|
||||
})
|
||||
|
||||
details['recTypes'] = recTypes
|
||||
@ -165,20 +165,20 @@ class ScannerAndroid(NFCBase):
|
||||
def create_AAR(self):
|
||||
'''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):
|
||||
'''Create our actual payload record.
|
||||
'''
|
||||
if BUILDVERSION >= 14:
|
||||
domain = "org.electrum_flo"
|
||||
domain = "org.electrum"
|
||||
stype = "externalType"
|
||||
extRecord = NdefRecord.createExternal(domain, stype, data)
|
||||
else:
|
||||
# Creating the NdefRecord manually:
|
||||
extRecord = NdefRecord(
|
||||
NdefRecord.TNF_EXTERNAL_TYPE,
|
||||
"org.electrum_flo:externalType",
|
||||
"org.electrum:externalType",
|
||||
'',
|
||||
data)
|
||||
return extRecord
|
||||
@ -213,7 +213,7 @@ class ScannerAndroid(NFCBase):
|
||||
# Create record
|
||||
ndef_record = NdefRecord(
|
||||
NdefRecord.TNF_MIME_MEDIA,
|
||||
'org.electrum_flo.kivy', '', data)
|
||||
'org.electrum.kivy', '', data)
|
||||
|
||||
# Create message
|
||||
ndef_message = NdefMessage([ndef_record])
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
from . import NFCBase
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
from kivy.app import App
|
||||
|
||||
class ScannerDummy(NFCBase):
|
||||
'''This is the dummy interface that gets selected in case any other
|
||||
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 30 KiB |
BIN
electrum/gui/kivy/theming/light/script.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |