Merge branch 'multisig' of github.com:Coldcard/electrum into multisig

This commit is contained in:
Peter D. Gray 2019-08-02 14:41:59 -04:00
commit 5cd2c52869
No known key found for this signature in database
GPG Key ID: F0E6CC6AFC16CF7B
105 changed files with 2045 additions and 844 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ contrib/build-wine/tmp/
contrib/build-wine/fresh_clone/
contrib/build-linux/appimage/build/
contrib/build-linux/appimage/.cache/
contrib/android_debug.keystore

View File

@ -23,7 +23,7 @@ cache:
script:
- tox
after_success:
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install requests && contrib/make_locale; fi
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install requests && contrib/push_locale; fi
- coveralls
jobs:
include:
@ -44,13 +44,10 @@ jobs:
- 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
- pip install requests && ./contrib/pull_locale
- ./contrib/make_packages
- sudo docker build --no-cache -t electrum-android-builder-img electrum/gui/kivy/tools
script:

View File

@ -75,7 +75,7 @@ Compile the protobuf description file::
Create translations (optional)::
sudo apt-get install python-requests gettext
./contrib/make_locale
./contrib/pull_locale

View File

@ -1,3 +1,44 @@
# Release 3.3.8 - (July 11, 2019)
* fix some bugs with recent bump fee (RBF) improvements (#5483, #5502)
* fix #5491: watch-only wallets could not bump fee in some cases
* appimage: URLs could not be opened on some desktop environments (#5425)
* faster tx signing for segwit inputs for really large txns (#5494)
* A few other minor bugfixes and usability improvements.
# Release 3.3.7 - (July 3, 2019)
* The AppImage Linux x86_64 binary and the Windows setup.exe
(so now all Windows binaries) are now built reproducibly.
* Bump fee (RBF) improvements:
Implemented a new fee-bump strategy that can add new inputs,
so now any tx can be fee-bumped (d0a4366). The old strategy
was to decrease the value of outputs (starting with change).
We will now try the new strategy first, and only use the old
as a fallback (needed e.g. when spending "Max").
* CoinChooser improvements:
- more likely to construct txs without change (when possible)
- less likely to construct txs with really small change (e864fa5)
- will now only spend negative effective value coins when
beneficial for privacy (cb69aa8)
* fix long-standing bug that broke wallets with >65k addresses (#5366)
* Windows binaries: we now build the PyInstaller boot loader ourselves,
as this seems to reduce anti-virus false positives (1d0f679)
* Android: (fix) BIP70 payment requests could not be paid (#5376)
* Android: allow copy-pasting partial transactions from/to clipboard
* Fix a performance regression for large wallets (c6a54f0)
* Qt: fix some high DPI issues related to text fields (37809be)
* Trezor:
- allow bypassing "too old firmware" error (#5391)
- use only the Bridge to scan devices if it is available (#5420)
* hw wallets: (known issue) on Win10-1903, some hw devices
(that also have U2F functionality) can only be detected with
Administrator privileges. (see #5420 and #5437)
A workaround is to run as Admin, or for Trezor to install the Bridge.
* Several other minor bugfixes and usability improvements.
# Release 3.3.6 - (May 16, 2019)
* qt: fix crash during 2FA wallet creation (#5334)

View File

@ -1,19 +1,15 @@
Source tarballs
===============
1. Build locale files
✗ _This script does not produce reproducible output (yet!)._
```
contrib/make_locale
```
2. Prepare python dependencies used by Electrum.
1. Prepare python dependencies used by Electrum.
```
contrib/make_packages
```
3. Create source tarball.
2. Create source tarball.
```
contrib/make_tgz

View File

@ -21,7 +21,7 @@ RUN apt-get update -q && \
libudev-dev=204-5ubuntu20.31 \
gettext=0.18.3.1-1ubuntu3.1 \
libzbar0=0.10+doc-9build1 \
faketime=0.9.5-2 \
libdbus-1-3=1.6.18-0ubuntu4.5 \
&& \
rm -rf /var/lib/apt/lists/* && \
apt-get autoremove -y && \

View File

@ -1,9 +1,17 @@
AppImage binary for Electrum
============================
✓ _This binary should be reproducible, meaning you should be able to generate
binaries that match the official releases._
This assumes an Ubuntu host, but it should not be too hard to adapt to another
similar system. The docker commands should be executed in the project's root
folder.
similar system. The host architecture should be x86_64 (amd64).
The docker commands should be executed in the project's root folder.
We currently only build a single AppImage, for x86_64 architecture.
Help to adapt these scripts to build for (some flavor of) ARM would be welcome,
see [issue #5159](https://github.com/spesmilo/electrum/issues/5159).
1. Install Docker
@ -17,7 +25,7 @@ folder.
2. Build image
```
$ sudo docker build --no-cache -t electrum-appimage-builder-img contrib/build-linux/appimage
$ sudo docker build -t electrum-appimage-builder-img contrib/build-linux/appimage
```
3. Build binary

View File

@ -4,33 +4,37 @@ set -e
PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.."
CONTRIB="$PROJECT_ROOT/contrib"
CONTRIB_APPIMAGE="$CONTRIB/build-linux/appimage"
DISTDIR="$PROJECT_ROOT/dist"
BUILDDIR="$CONTRIB/build-linux/appimage/build/appimage"
BUILDDIR="$CONTRIB_APPIMAGE/build/appimage"
APPDIR="$BUILDDIR/electrum.AppDir"
CACHEDIR="$CONTRIB/build-linux/appimage/.cache/appimage"
CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage"
# pinned versions
PYTHON_VERSION=3.6.8
PKG2APPIMAGE_COMMIT="83483c2971fcaa1cb0c1253acd6c731ef8404381"
PKG2APPIMAGE_COMMIT="eb8f3acdd9f11ab19b78f5cb15daa772367daf15"
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
SQUASHFSKIT_COMMIT="ae0d656efa2d0df2fcac795b6823b44462f19386"
VERSION=`git describe --tags --dirty --always`
APPIMAGE="$DISTDIR/electrum-$VERSION-x86_64.AppImage"
. "$CONTRIB"/build_tools_util.sh
rm -rf "$BUILDDIR"
mkdir -p "$APPDIR" "$CACHEDIR" "$DISTDIR"
. "$CONTRIB"/build_tools_util.sh
# potential leftover from setuptools that might make pip put garbage in binary
rm -rf "$PROJECT_ROOT/build"
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"
verify_hash "$CACHEDIR/functions.sh" "78b7ee5a04ffb84ee1c93f0cb2900123773bc6709e5d1e43c37519f590f86918"
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/appimagetool" "https://github.com/AppImage/AppImageKit/releases/download/12/appimagetool-x86_64.AppImage"
verify_hash "$CACHEDIR/appimagetool" "d918b4df547b388ef253f3c9e7f6529ca81a885395c31f619d9aaf7030499a13"
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"
@ -42,18 +46,38 @@ tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILDDIR"
(
cd "$BUILDDIR/Python-$PYTHON_VERSION"
export SOURCE_DATE_EPOCH=1530212462
TZ=UTC faketime -f '2019-01-01 01:01:01' ./configure \
LC_ALL=C export BUILD_DATE=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%b %d %Y")
LC_ALL=C export BUILD_TIME=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%H:%M:%S")
# Patch taken from Ubuntu python3.6_3.6.8-1~18.04.1.debian.tar.xz
patch -p1 < "$CONTRIB_APPIMAGE/patches/python-3.6.8-reproducible-buildinfo.diff"
./configure \
--cache-file="$CACHEDIR/python.config.cache" \
--prefix="$APPDIR/usr" \
--enable-ipv6 \
--enable-shared \
--with-threads \
-q
TZ=UTC faketime -f '2019-01-01 01:01:01' make -s
make -s install > /dev/null
make -j4 -s || fail "Could not build Python"
make -s install > /dev/null || fail "Could not install Python"
# When building in docker on macOS, python builds with .exe extension because the
# case insensitive file system of macOS leaks into docker. This causes the build
# to result in a different output on macOS compared to Linux. We simply patch
# sysconfigdata to remove the extension.
# Some more info: https://bugs.python.org/issue27631
sed -i -e 's/\.exe//g' "$APPDIR"/usr/lib/python3.6/_sysconfigdata*
)
info "Building squashfskit"
git clone "https://github.com/squashfskit/squashfskit.git" "$BUILDDIR/squashfskit"
(
cd "$BUILDDIR/squashfskit"
git checkout "$SQUASHFSKIT_COMMIT"
make -C squashfs-tools mksquashfs || fail "Could not build squashfskit"
)
MKSQUASHFS="$BUILDDIR/squashfskit/squashfs-tools/mksquashfs"
info "building libsecp256k1."
(
git clone https://github.com/bitcoin-core/secp256k1 "$CACHEDIR"/secp256k1 \
@ -71,8 +95,8 @@ info "building libsecp256k1."
--enable-module-ecdh \
--disable-jni \
-q
make -s
make -s install > /dev/null
make -j4 -s || fail "Could not build libsecp"
make -s install > /dev/null || fail "Could not install libsecp"
)
@ -97,8 +121,7 @@ info "preparing electrum-locale."
pushd "$CONTRIB"/deterministic-build/electrum-locale
if ! which msgfmt > /dev/null 2>&1; then
echo "Please install gettext"
exit 1
fail "Please install gettext"
fi
for i in ./locale/*; do
dir="$PROJECT_ROOT/electrum/$i/LC_MESSAGES"
@ -111,10 +134,10 @@ info "preparing electrum-locale."
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"
"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements.txt"
"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-binaries.txt"
"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-hw.txt"
"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" "$PROJECT_ROOT"
info "copying zbar"
@ -127,7 +150,7 @@ cp "$PROJECT_ROOT/electrum/gui/icons/electrum.png" "$APPDIR/electrum.png"
# add launcher
cp "$CONTRIB/build-linux/appimage/apprun.sh" "$APPDIR/AppRun"
cp "$CONTRIB_APPIMAGE/apprun.sh" "$APPDIR/AppRun"
info "finalizing AppDir."
(
@ -136,25 +159,23 @@ info "finalizing AppDir."
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
) || fail "Could not finalize AppDir"
# We copy some libraries here that are on the AppImage excludelist
info "Copying additional libraries"
(
# On some systems it can cause problems to use the system libusb
cp -f /usr/lib/x86_64-linux-gnu/libusb-1.0.so "$APPDIR/usr/lib/libusb-1.0.so" || fail "Could not copy libusb"
)
# copy libusb here because it is on the AppImage excludelist and it can cause problems if we use system libusb
info "Copying libusb"
cp -f /usr/lib/x86_64-linux-gnu/libusb-1.0.so "$APPDIR/usr/lib/libusb-1.0.so" || fail "Could not copy libusb"
info "stripping binaries from debug symbols."
# "-R .note.gnu.build-id" also strips the build id
strip_binaries()
@ -175,23 +196,33 @@ 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
for component in Web Designer Qml Quick Location Test Xml ; do
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5${component}*
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt${component}*
rm -rf "$APPDIR"/usr/{share,include}
PYDIR="$APPDIR"/usr/lib/python3.6
rm -rf "$PYDIR"/{test,ensurepip,lib2to3,idlelib,turtledemo}
rm -rf "$PYDIR"/{ctypes,sqlite3,tkinter,unittest}/test
rm -rf "$PYDIR"/distutils/{command,tests}
rm -rf "$PYDIR"/config-3.6m-x86_64-linux-gnu
rm -rf "$PYDIR"/site-packages/{opt,pip,setuptools,wheel}
rm -rf "$PYDIR"/site-packages/Cryptodome/SelfTest
rm -rf "$PYDIR"/site-packages/{psutil,qrcode,websocket}/tests
for component in connectivity declarative help location multimedia quickcontrols2 serialport webengine websockets xmlpatterns ; do
rm -rf "$PYDIR"/site-packages/PyQt5/Qt/translations/qt${component}_*
rm -rf "$PYDIR"/site-packages/PyQt5/Qt/resources/qt${component}_*
done
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt.so
rm -rf "$PYDIR"/site-packages/PyQt5/Qt/{qml,libexec}
rm -rf "$PYDIR"/site-packages/PyQt5/{pyrcc.so,pylupdate.so,uic}
rm -rf "$PYDIR"/site-packages/PyQt5/Qt/plugins/{bearer,gamepads,geometryloaders,geoservices,playlistformats,position,renderplugins,sceneparsers,sensors,sqldrivers,texttospeech,webview}
for component in Bluetooth Concurrent Designer Help Location NetworkAuth Nfc Positioning PositioningQuick Qml Quick Sensors SerialPort Sql Test Web Xml ; do
rm -rf "$PYDIR"/site-packages/PyQt5/Qt/lib/libQt5${component}*
rm -rf "$PYDIR"/site-packages/PyQt5/Qt${component}*
done
rm -rf "$PYDIR"/site-packages/PyQt5/Qt.so
# these are deleted as they were not deterministic; and are not needed anyway
find "$APPDIR" -path '*/__pycache__*' -delete
rm "$APPDIR"/usr/lib/libsecp256k1.a
rm "$APPDIR"/usr/lib/python3.6/site-packages/pyblake2-*.dist-info/RECORD
rm "$APPDIR"/usr/lib/python3.6/site-packages/hidapi-*.dist-info/RECORD
rm -rf "$PYDIR"/site-packages/*.dist-info/
rm -rf "$PYDIR"/site-packages/*.egg-info/
find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +
@ -200,9 +231,19 @@ find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +
info "creating the AppImage."
(
cd "$BUILDDIR"
chmod +x "$CACHEDIR/appimagetool"
"$CACHEDIR/appimagetool" --appimage-extract
env VERSION="$VERSION" ARCH=x86_64 ./squashfs-root/AppRun --no-appstream --verbose "$APPDIR" "$APPIMAGE"
cp "$CACHEDIR/appimagetool" "$CACHEDIR/appimagetool_copy"
# zero out "appimage" magic bytes, as on some systems they confuse the linker
sed -i 's|AI\x02|\x00\x00\x00|' "$CACHEDIR/appimagetool_copy"
chmod +x "$CACHEDIR/appimagetool_copy"
"$CACHEDIR/appimagetool_copy" --appimage-extract
# We build a small wrapper for mksquashfs that removes the -mkfs-fixed-time option
# that mksquashfs from squashfskit does not support. It is not needed for squashfskit.
cat > ./squashfs-root/usr/lib/appimagekit/mksquashfs << EOF
#!/bin/sh
args=\$(echo "\$@" | sed -e 's/-mkfs-fixed-time 0//')
"$MKSQUASHFS" \$args
EOF
env VERSION="$VERSION" ARCH=x86_64 SOURCE_DATE_EPOCH=1530212462 ./squashfs-root/AppRun --no-appstream --verbose "$APPDIR" "$APPIMAGE"
)

View File

@ -0,0 +1,13 @@
# DP: Build getbuildinfo.o with DATE/TIME values when defined
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -741,6 +741,8 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \
-DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \
-DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \
-DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \
+ $(if $(BUILD_DATE),-DDATE='"$(BUILD_DATE)"') \
+ $(if $(BUILD_TIME),-DTIME='"$(BUILD_TIME)"') \
-o $@ $(srcdir)/Modules/getbuildinfo.c
Modules/getpath.o: $(srcdir)/Modules/getpath.c Makefile

View File

@ -1,10 +1,10 @@
Deterministic Windows binaries with Docker
==========================================
Windows binaries
================
Produced binaries are deterministic, so you should be able to generate
binaries that match the official releases.
✓ _These binaries should be reproducible, meaning you should be able to generate
binaries that match the official releases._
This assumes an Ubuntu host, but it should not be too hard to adapt to another
This assumes an Ubuntu (x86_64) 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.
@ -32,7 +32,7 @@ folder.
```
$ FRESH_CLONE=contrib/build-wine/fresh_clone && \
rm -rf $FRESH_CLONE && \
sudo rm -rf $FRESH_CLONE && \
mkdir -p $FRESH_CLONE && \
cd $FRESH_CLONE && \
git clone https://github.com/spesmilo/electrum.git && \
@ -54,9 +54,6 @@ folder.
Note: the `setup` binary (NSIS installer) is not deterministic yet.
Code Signing
============

View File

@ -4,6 +4,7 @@ NAME_ROOT=electrum
# These settings probably don't need any change
export WINEPREFIX=/opt/wine64
export WINEDEBUG=-all
export PYTHONDONTWRITEBYTECODE=1
export PYTHONHASHSEED=22
@ -12,25 +13,23 @@ PYTHON="wine $PYHOME/python.exe -OO -B"
# Let's begin!
cd `dirname $0`
set -e
mkdir -p tmp
cd tmp
here="$(dirname "$(readlink -e "$0")")"
. "$CONTRIB"/build_tools_util.sh
pushd $WINEPREFIX/drive_c/electrum
# Load electrum-locale for this release
git submodule init
git submodule update
VERSION=`git describe --tags --dirty --always`
echo "Last commit: $VERSION"
info "Last commit: $VERSION"
# Load electrum-locale for this release
git submodule update --init
pushd ./contrib/deterministic-build/electrum-locale
if ! which msgfmt > /dev/null 2>&1; then
echo "Please install gettext"
exit 1
fail "Please install gettext"
fi
for i in ./locale/*; do
dir=$WINEPREFIX/drive_c/electrum/electrum/$i/LC_MESSAGES
@ -42,22 +41,23 @@ popd
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
popd
cp $WINEPREFIX/drive_c/electrum/LICENCE .
# Install frozen dependencies
$PYTHON -m pip install -r ../../deterministic-build/requirements.txt
$PYTHON -m pip install --no-warn-script-location -r "$CONTRIB"/deterministic-build/requirements.txt
$PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt
$PYTHON -m pip install --no-warn-script-location -r "$CONTRIB"/deterministic-build/requirements-hw.txt
pushd $WINEPREFIX/drive_c/electrum
$PYTHON -m pip install .
# see https://github.com/pypa/pip/issues/2195 -- pip makes a copy of the entire directory
info "Pip installing Electrum. This might take a long time if the project folder is large."
$PYTHON -m pip install --no-warn-script-location .
popd
cd ..
rm -rf dist/
# build standalone and portable versions
info "Running pyinstaller..."
wine "$PYHOME/scripts/pyinstaller.exe" --noconfirm --ascii --clean --name $NAME_ROOT-$VERSION -w deterministic.spec
# set timestamps in dist, in order to make the installer reproducible
@ -65,7 +65,7 @@ pushd dist
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
popd
# build NSIS installer
info "building NSIS installer"
# $VERSION could be passed to the electrum.nsi script, but this would require some rewriting in the script itself.
wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" /DPRODUCT_VERSION=$VERSION electrum.nsi
@ -73,5 +73,46 @@ cd dist
mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe
cd ..
echo "Done."
sha256sum dist/electrum*exe
info "Padding binaries to 8-byte boundaries, and fixing COFF image checksum in PE header"
# note: 8-byte boundary padding is what osslsigncode uses:
# https://github.com/mtrojnar/osslsigncode/blob/6c8ec4427a0f27c145973450def818e35d4436f6/osslsigncode.c#L3047
(
cd dist
for binary_file in ./*.exe; do
info ">> fixing $binary_file..."
# code based on https://github.com/erocarrera/pefile/blob/bbf28920a71248ed5c656c81e119779c131d9bd4/pefile.py#L5877
python3 <<EOF
pe_file = "$binary_file"
with open(pe_file, "rb") as f:
binary = bytearray(f.read())
pe_offset = int.from_bytes(binary[0x3c:0x3c+4], byteorder="little")
checksum_offset = pe_offset + 88
checksum = 0
# Pad data to 8-byte boundary.
remainder = len(binary) % 8
binary += bytes(8 - remainder)
for i in range(len(binary) // 4):
if i == checksum_offset // 4: # Skip the checksum field
continue
dword = int.from_bytes(binary[i*4:i*4+4], byteorder="little")
checksum = (checksum & 0xffffffff) + dword + (checksum >> 32)
if checksum > 2 ** 32:
checksum = (checksum & 0xffffffff) + (checksum >> 32)
checksum = (checksum & 0xffff) + (checksum >> 16)
checksum = (checksum) + (checksum >> 16)
checksum = checksum & 0xffff
checksum += len(binary)
# Set the checksum
binary[checksum_offset : checksum_offset + 4] = int.to_bytes(checksum, byteorder="little", length=4)
with open(pe_file, "wb") as f:
f.write(binary)
EOF
done
)
sha256sum dist/electrum*.exe

View File

@ -3,6 +3,14 @@
set -e
here="$(dirname "$(readlink -e "$0")")"
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
. "$CONTRIB"/build_tools_util.sh
info "building libsecp256k1..."
build_dll() {
#sudo apt-get install -y mingw-w64
export SOURCE_DATE_EPOCH=1530212462
@ -14,28 +22,31 @@ build_dll() {
--enable-experimental \
--enable-module-ecdh \
--disable-jni
make
make -j4
${1}-strip .libs/libsecp256k1-0.dll
}
cd /tmp/electrum-build
cd "$CACHEDIR"
if [ -f "secp256k1/libsecp256k1.dll" ]; then
info "libsecp256k1.dll already built, skipping"
exit 0
fi
if [ ! -d secp256k1 ]; then
git clone https://github.com/bitcoin-core/secp256k1.git
cd secp256k1;
else
cd secp256k1
git pull
fi
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
git reset --hard "$LIBSECP_VERSION"
cd secp256k1
git reset --hard
git clean -f -x -q
git checkout $LIBSECP_VERSION
build_dll i686-w64-mingw32 # 64-bit would be: x86_64-w64-mingw32
mv .libs/libsecp256k1-0.dll libsecp256k1.dll
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
echo "building libsecp256k1 finished"
info "building libsecp256k1 finished"

View File

@ -1,28 +1,36 @@
#!/bin/bash
set -e
# Lucky number
export PYTHONHASHSEED=22
here=$(dirname "$0")
here="$(dirname "$(readlink -e "$0")")"
test -n "$here" -a -d "$here" || exit
echo "Clearing $here/build and $here/dist..."
export CONTRIB="$here/.."
export CACHEDIR="$here/.cache"
export PIP_CACHE_DIR="$CACHEDIR/pip_cache"
. "$CONTRIB"/build_tools_util.sh
info "Clearing $here/build and $here/dist..."
rm "$here"/build/* -rf
rm "$here"/dist/* -rf
mkdir -p /tmp/electrum-build
mkdir -p /tmp/electrum-build/pip-cache
export PIP_CACHE_DIR="/tmp/electrum-build/pip-cache"
mkdir -p "$CACHEDIR" "$PIP_CACHE_DIR"
$here/build-secp256k1.sh || exit 1
$here/build-secp256k1.sh || fail "build-secp256k1 failed"
$here/prepare-wine.sh || exit 1
$here/prepare-wine.sh || fail "prepare-wine failed"
echo "Resetting modification time in C:\Python..."
info "Resetting modification time in C:\Python..."
# (Because of some bugs in pyinstaller)
pushd /opt/wine64/drive_c/python*
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
popd
ls -l /opt/wine64/drive_c/python*
$here/build-electrum-git.sh && \
echo "Done."
$here/build-electrum-git.sh || fail "build-electrum-git failed"
info "Done."

View File

@ -0,0 +1,108 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: User-ID: Steve Dower (Python Release Signing) <steve.dower@microsoft.com>
Comment: Created: 2015-04-06 02:32
Comment: Type: 4096-bit RSA
Comment: Usage: Signing, Encryption, Certifying User-IDs
Comment: Fingerprint: 7ED10B6531D7C8E1BC296021FC624643487034E5
mQINBFUh1AUBEACdUPt6PwJVO23zGZqgtgBeA9JsO22dk3CMzrwPJdUmMd6mcRWa
vl4BoAba66fuC17GvOgGXimKI+iaw5Vt9QI3uSjUjFSfc24J8T7NB/yAr/0zEcex
raHD2dxT/JpE/iY0yWHxRlitvwGSw1Qlq3NnY8tDI1DJEJD+gBuCktvVvu1FfQTw
6bd+aEq0c4sWJHAOnKLuLH0pNFOznnynAFGPGBBsm/YwYc5BP2JVvka775LUjA+W
1h2Sgg3FAUPIm64pc4Pq6mUo6Tulw72xsWMpCL1/5atXNPXT6rJUOB8euTcNMr4l
1O6GKSsiLeLAuvq4bmhOKtLzjWzXnY1gDVoOfdgpD6o4ZHk4xiVsdVE8hCa/ylz8
1ZwRW2gGo2jP8t3hKciR2i+Qs+6lPNZpeFIxa6Uo9ER1IBgCHHapIR/UdcOFyoS0
MNn7Ui7DLQNM4gI/G17eG9tfvjW2dl4SgFSYWMq/OtXnPDUBGqFUWsn8adOL2PFL
B7kM5ZRTPc5SnY9hoSGa5E20rJZIXcpy1aygRz/xUjoKwNzAySSEyyIorUxZ8KaH
EEBQSsqwe04MXIENqnDozH0/cvP4JXEDSl8EkzMSCWSoavQSIYD5pQppyFQpGHqa
5CuOA25Ja+sgp2xqahtr3fEqZUknPQSoYlnJbaHnzsGSlRAVWMsklsZibQARAQAB
tEBTdGV2ZSBEb3dlciAoUHl0aG9uIFJlbGVhc2UgU2lnbmluZykgPHN0ZXZlLmRv
d2VyQG1pY3Jvc29mdC5jb20+iQEcBBABCAAGBQJYsBphAAoJEEhSKohZ29goZggI
ALKlgyoecD5v3ulh1eoctRqtCOxkAoENEfPt3l5x6N8Wq89yHzf10T1rVioEXOHh
Di1m37DDoQmRJD0sOYQymq10xDGRYAJjyOf3X0pvRkZ+F7T0U4dSV3DasLIHcN26
kRwv1yCYsf0QvhgT6EJZKyUNHtV9qrb9u3A1Zp6epC/EyT8zMZj+21GzTUrnbnug
3Ak9p7+APCZS4Ahh9ZHFuD38MZ7+OwrUd6ot+6cbb1nnQLSAGQOHSp6EP6ktrnsK
zts0L+tzHurxtJgUkR01imJuSFfYpLoZa/L7qXNyEpEUTC/SWzRWD9y2QkM7DLzX
caReVAyJr9rix1lDQbEFIquJAhwEEAEIAAYFAlW2TwMACgkQKeBHm5nIo5fahg/+
IQSSE/yH8Cf82PYI7IGqDVNwRw2o7dq8iscB+fhFHfFFhXANwUUFpzPeDMrMrdmq
Bke7Vg1D3bIFocXYOiNwf2J7f4mBO6OL0VAvDX02Vyh/C2ZSc15uZyU6CWFQMCG8
JOSmgQFs3kMHkL4qtut1Y5reoYesmteIe06UVyRw8yT1R1BkxP2whZ97qwsvUUE9
cVD08wCvH486efw7EswIzYGa1KcZXji0MvjXfksVtkEQQbxMMI7SVXo0345ZReww
buioGL5gvvAPObgU43skORanFHFxiHEKmqgHBHXK/LKqaFUFMKcb4iFTNs2XKrhE
XsEi5EMI1AFsJzjcXRqT50Wi2cZhXeRc70uF6gzqrdWvowa2oOPiO6zGDiTqZCW1
AArk/QBzGtPjVh+nKEdHwnvpK9913UAkAN682h8QkoVPYXOvIKDYZRBr5EfpUyQt
y2r9MYewz0YN4zlGP1PFS9FxncdSZiZJqQVif0CkOp1tdSxLynHcujQgATZNtgcu
X9JwUwPp60MurgOcIZiW3nZw/z/5vzBBadSa9/TIFSJAFNBlqeKdIGQuik0UH+Cz
RRtSFb38F7jMPwr0QUSktuntQ0HWuvNqj4N8DFm45/n5rN190eRotrVDXZmjGein
qWPITuICslGIKAp+Q6y3t7JA71MIbeu/ZY6ZcftOka6JAhwEEAEIAAYFAlZRWicA
CgkQxiNM8COVzQq5bRAAktnXceO3GCivMt9yR1Qr0Ov4A4Q+CJSIL45efLFmS30k
cbkHHtaq+0FZNh2ZaMartC16MUja4a2OUejg53VBhaSVkQrVk/6M/HA6/o6CvIhb
FW/5C+nRWBd5gfvwsWvjrtC3cKZco4wg+yYclkDbSH+2EPDZOKIHpBy46YTz9WQ1
8SJ51WVkNUNiZqRBA6Ny5GFoyd6EpWZYEPelmzNemv3zOrQdVzLV24/mLejcLL2t
KmI6ngX4XViXUCRUU3MH8/V+V2YTQGcTM/6HGaHpN0LTqknf6zEto9q9FiRTaiU2
kzExhBq8Qf+cVqwm+1kMt0FGOgpT47VBWMeUWq62gQ3h5NfAs4DfriLgNURlTC1d
JYAEquFhB/8oBQD1h/d9CjQyk88iib2pJInRBDsK2FcfQBap9iaeBFYoBWTzMQJx
g+RuWK1wIm2n0oqa5urBYZtRHE5RIdDP8ZLogrBOFkfXGJxlRBQD1Gab77qohdp0
SnErGw4Ne3gJH/SNhK+zzHkHERIrRZCR95zdYkKfZ2jyOPzSuABVRigEQVQPCDn0
hbv3cblTCeJYwG2mfRdmfyqSMALKIgXe9yvJ2kl8QgaVOsJjNfQzIKeoHFPIm5Uw
3YB6jgDFc5uzEaH7WSz74A7KhGYjC7huw2TugosHbWxphJKddwxfK1WujYaAeJyJ
AhwEEAEIAAYFAlf2sPIACgkQfb+tds3soNuXEQ//XkWYHmJsKyeDZC8MFU+/vsVq
dhnFs6UXZkvf7MoNFkuMDL+zgVoMpFHftTdyBqNAoEnndakk212jK8YWF8g4kQXI
a9uMRqJLM4mqCl9yco/twJ9z9EMA+JLSXYK0ZbTkLdutSDZEDKgpHbmekx2C1OsW
lRLs9PahF5PAZQs0N+m+LJBnw6bEHOSTv4OE5uVUf9nvdes3OARvkGSEGURNmUaF
chxWtZ/SF1q9Jfj0K/xgs9Gt855oueveRXLIGpjiEVoKH/drsgyKFMJVrpZDDgS4
GVXG8bq3GTFiMAs7BPPd9bjI+jgvqttgItZcYsW/IQK1BIoG6Fere4cPvu+IshCc
km9T8nOK98tZuov8hLbND9mW2d7LChJI1r/HbzbKIl0k6OigdFMrJlun2zmtDxT9
Tp3uxOYSaW2YggcpNUjI28tv6AwoA8okVY93LWjO5kdZGkbliRnf/eJy7NJYn0LO
ogsvMUJClRAGnZTHLEr32Whq0MImlXa43kr6oPJT5dwXXyw5ELstEQztczCd1PYB
kbQHUpD5j3PwgNVOinCnbd4pc/qVtYSqpg2g6TJi1XiJ1638jhn2k+i8wop/dyet
iN8lGR76twYGex9AavEAUpVR9r6qfpp4KBibEhdvL6o2O03RQu17GcRzXSAYzmUi
5U5jZ3dBz5MYUjgUZM+JAhwEEAEKAAYFAllTh9gACgkQXLNh5VL7DRAk7Q//X8eU
hwEvl/d9Sv2kBNCZFjAW3QmZp2L/sxhScJZXrOFzKUdmjap9Xlul1qr6/Wif7YLK
bOdNUI7KziEBn+9SEd90XauoVkzU2F0Jn9ILGQfUHAIpocRTKuCwBrncaBozHQwD
O3Dk33AhZ6lqTv/AVLRKHQXwigGTBJxK4cCEZ+VwK9tKk6BrQB48Rm7pg9HF5ey5
JGPRWgUnn1v0IJN5ysZ5m9ChYbqF8VwvMw0txmgKgvdDKpXbF/S59Bp4TH/7Dr2D
kAeNTcuzTFBaFE+siMgksZIYKZ1VkVoiN2qQA7ZaA5LQbUom0WdrKZGefFfPt9ES
A4wyL3OfxRsmWmd/5Fxrwm1VbzgPoMd1Dc5ExlyqnecdGzDui2bmltNqRJd9ytRq
6YUGYzXp4qQkWO61CoC3mkm2M8Ex7DGbUtXhdg0zoa08w9lXuOtHVhY7XlLWjO1U
p8cp4DVxsN/wOXtyH1pcleGo4aEsgyU/DH57prFLGz7Egp2JhRDHnZmlonWp74G1
VLfqkOqZlqTU4mPA827C8qPCx6cMsRvFS7OEiDBswkFWBKjkUCw4rLC1tBMBCxJW
tZlc+Y0LNyOryJ3h6EJmRIHO57oLen345e1WOi4ROOC/wQMErFk7B3P41Lqmrwb8
HGuKn3ca+Aw70hVrZ+7Q3RRFTLlOS/vv107Fqu6JAjkEEwEIACMFAlUh1AUCGwMH
CwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRD8YkZDSHA05RfdD/97wPXnoe7e
ipP7UXQ942z1buV6pTGv0Lea2aHn20o2BBjHp97YXroF/e/8W6h+Y+Fq8hWoXdYJ
dC9DVgzJhvbXAIG8VrF6/IDGQ62r4ff/AIyQY+kiCOCCVhjwuqOTjVYw2pYRUcI3
UwXVPeptDSXcIZkHCLtEUnS5YMTdkPuZrAmucCCnfcJtevXbHD2yJYP4vwfXMbal
sNBDKJi6uYAFc4yv+/DyS13rfXJvu2pYGvtRd+fs7mBETvUTubhI440pIss6TX6M
lxWexX6Ty8vI5HCQT281H4zqdbe5GdzGmIx1EiYx1sJbgSBNqCh5sRJY5/BXzVJ3
dfM/Mv5QYY4ulO/qUNFdC8f1cZm0euOo3maB4jY+Sjaff7t0WIz0GufO4dHARwJg
3s0LO9Wf5+z/fbWOMcfvvcfaHNbhaKWk16kslc/g7NYvMfOuleM06YGyGPz//a9c
baX53OiMupNvLlhyPO5NfGppvRn5xAElcAw1RLhHJcgvTtIs/zVVfHPaK41u8A9c
XKnmIUC39K4BGvOpPzEvCdQ2ZbAqzQLmZ1UICr15w1Nfs6uoERJbnuq+JgOPOcOk
ezAWELi5LdZTElnpJpZPTDQ03+3GvxD4R9sR+l5RT8Ul7kF+3PPPzekfQzF+Nisr
BhPFb2lPt3Hw32FgTTIuXCMRTKEBb/6z77kCDQRVIdQFARAAtmnsZ9A8ovJIJ9Rl
WeIylEhHRyQifqzgc/r50uDZVPBjewOA462LjH3+F6zFGEkU+q2aqSe0A0SJPF/W
hj6MNYXLoibxi5D4mGkoIao9ExnXt4LXAc6ogQpY6vFQBJU5Nr8XCefQbm0loa/o
y5uK8JHLWCZ2jAossnVpzDwNeN27+B8h5+OifnWhQCTun1xz5EJiyc0yoBmf46zf
mU4CMUBsPvrXcLmw4J3wp35qmrHg1tNyPhd7VBlikMrgtrWX9IaPZ40dnrGG/WjO
FYB3CKxGb0pTCj7GC4ubxo2upeWZqHLmdIVc7Nzsfp8EcwJbTj+jZ2Zfq6F8y+je
sbgh8CaxYn4hEs23aPYRq5H4/buVmZhUw3/AAL9ZmyX6AtAQ0HktVtQe7ykP7DLs
EpeLG+vPJFY363QeDsLHwOoxnZSfGziVlB4N/KqIkixNWcFTG8GSE1zKcdJVNoW+
3MB3+FtMZWUJhH0FyKg5qLaJCtC7Yo5gsddU+QCqTn6gcZBnMX5j4LaAmW4hh1RX
ffwwsbfviK5uhXQCeUnbUaokieetDx4s6Kay6t9ahTRr0r/Z3VWzvr+xATxNWZzi
xTdezCGOB2ycZ0vq4bKXBuN8CAyOy5X1hf7Rc1BiAVQCILHJDtz0Ak/Hax6DAa2A
Hnx9YlugHQf000KroLEY+GaxqYEAEQEAAYkCHwQYAQgACQUCVSHUBQIbDAAKCRD8
YkZDSHA05RtyEACdOEmGolL1xG6I+lDVdot6oBZqC9e021aLWqCUpWJFDp0m0aTm
CfmOI1gTaFjScxhq1W0GPUoJKUZhk3tlVfdSCtUckI+xuWKEfqJYtvUtTXpK4jDe
aZBovJ3KNpJRIynbr1566zCSQJhHiCGWmE/M5KN3gPsORbCBQXEkONSVsslf1Wm6
6hU6uqSWUaceD+4fl5LClbck1DPWchAP7+uLKPEOtORyH6KRTgKl73zYo7xU1K4Q
MN/1aMjobPkqNvvkXnUNwO7QMz18Nx+WqPc4ksJgW1O1aPQ2qL/ARY5jatZ6BBd7
iytfz7d6JOh0FOIlmhBqbWd7fEGrLsSA+EjBGBwW5BnIMmxP1xhjhwrcI18y8kAK
5UzdW2hbbAlc2rlsuxEc+xOYh8kGcc+mZ1j/aMn4gALsTbSO/0T+YJhfODNnL1dC
j7oPbJGmmG6pb/o7P4azBUVC9lHOuV3XlAPjSmJylnNsV7+PxwPlXlvKgh4S4C4Z
PUc/iPetsxXR2djccOoNxVU4CqJBqYKgul/pUphXkh7QfEKyH+42UETbVhstdBVU
azJ6SeUnv9ClVDGsCEhfEZfNOnOoDzJGxDfESoAw7ih91vIhTyHHsK83p2HLDMLP
ptLzx/0AFBfo6MWGGpd2RSnMWNbvh59wiThlDeI+Das3ln5nsAo67dMYdA==
=fjOq
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -13,11 +13,15 @@ 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
PYINSTALLER_REPO="https://github.com/SomberNight/pyinstaller.git"
PYINSTALLER_COMMIT=d1cdd726d6a9edc70150d5302453fb90fdd09bf2
# ^ tag 3.4, plus a custom commit that fixes cross-compilation with MinGW
PYTHON_VERSION=3.6.8
## These settings probably don't need change
export WINEPREFIX=/opt/wine64
#export WINEARCH='win32'
export WINEDEBUG=-all
PYTHON_FOLDER="python3"
PYHOME="c:/$PYTHON_FOLDER"
@ -25,60 +29,84 @@ PYTHON="wine $PYHOME/python.exe -OO -B"
# Let's begin!
here="$(dirname "$(readlink -e "$0")")"
set -e
. $here/../build_tools_util.sh
here="$(dirname "$(readlink -e "$0")")"
. "$CONTRIB"/build_tools_util.sh
info "Booting wine."
wine 'wineboot'
cd /tmp/electrum-build
cd "$CACHEDIR"
# Install Python
info "Installing Python."
# note: you might need "sudo apt-get install dirmngr" for the following
# keys from https://www.python.org/downloads/#pubkeys
KEYLIST_PYTHON_DEV="531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5"
KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg"
for server in $(shuf -e ha.pool.sks-keyservers.net \
hkp://p80.pool.sks-keyservers.net:80 \
keyserver.ubuntu.com \
hkp://keyserver.ubuntu.com:80) ; do
retry gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --keyserver "$server" --recv-keys $KEYLIST_PYTHON_DEV \
&& break || : ;
done
gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --import "$here"/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc
PYTHON_DOWNLOADS="$CACHEDIR/python$PYTHON_VERSION"
mkdir -p "$PYTHON_DOWNLOADS"
for msifile in core dev exe lib pip tools; do
echo "Installing $msifile..."
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
download_if_not_exist "$PYTHON_DOWNLOADS/${msifile}.msi" "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi"
download_if_not_exist "$PYTHON_DOWNLOADS/${msifile}.msi.asc" "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc"
verify_signature "$PYTHON_DOWNLOADS/${msifile}.msi.asc" $KEYRING_PYTHON_DEV
wine msiexec /i "$PYTHON_DOWNLOADS/${msifile}.msi" /qb TARGETDIR=$PYHOME
done
# Install dependencies specific to binaries
info "Installing 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
$PYTHON -m pip install --no-warn-script-location -r "$CONTRIB"/deterministic-build/requirements-binaries.txt
# Install PyInstaller
$PYTHON -m pip install pyinstaller==3.4 --no-use-pep517
info "Installing ZBar."
download_if_not_exist "$CACHEDIR/$ZBAR_FILENAME" "$ZBAR_URL"
verify_hash "$CACHEDIR/$ZBAR_FILENAME" "$ZBAR_SHA256"
wine "$CACHEDIR/$ZBAR_FILENAME" /S
# Install ZBar
download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL"
verify_hash $ZBAR_FILENAME "$ZBAR_SHA256"
wine "$PWD/$ZBAR_FILENAME" /S
# Install NSIS installer
download_if_not_exist $NSIS_FILENAME "$NSIS_URL"
verify_hash $NSIS_FILENAME "$NSIS_SHA256"
wine "$PWD/$NSIS_FILENAME" /S
download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL"
verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256"
7z x -olibusb $LIBUSB_FILENAME -aoa
info "Installing NSIS."
download_if_not_exist "$CACHEDIR/$NSIS_FILENAME" "$NSIS_URL"
verify_hash "$CACHEDIR/$NSIS_FILENAME" "$NSIS_SHA256"
wine "$CACHEDIR/$NSIS_FILENAME" /S
info "Installing libusb."
download_if_not_exist "$CACHEDIR/$LIBUSB_FILENAME" "$LIBUSB_URL"
verify_hash "$CACHEDIR/$LIBUSB_FILENAME" "$LIBUSB_SHA256"
7z x -olibusb "$CACHEDIR/$LIBUSB_FILENAME" -aoa
cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/$PYTHON_FOLDER/
mkdir -p $WINEPREFIX/drive_c/tmp
cp secp256k1/libsecp256k1.dll $WINEPREFIX/drive_c/tmp/
cp "$CACHEDIR/secp256k1/libsecp256k1.dll" $WINEPREFIX/drive_c/tmp/
echo "Wine is configured."
info "Building PyInstaller."
# we build our own PyInstaller boot loader as the default one has high
# anti-virus false positives
(
cd "$WINEPREFIX/drive_c/electrum"
ELECTRUM_COMMIT_HASH=$(git rev-parse HEAD)
cd "$CACHEDIR"
rm -rf pyinstaller
mkdir pyinstaller
cd pyinstaller
# Shallow clone
git init
git remote add origin $PYINSTALLER_REPO
git fetch --depth 1 origin $PYINSTALLER_COMMIT
git checkout FETCH_HEAD
rm -fv PyInstaller/bootloader/Windows-*/run*.exe || true
# add reproducible randomness. this ensures we build a different bootloader for each commit.
# if we built the same one for all releases, that might also get anti-virus false positives
echo "const char *electrum_tag = \"tagged by Electrum@$ELECTRUM_COMMIT_HASH\";" >> ./bootloader/src/pyi_main.c
pushd bootloader
# cross-compile to Windows using host python
python3 ./waf all CC=i686-w64-mingw32-gcc CFLAGS="-Wno-stringop-overflow -static"
popd
# sanity check bootloader is there:
[[ -e PyInstaller/bootloader/Windows-32bit/runw.exe ]] || fail "Could not find runw.exe in target dir!"
) || fail "PyInstaller build failed"
info "Installing PyInstaller."
$PYTHON -m pip install --no-warn-script-location ./pyinstaller
info "Wine is configured."

View File

@ -24,28 +24,8 @@ for mine in $(ls dist/*.exe); do
echo "Downloading https://download.electrum.org/$version/$f"
wget -q https://download.electrum.org/$version/$f -O signed/$f
out="signed/stripped/$f"
size=$( wc -c < $mine )
# Step 1: Remove PE signature from signed binary
# Remove PE signature from signed binary
osslsigncode remove-signature -in signed/$f -out $out > /dev/null 2>&1
# Step 2: Remove checksum and padding from signed binary
python3 <<EOF
pe_file = "$out"
size= $size
with open(pe_file, "rb") as f:
binary = bytearray(f.read())
pe_offset = int.from_bytes(binary[0x3c:0x3c+4], byteorder="little")
checksum_offset = pe_offset + 88
for b in range(4):
binary[checksum_offset + b] = 0
l = len(binary)
n = l - size
if n > 0:
if binary[-n:] != bytearray(n):
print('expecting failure for', str(pe_file))
binary = binary[:size]
with open(pe_file, "wb") as f:
f.write(binary)
EOF
chmod +x $out
if cmp -s $out $mine; then
echo "Success: $f"

@ -1 +1 @@
Subproject commit 4a960a5ea9157e4112b49860c6e35267c79ce91f
Subproject commit aafd932d37f35a1f276909b6ec27d2f7a60e606a

View File

@ -1,6 +1,6 @@
pip==19.1 \
--hash=sha256:8f59b6cf84584d7962d79fd1be7a8ec0eb198aa52ea864896551736b3614eee9 \
--hash=sha256:d9137cb543d8a4d73140a3282f6d777b2e786bb6abb8add3ac5b6539c82cd624
pip==19.1.1 \
--hash=sha256:44d3d7d3d30a1eb65c7e5ff1173cdf8f7467850605ac7cc3707b6064bddd0958 \
--hash=sha256:993134f0475471b91452ca029d4390dc8f298ac63a712814f101cd1b6db46676
pycryptodomex==3.7.3 \
--hash=sha256:0bda549e20db1eb8e29fb365d10acf84b224d813b1131c828fc830b2ce313dcd \
--hash=sha256:1210c0818e5334237b16d99b5785aa0cee815d9997ee258bd5e2936af8e8aa50 \
@ -51,6 +51,6 @@ PyQt5-sip==4.19.13 \
setuptools==41.0.1 \
--hash=sha256:a222d126f5471598053c9a77f4b5d4f26eaa1f150ad6e01dcf1a42e185d05613 \
--hash=sha256:c7769ce668c7a333d84e17fe8b524b1c45e7ee9f7908ad0a73e1eda7e6a5aebf
wheel==0.33.1 \
--hash=sha256:66a8fd76f28977bb664b098372daef2b27f60dc4d1688cfab7b37a09448f0e9d \
--hash=sha256:8eb4a788b3aec8abf5ff68d4165441bc57420c9f64ca5f471f58c3969fe08668
wheel==0.33.4 \
--hash=sha256:5e79117472686ac0c4aef5bad5172ea73a1c2d1646b808c35926bd26bdfb0c08 \
--hash=sha256:62fcfa03d45b5b722539ccbc07b190e4bfff4bb9e3a4d470dd9f6a0981002565

View File

@ -6,43 +6,14 @@ certifi==2019.3.9 \
chardet==3.0.4 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
ckcc-protocol==0.7.4 \
--hash=sha256:5af1d268a62e03997832b6300453c8f005630591df30a7156b450c80dd74a881 \
--hash=sha256:fb41a4c2fb22c0bd04356d14b0c6dbf3e708bc3ad080dddbc088bb48cda03699
ckcc-protocol==0.7.6 \
--hash=sha256:b2a782aa37b22dd21b5859618b84a69bc19271c279eb89fe63aba378916d07b0 \
--hash=sha256:f2e8181f9814959e4a6dfa3d1175c11b4e622a32a2ce2b311f64e5bcb3e7b271
click==7.0 \
--hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \
--hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7
construct==2.9.45 \
--hash=sha256:2271a0efd0798679dea825ff47e22a4c550456a5db0ba8baa82f7eae0af0118c
Cython==0.29.7 \
--hash=sha256:0ce8f6c789c907472c9084a44b625eba76a85d0189513de1497ab102a9d39ef8 \
--hash=sha256:0d67964b747ac09758ba31fe25da2f66f575437df5f121ff481889a7a4485f56 \
--hash=sha256:1630823619a87a814e5c1fa9f96544272ce4f94a037a34093fbec74989342328 \
--hash=sha256:1a4c634bb049c8482b7a4f3121330de1f1c1f66eac3570e1e885b0c392b6a451 \
--hash=sha256:1ec91cc09e9f9a2c3173606232adccc68f3d14be1a15a8c5dc6ab97b47b31528 \
--hash=sha256:237a8fdd8333f7248718875d930d1e963ffa519fefeb0756d01d91cbfadab0bc \
--hash=sha256:28a308cbfdf9b7bb44def918ad4a26b2d25a0095fa2f123addda33a32f308d00 \
--hash=sha256:2fe3dde34fa125abf29996580d0182c18b8a240d7fa46d10984cc28d27808731 \
--hash=sha256:30bda294346afa78c49a343e26f3ab2ad701e09f6a6373f579593f0cfcb1235a \
--hash=sha256:33d27ea23e12bf0d420e40c20308c03ef192d312e187c1f72f385edd9bd6d570 \
--hash=sha256:34d24d9370a6089cdd5afe56aa3c4af456e6400f8b4abb030491710ee765bafc \
--hash=sha256:4e4877c2b96fae90f26ee528a87b9347872472b71c6913715ca15c8fe86a68c9 \
--hash=sha256:50d6f1f26702e5f2a19890c7bc3de00f9b8a0ec131b52edccd56a60d02519649 \
--hash=sha256:55d081162191b7c11c7bfcb7c68e913827dfd5de6ecdbab1b99dab190586c1e8 \
--hash=sha256:59d339c7f99920ff7e1d9d162ea309b35775172e4bab9553f1b968cd43b21d6d \
--hash=sha256:6cf4d10df9edc040c955fca708bbd65234920e44c30fccd057ecf3128efb31ad \
--hash=sha256:6ec362539e2a6cf2329cd9820dec64868d8f0babe0d8dc5deff6c87a84d13f68 \
--hash=sha256:7edc61a17c14b6e54d5317b0300d2da23d94a719c466f93cafa3b666b058c43b \
--hash=sha256:8e37fc4db3f2c4e7e1ed98fe4fe313f1b7202df985de4ee1451d2e331332afae \
--hash=sha256:b8c996bde5852545507bff45af44328fa48a7b22b5bec2f43083f0b8d1024fd9 \
--hash=sha256:bf9c16f3d46af82f89fdefc0d64b2fb02f899c20da64548a8ea336beefcf8d23 \
--hash=sha256:c1038aba898bed34ab1b5ddb0d3f9c9ae33b0649387ab9ffe6d0af677f66bfc1 \
--hash=sha256:d405649c1bfc42e20d86178257658a859a3217b6e6d950ee8cb76353fcea9c39 \
--hash=sha256:db6eeb20a3bd60e1cdcf6ce9a784bc82aec6ab891c800dc5d7824d5cfbfe77f2 \
--hash=sha256:e382f8cb40dca45c3b439359028a4b60e74e22d391dc2deb360c0b8239d6ddc0 \
--hash=sha256:f3f6c09e2c76f2537d61f907702dd921b04d1c3972f01d5530ef1f748f22bd89 \
--hash=sha256:f749287087f67957c020e1de26906e88b8b0c4ea588facb7349c115a63346f67 \
--hash=sha256:f86b96e014732c0d1ded2c1f51444c80176a98c21856d0da533db4e4aef54070
ecdsa==0.13.2 \
--hash=sha256:20c17e527e75acad8f402290e158a6ac178b91b881f941fc6ea305bfdfb9657c \
--hash=sha256:5c034ffa23413ac923541ceb3ac14ec15a0d2530690413bff58c12b80e56d884
@ -65,35 +36,35 @@ keepkey==6.1.0 \
--hash=sha256:058548e733e1df8d1879ea747eef167c84cb04cdd685240e50d599f48d08e5c6 \
--hash=sha256:2e1623409307c86f709054ad191bc7707c4feeacae2e497bd933f2f0054c6eb0 \
--hash=sha256:54ef1b134657d3d14ef24c0c98e29d0276ad8f0e053d5e50d836ba8a520230e7
libusb1==1.7 \
--hash=sha256:9d4f66d2ed699986b06bc3082cd262101cb26af7a76a34bd15b7eb56cba37e0f
libusb1==1.7.1 \
--hash=sha256:adf64a4f3f5c94643a1286f8153bcf4bc787c348b38934aacd7fe17fbeebc571
mnemonic==0.18 \
--hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d
pbkdf2==1.3 \
--hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979
pip==19.1 \
--hash=sha256:8f59b6cf84584d7962d79fd1be7a8ec0eb198aa52ea864896551736b3614eee9 \
--hash=sha256:d9137cb543d8a4d73140a3282f6d777b2e786bb6abb8add3ac5b6539c82cd624
protobuf==3.7.1 \
--hash=sha256:21e395d7959551e759d604940a115c51c6347d90a475c9baf471a1a86b5604a9 \
--hash=sha256:57e05e16955aee9e6a0389fcbd58d8289dd2420e47df1a1096b3a232c26eb2dd \
--hash=sha256:67819e8e48a74c68d87f25cad9f40edfe2faf278cdba5ca73173211b9213b8c9 \
--hash=sha256:75da7d43a2c8a13b0bc7238ab3c8ae217cbfd5979d33b01e98e1f78defb2d060 \
--hash=sha256:78e08371e236f193ce947712c072542ff19d0043ab5318c2ea46bbc2aaebdca6 \
--hash=sha256:7ee5b595db5abb0096e8c4755e69c20dfad38b2d0bcc9bc7bafc652d2496b471 \
--hash=sha256:86260ecfe7a66c0e9d82d2c61f86a14aa974d340d159b829b26f35f710f615db \
--hash=sha256:92c77db4bd33ea4ee5f15152a835273f2338a5246b2cbb84bab5d0d7f6e9ba94 \
--hash=sha256:9c7b90943e0e188394b4f068926a759e3b4f63738190d1ab3d500d53b9ce7614 \
--hash=sha256:a77f217ea50b2542bae5b318f7acee50d9fc8c95dd6d3656eaeff646f7cab5ee \
--hash=sha256:ad589ed1d1f83db22df867b10e01fe445516a5a4d7cfa37fe3590a5f6cfc508b \
--hash=sha256:b06a794901bf573f4b2af87e6139e5cd36ac7c91ac85d7ae3fe5b5f6fc317513 \
--hash=sha256:bd8592cc5f8b4371d0bad92543370d4658dc41a5ccaaf105597eb5524c616291 \
--hash=sha256:be48e5a6248a928ec43adf2bea037073e5da692c0b3c10b34f9904793bd63138 \
--hash=sha256:cc5eb13f5ccc4b1b642cc147c2cdd121a34278b341c7a4d79e91182fff425836 \
--hash=sha256:cd3b0e0ad69b74ee55e7c321f52a98effed2b4f4cc9a10f3683d869de00590d5 \
--hash=sha256:d6e88c4920660aa75c0c2c4b53407aef5efd9a6e0ca7d2fc84d79aba2ccbda3a \
--hash=sha256:ec3c49b6d247152e19110c3a53d9bb4cf917747882017f70796460728b02722e \
--hash=sha256:f1f5d8b8e0bc9651d81b40ad3d9fb7cdd858ea31fc116dd230393465849dbecd
pip==19.1.1 \
--hash=sha256:44d3d7d3d30a1eb65c7e5ff1173cdf8f7467850605ac7cc3707b6064bddd0958 \
--hash=sha256:993134f0475471b91452ca029d4390dc8f298ac63a712814f101cd1b6db46676
protobuf==3.8.0 \
--hash=sha256:03f43eac9d5b651f976e91cf46a25b75e5779d98f0f4114b0abfed83376d75f8 \
--hash=sha256:0c94b21e6de01362f91a86b372555d22a60b59708599ca9d5032ae9fdf8e3538 \
--hash=sha256:2d2a9f30f61f4063fadd7fb68a2510a6939b43c0d6ceeec5c4704f22225da28e \
--hash=sha256:34a0b05fca061e4abb77dd180209f68d8637115ff319f51e28a6a9382d69853a \
--hash=sha256:358710fd0db25372edcf1150fa691f48376a134a6c69ce29f38f185eea7699e6 \
--hash=sha256:3761ab21883f1d3add8643413b326a0026776879b13ecf904e1e05fe18532c03 \
--hash=sha256:41e47198b94c27ba05a08b4a95160656105745c462af574e4bcb0807164065c0 \
--hash=sha256:8c61cc8a76e9d381c665aecc5105fa0f1878cf7db8b5cd17202603bcb386d0fc \
--hash=sha256:a6eebc4db759e58fdac02efcd3028b811effac881d8a5bad1996e4e8ee6acb47 \
--hash=sha256:a9c12f7c98093da0a46ba76ec40ace725daa1ac4038c41e4b1466afb5c45bb01 \
--hash=sha256:cb95068492ba0859b8c9e61fa8ba206a83c64e5d0916fb4543700b2e2b214115 \
--hash=sha256:cd98476ce7bb4dcd6a7b101f5eecdc073dafea19f311e36eb8fba1a349346277 \
--hash=sha256:ce64cfbea18c535176bdaa10ba740c0fc4c6d998a3f511c17bedb0ae4b3b167c \
--hash=sha256:dcbb59eac73fd454e8f2c5fba9e3d3320fd4707ed6a9d3ea3717924a6f0903ea \
--hash=sha256:dd67f34458ae716029e2a71ede998e9092493b62a519236ca52e3c5202096c87 \
--hash=sha256:e3c96056eb5b7284a20e256cb0bf783c8f36ad82a4ae5434a7b7cd02384144a7 \
--hash=sha256:f612d584d7a27e2f39e7b17878430a959c1bc09a74ba09db096b468558e5e126 \
--hash=sha256:f6de8a7d6122297b81566e5bd4df37fd5d62bec14f8f90ebff8ede1c9726cd0a \
--hash=sha256:fa529d9261682b24c2aaa683667253175c9acebe0a31105394b221090da75832
pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
pyblake2==1.1.2 \
@ -106,9 +77,9 @@ 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.22.0 \
--hash=sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4 \
--hash=sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31
safet==0.1.4 \
--hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \
--hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1
@ -118,19 +89,19 @@ setuptools==41.0.1 \
six==1.12.0 \
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
trezor==0.11.2 \
--hash=sha256:7bdec3d6e35e41666580547674f2652c0c466172964da42b325cab2c30b4eb46 \
--hash=sha256:a6f4b47b37a21247535fc43411cb70a8c61ef0a5a2dfee668bd05611e2741fb8
trezor==0.11.3 \
--hash=sha256:c79a500e90d003073c8060d319dceb042caaba9472f13990c77ed37d04a82108 \
--hash=sha256:f3a99ec0fe7b28f83f936f87bf6ad89c77fef9f576934efc3a70dd569009ded1
typing-extensions==3.7.2 \
--hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \
--hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \
--hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71
urllib3==1.24.3 \
--hash=sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4 \
--hash=sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb
urllib3==1.25.3 \
--hash=sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1 \
--hash=sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232
websocket_client==0.56.0 \
--hash=sha256:1151d5fb3a62dc129164292e1227655e4bbc5dd5340a5165dfae61128ec50aa9 \
--hash=sha256:1fd5520878b68b84b5748bb30e592b10d0a91529d5383f74f4964e72b297fd3a
wheel==0.33.1 \
--hash=sha256:66a8fd76f28977bb664b098372daef2b27f60dc4d1688cfab7b37a09448f0e9d \
--hash=sha256:8eb4a788b3aec8abf5ff68d4165441bc57420c9f64ca5f471f58c3969fe08668
wheel==0.33.4 \
--hash=sha256:5e79117472686ac0c4aef5bad5172ea73a1c2d1646b808c35926bd26bdfb0c08 \
--hash=sha256:62fcfa03d45b5b722539ccbc07b190e4bfff4bb9e3a4d470dd9f6a0981002565

View File

@ -24,9 +24,9 @@ aiohttp==3.5.4 \
aiohttp-socks==0.2.2 \
--hash=sha256:e473ee222b001fe33798957b9ce3352b32c187cf41684f8e2259427925914993 \
--hash=sha256:eebd8939a7c3c1e3e7e1b2552c60039b4c65ef6b8b2351efcbdd98290538e310
aiorpcX==0.17.0 \
--hash=sha256:13ccc8361bc3049d649094b69aead6118f6deb5f1b88ad77211be85c4e2ed792 \
--hash=sha256:b08e7c350c78701ec698c851b405a07d20ac64380c394440c1740b48bb3c5502
aiorpcX==0.18.3 \
--hash=sha256:42e354c3e0088cb99a4a46e6f7ca777a08d989519ca1bc46323fef836e25579b \
--hash=sha256:b7a7ced5df95c79c74f7834e7cc58bb7747dbad9eb37bf7580da507e182ca44c
async_timeout==3.0.1 \
--hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \
--hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3
@ -83,29 +83,29 @@ multidict==4.5.2 \
--hash=sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9 \
--hash=sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7 \
--hash=sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b
pip==19.1 \
--hash=sha256:8f59b6cf84584d7962d79fd1be7a8ec0eb198aa52ea864896551736b3614eee9 \
--hash=sha256:d9137cb543d8a4d73140a3282f6d777b2e786bb6abb8add3ac5b6539c82cd624
protobuf==3.7.1 \
--hash=sha256:21e395d7959551e759d604940a115c51c6347d90a475c9baf471a1a86b5604a9 \
--hash=sha256:57e05e16955aee9e6a0389fcbd58d8289dd2420e47df1a1096b3a232c26eb2dd \
--hash=sha256:67819e8e48a74c68d87f25cad9f40edfe2faf278cdba5ca73173211b9213b8c9 \
--hash=sha256:75da7d43a2c8a13b0bc7238ab3c8ae217cbfd5979d33b01e98e1f78defb2d060 \
--hash=sha256:78e08371e236f193ce947712c072542ff19d0043ab5318c2ea46bbc2aaebdca6 \
--hash=sha256:7ee5b595db5abb0096e8c4755e69c20dfad38b2d0bcc9bc7bafc652d2496b471 \
--hash=sha256:86260ecfe7a66c0e9d82d2c61f86a14aa974d340d159b829b26f35f710f615db \
--hash=sha256:92c77db4bd33ea4ee5f15152a835273f2338a5246b2cbb84bab5d0d7f6e9ba94 \
--hash=sha256:9c7b90943e0e188394b4f068926a759e3b4f63738190d1ab3d500d53b9ce7614 \
--hash=sha256:a77f217ea50b2542bae5b318f7acee50d9fc8c95dd6d3656eaeff646f7cab5ee \
--hash=sha256:ad589ed1d1f83db22df867b10e01fe445516a5a4d7cfa37fe3590a5f6cfc508b \
--hash=sha256:b06a794901bf573f4b2af87e6139e5cd36ac7c91ac85d7ae3fe5b5f6fc317513 \
--hash=sha256:bd8592cc5f8b4371d0bad92543370d4658dc41a5ccaaf105597eb5524c616291 \
--hash=sha256:be48e5a6248a928ec43adf2bea037073e5da692c0b3c10b34f9904793bd63138 \
--hash=sha256:cc5eb13f5ccc4b1b642cc147c2cdd121a34278b341c7a4d79e91182fff425836 \
--hash=sha256:cd3b0e0ad69b74ee55e7c321f52a98effed2b4f4cc9a10f3683d869de00590d5 \
--hash=sha256:d6e88c4920660aa75c0c2c4b53407aef5efd9a6e0ca7d2fc84d79aba2ccbda3a \
--hash=sha256:ec3c49b6d247152e19110c3a53d9bb4cf917747882017f70796460728b02722e \
--hash=sha256:f1f5d8b8e0bc9651d81b40ad3d9fb7cdd858ea31fc116dd230393465849dbecd
pip==19.1.1 \
--hash=sha256:44d3d7d3d30a1eb65c7e5ff1173cdf8f7467850605ac7cc3707b6064bddd0958 \
--hash=sha256:993134f0475471b91452ca029d4390dc8f298ac63a712814f101cd1b6db46676
protobuf==3.8.0 \
--hash=sha256:03f43eac9d5b651f976e91cf46a25b75e5779d98f0f4114b0abfed83376d75f8 \
--hash=sha256:0c94b21e6de01362f91a86b372555d22a60b59708599ca9d5032ae9fdf8e3538 \
--hash=sha256:2d2a9f30f61f4063fadd7fb68a2510a6939b43c0d6ceeec5c4704f22225da28e \
--hash=sha256:34a0b05fca061e4abb77dd180209f68d8637115ff319f51e28a6a9382d69853a \
--hash=sha256:358710fd0db25372edcf1150fa691f48376a134a6c69ce29f38f185eea7699e6 \
--hash=sha256:3761ab21883f1d3add8643413b326a0026776879b13ecf904e1e05fe18532c03 \
--hash=sha256:41e47198b94c27ba05a08b4a95160656105745c462af574e4bcb0807164065c0 \
--hash=sha256:8c61cc8a76e9d381c665aecc5105fa0f1878cf7db8b5cd17202603bcb386d0fc \
--hash=sha256:a6eebc4db759e58fdac02efcd3028b811effac881d8a5bad1996e4e8ee6acb47 \
--hash=sha256:a9c12f7c98093da0a46ba76ec40ace725daa1ac4038c41e4b1466afb5c45bb01 \
--hash=sha256:cb95068492ba0859b8c9e61fa8ba206a83c64e5d0916fb4543700b2e2b214115 \
--hash=sha256:cd98476ce7bb4dcd6a7b101f5eecdc073dafea19f311e36eb8fba1a349346277 \
--hash=sha256:ce64cfbea18c535176bdaa10ba740c0fc4c6d998a3f511c17bedb0ae4b3b167c \
--hash=sha256:dcbb59eac73fd454e8f2c5fba9e3d3320fd4707ed6a9d3ea3717924a6f0903ea \
--hash=sha256:dd67f34458ae716029e2a71ede998e9092493b62a519236ca52e3c5202096c87 \
--hash=sha256:e3c96056eb5b7284a20e256cb0bf783c8f36ad82a4ae5434a7b7cd02384144a7 \
--hash=sha256:f612d584d7a27e2f39e7b17878430a959c1bc09a74ba09db096b468558e5e126 \
--hash=sha256:f6de8a7d6122297b81566e5bd4df37fd5d62bec14f8f90ebff8ede1c9726cd0a \
--hash=sha256:fa529d9261682b24c2aaa683667253175c9acebe0a31105394b221090da75832
pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
QDarkStyle==2.6.8 \
@ -124,9 +124,9 @@ typing-extensions==3.7.2 \
--hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \
--hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \
--hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71
wheel==0.33.1 \
--hash=sha256:66a8fd76f28977bb664b098372daef2b27f60dc4d1688cfab7b37a09448f0e9d \
--hash=sha256:8eb4a788b3aec8abf5ff68d4165441bc57420c9f64ca5f471f58c3969fe08668
wheel==0.33.4 \
--hash=sha256:5e79117472686ac0c4aef5bad5172ea73a1c2d1646b808c35926bd26bdfb0c08 \
--hash=sha256:62fcfa03d45b5b722539ccbc07b190e4bfff4bb9e3a4d470dd9f6a0981002565
yarl==1.3.0 \
--hash=sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9 \
--hash=sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f \

View File

@ -8,7 +8,7 @@ PACKAGES="$ROOT_FOLDER"/packages/
LOCALE="$ROOT_FOLDER"/electrum/locale/
if [ ! -d "$LOCALE" ]; then
echo "Run make_locale first!"
echo "Run pull_locale first!"
exit 1
fi
@ -30,6 +30,17 @@ if [[ -n "$1" && "$1" == "release" ]] ; then
export P4A_RELEASE_KEYALIAS=electrum
make release
else
export P4A_DEBUG_KEYSTORE="$CONTRIB"/android_debug.keystore
export P4A_DEBUG_KEYSTORE_PASSWD=unsafepassword
export P4A_DEBUG_KEYALIAS_PASSWD=unsafepassword
export P4A_DEBUG_KEYALIAS=electrum
if [ ! -f "$P4A_DEBUG_KEYSTORE" ]; then
keytool -genkey -v -keystore "$CONTRIB"/android_debug.keystore \
-alias "$P4A_DEBUG_KEYALIAS" -keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=mqttserver.ibm.com, OU=ID, O=IBM, L=Hursley, S=Hants, C=GB" \
-storepass "$P4A_DEBUG_KEYSTORE_PASSWD" \
-keypass "$P4A_DEBUG_KEYALIAS_PASSWD"
fi
make apk
fi

View File

@ -2,8 +2,15 @@
import re
import os
import sys
import importlib
from electrum.version import ELECTRUM_VERSION, APK_VERSION
# load version.py; needlessly complicated alternative to "imp.load_source":
version_spec = importlib.util.spec_from_file_location('version', 'electrum/version.py')
version_module = importlib.util.module_from_spec(version_spec)
version_spec.loader.exec_module(version_module)
ELECTRUM_VERSION = version_module.ELECTRUM_VERSION
APK_VERSION = version_module.APK_VERSION
print("version", ELECTRUM_VERSION)
dirname = sys.argv[1]

View File

@ -7,16 +7,28 @@ 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
git submodule update --init
(
rm -rf "$LOCALE"
cd "$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="$ROOT_FOLDER"/electrum/$i/LC_MESSAGES
mkdir -p $dir
msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true
cp $i/electrum.po "$ROOT_FOLDER"/electrum/$i/electrum.po
done
)
(
cd "$ROOT_FOLDER"

View File

@ -1,6 +1,9 @@
Building Mac OS binaries
========================
✗ _This script does not produce reproducible output (yet!).
Please help us remedy this._
This guide explains how to build Electrum binaries for macOS systems.
@ -10,6 +13,9 @@ This needs to be done on a system running macOS or OS X. We use El Capitan (10.1
on High Sierra (or later)
makes the binaries [incompatible with older versions](https://github.com/pyinstaller/pyinstaller/issues/1191).
Another factor for the minimum supported macOS version is the
[bundled Qt version](https://github.com/spesmilo/electrum/issues/3685).
Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`).
#### 1.1a Get Xcode

View File

@ -21,3 +21,7 @@ function DoCodeSignMaybe { # ARGS: infoName fileOrDirName codesignIdentity
info "Code signing ${infoName}..."
codesign -f -v $deep -s "$identity" "$file" || fail "Could not code sign ${infoName}"
}
function realpath() {
[[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}

View File

@ -9,6 +9,10 @@ LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
. $(dirname "$0")/base.sh
CONTRIB_OSX="$(dirname "$(realpath "$0")")"
CONTRIB="$CONTRIB_OSX/.."
ROOT_FOLDER="$CONTRIB/.."
src_dir=$(dirname "$0")
cd $src_dir/../..
@ -65,13 +69,24 @@ pyinstaller --version
rm -rf ./dist
git submodule init
git submodule update
git submodule update --init
rm -rf $BUILDDIR > /dev/null 2>&1
mkdir $BUILDDIR
cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./electrum/locale/
info "generating locale"
(
if ! which msgfmt > /dev/null 2>&1; then
brew install gettext
brew link --force gettext
fi
cd "$CONTRIB"/deterministic-build/electrum-locale
for i in ./locale/*; do
dir="$ROOT_FOLDER"/electrum/$i/LC_MESSAGES
mkdir -p $dir
msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true
done
) || fail "failed generating locale"
info "Downloading libusb..."
@ -89,7 +104,7 @@ git reset --hard $LIBSECP_VERSION
git clean -f -x -q
./autogen.sh
./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --disable-jni
make
make -j4
popd
cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/osx

View File

@ -34,28 +34,6 @@ os.chdir('electrum')
crowdin_identifier = 'electrum'
crowdin_file_name = 'files[electrum-client/messages.pot]'
locale_file_name = 'locale/messages.pot'
crowdin_api_key = None
filename = os.path.expanduser('~/.crowdin_api_key')
if os.path.exists(filename):
with open(filename) as f:
crowdin_api_key = f.read().strip()
if "crowdin_api_key" in os.environ:
crowdin_api_key = os.environ["crowdin_api_key"]
if crowdin_api_key:
# Push to Crowdin
print('Push to Crowdin')
url = ('https://api.crowdin.com/api/project/' + crowdin_identifier + '/update-file?key=' + crowdin_api_key)
with open(locale_file_name, 'rb') as f:
files = {crowdin_file_name: f}
response = requests.request('POST', url, files=files)
print("", "update-file:", "-"*20, response.text, "-"*20, sep="\n")
# Build translations
print('Build translations')
response = requests.request('GET', 'https://api.crowdin.com/api/project/' + crowdin_identifier + '/export?key=' + crowdin_api_key)
print("", "export:", "-" * 20, response.text, "-" * 20, sep="\n")
# Download & unzip
print('Download translations')

59
contrib/push_locale Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
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>'")
os.chdir(os.path.dirname(os.path.realpath(__file__)))
os.chdir('..')
cmd = "find electrum -type f -name '*.py' -o -name '*.kv'"
files = subprocess.check_output(cmd, shell=True)
with open("app.fil", "wb") as f:
f.write(files)
print("Found {} files to translate".format(len(files.splitlines())))
# Generate fresh translation template
if not os.path.exists('electrum/locale'):
os.mkdir('electrum/locale')
cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=electrum/locale/messages.pot'
print('Generate template')
os.system(cmd)
os.chdir('electrum')
crowdin_identifier = 'electrum'
crowdin_file_name = 'files[electrum-client/messages.pot]'
locale_file_name = 'locale/messages.pot'
crowdin_api_key = None
filename = os.path.expanduser('~/.crowdin_api_key')
if os.path.exists(filename):
with open(filename) as f:
crowdin_api_key = f.read().strip()
if "crowdin_api_key" in os.environ:
crowdin_api_key = os.environ["crowdin_api_key"]
if crowdin_api_key:
# Push to Crowdin
print('Push to Crowdin')
url = ('https://api.crowdin.com/api/project/' + crowdin_identifier + '/update-file?key=' + crowdin_api_key)
with open(locale_file_name, 'rb') as f:
files = {crowdin_file_name: f}
response = requests.request('POST', url, files=files)
print("", "update-file:", "-"*20, response.text, "-"*20, sep="\n")
# Build translations
print('Build translations')
response = requests.request('GET', 'https://api.crowdin.com/api/project/' + crowdin_identifier + '/export?key=' + crowdin_api_key)
print("", "export:", "-" * 20, response.text, "-" * 20, sep="\n")

View File

@ -1,4 +1,3 @@
Cython>=0.27
trezor[hidapi]>=0.11.0
safet[hidapi]>=0.1.0
keepkey>=6.0.3

View File

@ -4,8 +4,8 @@ qrcode
protobuf
dnspython
jsonrpclib-pelix
qdarkstyle<3.0
aiorpcx>=0.17,<0.18
qdarkstyle<2.7
aiorpcx>=0.18,<0.19
aiohttp>=3.3.0
aiohttp_socks
certifi

View File

@ -2,16 +2,17 @@
set -e
host=$1
version=`git describe --tags`
echo $version
here=$(dirname "$0")
cd $here/../dist
sftp -oBatchMode=no -b - thomasv@download.electrum.org << !
sftp -oBatchMode=no -b - thomasv@$host << !
cd electrum-downloads
mkdir $version
cd $version
mput *
bye
!
!

View File

@ -38,8 +38,8 @@ from .i18n import _
from .logging import Logger
if TYPE_CHECKING:
from .storage import WalletStorage
from .network import Network
from .json_db import JsonDB
TX_HEIGHT_LOCAL = -2
@ -60,9 +60,8 @@ class AddressSynchronizer(Logger):
inherited by wallet
"""
def __init__(self, storage: 'WalletStorage'):
self.storage = storage
self.db = self.storage.db
def __init__(self, db: 'JsonDB'):
self.db = db
self.network = None # type: Network
Logger.__init__(self)
# verifier (SPV) and synchronizer are started in start_network
@ -155,7 +154,7 @@ class AddressSynchronizer(Logger):
def on_blockchain_updated(self, event, *args):
self._get_addr_balance_cache = {} # invalidate cache
def stop_threads(self, write_to_disk=True):
def stop_threads(self):
if self.network:
if self.synchronizer:
asyncio.run_coroutine_threadsafe(self.synchronizer.stop(), self.network.asyncio_loop)
@ -164,9 +163,7 @@ class AddressSynchronizer(Logger):
asyncio.run_coroutine_threadsafe(self.verifier.stop(), self.network.asyncio_loop)
self.verifier = None
self.network.unregister_callback(self.on_blockchain_updated)
self.storage.put('stored_height', self.get_local_height())
if write_to_disk:
self.storage.write()
self.db.put('stored_height', self.get_local_height())
def add_address(self, address):
if not self.db.get_addr_history(address):
@ -192,7 +189,8 @@ class AddressSynchronizer(Logger):
if spending_tx_hash is None:
continue
# this outpoint has already been spent, by spending_tx
assert self.db.get_transaction(spending_tx_hash)
# annoying assert that has revealed several bugs over time:
assert self.db.get_transaction(spending_tx_hash), "spending tx not in wallet db"
conflicting_txns |= {spending_tx_hash}
if tx_hash in conflicting_txns:
# this tx is already in history, so it conflicts with itself
@ -366,12 +364,10 @@ class AddressSynchronizer(Logger):
@profiler
def check_history(self):
save = False
hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.db.get_history()))
hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.db.get_history()))
for addr in hist_addrs_not_mine:
self.db.remove_addr_history(addr)
save = True
for addr in hist_addrs_mine:
hist = self.db.get_addr_history(addr)
for tx_hash, tx_height in hist:
@ -380,9 +376,6 @@ class AddressSynchronizer(Logger):
tx = self.db.get_transaction(tx_hash)
if tx is not None:
self.add_transaction(tx_hash, tx, allow_unrelated=True)
save = True
if save:
self.storage.write()
def remove_local_transactions_we_dont_have(self):
for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()):
@ -394,7 +387,6 @@ class AddressSynchronizer(Logger):
with self.lock:
with self.transaction_lock:
self.db.clear_history()
self.storage.write()
def get_txpos(self, tx_hash):
"""Returns (height, txpos) tuple, even if the tx is unverified."""
@ -556,7 +548,7 @@ class AddressSynchronizer(Logger):
cached_local_height = getattr(self.threadlocal_cache, 'local_height', None)
if cached_local_height is not None:
return cached_local_height
return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
return self.network.get_local_height() if self.network else self.db.get('stored_height', 0)
def get_tx_height(self, tx_hash: str) -> TxMinedInfo:
with self.lock:
@ -576,8 +568,6 @@ class AddressSynchronizer(Logger):
self.up_to_date = up_to_date
if self.network:
self.network.notify('status')
if up_to_date:
self.storage.write()
def is_up_to_date(self):
with self.lock: return self.up_to_date

View File

@ -31,10 +31,10 @@ from .version import ELECTRUM_VERSION
from . import constants
from .i18n import _
from .util import make_aiohttp_session
from .logging import describe_os_version
from .logging import describe_os_version, Logger
class BaseCrashReporter:
class BaseCrashReporter(Logger):
report_server = "https://crashhub.electrum.org"
config_key = "show_crash_reporter"
issue_template = """<h2>Traceback</h2>
@ -59,9 +59,10 @@ class BaseCrashReporter:
ASK_CONFIRM_SEND = _("Do you want to send this report?")
def __init__(self, exctype, value, tb):
Logger.__init__(self)
self.exc_args = (exctype, value, tb)
def send_report(self, asyncio_loop, proxy, endpoint="/crash"):
def send_report(self, asyncio_loop, proxy, endpoint="/crash", *, timeout=None):
if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
# Gah! Some kind of altcoin wants to send us crash reports.
raise Exception(_("Missing report URL."))
@ -69,7 +70,7 @@ class BaseCrashReporter:
report.update(self.get_additional_info())
report = json.dumps(report)
coro = self.do_post(proxy, BaseCrashReporter.report_server + endpoint, data=report)
response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(5)
response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout)
return response
async def do_post(self, proxy, url, data):
@ -130,3 +131,20 @@ class BaseCrashReporter:
def get_wallet_type(self):
raise NotImplementedError
def trigger_crash():
# note: do not change the type of the exception, the message,
# or the name of this method. All reports generated through this
# method will be grouped together by the crash reporter, and thus
# don't spam the issue tracker.
class TestingException(Exception):
pass
def crash_test():
raise TestingException("triggered crash for testing purposes")
import threading
t = threading.Thread(target=crash_test)
t.start()

View File

@ -44,6 +44,7 @@ from .util import UserCancelled, InvalidPassword, WalletFileException
from .simple_config import SimpleConfig
from .plugin import Plugins, HardwarePluginLibraryUnavailable
from .logging import Logger
from .plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HW_PluginBase
if TYPE_CHECKING:
from .plugin import DeviceInfo
@ -297,14 +298,16 @@ class BaseWizard(Logger):
if not debug_msg:
debug_msg = ' {}'.format(_('No exceptions encountered.'))
if not devices:
msg = ''.join([
_('No hardware device detected.') + '\n',
_('To trigger a rescan, press \'Next\'.') + '\n\n',
_('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ',
_('On Linux, you might have to add a new permission to your udev rules.') + '\n\n',
_('Debug message') + '\n',
debug_msg
])
msg = (_('No hardware device detected.') + '\n' +
_('To trigger a rescan, press \'Next\'.') + '\n\n')
if sys.platform == 'win32':
msg += _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", '
'and do "Remove device". Then, plug your device again.') + '\n'
msg += _('While this is less than ideal, it might help if you run Electrum as Administrator.') + '\n'
else:
msg += _('On Linux, you might have to add a new permission to your udev rules.') + '\n'
msg += '\n\n'
msg += _('Debug message') + '\n' + debug_msg
self.confirm_dialog(title=title, message=msg,
run_next=lambda x: self.choose_hw_device(purpose, storage=storage))
return
@ -323,7 +326,7 @@ class BaseWizard(Logger):
run_next=lambda *args: self.on_device(*args, purpose=purpose, storage=storage))
def on_device(self, name, device_info, *, purpose, storage=None):
self.plugin = self.plugins.get_plugin(name)
self.plugin = self.plugins.get_plugin(name) # type: HW_PluginBase
try:
self.plugin.setup_device(device_info, self, purpose)
except OSError as e:
@ -335,6 +338,14 @@ class BaseWizard(Logger):
devmgr.unpair_id(device_info.device.id_)
self.choose_hw_device(purpose, storage=storage)
return
except OutdatedHwFirmwareException as e:
if self.question(e.text_ignore_old_fw_and_continue(), title=_("Outdated device firmware")):
self.plugin.set_ignore_outdated_fw()
# will need to re-pair
devmgr = self.plugins.device_manager
devmgr.unpair_id(device_info.device.id_)
self.choose_hw_device(purpose, storage=storage)
return
except (UserCancelled, GoBack):
self.choose_hw_device(purpose, storage=storage)
return

View File

@ -200,6 +200,8 @@ class BIP32Node(NamedTuple):
return isinstance(self.eckey, ecc.ECPrivkey)
def subkey_at_private_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node':
if path is None:
raise Exception("derivation path must not be None")
if isinstance(path, str):
path = convert_bip32_path_to_list_of_uint32(path)
if not self.is_private():
@ -224,6 +226,8 @@ class BIP32Node(NamedTuple):
child_number=child_number)
def subkey_at_public_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node':
if path is None:
raise Exception("derivation path must not be None")
if isinstance(path, str):
path = convert_bip32_path_to_list_of_uint32(path)
if not path:

View File

@ -24,7 +24,8 @@
# SOFTWARE.
from collections import defaultdict
from math import floor, log10
from typing import NamedTuple, List
from typing import NamedTuple, List, Callable
from decimal import Decimal
from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
from .transaction import Transaction, TxOutput
@ -74,11 +75,18 @@ class Bucket(NamedTuple):
desc: str
weight: int # as in BIP-141
value: int # in satoshis
effective_value: int # estimate of value left after subtracting fees. in satoshis
coins: List[dict] # UTXOs
min_height: int # min block height where a coin was confirmed
witness: bool # whether any coin uses segwit
class ScoredCandidate(NamedTuple):
penalty: float
tx: Transaction
buckets: List[Bucket]
def strip_unneeded(bkts, sufficient_funds):
'''Remove buckets that are unnecessary in achieving the spend amount'''
if sufficient_funds([], bucket_value_sum=0):
@ -103,11 +111,14 @@ class CoinChooserBase(Logger):
def keys(self, coins):
raise NotImplementedError
def bucketize_coins(self, coins):
def bucketize_coins(self, coins, *, fee_estimator_vb):
keys = self.keys(coins)
buckets = defaultdict(list)
for key, coin in zip(keys, coins):
buckets[key].append(coin)
# fee_estimator returns fee to be paid, for given vbytes.
# guess whether it is just returning a constant as follows.
constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200)
def make_Bucket(desc, coins):
witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in coins)
@ -117,16 +128,30 @@ class CoinChooserBase(Logger):
for coin in coins)
value = sum(coin['value'] for coin in coins)
min_height = min(coin['height'] for coin in coins)
return Bucket(desc, weight, value, coins, min_height, witness)
# the fee estimator is typically either a constant or a linear function,
# so the "function:" effective_value(bucket) will be homomorphic for addition
# i.e. effective_value(b1) + effective_value(b2) = effective_value(b1 + b2)
if constant_fee:
effective_value = value
else:
# when converting from weight to vBytes, instead of rounding up,
# keep fractional part, to avoid overestimating fee
fee = fee_estimator_vb(Decimal(weight) / 4)
effective_value = value - fee
return Bucket(desc=desc,
weight=weight,
value=value,
effective_value=effective_value,
coins=coins,
min_height=min_height,
witness=witness)
return list(map(make_Bucket, buckets.keys(), buckets.values()))
def penalty_func(self, tx):
def penalty(candidate):
return 0
return penalty
def penalty_func(self, base_tx, *, tx_from_buckets) -> Callable[[List[Bucket]], ScoredCandidate]:
raise NotImplementedError
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
def _change_amounts(self, tx, count, fee_estimator_numchange) -> List[int]:
# Break change up if bigger than max_change
output_amounts = [o.value for o in tx.outputs()]
# Don't split change of less than 0.02 BTC
@ -135,7 +160,7 @@ class CoinChooserBase(Logger):
# Use N change outputs
for n in range(1, count + 1):
# How much is left if we add this many change outputs?
change_amount = max(0, tx.get_fee() - fee_estimator(n))
change_amount = max(0, tx.get_fee() - fee_estimator_numchange(n))
if change_amount // n <= max_change:
break
@ -172,7 +197,7 @@ class CoinChooserBase(Logger):
# no more than 10**max_dp_to_round_for_privacy
# e.g. a max of 2 decimal places means losing 100 satoshis to fees
max_dp_to_round_for_privacy = 2 if self.enable_output_value_rounding else 0
N = pow(10, min(max_dp_to_round_for_privacy, zeroes[0]))
N = int(pow(10, min(max_dp_to_round_for_privacy, zeroes[0])))
amount = (remaining // N) * N
amounts.append(amount)
@ -180,84 +205,25 @@ class CoinChooserBase(Logger):
return amounts
def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold):
amounts = self.change_amounts(tx, len(change_addrs), fee_estimator,
dust_threshold)
def _change_outputs(self, tx, change_addrs, fee_estimator_numchange, dust_threshold):
amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange)
assert min(amounts) >= 0
assert len(change_addrs) >= len(amounts)
assert all([isinstance(amt, int) for amt in amounts])
# If change is above dust threshold after accounting for the
# size of the change output, add it to the transaction.
dust = sum(amount for amount in amounts if amount < dust_threshold)
amounts = [amount for amount in amounts if amount >= dust_threshold]
change = [TxOutput(TYPE_ADDRESS, addr, amount)
for addr, amount in zip(change_addrs, amounts)]
self.logger.info(f'change: {change}')
if dust:
self.logger.info(f'not keeping dust {dust}')
return change
def make_tx(self, coins, inputs, 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
the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.
Note: fee_estimator expects virtual bytes
"""
# Deterministic randomness from coins
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
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()
# 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()
def fee_estimator_w(weight):
return fee_estimator(Transaction.virtual_size_from_weight(weight))
def get_tx_weight(buckets):
total_weight = base_weight + sum(bucket.weight for bucket in buckets)
is_segwit_tx = any(bucket.witness for bucket in buckets)
if is_segwit_tx:
total_weight += 2 # marker and flag
# non-segwit inputs were previously assumed to have
# a witness of '' instead of '00' (hex)
# note that mixed legacy/segwit buckets are already ok
num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins)
for bucket in buckets)
total_weight += num_legacy_inputs
return total_weight
def sufficient_funds(buckets, *, bucket_value_sum):
'''Given a list of buckets, return True if it has enough
value to pay for the transaction'''
# assert bucket_value_sum == sum(bucket.value for bucket in buckets) # expensive!
total_input = input_value + bucket_value_sum
if total_input < spent_amount: # shortcut for performance
return False
# note re performance: so far this was constant time
# what follows is linear in len(buckets)
total_weight = get_tx_weight(buckets)
return total_input >= spent_amount + fee_estimator_w(total_weight)
# Collect the coins into buckets, choose a subset of the buckets
buckets = self.bucketize_coins(coins)
buckets = self.choose_buckets(buckets, sufficient_funds,
self.penalty_func(tx))
def _construct_tx_from_selected_buckets(self, *, buckets, base_tx, change_addrs,
fee_estimator_w, dust_threshold, base_weight):
# make a copy of base_tx so it won't get mutated
tx = Transaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:])
tx.add_inputs([coin for b in buckets for coin in b.coins])
tx_weight = get_tx_weight(buckets)
tx_weight = self._get_tx_weight(buckets, base_weight=base_weight)
# change is sent back to sending address unless specified
if not change_addrs:
@ -268,16 +234,104 @@ class CoinChooserBase(Logger):
# This takes a count of change outputs and returns a tx fee
output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
fee_estimator_numchange = lambda count: fee_estimator_w(tx_weight + count * output_weight)
change = self._change_outputs(tx, change_addrs, fee_estimator_numchange, dust_threshold)
tx.add_outputs(change)
return tx, change
def _get_tx_weight(self, buckets, *, base_weight) -> int:
"""Given a collection of buckets, return the total weight of the
resulting transaction.
base_weight is the weight of the tx that includes the fixed (non-change)
outputs and potentially some fixed inputs. Note that the change outputs
at this point are not yet known so they are NOT accounted for.
"""
total_weight = base_weight + sum(bucket.weight for bucket in buckets)
is_segwit_tx = any(bucket.witness for bucket in buckets)
if is_segwit_tx:
total_weight += 2 # marker and flag
# non-segwit inputs were previously assumed to have
# a witness of '' instead of '00' (hex)
# note that mixed legacy/segwit buckets are already ok
num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins)
for bucket in buckets)
total_weight += num_legacy_inputs
return total_weight
def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator_vb,
dust_threshold):
"""Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.
`inputs` and `outputs` are guaranteed to be a subset of the
inputs and outputs of the resulting transaction.
`coins` are further UTXOs we can choose from.
Note: fee_estimator_vb expects virtual bytes
"""
# Deterministic randomness from coins
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
self.p = PRNG(''.join(sorted(utxos)))
# Copy the outputs so when adding change we don't modify "outputs"
base_tx = Transaction.from_io(inputs[:], outputs[:])
input_value = base_tx.input_value()
# 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 = base_tx.estimated_weight()
spent_amount = base_tx.output_value()
def fee_estimator_w(weight):
return fee_estimator_vb(Transaction.virtual_size_from_weight(weight))
def sufficient_funds(buckets, *, bucket_value_sum):
'''Given a list of buckets, return True if it has enough
value to pay for the transaction'''
# assert bucket_value_sum == sum(bucket.value for bucket in buckets) # expensive!
total_input = input_value + bucket_value_sum
if total_input < spent_amount: # shortcut for performance
return False
# note re performance: so far this was constant time
# what follows is linear in len(buckets)
total_weight = self._get_tx_weight(buckets, base_weight=base_weight)
return total_input >= spent_amount + fee_estimator_w(total_weight)
def tx_from_buckets(buckets):
return self._construct_tx_from_selected_buckets(buckets=buckets,
base_tx=base_tx,
change_addrs=change_addrs,
fee_estimator_w=fee_estimator_w,
dust_threshold=dust_threshold,
base_weight=base_weight)
# Collect the coins into buckets
all_buckets = self.bucketize_coins(coins, fee_estimator_vb=fee_estimator_vb)
# Filter some buckets out. Only keep those that have positive effective value.
# Note that this filtering is intentionally done on the bucket level
# instead of per-coin, as each bucket should be either fully spent or not at all.
# (e.g. CoinChooserPrivacy ensures that same-address coins go into one bucket)
all_buckets = list(filter(lambda b: b.effective_value > 0, all_buckets))
# Choose a subset of the buckets
scored_candidate = self.choose_buckets(all_buckets, sufficient_funds,
self.penalty_func(base_tx, tx_from_buckets=tx_from_buckets))
tx = scored_candidate.tx
self.logger.info(f"using {len(tx.inputs())} inputs")
self.logger.info(f"using buckets: {[bucket.desc for bucket in buckets]}")
self.logger.info(f"using buckets: {[bucket.desc for bucket in scored_candidate.buckets]}")
return tx
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
def choose_buckets(self, buckets, sufficient_funds,
penalty_func: Callable[[List[Bucket]], ScoredCandidate]) -> ScoredCandidate:
raise NotImplemented('To be subclassed')
@ -312,8 +366,7 @@ class CoinChooserRandom(CoinChooserBase):
candidates.add(tuple(sorted(permutation[:count + 1])))
break
else:
# FIXME this assumes that the effective value of any bkt is >= 0
# we should make sure not to choose buckets with <= 0 eff. val.
# note: this assumes that the effective value of any bkt is >= 0
raise NotEnoughFunds()
candidates = [[buckets[n] for n in c] for c in candidates]
@ -359,12 +412,14 @@ class CoinChooserRandom(CoinChooserBase):
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
candidates = self.bucket_candidates_prefer_confirmed(buckets, sufficient_funds)
penalties = [penalty_func(cand) for cand in candidates]
winner = candidates[penalties.index(min(penalties))]
self.logger.info(f"Bucket sets: {len(buckets)}")
self.logger.info(f"Winning penalty: {min(penalties)}")
scored_candidates = [penalty_func(cand) for cand in candidates]
winner = min(scored_candidates, key=lambda x: x.penalty)
self.logger.info(f"Total number of buckets: {len(buckets)}")
self.logger.info(f"Num candidates considered: {len(candidates)}. "
f"Winning penalty: {winner.penalty}")
return winner
class CoinChooserPrivacy(CoinChooserRandom):
"""Attempts to better preserve user privacy.
First, if any coin is spent from a user address, all coins are.
@ -379,24 +434,28 @@ class CoinChooserPrivacy(CoinChooserRandom):
def keys(self, coins):
return [coin['address'] for coin in coins]
def penalty_func(self, tx):
min_change = min(o.value for o in tx.outputs()) * 0.75
max_change = max(o.value for o in tx.outputs()) * 1.33
spent_amount = sum(o.value for o in tx.outputs())
def penalty_func(self, base_tx, *, tx_from_buckets):
min_change = min(o.value for o in base_tx.outputs()) * 0.75
max_change = max(o.value for o in base_tx.outputs()) * 1.33
def penalty(buckets):
def penalty(buckets) -> ScoredCandidate:
# Penalize using many buckets (~inputs)
badness = len(buckets) - 1
total_input = sum(bucket.value for bucket in buckets)
# FIXME "change" here also includes fees
change = float(total_input - spent_amount)
tx, change_outputs = tx_from_buckets(buckets)
change = sum(o.value for o in change_outputs)
# Penalize change not roughly in output range
if change < min_change:
if change == 0:
pass # no change is great!
elif change < min_change:
badness += (min_change - change) / (min_change + 10000)
# Penalize really small change; under 1 mBTC ~= using 1 more input
if change < COIN / 1000:
badness += 1
elif change > max_change:
badness += (change - max_change) / (max_change + 10000)
# Penalize large change; 5 BTC excess ~= using 1 more input
badness += change / (COIN * 5)
return badness
return ScoredCandidate(badness, tx, buckets)
return penalty

View File

@ -415,7 +415,7 @@ class Commands:
addr = self.wallet.import_private_key(privkey, password)
out = "Keypair imported: " + addr
except Exception as e:
out = "Error: " + str(e)
out = "Error: " + repr(e)
return out
def _resolver(self, x):

View File

@ -39,6 +39,10 @@ def read_json(filename, default):
return r
GIT_REPO_URL = "https://github.com/spesmilo/electrum"
GIT_REPO_ISSUES_URL = "https://github.com/spesmilo/electrum/issues"
class AbstractNet:
@classmethod

View File

@ -352,10 +352,7 @@
],
"Bitcointoyou": [
"BRL"
],
"Bitmarket": [
"PLN"
],
],
"Bitso": [
"MXN"
],
@ -895,4 +892,4 @@
"JPY"
],
"itBit": []
}
}

View File

@ -265,7 +265,7 @@ def query(url, rtype):
out = get_and_validate(ns, url, rtype)
validated = True
except BaseException as e:
_logger.info(f"DNSSEC error: {str(e)}")
_logger.info(f"DNSSEC error: {repr(e)}")
resolver = dns.resolver.get_default_resolver()
out = resolver.query(url, rtype)
validated = False

View File

@ -190,13 +190,6 @@ class BitFlyer(ExchangeBase):
return {'JPY': Decimal(json['mid'])}
class Bitmarket(ExchangeBase):
async def get_rates(self, ccy):
json = await self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
return {'PLN': Decimal(json['last'])}
class BitPay(ExchangeBase):
async def get_rates(self, ccy):
@ -563,7 +556,8 @@ class FxThread(ThreadJob):
self.logger.info(f"using exchange {name}")
if self.config_exchange() != name:
self.config.set_key('use_exchange', name, True)
self.exchange = class_(self.on_quotes, self.on_history)
assert issubclass(class_, ExchangeBase), f"unexpected type {class_} for {name}"
self.exchange = class_(self.on_quotes, self.on_history) # type: ExchangeBase
# A new exchange means new fx quotes, initially empty. Force
# a quote refresh
self.trigger_update()
@ -614,9 +608,11 @@ class FxThread(ThreadJob):
rate = self.exchange.historical_rate(self.ccy, d_t)
# Frequently there is no rate for today, until tomorrow :)
# Use spot quotes in that case
if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2:
if rate in ('NaN', None) and (datetime.today().date() - d_t.date()).days <= 2:
rate = self.exchange.quotes.get(self.ccy, 'NaN')
self.history_used_spot = True
if rate is None:
rate = 'NaN'
return Decimal(rate)
def historical_value_str(self, satoshis, d_t):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -5,7 +5,10 @@ To generate an APK file, follow these instructions.
## Android binary with Docker
This assumes an Ubuntu host, but it should not be too hard to adapt to another
✗ _This script does not produce reproducible output (yet!).
Please help us remedy this._
This assumes an Ubuntu (x86_64) 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.
@ -27,7 +30,7 @@ folder.
3. Build locale files
```
$ ./contrib/make_locale
$ ./contrib/pull_locale
```
4. Prepare pure python dependencies
@ -78,13 +81,34 @@ $ sudo docker run -it --rm \
```
### How do I get more verbose logs?
### How do I get more verbose logs for the build?
See `log_level` in `buildozer.spec`
### How can I see logs at runtime?
This should work OK for most scenarios:
```
adb logcat | grep python
```
Better `grep` but fragile because of `cut`:
```
adb logcat | grep -F "`adb shell ps | grep org.electrum.electrum | cut -c14-19`"
```
### 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`
### debug vs release build
If you just follow the instructions above, you will build the apk
in debug mode. The most notable difference is that the apk will be
signed using a debug keystore. If you are planning to upload
what you build to e.g. the Play Store, you should create your own
keystore, back it up safely, and run `./contrib/make_apk release`.
See e.g. [kivy wiki](https://github.com/kivy/kivy/wiki/Creating-a-Release-APK)
and [android dev docs](https://developer.android.com/studio/build/building-cmdline#sign_cmdline).

View File

@ -14,7 +14,7 @@ from electrum.wallet import Wallet, InternalAddressCorruption
from electrum.paymentrequest import InvoiceStore
from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
from electrum.plugin import run_hook
from electrum.util import format_satoshis, format_satoshis_plain
from electrum.util import format_satoshis, format_satoshis_plain, format_fee_satoshis
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
from electrum import blockchain
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
@ -435,7 +435,8 @@ class ElectrumWindow(App):
msg += '\n' + _('Text copied to clipboard.')
self._clipboard.copy(text_for_clipboard)
Clock.schedule_once(lambda dt: self.show_info(msg))
popup = QRDialog(title, data, show_text, on_qr_failure)
popup = QRDialog(title, data, show_text, failure_cb=on_qr_failure,
text_for_clipboard=text_for_clipboard)
popup.open()
def scan_qr(self, on_complete):
@ -807,6 +808,10 @@ class ElectrumWindow(App):
def format_amount_and_units(self, x):
return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit
def format_fee_rate(self, fee_rate):
# fee_rate is in sat/kB
return format_fee_satoshis(fee_rate/1000) + ' sat/byte'
#@profiler
def update_wallet(self, *dt):
self._trigger_update_status()

View File

@ -127,6 +127,8 @@ USER ${USER}
RUN python3 -m pip install --upgrade cython==0.28.6
RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install --user wheel
# prepare git
RUN git config --global user.name "John Doe" \
@ -136,8 +138,8 @@ RUN git config --global user.name "John Doe" \
RUN cd /opt \
&& git clone https://github.com/kivy/buildozer \
&& cd buildozer \
&& git checkout 88e4a4b0c7733eec1d14c00579ec412fb59ad7f2 \
&& python3 -m pip install -e .
&& git checkout 678b1bf52cf63daa51b06e86a43ea4e2ea8a0b24 \
&& python3 -m pip install --user -e .
# install python-for-android
RUN cd /opt \
@ -145,12 +147,14 @@ RUN cd /opt \
&& cd python-for-android \
&& git remote add sombernight https://github.com/SomberNight/python-for-android \
&& git fetch --all \
&& git checkout dec1badc3bd134a9a1c69275339423a95d63413e \
&& git checkout ccb0f8e1bab36f1b7d1508216b4b4afb076e614f \
# allowBackup="false":
&& git cherry-pick d7f722e4e5d4b3e6f5b1733c95e6a433f78ee570 \
# enable IPv6:
&& git cherry-pick a607f4a446773ac0b0a5150171092b0617fbe670 \
&& python3 -m pip install -e .
# fix gradle "versionCode" overflow:
&& git cherry-pick ed20e196fbcdce718a180f88f23bb2d165c4c5d8 \
# gradle: persist debug keystore:
&& git cherry-pick aaa0d5d0e7a334631df71e0a9bf127817e0ab9ab \
&& python3 -m pip install --user -e .
# build env vars
ENV USE_SDK_WRAPPER=1

View File

@ -35,7 +35,14 @@ version.filename = %(source.dir)s/electrum/version.py
#version = 1.9.8
# (list) Application requirements
requirements = python3, android, openssl, plyer, kivy==b47f669f44dbda4f463bcb7d2cada639f7fed3bc, libffi, libsecp256k1
requirements =
python3,
android,
openssl,
plyer,
kivy==82d561d62577757d478df52173610f925c05ecab,
libffi,
libsecp256k1
# (str) Presplash of the application
#presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png
@ -64,11 +71,8 @@ android.api = 28
# (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value.
android.minapi = 21
# (int) Android SDK version to use
android.sdk = 24
# (str) Android NDK version to use
android.ndk = 14b
android.ndk = 17c
# (int) Android NDK API to use (optional). This is the minimum API your app will support.
android.ndk_api = 21
@ -82,6 +86,9 @@ android.ndk_path = /opt/android/android-ndk
# (str) Android SDK directory (if empty, it will be automatically downloaded.)
android.sdk_path = /opt/android/android-sdk
# (str) ANT directory (if empty, it will be automatically downloaded.)
#android.ant_path =
# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.renpy.android.PythonActivity
@ -124,6 +131,9 @@ android.manifest.launch_mode = singleTask
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False
# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.arch = armeabi-v7a
# (list) Android application meta-data to set (key=value format)
#android.meta_data =
@ -133,9 +143,27 @@ android.manifest.launch_mode = singleTask
android.whitelist = lib-dynload/_csv.so
# local version that merges branch 866
#
# Python for android (p4a) specific
#
# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
p4a.source_dir = /opt/python-for-android
# (str) The directory in which python-for-android should look for your own build recipes (if any)
#p4a.local_recipes =
# (str) Filename to the hook for p4a
#p4a.hook =
# (str) Bootstrap to use for android builds
# p4a.bootstrap = sdl2
# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =
#
# iOS specific
#

View File

@ -24,8 +24,8 @@ Builder.load_string('''
text: _('Current Fee')
value: ''
BoxLabel:
id: new_fee
text: _('New Fee')
id: old_feerate
text: _('Current Fee rate')
value: ''
Label:
id: tooltip1
@ -78,15 +78,14 @@ class BumpFeeDialog(Factory.Popup):
self.mempool = self.config.use_mempool_fees()
self.dynfees = self.config.is_dynfee() and bool(self.app.network) and self.config.has_dynamic_fees_ready()
self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee)
self.ids.old_feerate.value = self.app.format_fee_rate(fee / self.tx_size * 1000)
self.update_slider()
self.update_text()
def update_text(self):
fee = self.get_fee()
self.ids.new_fee.value = self.app.format_amount_and_units(fee)
pos = int(self.ids.slider.value)
fee_rate = self.get_fee_rate()
text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, fee_rate)
new_fee_rate = self.get_fee_rate()
text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, new_fee_rate)
self.ids.tooltip1.text = text
self.ids.tooltip2.text = tooltip
@ -103,16 +102,12 @@ class BumpFeeDialog(Factory.Popup):
fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(pos)
else:
fee_rate = self.config.static_fee(pos)
return fee_rate
def get_fee(self):
fee_rate = self.get_fee_rate()
return int(fee_rate * self.tx_size // 1000)
return fee_rate # sat/kbyte
def on_ok(self):
new_fee = self.get_fee()
new_fee_rate = self.get_fee_rate() / 1000
is_final = self.ids.final_cb.active
self.callback(self.init_fee, new_fee, is_final)
self.callback(new_fee_rate, is_final)
def on_slider(self, value):
self.update_text()

View File

@ -119,8 +119,11 @@ class CrashReporter(BaseCrashReporter, Factory.Popup):
try:
loop = self.main_window.network.asyncio_loop
proxy = self.main_window.network.proxy
response = json.loads(BaseCrashReporter.send_report(self, loop, proxy, "/crash.json"))
# FIXME network request in GUI thread...
response = json.loads(BaseCrashReporter.send_report(self, loop, proxy,
"/crash.json", timeout=10))
except (ValueError, ClientError):
#self.logger.debug("", exc_info=True)
self.show_popup(_('Unable to send report'), _("Please check your network connection."))
else:
self.show_popup(_('Report sent'), response["text"])

View File

@ -1,5 +1,11 @@
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.core.clipboard import Clipboard
from kivy.app import App
from kivy.clock import Clock
from electrum.gui.kivy.i18n import _
Builder.load_string('''
<QRDialog@Popup>
@ -24,9 +30,12 @@ Builder.load_string('''
BoxLayout:
size_hint: 1, None
height: '48dp'
Widget:
Button:
size_hint: 1, None
height: '48dp'
text: _('Copy to clipboard')
on_release:
root.copy_to_clipboard()
Button:
size_hint: 1, None
height: '48dp'
@ -36,12 +45,20 @@ Builder.load_string('''
''')
class QRDialog(Factory.Popup):
def __init__(self, title, data, show_text, failure_cb=None):
def __init__(self, title, data, show_text, *,
failure_cb=None, text_for_clipboard=None):
Factory.Popup.__init__(self)
self.app = App.get_running_app()
self.title = title
self.data = data
self.show_text = show_text
self.failure_cb = failure_cb
self.text_for_clipboard = text_for_clipboard if text_for_clipboard else data
def on_open(self):
self.ids.qr.set_data(self.data, self.failure_cb)
def copy_to_clipboard(self):
Clipboard.copy(self.text_for_clipboard)
msg = _('Text copied to clipboard.')
Clock.schedule_once(lambda dt: self.app.show_info(msg))

View File

@ -15,6 +15,7 @@ from electrum.gui.kivy.i18n import _
from electrum.util import InvalidPassword
from electrum.address_synchronizer import TX_HEIGHT_LOCAL
from electrum.wallet import CannotBumpFee
Builder.load_string('''
@ -27,6 +28,7 @@ Builder.load_string('''
can_broadcast: False
can_rbf: False
fee_str: ''
feerate_str: ''
date_str: ''
date_label:''
amount_str: ''
@ -65,6 +67,9 @@ Builder.load_string('''
BoxLabel:
text: _('Transaction fee') if root.fee_str else ''
value: root.fee_str
BoxLabel:
text: _('Transaction fee rate') if root.feerate_str else ''
value: root.feerate_str
TopLabel:
text: _('Transaction ID') + ':' if root.tx_hash else ''
TxHashLabel:
@ -148,7 +153,13 @@ class TxDialog(Factory.Popup):
else:
self.is_mine = True
self.amount_str = format_amount(-amount)
self.fee_str = format_amount(fee) if fee is not None else _('unknown')
if fee is not None:
self.fee_str = format_amount(fee)
fee_per_kb = fee / self.tx.estimated_size() * 1000
self.feerate_str = self.app.format_fee_rate(fee_per_kb)
else:
self.fee_str = _('unknown')
self.feerate_str = _('unknown')
self.can_sign = self.wallet.can_sign(self.tx)
self.ids.output_list.update(self.tx.get_outputs_for_UI())
self.is_local_tx = tx_mined_status.height == TX_HEIGHT_LOCAL
@ -184,7 +195,7 @@ class TxDialog(Factory.Popup):
self._action_button_fn = dropdown.open
for option in options:
if option.enabled:
btn = Button(text=option.text, size_hint_y=None, height=48)
btn = Button(text=option.text, size_hint_y=None, height='48dp')
btn.bind(on_release=option.func)
dropdown.add_widget(btn)
@ -202,16 +213,14 @@ class TxDialog(Factory.Popup):
d = BumpFeeDialog(self.app, fee, size, self._do_rbf)
d.open()
def _do_rbf(self, old_fee, new_fee, is_final):
if new_fee is None:
return
delta = new_fee - old_fee
if delta < 0:
self.app.show_error("fee too low")
def _do_rbf(self, new_fee_rate, is_final):
if new_fee_rate is None:
return
try:
new_tx = self.wallet.bump_fee(self.tx, delta)
except BaseException as e:
new_tx = self.wallet.bump_fee(tx=self.tx,
new_fee_rate=new_fee_rate,
config=self.app.electrum_config)
except CannotBumpFee as e:
self.app.show_error(str(e))
return
if is_final:

View File

@ -290,7 +290,7 @@ class SendScreen(CScreen):
return
except Exception as e:
traceback.print_exc(file=sys.stdout)
self.app.show_error(str(e))
self.app.show_error(repr(e))
return
if rbf:
tx.set_rbf(True)
@ -410,7 +410,7 @@ class ReceiveScreen(CScreen):
self.app.wallet.add_payment_request(req, self.app.electrum_config)
added_request = True
except Exception as e:
self.app.show_error(_('Error adding payment request') + ':\n' + str(e))
self.app.show_error(_('Error adding payment request') + ':\n' + repr(e))
added_request = False
finally:
self.app.update_tab('requests')

View File

@ -230,7 +230,7 @@ class ElectrumGui(Logger):
custom_message_box(icon=QMessageBox.Warning,
parent=None,
title=_('Error'),
text=_('Cannot load wallet') + ' (1):\n' + str(e))
text=_('Cannot load wallet') + ' (1):\n' + repr(e))
# if app is starting, still let wizard to appear
if not app_is_starting:
return
@ -242,7 +242,7 @@ class ElectrumGui(Logger):
custom_message_box(icon=QMessageBox.Warning,
parent=None,
title=_('Error'),
text=_('Cannot load wallet') + ' (2):\n' + str(e))
text=_('Cannot load wallet') + ' (2):\n' + repr(e))
if not wallet:
return
# create or raise window
@ -257,7 +257,7 @@ class ElectrumGui(Logger):
custom_message_box(icon=QMessageBox.Warning,
parent=None,
title=_('Error'),
text=_('Cannot create window for wallet') + ':\n' + str(e))
text=_('Cannot create window for wallet') + ':\n' + repr(e))
if app_is_starting:
wallet_dir = os.path.dirname(path)
path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir))

View File

@ -99,4 +99,4 @@ class AddressDialog(WindowModalDialog):
try:
self.parent.show_qrcode(text, 'Address', parent=self)
except Exception as e:
self.show_message(str(e))
self.show_message(repr(e))

View File

@ -23,7 +23,6 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import webbrowser
from enum import IntEnum
from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex
@ -31,12 +30,12 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
from PyQt5.QtWidgets import QAbstractItemView, QComboBox, QLabel, QMenu
from electrum.i18n import _
from electrum.util import block_explorer_URL
from electrum.util import block_explorer_URL, profiler
from electrum.plugin import run_hook
from electrum.bitcoin import is_address
from electrum.wallet import InternalAddressCorruption
from .util import MyTreeView, MONOSPACE_FONT, ColorScheme
from .util import MyTreeView, MONOSPACE_FONT, ColorScheme, webopen
class AddressList(MyTreeView):
@ -107,6 +106,7 @@ class AddressList(MyTreeView):
self.show_used = state
self.update()
@profiler
def update(self):
self.wallet = self.parent.wallet
current_address = self.current_item_user_role(col=self.Columns.LABEL)
@ -187,6 +187,8 @@ class AddressList(MyTreeView):
menu = QMenu()
if not multi_select:
idx = self.indexAt(position)
if not idx.isValid():
return
col = idx.column()
item = self.model().itemFromIndex(idx)
if not item:
@ -214,7 +216,7 @@ class AddressList(MyTreeView):
menu.addAction(_("Remove from wallet"), lambda: self.parent.remove_address(addr))
addr_URL = block_explorer_URL(self.config, 'addr', addr)
if addr_URL:
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(addr_URL))
menu.addAction(_("View on block explorer"), lambda: webopen(addr_URL))
if not self.wallet.is_frozen_address(addr):
menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], True))

View File

@ -3,9 +3,11 @@
from decimal import Decimal
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtGui import QPalette, QPainter
from PyQt5.QtGui import QPalette, QPainter, QFontMetrics
from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame)
from .util import char_width_in_lineedit
from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name,
FEERATE_PRECISION, quantize_feerate)
@ -24,7 +26,7 @@ class AmountEdit(MyLineEdit):
def __init__(self, base_unit, is_int=False, parent=None):
QLineEdit.__init__(self, parent)
# This seems sufficient for hundred-BTC amounts with 8 decimals
self.setFixedWidth(140)
self.setFixedWidth(16 * char_width_in_lineedit())
self.base_unit = base_unit
self.textChanged.connect(self.numbify)
self.is_int = is_int

View File

@ -23,7 +23,6 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import webbrowser
from enum import IntEnum
from PyQt5.QtGui import QStandardItemModel, QStandardItem
@ -35,7 +34,7 @@ from electrum.bitcoin import is_address
from electrum.util import block_explorer_URL
from electrum.plugin import run_hook
from .util import MyTreeView, import_meta_gui, export_meta_gui
from .util import MyTreeView, import_meta_gui, export_meta_gui, webopen
class ContactList(MyTreeView):
@ -97,7 +96,7 @@ class ContactList(MyTreeView):
menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(selected_keys))
URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, selected_keys)]
if URLs:
menu.addAction(_("View on block explorer"), lambda: [webbrowser.open(u) for u in URLs])
menu.addAction(_("View on block explorer"), lambda: [webopen(u) for u in URLs])
run_hook('create_contact_menu', menu, selected_keys)
menu.exec_(self.viewport().mapToGlobal(position))

View File

@ -33,7 +33,9 @@ from PyQt5.QtWidgets import (QWidget, QLabel, QPushButton, QTextEdit,
from electrum.i18n import _
from electrum.base_crash_reporter import BaseCrashReporter
from electrum.logging import Logger
from .util import MessageBoxMixin, read_QIcon
from electrum import constants
from .util import MessageBoxMixin, read_QIcon, WaitingDialog
class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger):
@ -69,6 +71,8 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger):
self.description_textfield = QTextEdit()
self.description_textfield.setFixedHeight(50)
self.description_textfield.setPlaceholderText(_("Do not enter sensitive/private information here. "
"The report will be visible on the public issue tracker."))
main_box.addWidget(self.description_textfield)
main_box.addWidget(QLabel(BaseCrashReporter.ASK_CONFIRM_SEND))
@ -94,17 +98,28 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger):
self.show()
def send_report(self):
try:
proxy = self.main_window.network.proxy
response = BaseCrashReporter.send_report(self, self.main_window.network.asyncio_loop, proxy)
except BaseException as e:
self.logger.exception('There was a problem with the automatic reporting')
self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' +
str(e) + '\n' +
_("Please report this issue manually."))
return
QMessageBox.about(self, _("Crash report"), response)
self.close()
def on_success(response):
# note: 'response' coming from (remote) crash reporter server.
# It contains a URL to the GitHub issue, so we allow rich text.
self.show_message(parent=self,
title=_("Crash report"),
msg=response,
rich_text=True)
self.close()
def on_failure(exc_info):
e = exc_info[1]
self.logger.error('There was a problem with the automatic reporting', exc_info=exc_info)
self.show_critical(parent=self,
msg=(_('There was a problem with the automatic reporting:') + '<br/>' +
repr(e)[:120] + '<br/>' +
_("Please report this issue manually") +
f' <a href="{constants.GIT_REPO_ISSUES_URL}">on GitHub</a>.'),
rich_text=True)
proxy = self.main_window.network.proxy
task = lambda: BaseCrashReporter.send_report(self, self.main_window.network.asyncio_loop, proxy)
msg = _('Sending crash report...')
WaitingDialog(self, msg, task, on_success, on_failure)
def on_close(self):
Exception_Window._active_window = None

View File

@ -24,7 +24,6 @@
# SOFTWARE.
import os
import webbrowser
import datetime
from datetime import date
from typing import TYPE_CHECKING, Tuple, Dict
@ -47,7 +46,7 @@ from electrum.logging import get_logger, Logger
from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton,
filename_field, MyTreeView, AcceptFileDragDrop, WindowModalDialog,
CloseButton)
CloseButton, webopen)
if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet
@ -149,7 +148,7 @@ class HistoryModel(QAbstractItemModel, Logger):
HistoryColumns.STATUS_ICON:
# height breaks ties for unverified txns
# txpos breaks ties for verified same block txns
(status, conf, -height, -txpos),
(conf, -status, -height, -txpos),
HistoryColumns.STATUS_TEXT: status_str,
HistoryColumns.DESCRIPTION: tx_item['label'],
HistoryColumns.COIN_VALUE: tx_item['value'].value,
@ -608,7 +607,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
if pr_key:
menu.addAction(read_QIcon("seal"), _("View invoice"), lambda: self.parent.show_invoice(pr_key))
if tx_URL:
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
menu.addAction(_("View on block explorer"), lambda: webopen(tx_URL))
menu.exec_(self.viewport().mapToGlobal(position))
def remove_local_tx(self, delete_tx):

View File

@ -23,7 +23,7 @@ from electrum.i18n import _
from .seed_dialog import SeedLayout, KeysLayout
from .network_dialog import NetworkChoiceLayout
from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel,
InfoButton)
InfoButton, char_width_in_lineedit)
from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
from electrum.plugin import run_hook
@ -121,8 +121,6 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
self.setWindowTitle('Electrum - ' + _('Install Wizard'))
self.app = app
self.config = config
# Set for base base class
self.language_for_seed = config.get('language')
self.setMinimumSize(600, 400)
self.accept_signal.connect(self.accept)
self.title = QLabel()
@ -182,7 +180,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
vbox.addWidget(self.msg_label)
hbox2 = QHBoxLayout()
self.pw_e = QLineEdit('', self)
self.pw_e.setFixedWidth(150)
self.pw_e.setFixedWidth(17 * char_width_in_lineedit())
self.pw_e.setEchoMode(2)
self.pw_label = QLabel(_('Password') + ':')
hbox2.addWidget(self.pw_label)
@ -267,7 +265,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
continue
except BaseException as e:
self.logger.exception('')
self.show_message(title=_('Error'), msg=str(e))
self.show_message(title=_('Error'), msg=repr(e))
raise UserCancelled()
elif self.temp_storage.is_encrypted_with_hw_device():
try:
@ -280,7 +278,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
return self.select_storage(path, get_wallet_from_daemon)
except BaseException as e:
self.logger.exception('')
self.show_message(title=_('Error'), msg=str(e))
self.show_message(title=_('Error'), msg=repr(e))
raise UserCancelled()
if self.temp_storage.is_past_initial_decryption():
break

View File

@ -30,13 +30,13 @@ import traceback
import json
import shutil
import weakref
import webbrowser
import csv
from decimal import Decimal
import base64
from functools import partial
import queue
import asyncio
from typing import Optional
from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor
from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal
@ -71,6 +71,7 @@ from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from electrum.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig
from electrum.logging import Logger
from electrum.paymentrequest import PR_PAID
from .exception_window import Exception_Hook
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
@ -83,7 +84,7 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo
OkButton, InfoButton, WWLabel, TaskThread, CancelButton,
CloseButton, HelpButton, MessageBoxMixin, EnterButton, expiration_values,
ButtonsLineEdit, CopyCloseButton, import_meta_gui, export_meta_gui,
filename_field, address_field)
filename_field, address_field, char_width_in_lineedit, webopen)
from .installwizard import WIF_HELP_TEXT
from .history_list import HistoryList, HistoryModel
from .update_checker import UpdateCheck, UpdateCheckThread
@ -109,9 +110,6 @@ class StatusBarButton(QPushButton):
self.func()
from electrum.paymentrequest import PR_PAID
class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
payment_request_ok_signal = pyqtSignal()
@ -141,7 +139,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.tray = gui_object.tray
self.app = gui_object.app
self.cleaned_up = False
self.payment_request = None
self.payment_request = None # type: Optional[paymentrequest.PaymentRequest]
self.checking_accounts = False
self.qr_window = None
self.not_enough_funds = False
@ -347,7 +345,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.logger.error("on_error", exc_info=exc_info)
except OSError:
pass # see #4418
self.show_error(str(e))
self.show_error(repr(e))
def on_network(self, event, *args):
if event == 'wallet_updated':
@ -634,9 +632,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
help_menu = menubar.addMenu(_("&Help"))
help_menu.addAction(_("&About"), self.show_about)
help_menu.addAction(_("&Check for updates"), self.show_update_check)
help_menu.addAction(_("&Official website"), lambda: webbrowser.open("https://electrum.org"))
help_menu.addAction(_("&Official website"), lambda: webopen("https://electrum.org"))
help_menu.addSeparator()
help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://docs.electrum.org/")).setShortcut(QKeySequence.HelpContents)
help_menu.addAction(_("&Documentation"), lambda: webopen("http://docs.electrum.org/")).setShortcut(QKeySequence.HelpContents)
help_menu.addAction(_("&Report Bug"), self.show_report_bug)
help_menu.addSeparator()
help_menu.addAction(_("&Donate to server"), self.donate_to_server)
@ -667,7 +665,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def show_report_bug(self):
msg = ' '.join([
_("Please report any bugs as issues on github:<br/>"),
"<a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a><br/><br/>",
f'''<a href="{constants.GIT_REPO_ISSUES_URL}">{constants.GIT_REPO_ISSUES_URL}</a><br/><br/>''',
_("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."),
_("Try to explain not only what the bug is, but how it occurs.")
])
@ -907,6 +905,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
msg = _('Bitcoin address where the payment should be received. Note that each payment request uses a different Bitcoin address.')
self.receive_address_label = HelpLabel(_('Receiving address'), msg)
self.receive_address_e.textChanged.connect(self.update_receive_qr)
self.receive_address_e.textChanged.connect(self.update_receive_address_styling)
self.receive_address_e.setFocusPolicy(Qt.ClickFocus)
grid.addWidget(self.receive_address_label, 0, 0)
grid.addWidget(self.receive_address_e, 0, 1, 1, -1)
@ -1027,7 +1026,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
try:
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
except Exception as e:
self.show_error(str(e))
self.show_error(repr(e))
return
else:
return
@ -1046,7 +1045,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.wallet.add_payment_request(req, self.config)
except Exception as e:
self.logger.exception('Error adding payment request')
self.show_error(_('Error adding payment request') + ':\n' + str(e))
self.show_error(_('Error adding payment request') + ':\n' + repr(e))
else:
self.sign_payment_request(addr)
self.save_request_button.setEnabled(False)
@ -1152,6 +1151,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if self.qr_window and self.qr_window.isVisible():
self.qr_window.qrw.setData(uri)
def update_receive_address_styling(self):
addr = str(self.receive_address_e.text())
if self.wallet.is_used(addr):
self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
self.receive_address_e.setToolTip(_("This address has already been used. "
"For better privacy, do not reuse it for new payments."))
else:
self.receive_address_e.setStyleSheet("")
self.receive_address_e.setToolTip("")
def set_feerounding_text(self, num_satoshis_added):
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
.format(num_satoshis_added))
@ -1206,7 +1215,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
self.max_button = EnterButton(_("Max"), self.spend_max)
self.max_button.setFixedWidth(140)
self.max_button.setFixedWidth(self.amount_e.width())
self.max_button.setCheckable(True)
grid.addWidget(self.max_button, 4, 3)
hbox = QHBoxLayout()
@ -1238,7 +1247,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.spend_max() if self.max_button.isChecked() else self.update_fee()
self.fee_slider = FeeSlider(self, self.config, fee_cb)
self.fee_slider.setFixedWidth(140)
self.fee_slider.setFixedWidth(self.amount_e.width())
def on_fee_or_feerate(edit_changed, editing_finished):
edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e
@ -1261,7 +1270,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.size_e = TxSizeLabel()
self.size_e.setAlignment(Qt.AlignCenter)
self.size_e.setAmount(0)
self.size_e.setFixedWidth(140)
self.size_e.setFixedWidth(self.amount_e.width())
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
self.feerate_e = FeerateEdit(lambda: 0)
@ -1283,7 +1292,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.show_message(title=_('Fee rounding'), msg=text)
self.feerounding_icon = QPushButton(read_QIcon('info.png'), '')
self.feerounding_icon.setFixedWidth(20)
self.feerounding_icon.setFixedWidth(round(2.2 * char_width_in_lineedit()))
self.feerounding_icon.setFlat(True)
self.feerounding_icon.clicked.connect(feerounding_onclick)
self.feerounding_icon.setVisible(False)
@ -1591,11 +1600,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
"""Returns whether there are errors with outputs.
Also shows error dialog to user if so.
"""
if self.payment_request and self.payment_request.has_expired():
self.show_error(_('Payment request has expired'))
return True
pr = self.payment_request
if pr:
if pr.has_expired():
self.show_error(_('Payment request has expired'))
return True
if not self.payment_request:
if not pr:
errors = self.payto_e.get_errors()
if errors:
self.show_warning(_("Invalid Lines found:") + "\n\n" + '\n'.join([ _("Line #") + str(x[0]+1) + ": " + x[1] for x in errors]))
@ -1809,6 +1820,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def payment_request_ok(self):
pr = self.payment_request
if not pr:
return
key = self.invoices.add(pr)
status = self.invoices.get_status(key)
self.invoice_list.update()
@ -1829,7 +1842,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.amount_e.textEdited.emit("")
def payment_request_error(self):
self.show_message(self.payment_request.error)
pr = self.payment_request
if not pr:
return
self.show_message(pr.error)
self.payment_request = None
self.do_clear()
@ -2135,7 +2151,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return
except BaseException as e:
self.logger.exception('')
self.show_error(str(e))
self.show_error(repr(e))
return
old_password = hw_dev_pw if self.wallet.has_password() else None
new_password = hw_dev_pw if encrypt_file else None
@ -2181,9 +2197,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
vbox.addWidget(QLabel(_('New Contact') + ':'))
grid = QGridLayout()
line1 = QLineEdit()
line1.setFixedWidth(280)
line1.setFixedWidth(32 * char_width_in_lineedit())
line2 = QLineEdit()
line2.setFixedWidth(280)
line2.setFixedWidth(32 * char_width_in_lineedit())
grid.addWidget(QLabel(_("Address")), 1, 0)
grid.addWidget(line1, 1, 1)
grid.addWidget(QLabel(_("Name")), 2, 0)
@ -2226,7 +2242,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def show_mpk(index):
mpk_text.setText(mpk_list[index])
mpk_text.repaint() # macOS hack for #4777
# only show the combobox in case multiple accounts are available
if len(mpk_list) > 1:
# only show the combobox if multiple master keys are defined
def label(idx, ks):
@ -2280,7 +2298,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
seed = keystore.get_seed(password)
passphrase = keystore.get_passphrase(password)
except BaseException as e:
self.show_error(str(e))
self.show_error(repr(e))
return
from .seed_dialog import SeedDialog
d = SeedDialog(self, seed, passphrase)
@ -2300,7 +2318,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
pk, redeem_script = self.wallet.export_private_key(address, password)
except Exception as e:
self.logger.exception('')
self.show_message(str(e))
self.show_message(repr(e))
return
xtype = bitcoin.deserialize_privkey(pk)[0]
d = WindowModalDialog(self, _("Private key"))
@ -2494,7 +2512,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
tx = tx_from_str(txt)
return Transaction(tx)
except BaseException as e:
self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + str(e))
self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
return
def read_tx_from_qrcode(self):
@ -2502,7 +2520,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
try:
data = qrscanner.scan_barcode(self.config.get_video_device())
except BaseException as e:
self.show_error(str(e))
self.show_error(repr(e))
return
if not data:
return
@ -2555,7 +2573,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
raw_tx = self.network.run_from_another_thread(
self.network.get_transaction(txid, timeout=10))
except Exception as e:
self.show_message(_("Error getting transaction from network") + ":\n" + str(e))
self.show_message(_("Error getting transaction from network") + ":\n" + repr(e))
return
tx = transaction.Transaction(raw_tx)
self.show_transaction(tx)
@ -2647,7 +2665,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.show_critical(txt, title=_("Unable to create csv"))
except Exception as e:
self.show_message(str(e))
self.show_message(repr(e))
return
self.show_message(_("Private keys exported."))
@ -2724,7 +2742,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
try:
valid_privkeys = get_pk(raise_on_error=True) is not None
except Exception as e:
button.setToolTip(f'{_("Error")}: {str(e)}')
button.setToolTip(f'{_("Error")}: {repr(e)}')
else:
button.setToolTip('')
button.setEnabled(get_address() is not None and valid_privkeys)
@ -2745,7 +2763,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
try:
coins, keypairs = sweep_preparations(get_pk(), self.network)
except Exception as e: # FIXME too broad...
self.show_message(str(e))
self.show_message(repr(e))
return
self.do_clear()
self.tx_external_keypairs = keypairs
@ -2938,7 +2956,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
SSL_error = None
except BaseException as e:
SSL_identity = "error"
SSL_error = str(e)
SSL_error = repr(e)
else:
SSL_identity = ""
SSL_error = None
@ -3417,19 +3435,27 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return
tx_label = self.wallet.get_label(tx.txid())
tx_size = tx.estimated_size()
old_fee_rate = fee / tx_size # sat/vbyte
d = WindowModalDialog(self, _('Bump Fee'))
vbox = QVBoxLayout(d)
vbox.addWidget(WWLabel(_("Increase your transaction's fee to improve its position in mempool.")))
vbox.addWidget(QLabel(_('Current fee') + ': %s'% self.format_amount(fee) + ' ' + self.base_unit()))
vbox.addWidget(QLabel(_('New fee' + ':')))
fee_e = BTCAmountEdit(self.get_decimal_point)
fee_e.setAmount(fee * 1.5)
vbox.addWidget(fee_e)
vbox.addWidget(QLabel(_('Current Fee') + ': %s'% self.format_amount(fee) + ' ' + self.base_unit()))
vbox.addWidget(QLabel(_('Current Fee rate') + ': %s' % self.format_fee_rate(1000 * old_fee_rate)))
vbox.addWidget(QLabel(_('New Fee rate') + ':'))
def on_rate(dyn, pos, fee_rate):
fee = fee_rate * tx_size / 1000
fee_e.setAmount(fee)
fee_slider = FeeSlider(self, self.config, on_rate)
def on_textedit_rate():
fee_slider.deactivate()
feerate_e = FeerateEdit(lambda: 0)
feerate_e.setAmount(max(old_fee_rate * 1.5, old_fee_rate + 1))
feerate_e.textEdited.connect(on_textedit_rate)
vbox.addWidget(feerate_e)
def on_slider_rate(dyn, pos, fee_rate):
fee_slider.activate()
if fee_rate is not None:
feerate_e.setAmount(fee_rate / 1000)
fee_slider = FeeSlider(self, self.config, on_slider_rate)
fee_slider.deactivate()
vbox.addWidget(fee_slider)
cb = QCheckBox(_('Final'))
vbox.addWidget(cb)
@ -3437,13 +3463,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if not d.exec_():
return
is_final = cb.isChecked()
new_fee = fee_e.get_amount()
delta = new_fee - fee
if delta < 0:
self.show_error("fee too low")
return
new_fee_rate = feerate_e.get_amount()
try:
new_tx = self.wallet.bump_fee(tx, delta)
new_tx = self.wallet.bump_fee(tx=tx, new_fee_rate=new_fee_rate, config=self.config)
except CannotBumpFee as e:
self.show_error(str(e))
return

View File

@ -26,11 +26,13 @@
import socket
import time
from enum import IntEnum
from typing import Tuple
from PyQt5.QtCore import Qt, pyqtSignal, QThread
from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox,
QLineEdit, QDialog, QVBoxLayout, QHeaderView, QCheckBox,
QTabWidget, QWidget, QLabel)
from PyQt5.QtGui import QFontMetrics
from electrum.i18n import _
from electrum import constants, blockchain
@ -38,7 +40,7 @@ from electrum.interface import serialize_server, deserialize_server
from electrum.network import Network
from electrum.logging import get_logger
from .util import Buttons, CloseButton, HelpButton, read_QIcon
from .util import Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit
_logger = get_logger(__name__)
@ -213,14 +215,17 @@ class NetworkChoiceLayout(object):
tabs.addTab(server_tab, _('Server'))
tabs.addTab(proxy_tab, _('Proxy'))
fixed_width_hostname = 24 * char_width_in_lineedit()
fixed_width_port = 6 * char_width_in_lineedit()
# server tab
grid = QGridLayout(server_tab)
grid.setSpacing(8)
self.server_host = QLineEdit()
self.server_host.setFixedWidth(200)
self.server_host.setFixedWidth(fixed_width_hostname)
self.server_port = QLineEdit()
self.server_port.setFixedWidth(60)
self.server_port.setFixedWidth(fixed_width_port)
self.autoconnect_cb = QCheckBox(_('Select server automatically'))
self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect'))
@ -257,15 +262,15 @@ class NetworkChoiceLayout(object):
self.proxy_mode = QComboBox()
self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
self.proxy_host = QLineEdit()
self.proxy_host.setFixedWidth(200)
self.proxy_host.setFixedWidth(fixed_width_hostname)
self.proxy_port = QLineEdit()
self.proxy_port.setFixedWidth(60)
self.proxy_port.setFixedWidth(fixed_width_port)
self.proxy_user = QLineEdit()
self.proxy_user.setPlaceholderText(_("Proxy user"))
self.proxy_password = QLineEdit()
self.proxy_password.setPlaceholderText(_("Password"))
self.proxy_password.setEchoMode(QLineEdit.Password)
self.proxy_password.setFixedWidth(60)
self.proxy_password.setFixedWidth(fixed_width_port)
self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
self.proxy_host.editingFinished.connect(self.set_proxy)
@ -354,8 +359,9 @@ class NetworkChoiceLayout(object):
net_params = self.network.get_parameters()
host, port, protocol = net_params.host, net_params.port, net_params.protocol
proxy_config, auto_connect = net_params.proxy, net_params.auto_connect
self.server_host.setText(host)
self.server_port.setText(str(port))
if not self.server_host.hasFocus() and not self.server_port.hasFocus():
self.server_host.setText(host)
self.server_port.setText(str(port))
self.autoconnect_cb.setChecked(auto_connect)
interface = self.network.interface
@ -521,19 +527,20 @@ class TorDetector(QThread):
ports = [9050, 9150]
while True:
for p in ports:
if TorDetector.is_tor_port(p):
self.found_proxy.emit(("127.0.0.1", p))
net_addr = ("127.0.0.1", p)
if TorDetector.is_tor_port(net_addr):
self.found_proxy.emit(net_addr)
break
else:
self.found_proxy.emit(None)
time.sleep(10)
@staticmethod
def is_tor_port(port):
def is_tor_port(net_addr: Tuple[str, int]) -> bool:
try:
s = (socket._socketobject if hasattr(socket, "_socketobject") else socket.socket)(socket.AF_INET, socket.SOCK_STREAM)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.1)
s.connect(("127.0.0.1", port))
s.connect(net_addr)
# Tor responds uniquely to HTTP-like requests
s.send(b"GET\n")
if b"Tor is not an HTTP Proxy" in s.recv(1024):

View File

@ -49,7 +49,7 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
with open(fileName, "r") as f:
data = f.read()
except BaseException as e:
self.show_error(_('Error opening file') + ':\n' + str(e))
self.show_error(_('Error opening file') + ':\n' + repr(e))
else:
self.setText(data)
@ -58,7 +58,7 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
try:
data = qrscanner.scan_barcode(get_config().get_video_device())
except BaseException as e:
self.show_error(str(e))
self.show_error(repr(e))
data = ''
if not data:
data = ''

View File

@ -105,9 +105,10 @@ class RequestList(MyTreeView):
except InternalAddressCorruption as e:
self.parent.show_error(str(e))
addr = ''
if not current_address in domain and addr:
if current_address not in domain and addr:
self.parent.set_receive_address(addr)
self.parent.new_request_button.setEnabled(addr != current_address)
self.parent.update_receive_address_styling()
self.model().clear()
self.update_headers(self.__class__.headers)

View File

@ -24,16 +24,16 @@
# SOFTWARE.
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtGui import QPixmap, QPalette
from PyQt5.QtWidgets import (QVBoxLayout, QCheckBox, QHBoxLayout, QLineEdit,
QLabel, QCompleter, QDialog)
QLabel, QCompleter, QDialog, QStyledItemDelegate)
from electrum.i18n import _
from electrum.mnemonic import Mnemonic, seed_type
import electrum.old_mnemonic
from .util import (Buttons, OkButton, WWLabel, ButtonsTextEdit, icon_path,
EnterButton, CloseButton, WindowModalDialog)
EnterButton, CloseButton, WindowModalDialog, ColorScheme)
from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
from .completion_text_edit import CompletionTextEdit
@ -149,11 +149,26 @@ class SeedLayout(QVBoxLayout):
self.addWidget(self.seed_warning)
def initialize_completer(self):
english_list = Mnemonic('en').wordlist
bip39_english_list = Mnemonic('en').wordlist
old_list = electrum.old_mnemonic.words
self.wordlist = english_list + list(set(old_list) - set(english_list)) #concat both lists
only_old_list = set(old_list) - set(bip39_english_list)
self.wordlist = bip39_english_list + list(only_old_list) # concat both lists
self.wordlist.sort()
class CompleterDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
# Some people complained that due to merging the two word lists,
# it is difficult to restore from a metal backup, as they planned
# to rely on the "4 letter prefixes are unique in bip39 word list" property.
# So we color words that are only in old list.
if option.text in only_old_list:
# yellow bg looks ~ok on both light/dark theme, regardless if (un)selected
option.backgroundBrush = ColorScheme.YELLOW.as_color(background=True)
self.completer = QCompleter(self.wordlist)
delegate = CompleterDelegate(self.seed_e)
self.completer.popup().setItemDelegate(delegate)
self.seed_e.set_completer(self.completer)
def get_seed(self):
@ -174,7 +189,7 @@ class SeedLayout(QVBoxLayout):
self.seed_type_label.setText(label)
self.parent.next_button.setEnabled(b)
# to account for bip39 seeds
# disable suggestions if user already typed an unknown word
for word in self.get_seed().split(" ")[:-1]:
if word not in self.wordlist:
self.seed_e.disable_suggestions()

View File

@ -191,7 +191,7 @@ class TxDialog(QDialog, MessageBoxMixin):
self.show_error(_('Failed to display QR code.') + '\n' +
_('Transaction is too large in size.'))
except Exception as e:
self.show_error(_('Failed to display QR code.') + '\n' + str(e))
self.show_error(_('Failed to display QR code.') + '\n' + repr(e))
def sign(self):
def sign_done(success):

View File

@ -5,12 +5,14 @@ import sys
import platform
import queue
import traceback
import os
import webbrowser
from functools import partial, lru_cache
from typing import NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict
from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem,
QPalette, QIcon)
QPalette, QIcon, QFontMetrics)
from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal,
QCoreApplication, QItemSelectionModel, QThread,
QSortFilterProxyModel, QSize, QLocale)
@ -92,6 +94,7 @@ class WWLabel(QLabel):
def __init__ (self, text="", parent=None):
QLabel.__init__(self, text, parent)
self.setWordWrap(True)
self.setTextInteractionFlags(Qt.TextSelectableByMouse)
class HelpLabel(QLabel):
@ -126,7 +129,7 @@ class HelpButton(QPushButton):
QPushButton.__init__(self, '?')
self.help_text = text
self.setFocusPolicy(Qt.NoFocus)
self.setFixedWidth(20)
self.setFixedWidth(round(2.2 * char_width_in_lineedit()))
self.clicked.connect(self.onclick)
def onclick(self):
@ -142,7 +145,7 @@ class InfoButton(QPushButton):
QPushButton.__init__(self, 'Info')
self.help_text = text
self.setFocusPolicy(Qt.NoFocus)
self.setFixedWidth(60)
self.setFixedWidth(6 * char_width_in_lineedit())
self.clicked.connect(self.onclick)
def onclick(self):
@ -206,11 +209,15 @@ class MessageBoxMixin(object):
def top_level_window(self, test_func=None):
return self.top_level_window_recurse(test_func)
def question(self, msg, parent=None, title=None, icon=None):
def question(self, msg, parent=None, title=None, icon=None, **kwargs) -> bool:
Yes, No = QMessageBox.Yes, QMessageBox.No
return self.msg_box(icon or QMessageBox.Question,
parent, title or '',
msg, buttons=Yes|No, defaultButton=No) == Yes
return Yes == self.msg_box(icon=icon or QMessageBox.Question,
parent=parent,
title=title or '',
text=msg,
buttons=Yes|No,
defaultButton=No,
**kwargs)
def show_warning(self, msg, parent=None, title=None, **kwargs):
return self.msg_box(QMessageBox.Warning, parent,
@ -254,7 +261,11 @@ def custom_message_box(*, icon, parent, title, text, buttons=QMessageBox.Ok,
d.setDefaultButton(defaultButton)
if rich_text:
d.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse)
d.setTextFormat(Qt.RichText)
# set AutoText instead of RichText
# AutoText lets Qt figure out whether to render as rich text.
# e.g. if text is actually plain text and uses "\n" newlines;
# and we set RichText here, newlines would be swallowed
d.setTextFormat(Qt.AutoText)
else:
d.setTextInteractionFlags(Qt.TextSelectableByMouse)
d.setTextFormat(Qt.PlainText)
@ -863,6 +874,25 @@ class FromList(QTreeWidget):
self.header().setSectionResizeMode(1, sm)
def char_width_in_lineedit() -> int:
char_width = QFontMetrics(QLineEdit().font()).averageCharWidth()
# 'averageCharWidth' seems to underestimate on Windows, hence 'max()'
return max(9, char_width)
def webopen(url: str):
if sys.platform == 'linux' and os.environ.get('APPIMAGE'):
# When on Linux webbrowser.open can fail in AppImage because it can't find the correct libdbus.
# We just fork the process and unset LD_LIBRARY_PATH before opening the URL.
# See #5425
if os.fork() == 0:
del os.environ['LD_LIBRARY_PATH']
webbrowser.open(url)
sys.exit(0)
else:
webbrowser.open(url)
if __name__ == "__main__":
app = QApplication([])
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))

View File

@ -50,7 +50,7 @@ class UTXOList(MyTreeView):
Columns.HEIGHT: _('Height'),
Columns.OUTPOINT: _('Output point'),
}
filter_columns = [Columns.ADDRESS, Columns.LABEL]
filter_columns = [Columns.ADDRESS, Columns.LABEL, Columns.OUTPOINT]
def __init__(self, parent=None):
super().__init__(parent, self.create_menu,
@ -124,6 +124,8 @@ class UTXOList(MyTreeView):
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label))
# "Copy ..."
idx = self.indexAt(position)
if not idx.isValid():
return
col = idx.column()
column_title = self.model().horizontalHeaderItem(col).text()
copy_text = self.model().itemFromIndex(idx).text() if col != self.Columns.OUTPOINT else selected[0]

View File

@ -199,7 +199,7 @@ class ElectrumGui:
tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)],
password, self.config, fee)
except Exception as e:
print(str(e))
print(repr(e))
return
if self.str_description:

View File

@ -362,7 +362,7 @@ class ElectrumGui:
tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)],
password, self.config, fee)
except Exception as e:
self.show_message(str(e))
self.show_message(repr(e))
return
if self.str_description:

View File

@ -38,7 +38,8 @@ import logging
import aiorpcx
from aiorpcx import RPCSession, Notification, NetAddress
from aiorpcx.curio import timeout_after, TaskTimeout
from aiorpcx.jsonrpc import JSONRPC
from aiorpcx.jsonrpc import JSONRPC, CodeMessageError
from aiorpcx.rawsocket import RSClient
import certifi
from .util import ignore_exceptions, log_exceptions, bfh, SilentTaskGroup
@ -114,6 +115,9 @@ class NotificationSession(RPCSession):
timeout)
except (TaskTimeout, asyncio.TimeoutError) as e:
raise RequestTimedOut(f'request timed out: {args} (id: {msg_id})') from e
except CodeMessageError as e:
self.maybe_log(f"--> {repr(e)} (id: {msg_id})")
raise
else:
self.maybe_log(f"--> {response} (id: {msg_id})")
return response
@ -172,12 +176,13 @@ class ErrorGettingSSLCertFromServer(Exception): pass
class ConnectError(Exception): pass
class _Connector(aiorpcx.Connector):
class _RSClient(RSClient):
async def create_connection(self):
try:
return await super().create_connection()
except OSError as e:
raise ConnectError(e)
# note: using "from e" here will set __cause__ of ConnectError
raise ConnectError(e) from e
def deserialize_server(server_str: str) -> Tuple[str, str, str]:
@ -254,11 +259,11 @@ class Interface(Logger):
"""
try:
await self.open_session(ca_ssl_context, exit_early=True)
except ssl.SSLError as e:
if e.reason == 'CERTIFICATE_VERIFY_FAILED':
except ConnectError as e:
cause = e.__cause__
if isinstance(cause, ssl.SSLError) and cause.reason == 'CERTIFICATE_VERIFY_FAILED':
# failures due to self-signed certs are normal
return False
# e.g. too weak crypto
raise
return True
@ -326,9 +331,13 @@ class Interface(Logger):
return await func(self, *args, **kwargs)
except GracefulDisconnect as e:
self.logger.log(e.log_level, f"disconnecting due to {repr(e)}")
except aiorpcx.jsonrpc.RPCError as e:
self.logger.warning(f"disconnecting due to {repr(e)}")
self.logger.debug(f"(disconnect) trace for {repr(e)}", exc_info=True)
finally:
await self.network.connection_down(self)
self.got_disconnected.set_result(1)
if not self.got_disconnected.done():
self.got_disconnected.set_result(1)
# if was not 'ready' yet, schedule waiting coroutines:
self.ready.cancel()
return wrapper_func
@ -391,10 +400,10 @@ class Interface(Logger):
async def get_certificate(self):
sslc = ssl.SSLContext()
try:
async with _Connector(RPCSession,
host=self.host, port=self.port,
ssl=sslc, proxy=self.proxy) as session:
return session.transport._ssl_protocol._sslpipe._sslobj.getpeercert(True)
async with _RSClient(session_factory=RPCSession,
host=self.host, port=self.port,
ssl=sslc, proxy=self.proxy) as session:
return session.transport._asyncio_transport._ssl_protocol._sslpipe._sslobj.getpeercert(True)
except ValueError:
return None
@ -429,9 +438,9 @@ class Interface(Logger):
return self.network.default_server == self.server
async def open_session(self, sslc, exit_early=False):
async with _Connector(NotificationSession,
host=self.host, port=self.port,
ssl=sslc, proxy=self.proxy) as session:
async with _RSClient(session_factory=NotificationSession,
host=self.host, port=self.port,
ssl=sslc, proxy=self.proxy) as session:
self.session = session # type: NotificationSession
self.session.interface = self
self.session.set_default_timeout(self.network.get_network_timeout_seconds(NetworkTimeout.Generic))
@ -452,8 +461,10 @@ class Interface(Logger):
await group.spawn(self.run_fetch_blocks)
await group.spawn(self.monitor_connection)
except aiorpcx.jsonrpc.RPCError as e:
if e.code in (JSONRPC.EXCESSIVE_RESOURCE_USAGE, JSONRPC.SERVER_BUSY):
raise GracefulDisconnect(e, log_level=logging.ERROR) from e
if e.code in (JSONRPC.EXCESSIVE_RESOURCE_USAGE,
JSONRPC.SERVER_BUSY,
JSONRPC.METHOD_NOT_FOUND):
raise GracefulDisconnect(e, log_level=logging.WARNING) from e
raise
async def monitor_connection(self):

View File

@ -59,12 +59,12 @@ class JsonDB(Logger):
self.data = {}
self._modified = False
self.manual_upgrades = manual_upgrades
self._called_load_transactions = False
self._called_after_upgrade_tasks = False
if raw: # loading existing db
self.load_data(raw)
else: # creating new db
self.put('seed_version', FINAL_SEED_VERSION)
self.load_transactions()
self._after_upgrade_tasks()
def set_modified(self, b):
with self.lock:
@ -108,12 +108,6 @@ class JsonDB(Logger):
self.data[key] = copy.deepcopy(value)
return True
elif key in self.data:
# clear current contents in case of references
cur_val = self.data[key]
clear_method = getattr(cur_val, "clear", None)
if callable(clear_method):
clear_method()
# pop from dict to delete key
self.data.pop(key)
return True
return False
@ -149,9 +143,9 @@ class JsonDB(Logger):
if not self.manual_upgrades and self.requires_split():
raise WalletFileException("This wallet has multiple accounts and must be split")
self.load_transactions()
if not self.manual_upgrades and self.requires_upgrade():
if not self.requires_upgrade():
self._after_upgrade_tasks()
elif not self.manual_upgrades:
self.upgrade()
def requires_split(self):
@ -204,11 +198,9 @@ class JsonDB(Logger):
@profiler
def upgrade(self):
self.logger.info('upgrading wallet format')
if not self._called_load_transactions:
# note: not sure if this is how we should go about this...
# alternatively, we could make sure load_transactions is always called after upgrade
# still, we need strict ordering between the two.
raise Exception("'load_transactions' must be called before 'upgrade'")
if self._called_after_upgrade_tasks:
# we need strict ordering between upgrade() and after_upgrade_tasks()
raise Exception("'after_upgrade_tasks' must NOT be called before 'upgrade'")
self._convert_imported()
self._convert_wallet_type()
self._convert_account()
@ -220,6 +212,12 @@ class JsonDB(Logger):
self._convert_version_18()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks()
def _after_upgrade_tasks(self):
self._called_after_upgrade_tasks = True
self._load_transactions()
def _convert_wallet_type(self):
if not self._is_upgrade_method_needed(0, 13):
return
@ -415,15 +413,16 @@ class JsonDB(Logger):
self.put('pruned_txo', None)
transactions = self.get('transactions', {}) # txid -> Transaction
transactions = self.get('transactions', {}) # txid -> raw_tx
spent_outpoints = defaultdict(dict)
for txid, tx in transactions.items():
for txid, raw_tx in transactions.items():
tx = Transaction(raw_tx)
for txin in tx.inputs():
if txin['type'] == 'coinbase':
continue
prevout_hash = txin['prevout_hash']
prevout_n = txin['prevout_n']
spent_outpoints[prevout_hash][prevout_n] = txid
spent_outpoints[prevout_hash][str(prevout_n)] = txid
self.put('spent_outpoints', spent_outpoints)
self.put('seed_version', 17)
@ -475,6 +474,7 @@ class JsonDB(Logger):
self.put('accounts', None)
def _is_upgrade_method_needed(self, min_version, max_version):
assert min_version <= max_version
cur_version = self.get_seed_version()
if cur_version > max_version:
return False
@ -582,19 +582,22 @@ class JsonDB(Logger):
@locked
def get_spent_outpoint(self, prevout_hash, prevout_n):
return self.spent_outpoints.get(prevout_hash, {}).get(str(prevout_n))
prevout_n = str(prevout_n)
return self.spent_outpoints.get(prevout_hash, {}).get(prevout_n)
@modifier
def remove_spent_outpoint(self, prevout_hash, prevout_n):
self.spent_outpoints[prevout_hash].pop(prevout_n, None) # FIXME
prevout_n = str(prevout_n)
self.spent_outpoints[prevout_hash].pop(prevout_n, None)
if not self.spent_outpoints[prevout_hash]:
self.spent_outpoints.pop(prevout_hash)
@modifier
def set_spent_outpoint(self, prevout_hash, prevout_n, tx_hash):
prevout_n = str(prevout_n)
if prevout_hash not in self.spent_outpoints:
self.spent_outpoints[prevout_hash] = {}
self.spent_outpoints[prevout_hash][str(prevout_n)] = tx_hash
self.spent_outpoints[prevout_hash][prevout_n] = tx_hash
@modifier
def add_transaction(self, tx_hash: str, tx: Transaction) -> None:
@ -673,6 +676,8 @@ class JsonDB(Logger):
@locked
def get_data_ref(self, name):
# Warning: interacts un-intuitively with 'put': certain parts
# of 'data' will have pointers saved as separate variables.
if name not in self.data:
self.data[name] = {}
return self.data[name]
@ -686,12 +691,14 @@ class JsonDB(Logger):
return len(self.receiving_addresses)
@locked
def get_change_addresses(self):
return list(self.change_addresses)
def get_change_addresses(self, *, slice_start=None, slice_stop=None):
# note: slicing makes a shallow copy
return self.change_addresses[slice_start:slice_stop]
@locked
def get_receiving_addresses(self):
return list(self.receiving_addresses)
def get_receiving_addresses(self, *, slice_start=None, slice_stop=None):
# note: slicing makes a shallow copy
return self.receiving_addresses[slice_start:slice_stop]
@modifier
def add_change_address(self, addr):
@ -745,8 +752,7 @@ class JsonDB(Logger):
self._addr_to_addr_index[addr] = (True, i)
@profiler
def load_transactions(self):
self._called_load_transactions = True
def _load_transactions(self):
# references in self.data
self.txi = self.get_data_ref('txi') # txid -> address -> list of (prev_outpoint, value)
self.txo = self.get_data_ref('txo') # txid -> address -> list of (output_index, value, is_coinbase)

View File

@ -68,10 +68,10 @@ class VerifyingJSONRPCServer(SimpleJSONRPCServer, Logger):
return True
except (RPCAuthCredentialsInvalid, RPCAuthCredentialsMissing,
RPCAuthUnsupportedType) as e:
myself.send_error(401, str(e))
myself.send_error(401, repr(e))
except BaseException as e:
self.logger.exception('')
myself.send_error(500, str(e))
myself.send_error(500, repr(e))
return False
SimpleJSONRPCServer.__init__(

View File

@ -243,7 +243,8 @@ def configure_logging(config):
logging.getLogger('kivy').propagate = False
from . import ELECTRUM_VERSION
_logger.info(f"Electrum version: {ELECTRUM_VERSION} - https://electrum.org - https://github.com/spesmilo/electrum")
from .constants import GIT_REPO_URL
_logger.info(f"Electrum version: {ELECTRUM_VERSION} - https://electrum.org - {GIT_REPO_URL}")
_logger.info(f"Python version: {sys.version}. On platform: {describe_os_version()}")
_logger.info(f"Logging to file: {str(_logfile_path)}")
_logger.info(f"Log filters: verbosity {repr(verbosity)}, verbosity_shortcuts {repr(verbosity_shortcuts)}")

View File

@ -476,20 +476,26 @@ class Network(Logger):
@with_recent_servers_lock
def get_servers(self):
# start with hardcoded servers
out = dict(constants.net.DEFAULT_SERVERS) # copy
# note: order of sources when adding servers here is crucial!
# don't let "server_peers" overwrite anything,
# otherwise main server can eclipse the client
out = dict()
# add servers received from main interface
server_peers = self.server_peers
if server_peers:
out.update(filter_version(server_peers.copy()))
# hardcoded servers
out.update(constants.net.DEFAULT_SERVERS)
# add recent servers
for s in self.recent_servers:
try:
host, port, protocol = deserialize_server(s)
except:
continue
if host not in out:
if host in out:
out[host].update({protocol: port})
else:
out[host] = {protocol: port}
# add servers received from main interface
server_peers = self.server_peers
if server_peers:
out.update(filter_version(server_peers.copy()))
# potentially filter out some
if self.config.get('noonion'):
out = filter_noonion(out)

View File

@ -27,6 +27,7 @@ import sys
import time
import traceback
import json
from typing import Optional
import certifi
import urllib.parse
@ -92,9 +93,19 @@ async def get_payment_request(url: str) -> 'PaymentRequest':
data_len = len(data) if data is not None else None
_logger.info(f'fetched payment request {url} {data_len}')
except aiohttp.ClientError as e:
error = f"Error while contacting payment URL:\n{repr(e)}"
if isinstance(e, aiohttp.ClientResponseError) and e.status == 400 and resp_content:
error += "\n" + resp_content.decode("utf8")
error = f"Error while contacting payment URL: {url}.\nerror type: {type(e)}"
if isinstance(e, aiohttp.ClientResponseError):
error += f"\nGot HTTP status code {e.status}."
if resp_content:
try:
error_text_received = resp_content.decode("utf8")
except UnicodeDecodeError:
error_text_received = "(failed to decode error)"
else:
error_text_received = error_text_received[:400]
error_oneline = ' -- '.join(error.split('\n'))
_logger.info(f"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] "
f"{repr(e)} text: {error_text_received}")
data = None
elif u.scheme == 'file':
try:
@ -106,15 +117,15 @@ async def get_payment_request(url: str) -> 'PaymentRequest':
else:
data = None
error = f"Unknown scheme for payment request. URL: {url}"
pr = PaymentRequest(data, error)
pr = PaymentRequest(data, error=error)
return pr
class PaymentRequest:
def __init__(self, data, error=None):
def __init__(self, data, *, error=None):
self.raw = data
self.error = error
self.error = error # FIXME overloaded and also used when 'verify' succeeds
self.parse(data)
self.requestor = None # known after verify
self.tx = None
@ -123,6 +134,7 @@ class PaymentRequest:
return str(self.raw)
def parse(self, r):
self.outputs = []
if self.error:
return
self.id = bh2u(sha256(r)[0:16])
@ -134,7 +146,6 @@ class PaymentRequest:
return
self.details = pb2.PaymentDetails()
self.details.ParseFromString(self.data.serialized_payment_details)
self.outputs = []
for o in self.details.outputs:
type_, addr = transaction.get_address_from_output_script(o.script)
if type_ != TYPE_ADDRESS:
@ -235,7 +246,9 @@ class PaymentRequest:
self.error = "unknown algo"
return False
def has_expired(self):
def has_expired(self) -> Optional[bool]:
if not hasattr(self, 'details'):
return None
return self.details.expires and self.details.expires < int(time.time())
def get_expiration_date(self):
@ -302,9 +315,19 @@ class PaymentRequest:
print(f"PaymentACK message received: {paymntack.memo}")
return True, paymntack.memo
except aiohttp.ClientError as e:
error = f"Payment Message/PaymentACK Failed:\n{repr(e)}"
if isinstance(e, aiohttp.ClientResponseError) and e.status == 400 and resp_content:
error += "\n" + resp_content.decode("utf8")
error = f"Payment Message/PaymentACK Failed:\nerror type: {type(e)}"
if isinstance(e, aiohttp.ClientResponseError):
error += f"\nGot HTTP status code {e.status}."
if resp_content:
try:
error_text_received = resp_content.decode("utf8")
except UnicodeDecodeError:
error_text_received = "(failed to decode error)"
else:
error_text_received = error_text_received[:400]
error_oneline = ' -- '.join(error.split('\n'))
_logger.info(f"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] "
f"{repr(e)} text: {error_text_received}")
return False, error

View File

@ -27,6 +27,7 @@ import pkgutil
import importlib.util
import time
import threading
import sys
from typing import NamedTuple, Any, Union, TYPE_CHECKING, Optional
from .i18n import _
@ -73,6 +74,9 @@ class Plugins(DaemonThread):
raise Exception(f"Error pre-loading {full_name}: no spec")
try:
module = importlib.util.module_from_spec(spec)
# sys.modules needs to be modified for relative imports to work
# see https://stackoverflow.com/a/50395128
sys.modules[spec.name] = module
spec.loader.exec_module(module)
except Exception as e:
raise Exception(f"Error pre-loading {full_name}: {repr(e)}") from e
@ -403,7 +407,7 @@ class DeviceMgr(ThreadJob):
def unpair_xpub(self, xpub):
with self.lock:
if not xpub in self.xpub_ids:
if xpub not in self.xpub_ids:
return
_id = self.xpub_ids.pop(xpub)
self._close_client(_id)
@ -602,7 +606,7 @@ class DeviceMgr(ThreadJob):
new_devices = f()
except BaseException as e:
self.logger.error('custom device enum failed. func {}, error {}'
.format(str(f), str(e)))
.format(str(f), repr(e)))
else:
devices.extend(new_devices)

View File

@ -178,7 +178,7 @@ class Plugin(BasePlugin):
e = exc_info[1]
try: self.logger.error("on_failure", exc_info=exc_info)
except OSError: pass
window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + str(e))
window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + repr(e))
for window, xpub, K, _hash in self.cosigner_list:
if not self.cosigner_can_sign(tx, xpub):
@ -226,7 +226,7 @@ class Plugin(BasePlugin):
message = bh2u(privkey.decrypt_message(message))
except Exception as e:
self.logger.exception('')
window.show_error(_('Error decrypting message') + ':\n' + str(e))
window.show_error(_('Error decrypting message') + ':\n' + repr(e))
return
self.listener.clear(keyhash)

View File

@ -196,7 +196,7 @@ class Plugin(BasePlugin):
self.processor.send(recipient, message, payload)
except BaseException as e:
self.logger.exception('')
window.show_message(str(e))
window.show_message(repr(e))
else:
window.show_message(_('Request sent.'))
@ -269,4 +269,4 @@ class CheckConnectionThread(QThread):
conn = imaplib.IMAP4_SSL(self.server)
conn.login(self.username, self.password)
except BaseException as e:
self.connection_error_signal.emit(str(e))
self.connection_error_signal.emit(repr(e))

View File

@ -106,6 +106,6 @@ class Plugin(BasePlugin):
d.show_warning(_('{} is not covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification failed!'))
except BaseException as e:
self.logger.exception('')
d.show_error(str(e))
d.show_error(repr(e))
finally:
d.verify_button.setText(self.button_label)

View File

@ -44,6 +44,7 @@ class HW_PluginBase(BasePlugin):
BasePlugin.__init__(self, parent, config, name)
self.device = self.keystore_class.device
self.keystore_class.plugin = self
self._ignore_outdated_fw = False
def is_enabled(self):
return True
@ -124,6 +125,12 @@ class HW_PluginBase(BasePlugin):
message += '\n' + _("Make sure you install it with python3")
return message
def set_ignore_outdated_fw(self):
self._ignore_outdated_fw = True
def is_outdated_fw_ignored(self) -> bool:
return self._ignore_outdated_fw
def is_any_tx_output_on_change_branch(tx: Transaction):
if not tx.output_info:
@ -160,3 +167,16 @@ def only_hook_if_libraries_available(func):
class LibraryFoundButUnusable(Exception):
def __init__(self, library_version='unknown'):
self.library_version = library_version
class OutdatedHwFirmwareException(UserFacingException):
def text_ignore_old_fw_and_continue(self) -> str:
suffix = (_("The firmware of your hardware device is too old. "
"If possible, you should upgrade it. "
"You can ignore this error and try to continue, however things are likely to break.") + "\n\n" +
_("Ignore and continue?"))
if str(self):
return str(self) + "\n\n" + suffix
else:
return suffix

View File

@ -32,11 +32,13 @@ from PyQt5.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel
from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE
from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog,
Buttons, CancelButton, TaskThread)
Buttons, CancelButton, TaskThread, char_width_in_lineedit)
from electrum.i18n import _
from electrum.logging import Logger
from .plugin import OutdatedHwFirmwareException
# The trickiest thing about this handler was getting windows properly
# parented on macOS.
@ -147,7 +149,7 @@ class QtHandlerBase(QObject, Logger):
hbox = QHBoxLayout(dialog)
hbox.addWidget(QLabel(msg))
text = QLineEdit()
text.setMaximumWidth(100)
text.setMaximumWidth(12 * char_width_in_lineedit())
text.returnPressed.connect(dialog.accept)
hbox.addWidget(text)
hbox.addStretch(1)
@ -212,11 +214,27 @@ class QtPluginBase(object):
handler = self.create_handler(window)
handler.button = button
keystore.handler = handler
keystore.thread = TaskThread(window, window.on_error)
keystore.thread = TaskThread(window, on_error=partial(self.on_task_thread_error, window, keystore))
self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window)
# Trigger a pairing
keystore.thread.add(partial(self.get_client, keystore))
def on_task_thread_error(self, window, keystore, exc_info):
e = exc_info[1]
if isinstance(e, OutdatedHwFirmwareException):
if window.question(e.text_ignore_old_fw_and_continue(), title=_("Outdated device firmware")):
self.set_ignore_outdated_fw()
# will need to re-pair
devmgr = self.device_manager()
def re_pair_device():
device_id = self.choose_device(window, keystore)
devmgr.unpair_id(device_id)
self.get_client(keystore)
keystore.thread.add(re_pair_device)
return
else:
window.on_error(exc_info)
def choose_device(self, window, keystore):
'''This dialog box should be usable even if the user has
forgotten their PIN or it is in bootloader mode.'''

View File

@ -216,7 +216,7 @@ class KeepKeyPlugin(HW_PluginBase):
exit_code = 1
except BaseException as e:
self.logger.exception('')
handler.show_error(str(e))
handler.show_error(repr(e))
exit_code = 1
finally:
wizard.loop.exit(exit_code)

View File

@ -160,7 +160,7 @@ class LabelsPlugin(BasePlugin):
try:
await self.pull_thread(wallet, force)
except ErrorConnectingServer as e:
self.logger.info(str(e))
self.logger.info(repr(e))
def pull(self, wallet, force):
if not wallet.network: raise Exception(_('You are offline.'))

View File

@ -1,7 +1,5 @@
from functools import partial
#from btchip.btchipPersoWizard import StartBTChipPersoDialog
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QInputDialog, QLabel, QVBoxLayout, QLineEdit
@ -62,7 +60,7 @@ class Ledger_Handler(QtHandlerBase):
try:
from .auth2fa import LedgerAuthDialog
except ImportError as e:
self.message_dialog(str(e))
self.message_dialog(repr(e))
return
dialog = LedgerAuthDialog(self, data)
dialog.exec_()
@ -83,6 +81,3 @@ class Ledger_Handler(QtHandlerBase):
def setup_dialog(self):
self.show_error(_('Initialization of Ledger HW devices is currently disabled.'))
return
dialog = StartBTChipPersoDialog()
dialog.exec_()

View File

@ -200,7 +200,7 @@ class SafeTPlugin(HW_PluginBase):
exit_code = 1
except BaseException as e:
self.logger.exception('')
handler.show_error(str(e))
handler.show_error(repr(e))
exit_code = 1
finally:
wizard.loop.exit(exit_code)

View File

@ -7,6 +7,7 @@ from electrum.util import UserCancelled, UserFacingException
from electrum.keystore import bip39_normalize_passphrase
from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
from electrum.logging import Logger
from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException
from trezorlib.client import TrezorClient
from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError
@ -29,6 +30,8 @@ MESSAGES = {
class TrezorClientBase(Logger):
def __init__(self, transport, handler, plugin):
if plugin.is_outdated_fw_ignored():
TrezorClient.is_outdated = lambda *args, **kwargs: False
self.client = TrezorClient(transport, ui=self)
self.plugin = plugin
self.device = plugin.device
@ -62,15 +65,15 @@ class TrezorClientBase(Logger):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
def __exit__(self, exc_type, e, traceback):
self.end_flow()
if exc_value is not None:
if issubclass(exc_type, Cancelled):
raise UserCancelled from exc_value
elif issubclass(exc_type, TrezorFailure):
raise RuntimeError(str(exc_value)) from exc_value
elif issubclass(exc_type, OutdatedFirmwareError):
raise UserFacingException(exc_value) from exc_value
if e is not None:
if isinstance(e, Cancelled):
raise UserCancelled from e
elif isinstance(e, TrezorFailure):
raise RuntimeError(str(e)) from e
elif isinstance(e, OutdatedFirmwareError):
raise OutdatedHwFirmwareException(e) from e
else:
return False
return True

View File

@ -15,7 +15,7 @@ from electrum.logging import get_logger
from ..hw_wallet import HW_PluginBase
from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data,
LibraryFoundButUnusable)
LibraryFoundButUnusable, OutdatedHwFirmwareException)
_logger = get_logger(__name__)
@ -23,6 +23,7 @@ _logger = get_logger(__name__)
try:
import trezorlib
import trezorlib.transport
from trezorlib.transport.bridge import BridgeTransport, call_bridge
from .clientbase import TrezorClientBase
@ -137,7 +138,16 @@ class TrezorPlugin(HW_PluginBase):
raise LibraryFoundButUnusable(library_version=version)
def enumerate(self):
devices = trezorlib.transport.enumerate_devices()
# If there is a bridge, prefer that.
# On Windows, the bridge runs as Admin (and Electrum usually does not),
# so the bridge has better chances of finding devices. see #5420
# This also avoids duplicate entries.
try:
call_bridge("enumerate")
except Exception:
devices = trezorlib.transport.enumerate_devices()
else:
devices = BridgeTransport.enumerate()
return [Device(path=d.get_path(),
interface_number=-1,
id_=d.get_path(),
@ -212,7 +222,7 @@ class TrezorPlugin(HW_PluginBase):
exit_code = 1
except BaseException as e:
self.logger.exception('')
handler.show_error(str(e))
handler.show_error(repr(e))
exit_code = 1
finally:
wizard.loop.exit(exit_code)
@ -275,7 +285,7 @@ class TrezorPlugin(HW_PluginBase):
msg = (_('Outdated {} firmware for device labelled {}. Please '
'download the updated firmware from {}')
.format(self.device, client.label(), self.firmware_URL))
raise UserFacingException(msg)
raise OutdatedHwFirmwareException(msg)
# fixme: we should use: client.handler = wizard
client.handler = self.create_handler(wizard)

View File

@ -74,7 +74,7 @@ class Plugin(TrustedCoinPlugin):
def accept_terms_of_use(self, wizard):
def handle_error(msg, e):
wizard.show_error(msg + ':\n' + str(e))
wizard.show_error(msg + ':\n' + repr(e))
wizard.terminate()
try:
tos = server.get_terms_of_service()

View File

@ -132,7 +132,7 @@ class Plugin(TrustedCoinPlugin):
e = exc_info[1]
window.show_error("{header}\n{exc}\n\n{tor}"
.format(header=_('Error getting TrustedCoin account info.'),
exc=str(e),
exc=repr(e),
tor=_('If you keep experiencing network problems, try using a Tor proxy.')))
return WaitingDialog(parent=window,
message=_('Requesting account info from TrustedCoin server...'),
@ -253,7 +253,7 @@ class Plugin(TrustedCoinPlugin):
except Exception as e:
self.logger.exception('Could not retrieve Terms of Service')
tos_e.error_signal.emit(_('Could not retrieve Terms of Service:')
+ '\n' + str(e))
+ '\n' + repr(e))
return
self.TOS = tos
tos_e.tos_signal.emit()

View File

@ -485,7 +485,7 @@ class TrustedCoinPlugin(BasePlugin):
billing_info = server.get(wallet.get_user_id()[1])
except ErrorConnectingServer as e:
if suppress_connection_error:
self.logger.info(str(e))
self.logger.info(repr(e))
return
raise
billing_index = billing_info['billing_index']
@ -709,7 +709,7 @@ class TrustedCoinPlugin(BasePlugin):
wizard.show_message(str(e))
wizard.terminate()
except Exception as e:
wizard.show_message(str(e))
wizard.show_message(repr(e))
wizard.terminate()
else:
k3 = keystore.from_xpub(xpub3)

View File

@ -220,12 +220,6 @@
"t": "50001",
"version": "1.4"
},
"erbium1.sytes.net": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"fedaykin.goip.de": {
"pruning": "-",
"s": "50002",
@ -404,7 +398,7 @@
},
"fortress.qtornado.com": {
"pruning": "-",
"s": "50002",
"s": "443",
"t": "50001",
"version": "1.4"
},
@ -413,5 +407,11 @@
"s": "56002",
"t": "56001",
"version": "1.4"
},
"electrumx.erbium.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
}
}

View File

@ -533,14 +533,16 @@ class SimpleConfig(Logger):
fee_per_kb = self.fee_per_kb()
return fee_per_kb / 1000 if fee_per_kb is not None else None
def estimate_fee(self, size):
def estimate_fee(self, size: Union[int, float, Decimal]) -> int:
fee_per_kb = self.fee_per_kb()
if fee_per_kb is None:
raise NoDynamicFeeEstimates()
return self.estimate_fee_for_feerate(fee_per_kb, size)
@classmethod
def estimate_fee_for_feerate(cls, fee_per_kb, size):
def estimate_fee_for_feerate(cls, fee_per_kb: Union[int, float, Decimal],
size: Union[int, float, Decimal]) -> int:
size = Decimal(size)
fee_per_kb = Decimal(fee_per_kb)
fee_per_byte = fee_per_kb / 1000
# to be consistent with what is displayed in the GUI,

View File

@ -226,6 +226,9 @@ class WalletStorage(Logger):
raise Exception("storage not yet decrypted!")
return self.db.requires_upgrade()
def is_ready_to_be_used_by_wallet(self):
return not self.requires_upgrade() and self.db._called_after_upgrade_tasks
def upgrade(self):
self.db.upgrade()
self.write()
@ -240,6 +243,7 @@ class WalletStorage(Logger):
path = self.path + '.' + data['suffix']
storage = WalletStorage(path)
storage.db.data = data
storage.db._called_after_upgrade_tasks = False
storage.db.upgrade()
storage.write()
out.append(path)

View File

@ -147,7 +147,7 @@ class Synchronizer(SynchronizerBase):
def _reset(self):
super()._reset()
self.requested_tx = {}
self.requested_histories = {}
self.requested_histories = set()
def diagnostic_name(self):
return self.wallet.diagnostic_name()
@ -161,10 +161,10 @@ class Synchronizer(SynchronizerBase):
history = self.wallet.db.get_addr_history(addr)
if history_status(history) == status:
return
if addr in self.requested_histories:
if (addr, status) in self.requested_histories:
return
# request address history
self.requested_histories[addr] = status
self.requested_histories.add((addr, status))
h = address_to_scripthash(addr)
self._requests_sent += 1
result = await self.network.get_history_for_scripthash(h)
@ -188,7 +188,7 @@ class Synchronizer(SynchronizerBase):
await self._request_missing_txs(hist)
# Remove request; this allows up_to_date to be True
self.requested_histories.pop(addr)
self.requested_histories.discard((addr, status))
async def _request_missing_txs(self, hist, *, allow_server_not_finding_tx=False):
# "hist" is a list of [tx_hash, tx_height] lists
@ -292,6 +292,6 @@ class Notifier(SynchronizerBase):
async with session.post(url, json=data, headers=headers) as resp:
await resp.text()
except Exception as e:
self.logger.info(str(e))
self.logger.info(repr(e))
else:
self.logger.info(f'Got Response for {addr}')

View File

@ -75,6 +75,43 @@ class TestCommands(unittest.TestCase):
ciphertext = cmds.encrypt(pubkey, cleartext)
self.assertEqual(cleartext, cmds.decrypt(pubkey, ciphertext))
@mock.patch.object(storage.WalletStorage, '_write')
def test_export_private_key_imported(self, mock_write):
wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL',
path='if_this_exists_mocking_failed_648151893')['wallet']
cmds = Commands(config=None, wallet=wallet, network=None)
# single address tests
with self.assertRaises(Exception):
cmds.getprivatekeys("asdasd") # invalid addr, though might raise "not in wallet"
with self.assertRaises(Exception):
cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23") # not in wallet
self.assertEqual("p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL",
cmds.getprivatekeys("bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw"))
# list of addresses tests
with self.assertRaises(Exception):
cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'asd'])
self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'],
cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n']))
@mock.patch.object(storage.WalletStorage, '_write')
def test_export_private_key_deterministic(self, mock_write):
wallet = restore_wallet_from_text('bitter grass shiver impose acquire brush forget axis eager alone wine silver',
gap_limit=2,
path='if_this_exists_mocking_failed_648151893')['wallet']
cmds = Commands(config=None, wallet=wallet, network=None)
# single address tests
with self.assertRaises(Exception):
cmds.getprivatekeys("asdasd") # invalid addr, though might raise "not in wallet"
with self.assertRaises(Exception):
cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23") # not in wallet
self.assertEqual("p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2",
cmds.getprivatekeys("bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af"))
# list of addresses tests
with self.assertRaises(Exception):
cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'asd'])
self.assertEqual(['p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'],
cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n']))
class TestCommandsTestnet(TestCaseForTestnet):

View File

@ -305,7 +305,13 @@ class TestStorageUpgrade(WalletTestCase):
storage2 = self._load_storage_from_json_string(wallet_json=wallet_json,
path=path2,
manual_upgrades=False)
storage2.write()
self._sanity_check_upgraded_storage(storage2)
# test opening upgraded storages again
s1 = WalletStorage(path2, manual_upgrades=False)
self._sanity_check_upgraded_storage(s1)
s2 = WalletStorage(path2, manual_upgrades=True)
self._sanity_check_upgraded_storage(s2)
else:
storage = self._load_storage_from_json_string(wallet_json=wallet_json,
path=self.wallet_path,

View File

@ -156,7 +156,8 @@ class TestCreateRestoreWallet(WalletTestCase):
passphrase=passphrase,
password=password,
encrypt_file=encrypt_file,
segwit=True)
segwit=True,
gap_limit=1)
wallet = d['wallet'] # type: Standard_Wallet
wallet.check_password(password)
self.assertEqual(passphrase, wallet.keystore.get_passphrase(password))
@ -173,7 +174,8 @@ class TestCreateRestoreWallet(WalletTestCase):
network=None,
passphrase=passphrase,
password=password,
encrypt_file=encrypt_file)
encrypt_file=encrypt_file,
gap_limit=1)
wallet = d['wallet'] # type: Standard_Wallet
self.assertEqual(passphrase, wallet.keystore.get_passphrase(password))
self.assertEqual(text, wallet.keystore.get_seed(password))
@ -182,14 +184,21 @@ class TestCreateRestoreWallet(WalletTestCase):
def test_restore_wallet_from_text_xpub(self):
text = 'zpub6nydoME6CFdJtMpzHW5BNoPz6i6XbeT9qfz72wsRqGdgGEYeivso6xjfw8cGcCyHwF7BNW4LDuHF35XrZsovBLWMF4qXSjmhTXYiHbWqGLt'
d = restore_wallet_from_text(text, path=self.wallet_path, network=None)
d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1)
wallet = d['wallet'] # type: Standard_Wallet
self.assertEqual(text, wallet.keystore.get_master_public_key())
self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0])
def test_restore_wallet_from_text_xkey_that_is_also_a_valid_electrum_seed_by_chance(self):
text = 'yprvAJBpuoF4FKpK92ofzQ7ge6VJMtorow3maAGPvPGj38ggr2xd1xCrC9ojUVEf9jhW5L9SPu6fU2U3o64cLrRQ83zaQGNa6YP3ajZS6hHNPXj'
d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1)
wallet = d['wallet'] # type: Standard_Wallet
self.assertEqual(text, wallet.keystore.get_master_private_key(password=None))
self.assertEqual('3Pa4hfP3LFWqa2nfphYaF7PZfdJYNusAnp', wallet.get_receiving_addresses()[0])
def test_restore_wallet_from_text_xprv(self):
text = 'zprvAZzHPqhCMt51fskXBUYB1fTFYgG3CBjJUT4WEZTpGw6hPSDWBPZYZARC5sE9xAcX8NeWvvucFws8vZxEa65RosKAhy7r5MsmKTxr3hmNmea'
d = restore_wallet_from_text(text, path=self.wallet_path, network=None)
d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1)
wallet = d['wallet'] # type: Standard_Wallet
self.assertEqual(text, wallet.keystore.get_master_private_key(password=None))
self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0])

View File

@ -1,3 +1,4 @@
import unittest
from unittest import mock
import shutil
import tempfile
@ -8,7 +9,7 @@ from electrum import storage, bitcoin, keystore, bip32
from electrum import Transaction
from electrum import SimpleConfig
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet
from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, restore_wallet_from_text
from electrum.util import bfh, bh2u
from electrum.transaction import TxOutput
from electrum.mnemonic import seed_type
@ -859,7 +860,20 @@ class TestWalletSending(TestCaseForTestnet):
@needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write')
def test_bump_fee_p2pkh(self, mock_write):
def test_rbf(self, mock_write):
for simulate_moving_txs in (False, True):
with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_p2pkh_when_there_is_a_change_address(simulate_moving_txs=simulate_moving_txs)
with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_p2wpkh_when_there_is_a_change_address(simulate_moving_txs=simulate_moving_txs)
with self.subTest(msg="_bump_fee_when_user_sends_max", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_when_user_sends_max(simulate_moving_txs=simulate_moving_txs)
with self.subTest(msg="_bump_fee_when_new_inputs_need_to_be_added", simulate_moving_txs=simulate_moving_txs):
self._bump_fee_when_new_inputs_need_to_be_added(simulate_moving_txs=simulate_moving_txs)
with self.subTest(msg="_rbf_batching", simulate_moving_txs=simulate_moving_txs):
self._rbf_batching(simulate_moving_txs=simulate_moving_txs)
def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs):
wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean')
# bootstrap wallet
@ -876,6 +890,8 @@ class TestWalletSending(TestCaseForTestnet):
tx.set_rbf(True)
tx.locktime = 1325501
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
@ -895,22 +911,24 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance())
# bump tx
tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000)
tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config)
tx.locktime = 1325501
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
self.assertFalse(tx.is_complete())
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit())
tx_copy = Transaction(tx.serialize())
self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006a473044022055b7e6b7e89a55740f7aa2ad1ffcd4b5c913f0de63cf512438921534bc9c3a8d022043b3b27bdc2da4cc6265e4cc9673a3780ccd5cd6f0ee2eaedb51720c15b7a00a012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987d0497200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400',
self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006b483045022100a30c21d1ba8cf751b1b78b5a41684cbab6e39687fa188a4295881c7b06f10a6202204ba4f56cbfdeb8ed948d8a18e34112c256c48e921db048f134819b2ca7ed85fd012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987a0337200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400',
str(tx_copy))
self.assertEqual('f26edcf20991dccedf16058adbee923db7057c9b102db660156b8142b6a59bc7', tx_copy.txid())
self.assertEqual('f26edcf20991dccedf16058adbee923db7057c9b102db660156b8142b6a59bc7', tx_copy.wtxid())
self.assertEqual('40768e1e418f8e851d496448c9627ee29f04c33f67a59ac49d2bbc66288d2077', tx_copy.txid())
self.assertEqual('40768e1e418f8e851d496448c9627ee29f04c33f67a59ac49d2bbc66288d2077', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, funding_output_value - 2500000 - 10000, 0), wallet.get_balance())
self.assertEqual((0, 7484320, 0), wallet.get_balance())
@needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write')
@ -946,9 +964,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance())
@needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write')
def test_bump_fee_p2wpkh(self, mock_write):
def _bump_fee_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage')
# bootstrap wallet
@ -965,6 +981,8 @@ class TestWalletSending(TestCaseForTestnet):
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
@ -984,22 +1002,247 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance())
# bump tx
tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000)
tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config)
tx.locktime = 1325500
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
self.assertFalse(tx.is_complete())
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
tx_copy = Transaction(tx.serialize())
self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987d049720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5024730440220517fed3a902b5b41fa718ffd5f229b835b8ed26f23433c4ea437d24eff66d15b0220526854a6ebcd351ab2373d0e7c4e20f17c420520b5d570c2df7ca1d773d6a55d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',
self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870c4a720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402202a7e412d37f7a54f7ede0f85e58c7f9dc0f7244d222a4f50a90f87b05badeed40220788d4a4a13f660de7d5464dce5e79419361fdd5d1853c7da65469cd32f7981a90121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',
str(tx_copy))
self.assertEqual('9a1c0ef7e871798b86074c7f8dd1e81b6d9a758ff07e0059eee31dc6fbf4f438', tx_copy.txid())
self.assertEqual('59144d30c911ac33359b0a32d5a3fdd2ca806982c85838e193eb95f5d315e813', tx_copy.wtxid())
self.assertEqual('dad75ab7078b9ce9698a83e7a954c1c38b235d3a4ab79bcb340245e3d9b62b93', tx_copy.txid())
self.assertEqual('05a484c64a094724b1c58a15463c8c772a98f084cc23ee636204ad9c4d9e5b51', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, funding_output_value - 2500000 - 10000, 0), wallet.get_balance())
self.assertEqual((0, 7490060, 0), wallet.get_balance())
def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage')
# bootstrap wallet
funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')
funding_txid = funding_tx.txid()
self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid)
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create tx
outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
coins = wallet.get_spendable_coins(domain=None, config=self.config)
tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000)
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
tx_copy = Transaction(tx.serialize())
self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
self.assertEqual(tx.txid(), tx_copy.txid())
self.assertEqual(tx.wtxid(), tx_copy.wtxid())
self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220520ab41536d5d0fac8ad44e6aa4a8258a266121bab1eb6599f1ee86bbc65719d02205944c2fb765fca4753a850beadac49f5305c6722410c347c08cec4d90e3eb4430121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',
str(tx_copy))
self.assertEqual('dc4b622f3225f00edb886011fa02b74630cdbc24cebdd3210d5ea3b68bef5cc9', tx_copy.txid())
self.assertEqual('a00340ee8c90673e05f2cf368601b6bba6a7f0513bd974feb218a326e39b1874', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 0, 0), wallet.get_balance())
# bump tx
tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config)
tx.locktime = 1325500
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
self.assertFalse(tx.is_complete())
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
tx_copy = Transaction(tx.serialize())
self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01267898000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98702473044022069412007c3a6509fdfcfbe90679395c202c973740b0530b8ff366bc86ebff99d02206a02e3c0beb0921fa7d30379db4999d685d4b97239a2b8c7dd839531c72863110121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',
str(tx_copy))
self.assertEqual('53824cc67e8fe973b0dfa1b8cc10f4e2441b9b4b2b1eb92576fbba7000c2908a', tx_copy.txid())
self.assertEqual('bb137a5a810bb44d3b1cc77fb4f840e7c8c0f84771f7ce4671c3b1a9f5f93724', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 0, 0), wallet.get_balance())
def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_txs):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage')
# bootstrap wallet (incoming funding_tx1)
funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')
funding_txid1 = funding_tx1.txid()
#funding_output_value = 10_000_000
self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid1)
wallet.receive_tx_callback(funding_txid1, funding_tx1, TX_HEIGHT_UNCONFIRMED)
# create tx
outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
coins = wallet.get_spendable_coins(domain=None, config=self.config)
tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000)
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
tx_copy = Transaction(tx.serialize())
self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
self.assertEqual(tx.txid(), tx_copy.txid())
self.assertEqual(tx.wtxid(), tx_copy.wtxid())
self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220520ab41536d5d0fac8ad44e6aa4a8258a266121bab1eb6599f1ee86bbc65719d02205944c2fb765fca4753a850beadac49f5305c6722410c347c08cec4d90e3eb4430121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',
str(tx_copy))
self.assertEqual('dc4b622f3225f00edb886011fa02b74630cdbc24cebdd3210d5ea3b68bef5cc9', tx_copy.txid())
self.assertEqual('a00340ee8c90673e05f2cf368601b6bba6a7f0513bd974feb218a326e39b1874', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 0, 0), wallet.get_balance())
# another incoming transaction (funding_tx2)
funding_tx2 = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400')
funding_txid2 = funding_tx2.txid()
#funding_output_value = 5_000_000
self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid2)
wallet.receive_tx_callback(funding_txid2, funding_tx2, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 5_000_000, 0), wallet.get_balance())
# bump tx
tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config)
tx.locktime = 1325500
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
self.assertFalse(tx.is_complete())
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
tx_copy = Transaction(tx.serialize())
self.assertEqual('01000000000102c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000feffffff025c254c0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220075992f2696076ca14265372c797fa5c6116ef9b8023f36fa7500442fe3e21430220252677cce7b009d8a65681e8f50b78c9a31c6461f67c995b8804041a290893660121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c502473044022018379b52ea52436eaeef1593e08aba78db1fd624b804ab747722f748203d553702204cbe4c87a010c8b67be9034014b503354e72f9c8205172269c00de20883fac61012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbc391400',
str(tx_copy))
self.assertEqual('056aaf5ec628a492742b083ad7790836e2d12e89061f32d5b517679764fdaff1', tx_copy.txid())
self.assertEqual('0c26d17386408d0111ebc94a5d05f6afd681add632dfbcd986658f9d9fe25ff7', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 4_990_300, 0), wallet.get_balance())
def _rbf_batching(self, *, simulate_moving_txs):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage')
config = SimpleConfig({'electrum_path': self.electrum_path, 'batch_rbf': True})
# bootstrap wallet (incoming funding_tx1)
funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')
funding_txid1 = funding_tx1.txid()
#funding_output_value = 10_000_000
self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid1)
wallet.receive_tx_callback(funding_txid1, funding_tx1, TX_HEIGHT_UNCONFIRMED)
# create tx
outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2_500_000)]
coins = wallet.get_spendable_coins(domain=None, config=config)
tx = wallet.make_unsigned_transaction(coins, outputs, config=config, fixed_fee=5000)
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
tx_copy = Transaction(tx.serialize())
self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
self.assertEqual(tx.txid(), tx_copy.txid())
self.assertEqual(tx.wtxid(), tx_copy.wtxid())
self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402205442705e988abe74bf391b293bb1b886674284a92ed0788c33024f9336d60aef022013a93049d3bed693254cd31a704d70bb988a36750f0b74d0a5b4d9e29c54ca9d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',
str(tx_copy))
self.assertEqual('b019bbad45a46ed25365e46e4cae6428fb12ae425977eb93011ffb294cb4977e', tx_copy.txid())
self.assertEqual('ba87313e2b3b42f1cc478843d4d53c72d6e06f6c66ac8cfbe2a59cdac2fd532d', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 7_495_000, 0), wallet.get_balance())
# another incoming transaction (funding_tx2)
funding_tx2 = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400')
funding_txid2 = funding_tx2.txid()
#funding_output_value = 5_000_000
self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid2)
wallet.receive_tx_callback(funding_txid2, funding_tx2, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 12_495_000, 0), wallet.get_balance())
# create new tx (output should be batched with existing!)
# no new input will be needed. just a new output, and change decreased.
outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)]
coins = wallet.get_spendable_coins(domain=None, config=config)
tx = wallet.make_unsigned_transaction(coins, outputs, config=config, fixed_fee=20000)
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
tx_copy = Transaction(tx.serialize())
self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
self.assertEqual(tx.txid(), tx_copy.txid())
self.assertEqual(tx.wtxid(), tx_copy.wtxid())
self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff03a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98720fd4b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402206add1d6fc8b5fc6fd1bbf50d06fe432e65b16a9d715dbfe7f2d26473f48a128302207983d8db3508e3b953e6e26581d2bbba5a7ca0ff0dd07361de60977dc61ed1580121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',
str(tx_copy))
self.assertEqual('21112d35fa08b9577bfe46405ad17720d0fa85bcefab0b0a1cffe79b9d6167c4', tx_copy.txid())
self.assertEqual('d49ffdaa832a35d88f3f43bcfb08306347c2342200098f450e41ccb289b26db3', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 9_980_000, 0), wallet.get_balance())
# create new tx (output should be batched with existing!)
# new input will be needed!
outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)]
coins = wallet.get_spendable_coins(domain=None, config=config)
tx = wallet.make_unsigned_transaction(coins, outputs, config=config, fixed_fee=100_000)
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
if simulate_moving_txs:
tx = Transaction(str(tx))
wallet.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
self.assertEqual(2, len(tx.inputs()))
tx_copy = Transaction(tx.serialize())
self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
self.assertEqual(tx.txid(), tx_copy.txid())
self.assertEqual(tx.wtxid(), tx_copy.wtxid())
self.assertEqual('01000000000102c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff04a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98760823b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5808d5b000000000017a914d332f2f63019da6f2d23ee77bbe30eed7739790587024730440220730ac17af4ac14f008ee5d0a7be524d8ca344afc19b548faa9ac8c21a216df81022010d9cc878402103c1dd6b06e97e7910a23b7ec88251627f47ed1d5a8d741beba0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50247304402201005fc1e9091ac36d98b60c1c8b65aada0d4fe4da438d69b3262028644005cfc02207353c987be9e33d1e8702689960df76ac28adacc2f9093d731bc56c9578c5458012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbb391400',
str(tx_copy))
self.assertEqual('88791bcd352b50592a5521c15595972b14b5d6be165be2df0e57ea19e588c025', tx_copy.txid())
self.assertEqual('7c5e5bff601e5467036b574b41090681a86de403867dd2b14097920b95e392ed', tx_copy.wtxid())
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 3_900_000, 0), wallet.get_balance())
@needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write')
@ -1745,3 +1988,47 @@ class TestWalletHistory_EvilGapLimit(TestCaseForTestnet):
{})
w.synchronize()
self.assertEqual(9999788, sum(w.get_balance()))
class TestWalletHistory_DoubleSpend(TestCaseForTestnet):
transactions = {
# txn A:
"a3849040f82705151ba12a4389310b58a17b78025d81116a3338595bdefa1625": "020000000001011b7eb29921187b40209c234344f57a3365669c8883a3d511fbde5155f11f64d10000000000fdffffff024c400f0000000000160014b50d21483fb5e088db90bf766ea79219fb377fef40420f0000000000160014aaf5fc4a6297375c32403a9c2768e7029c8dbd750247304402206efd510954b289829f8f778163b98a2a4039deb93c3b0beb834b00cd0add14fd02201c848315ddc52ced0350a981fe1a7f3cbba145c7a43805db2f126ed549eaa500012103083a50d63264743456a3e812bfc91c11bd2a673ba4628c09f02d78f62157e56d788d1700",
# txn B:
"0e2182ead6660790290371516cb0b80afa8baebd30dad42b5e58a24ceea17f1c": "020000000001012516fade5b5938336a11815d02787ba1580b3189432aa11b150527f8409084a30100000000fdffffff02a086010000000000160014cb893c9fbb565363556fb18a3bcdda6f20af0bf8d8ba0d0000000000160014478902f02c2b6cd405bb6bd1f90e9860bec173e20247304402206940671b5bdb230a9721aa57396af73d399fb210d795e7dbb8ec1977e101a5470220625505de035d4006b72bd6dfcf09468d1e8da53071080b37b16b0dbbf776db78012102254b5b20ed21c3bba75ec2a9ff230257d13a2493f6b7da066d8195dcdd484310788d1700",
# txn C:
"2c9aa33d9c8ec649f9bfb84af027a5414b760be5231fe9eca4a95b9eb3f8a017": "020000000001012516fade5b5938336a11815d02787ba1580b3189432aa11b150527f8409084a30100000000fdffffff01d2410f00000000001600147880a7c79744b908a5f6d6235f2eb46c174c84f002483045022100974d27c872f09115e57c6acb674cd4da6d0b26656ad967ddb2678ff409714b9502206d91b49cf778ced6ca9e40b4094fb57b86c86fac09ce46ce53aea4afa68ff311012102254b5b20ed21c3bba75ec2a9ff230257d13a2493f6b7da066d8195dcdd484310788d1700",
}
@mock.patch.object(storage.WalletStorage, '_write')
def test_restoring_wallet_without_manual_delete(self, mock_write):
w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel",
path='if_this_exists_mocking_failed_648151893',
gap_limit=5)['wallet']
for txid in self.transactions:
tx = Transaction(self.transactions[txid])
w.add_transaction(tx.txid(), tx)
# txn A is an external incoming txn funding the wallet
# txn B is an outgoing payment to an external address
# txn C is double-spending txn B, to a wallet address
self.assertEqual(999890, sum(w.get_balance()))
@mock.patch.object(storage.WalletStorage, '_write')
def test_restoring_wallet_with_manual_delete(self, mock_write):
w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel",
path='if_this_exists_mocking_failed_648151893',
gap_limit=5)['wallet']
# txn A is an external incoming txn funding the wallet
txA = Transaction(self.transactions["a3849040f82705151ba12a4389310b58a17b78025d81116a3338595bdefa1625"])
w.add_transaction(txA.txid(), txA)
# txn B is an outgoing payment to an external address
txB = Transaction(self.transactions["0e2182ead6660790290371516cb0b80afa8baebd30dad42b5e58a24ceea17f1c"])
w.add_transaction(txB.txid(), txB)
# now the user manually deletes txn B to attempt the double spend
# txn C is double-spending txn B, to a wallet address
# rationale1: user might do this with opt-in RBF transactions
# rationale2: this might be a local transaction, in which case the GUI even allows it
w.remove_transaction(txB)
txC = Transaction(self.transactions["2c9aa33d9c8ec649f9bfb84af027a5414b760be5231fe9eca4a95b9eb3f8a017"])
w.add_transaction(txC.txid(), txC)
self.assertEqual(999890, sum(w.get_balance()))

View File

@ -86,6 +86,12 @@ class TxOutputHwInfo(NamedTuple):
script_type: str
class BIP143SharedTxDigestFields(NamedTuple):
hashPrevouts: str
hashSequence: str
hashOutputs: str
class BCDataStream(object):
"""Workalike python implementation of Bitcoin's CDataStream class."""
@ -688,7 +694,10 @@ class Transaction:
def remove_signatures(self):
for txin in self.inputs():
txin['signatures'] = [None] * len(txin['signatures'])
txin['scriptSig'] = None
txin['witness'] = None
assert not self.is_complete()
self.raw = None
def deserialize(self, force_full_parse=False):
if self.raw is None:
@ -955,18 +964,30 @@ class Transaction:
s += script
return s
def serialize_preimage(self, i):
def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields:
inputs = self.inputs()
outputs = self.outputs()
hashPrevouts = bh2u(sha256d(bfh(''.join(self.serialize_outpoint(txin) for txin in inputs))))
hashSequence = bh2u(sha256d(bfh(''.join(int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) for txin in inputs))))
hashOutputs = bh2u(sha256d(bfh(''.join(self.serialize_output(o) for o in outputs))))
return BIP143SharedTxDigestFields(hashPrevouts=hashPrevouts,
hashSequence=hashSequence,
hashOutputs=hashOutputs)
def serialize_preimage(self, txin_index: int, *,
bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str:
nVersion = int_to_hex(self.version, 4)
nHashType = int_to_hex(1, 4)
nHashType = int_to_hex(1, 4) # SIGHASH_ALL
nLocktime = int_to_hex(self.locktime, 4)
inputs = self.inputs()
outputs = self.outputs()
txin = inputs[i]
# TODO: py3 hex
txin = inputs[txin_index]
if self.is_segwit_input(txin):
hashPrevouts = bh2u(sha256d(bfh(''.join(self.serialize_outpoint(txin) for txin in inputs))))
hashSequence = bh2u(sha256d(bfh(''.join(int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) for txin in inputs))))
hashOutputs = bh2u(sha256d(bfh(''.join(self.serialize_output(o) for o in outputs))))
if bip143_shared_txdigest_fields is None:
bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields()
hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts
hashSequence = bip143_shared_txdigest_fields.hashSequence
hashOutputs = bip143_shared_txdigest_fields.hashOutputs
outpoint = self.serialize_outpoint(txin)
preimage_script = self.get_preimage_script(txin)
scriptCode = var_int(len(preimage_script) // 2) + preimage_script
@ -974,7 +995,8 @@ class Transaction:
nSequence = int_to_hex(txin.get('sequence', 0xffffffff - 1), 4)
preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType
else:
txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.get_preimage_script(txin) if i==k else '') for k, txin in enumerate(inputs))
txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.get_preimage_script(txin) if txin_index==k else '')
for k, txin in enumerate(inputs))
txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs)
preimage = nVersion + txins + txouts + nLocktime + nHashType
return preimage
@ -1039,13 +1061,13 @@ class Transaction:
self.raw = None
self.BIP69_sort(inputs=False)
def input_value(self):
def input_value(self) -> int:
return sum(x['value'] for x in self.inputs())
def output_value(self):
return sum(val for tp, addr, val in self.outputs())
def output_value(self) -> int:
return sum(o.value for o in self.outputs())
def get_fee(self):
def get_fee(self) -> int:
return self.input_value() - self.output_value()
def is_final(self):
@ -1126,6 +1148,7 @@ class Transaction:
def sign(self, keypairs) -> None:
# keypairs: (x_)pubkey -> secret_bytes
bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields()
for i, txin in enumerate(self.inputs()):
pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
for j, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)):
@ -1139,14 +1162,15 @@ class Transaction:
continue
_logger.info(f"adding signature for {_pubkey}")
sec, compressed = keypairs.get(_pubkey)
sig = self.sign_txin(i, sec)
sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields)
self.add_signature_to_txin(i, j, sig)
_logger.info(f"is_complete {self.is_complete()}")
self.raw = self.serialize()
def sign_txin(self, txin_index, privkey_bytes) -> str:
pre_hash = sha256d(bfh(self.serialize_preimage(txin_index)))
def sign_txin(self, txin_index, privkey_bytes, *, bip143_shared_txdigest_fields=None) -> str:
pre_hash = sha256d(bfh(self.serialize_preimage(txin_index,
bip143_shared_txdigest_fields=bip143_shared_txdigest_fields)))
privkey = ecc.ECPrivkey(privkey_bytes)
sig = privkey.sign_transaction(pre_hash)
sig = bh2u(sig) + '01'

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