Merge branch 'master' into master

This commit is contained in:
nimbosa 2019-01-05 16:28:54 +08:00 committed by GitHub
commit 4694a65592
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 5408 additions and 2571 deletions

2
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,2 @@
<!-- Note: This website is for bug reports, not general questions.
Do not post issues about non-bitcoin versions of Electrum. -->

View File

@ -4,11 +4,28 @@ python:
- 3.5 - 3.5
- 3.6 - 3.6
install: install:
- pip install -r requirements_travis.txt - pip install -r contrib/requirements/requirements-travis.txt
cache: cache:
- pip - pip: true
- directories:
- /tmp/electrum-build
script: script:
- tox - tox
after_success: after_success:
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi
- coveralls - coveralls
jobs:
include:
- stage: windows build
sudo: true
python: 3.5
install:
- sudo dpkg --add-architecture i386
- wget -nc https://dl.winehq.org/wine-builds/Release.key
- sudo apt-key add Release.key
- sudo apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/
- sudo apt-get update -qq
- sudo apt-get install -qq winehq-stable dirmngr gnupg2 p7zip-full
before_script: ls -lah /tmp/electrum-build
script: ./contrib/build-wine/build.sh
after_success: true

View File

@ -1,6 +1,6 @@
FROM ubuntu:18.04 FROM ubuntu:18.04
ENV VERSION 1.0.6 ENV VERSION 1.0.7
RUN set -x \ RUN set -x \
&& apt-get update \ && apt-get update \

View File

@ -101,30 +101,18 @@ Run the docker image::
./run-docker.sh ./run-docker.sh
Building Releases See `contrib/build-osx/`.
=================
MacOS
------
Simply - ::
./setup-mac.sh
sudo ./create-dmg.sh
Windows Windows
------- -------
See `contrib/build-wine/README` file. See `contrib/build-wine/`.
Android Android
------- -------
See `gui/kivy/Readme.txt` file. See `gui/kivy/Readme.txt` file.
UPSTREAM PATCH: https://github.com/spesmilo/electrum/blob/master/gui/kivy/Readme.md
--- ---

View File

@ -1,3 +1,80 @@
# Release 3.1 - (March 5, 2018)
* Memory-pool based fee estimation. Dynamic fees can target a desired
depth in the memory pool. This feature is optional, and ETA-based
estimates from Bitcoin Core are still available. Note that miners
could exploit this feature, if they conspired and filled the memory
pool with expensive transactions that never get mined. However,
since the Electrum client already trusts an Electrum server with
fee estimates, activating this feature does not introduce any new
vulnerability. In addition, the client uses a hard threshold to
protect itself from servers sending excessive fee estimates. In
practice, ETA-based estimates have resulted in sticky fees, and
caused many users to overpay for transactions. Advanced users tend
to visit (and trust) websites that display memory-pool data in
order to set their fees.
* Capital gains: For each outgoing transaction, the difference
between the acquisition and liquidation prices of outgoing coins is
displayed in the wallet history. By default, historical exchange
rates are used to compute acquisition and liquidation prices. These
values can also be entered manually, in order to match the actual
price realized by the user. The order of liquidation of coins is
the natural order defined by the blockchain; this results in
capital gain values that are invariant to changes in the set of
addresses that are in the wallet. Any other ordering strategy (such
as FIFO, LIFO) would result in capital gain values that depend on
the presence of other addresses in the wallet.
* Local transactions: Transactions can be saved in the wallet without
being broadcast. The inputs of local transactions are considered as
spent, and their change outputs can be re-used in subsequent
transactions. This can be combined with cold storage, in order to
create several transactions before broadcasting them. Outgoing
transactions that have been removed from the memory pool are also
saved in the wallet, and can be broadcast again.
* Checkpoints: The initial download of a headers file was replaced
with hardcoded checkpoints. The wallet uses one checkpoint per
retargeting period. The headers for a retargeting period are
downloaded only if transactions need to be verified in this period.
* The 'privacy' and 'priority' coin selection policies have been
merged into one. Previously, the 'privacy' policy has been unusable
because it was was not prioritizing confirmed coins. The new policy
is similar to 'privacy', except that it de-prioritizes addresses
that have unconfirmed coins.
* The 'Send' tab of the Qt GUI displays how transaction fees are
computed from transaction size.
* The wallet history can be filtered by time interval.
* Replace-by-fee is enabled by default. Note that this might cause
some issues with wallets that do not display RBF transactions until
they are confirmed.
* Watching-only wallets and hardware wallets can be encrypted.
* Semi-automated crash reporting
* The SSL checkbox option was removed from the GUI.
* The Trezor T hardware wallet is now supported.
* BIP84: native segwit p2wpkh scripts for bip39 seeds and hardware
wallets can now be created when specifying a BIP84 derivation
path. This is usable with Trezor and Ledger.
* Windows: the binaries now include ZBar, and QR code scanning should work.
* The Wallet Import Format (WIF) for private keys that was extended in 3.0
is changed. Keys in the previous format can be imported, compatibility
is maintained. Newly exported keys will be serialized as
"script_type:original_wif_format_key".
* BIP32 master keys for testnet once again have different version bytes than
on mainnet. For the mainnet prefixes {x,y,Y,z,Z}|{pub,prv}, the
corresponding testnet prefixes are {t,u,U,v,V}|{pub,prv}.
More details and exact version bytes are specified at:
https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst
Note that due to this change, testnet wallet files created with previous
versions of Electrum must be considered broken, and they need to be
recreated from seed words.
* A new version of the Electrum protocol is required by the client
(version 1.2). Servers using older versions of the protocol will
not be displayed in the GUI.
# Release 3.0.6 :
* Fix transaction parsing bug #3788
# Release 3.0.5 : (Security update) # Release 3.0.5 : (Security update)
This is a follow-up to the 3.0.4 release, which did not completely fix This is a follow-up to the 3.0.4 release, which did not completely fix

View File

@ -0,0 +1,17 @@
Building Mac OS binaries
========================
This guide explains how to build Electrum binaries for macOS systems.
We build our binaries on El Capitan (10.11.6) as building it on High Sierra
makes the binaries incompatible with older versions.
This assumes that the Xcode command line tools (and thus git) are already installed.
## 1. Run the script
./make_osx
## 2. Done

84
contrib/build-osx/make_osx Executable file
View File

@ -0,0 +1,84 @@
#!/bin/bash
RED='\033[0;31m'
BLUE='\033[0,34m'
NC='\033[0m' # No Color
function info {
printf "\r💬 ${BLUE}INFO:${NC} ${1}\n"
}
function fail {
printf "\r🗯 ${RED}ERROR:${NC} ${1}\n"
exit 1
}
build_dir=$(dirname "$0")
test -n "$build_dir" -a -d "$build_dir" || exit
cd $build_dir/../..
export PYTHONHASHSEED=22
VERSION=`git describe --tags`
# Paramterize
PYTHON_VERSION=3.6.4
BUILDDIR=/tmp/electrum-build
PACKAGE=Electrum-ZCL
GIT_REPO=https://github.com/z-classic/electrum-zcl
info "Installing Python $PYTHON_VERSION"
export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH"
if [ -d "~/.pyenv" ]; then
pyenv update
else
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash > /dev/null 2>&1
fi
PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install -s $PYTHON_VERSION && \
pyenv global $PYTHON_VERSION || \
fail "Unable to use Python $PYTHON_VERSION"
info "Installing pyinstaller"
python3 -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952 -I --user || fail "Could not install pyinstaller"
info "Using these versions for building $PACKAGE:"
sw_vers
python3 --version
echo -n "Pyinstaller "
pyinstaller --version
rm -rf ./dist
rm -rf $BUILDDIR > /dev/null 2>&1
mkdir $BUILDDIR
info "Downloading icons and locale..."
for repo in icons locale; do
git clone $GIT_REPO-$repo $BUILDDIR/electrum-$repo
done
cp -R $BUILDDIR/electrum-locale/locale/ ./lib/locale/
cp $BUILDDIR/electrum-icons/icons_rc.py ./gui/qt/
info "Downloading libusb..."
curl https://homebrew.bintray.com/bottles/libusb-1.0.21.el_capitan.bottle.tar.gz | \
tar xz --directory $BUILDDIR
cp $BUILDDIR/libusb/1.0.21/lib/libusb-1.0.dylib contrib/build-osx
info "Installing requirements..."
python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user || \
fail "Could not install requirements"
info "Installing hardware wallet requirements..."
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --user || \
fail "Could not install hardware wallet requirements"
info "Building $PACKAGE..."
python3 setup.py install --user > /dev/null || fail "Could not build $PACKAGE"
info "Building binary"
pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary"
info "Creating .DMG"
hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/Electrum-ZCL-$VERSION.dmg || fail "Could not create .DMG"

View File

@ -0,0 +1,99 @@
# -*- mode: python -*-
from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
import sys
import os
PACKAGE='Electrum-ZCL'
PYPKG='electrum'
MAIN_SCRIPT='electrum'
ICONS_FILE='electrum.icns'
for i, x in enumerate(sys.argv):
if x == '--name':
VERSION = sys.argv[i+1]
break
else:
raise BaseException('no version')
electrum = os.path.abspath(".") + "/"
block_cipher = None
# see https://github.com/pyinstaller/pyinstaller/issues/2005
hiddenimports = []
hiddenimports += collect_submodules('trezorlib')
hiddenimports += collect_submodules('btchip')
hiddenimports += collect_submodules('keepkeylib')
hiddenimports += collect_submodules('websocket')
datas = [
(electrum+'lib/currencies.json', PYPKG),
(electrum+'lib/servers.json', PYPKG),
(electrum+'lib/checkpoints.json', PYPKG),
(electrum+'lib/servers_testnet.json', PYPKG),
(electrum+'lib/checkpoints_testnet.json', PYPKG),
(electrum+'lib/wordlist/english.txt', PYPKG + '/wordlist'),
(electrum+'lib/locale', PYPKG + '/locale'),
(electrum+'plugins', PYPKG + '_plugins'),
]
datas += collect_data_files('trezorlib')
datas += collect_data_files('btchip')
datas += collect_data_files('keepkeylib')
# Add libusb so Trezor will work
binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
# Workaround for "Retro Look":
binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
a = Analysis([electrum+MAIN_SCRIPT,
electrum+'gui/qt/main_window.py',
electrum+'gui/text.py',
electrum+'lib/util.py',
electrum+'lib/wallet.py',
electrum+'lib/simple_config.py',
electrum+'lib/bitcoin.py',
electrum+'lib/dnssec.py',
electrum+'lib/commands.py',
electrum+'plugins/cosigner_pool/qt.py',
electrum+'plugins/email_requests/qt.py',
electrum+'plugins/trezor/client.py',
electrum+'plugins/trezor/qt.py',
electrum+'plugins/keepkey/qt.py',
electrum+'plugins/ledger/qt.py',
],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[])
# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal
for d in a.datas:
if 'pyconfig' in d[0]:
a.datas.remove(d)
break
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.datas,
name=PACKAGE,
debug=False,
strip=False,
upx=True,
icon=electrum+ICONS_FILE,
console=False)
app = BUNDLE(exe,
version = VERSION,
name=PACKAGE + '.app',
icon=electrum+ICONS_FILE,
bundle_identifier=None,
info_plist = {
'NSHighResolutionCapable':'True'
}
)

View File

@ -2,7 +2,7 @@ Windows Binary Builds
===================== =====================
These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine. These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine.
Produced binaries are deterministic so you should be able to generate binaries that match the official releases. Produced binaries are deterministic, so you should be able to generate binaries that match the official releases.
Usage: Usage:
@ -12,6 +12,7 @@ Usage:
- dirmngr - dirmngr
- gpg - gpg
- 7Zip
- Wine (>= v2) - Wine (>= v2)
@ -19,8 +20,7 @@ For example:
``` ```
$ sudo apt-get install wine-development dirmngr gnupg2 $ sudo apt-get install wine-development dirmngr gnupg2 p7zip-full
$ sudo ln -sf /usr/bin/wine-development /usr/local/bin/wine
$ wine --version $ wine --version
wine-2.0 (Debian 2.0-3+b2) wine-2.0 (Debian 2.0-3+b2)
``` ```

View File

@ -16,6 +16,7 @@ PYTHON="wine $PYHOME/python.exe -OO -B"
cd `dirname $0` cd `dirname $0`
set -e set -e
mkdir -p tmp
cd tmp cd tmp
for repo in electrum electrum-locale electrum-icons; do for repo in electrum electrum-locale electrum-icons; do
@ -55,7 +56,9 @@ cp -r electrum-locale/locale $WINEPREFIX/drive_c/electrum/lib/
cp electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/ cp electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/
# Install frozen dependencies # Install frozen dependencies
$PYTHON -m pip install -r ../../requirements.txt $PYTHON -m pip install -r ../../deterministic-build/requirements.txt
$PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt
pushd $WINEPREFIX/drive_c/electrum pushd $WINEPREFIX/drive_c/electrum
$PYTHON setup.py install $PYTHON setup.py install

View File

@ -7,14 +7,13 @@ if [ ! -z "$1" ]; then
fi fi
here=$(dirname "$0") here=$(dirname "$0")
test -n "$here" -a -d "$here" || exit
echo "Clearing $here/build and $here/dist..." echo "Clearing $here/build and $here/dist..."
rm $here/build/* -rf rm "$here"/build/* -rf
rm $here/dist/* -rf rm "$here"/dist/* -rf
$here/prepare-wine.sh && \ $here/prepare-wine.sh || exit 1
$here/prepare-pyinstaller.sh && \
$here/prepare-hw.sh || exit 1
echo "Resetting modification time in C:\Python..." echo "Resetting modification time in C:\Python..."
# (Because of some bugs in pyinstaller) # (Because of some bugs in pyinstaller)

View File

@ -1,9 +1,8 @@
# -*- mode: python -*- # -*- mode: python -*-
from PyInstaller.utils.hooks import collect_data_files, collect_submodules from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
import sys import sys
import os
for i, x in enumerate(sys.argv): for i, x in enumerate(sys.argv):
if x == '--name': if x == '--name':
cmdline_name = sys.argv[i+1] cmdline_name = sys.argv[i+1]
@ -11,13 +10,21 @@ for i, x in enumerate(sys.argv):
else: else:
raise BaseException('no name') raise BaseException('no name')
home = os.getcwd()+'\\'
home = 'C:\\electrum-zcl\\'
# see https://github.com/pyinstaller/pyinstaller/issues/2005 # see https://github.com/pyinstaller/pyinstaller/issues/2005
hiddenimports = [] hiddenimports = []
# hiddenimports += collect_submodules('trezorlib') hiddenimports += collect_submodules('trezorlib')
# hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('btchip')
# hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('keepkeylib')
hiddenimports += collect_submodules('websocket')
# Add libusb binary
binaries = [("c:/python3.5.4/libusb-1.0.dll", ".")]
# Workaround for "Retro Look":
binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]]
datas = [ datas = [
(home+'lib/currencies.json', 'electrum'), (home+'lib/currencies.json', 'electrum'),
@ -26,12 +33,13 @@ datas = [
(home+'lib/servers_testnet.json', 'electrum'), (home+'lib/servers_testnet.json', 'electrum'),
(home+'lib/checkpoints_testnet.json', 'electrum'), (home+'lib/checkpoints_testnet.json', 'electrum'),
(home+'lib/wordlist/english.txt', 'electrum/wordlist'), (home+'lib/wordlist/english.txt', 'electrum/wordlist'),
# (home+'lib/locale', 'electrum/locale'), (home+'lib/locale', 'electrum-zcl/locale'),
(home+'plugins', 'electrum_plugins'), (home+'plugins', 'electrum_plugins'),
('C:\\Program Files (x86)\\ZBar\\bin\\', '.')
] ]
# datas += collect_data_files('trezorlib') datas += collect_data_files('trezorlib')
# datas += collect_data_files('btchip') datas += collect_data_files('btchip')
# datas += collect_data_files('keepkeylib') datas += collect_data_files('keepkeylib')
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
a = Analysis([home+'electrum-zcl', a = Analysis([home+'electrum-zcl',
@ -45,12 +53,13 @@ a = Analysis([home+'electrum-zcl',
home+'lib/commands.py', home+'lib/commands.py',
home+'plugins/cosigner_pool/qt.py', home+'plugins/cosigner_pool/qt.py',
home+'plugins/email_requests/qt.py', home+'plugins/email_requests/qt.py',
#home+'plugins/trezor/client.py', home+'plugins/trezor/client.py',
#home+'plugins/trezor/qt.py', home+'plugins/trezor/qt.py',
#home+'plugins/keepkey/qt.py', home+'plugins/keepkey/qt.py',
#home+'plugins/ledger/qt.py', home+'plugins/ledger/qt.py',
#home+'packages/requests/utils.py' #home+'packages/requests/utils.py'
], ],
binaries=binaries,
datas=datas, datas=datas,
#pathex=[home+'lib', home+'gui', home+'plugins'], #pathex=[home+'lib', home+'gui', home+'plugins'],
hiddenimports=hiddenimports, hiddenimports=hiddenimports,
@ -85,40 +94,40 @@ exe_standalone = EXE(
console=False) console=False)
# console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used # console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used
# exe_portable = EXE( exe_portable = EXE(
# pyz, pyz,
# a.scripts, a.scripts,
# a.binaries, a.binaries,
# a.datas, a.datas + [ ('is_portable', 'README.md', 'DATA' ) ],
# name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + "-portable.exe"), name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + "-portable.exe"),
# debug=False, debug=False,
# strip=None, strip=None,
# upx=False, upx=False,
# icon=home+'icons/electrum.ico', icon=home+'icons/electrum.ico',
# console=False) console=False)
# ##### #####
# # exe and separate files that NSIS uses to build installer "setup" exe # exe and separate files that NSIS uses to build installer "setup" exe
# exe_dependent = EXE( exe_dependent = EXE(
# pyz, pyz,
# a.scripts, a.scripts,
# exclude_binaries=True, exclude_binaries=True,
# name=os.path.join('build\\pyi.win32\\electrum', cmdline_name), name=os.path.join('build\\pyi.win32\\electrum', cmdline_name),
# debug=False, debug=False,
# strip=None, strip=None,
# upx=False, upx=False,
# icon=home+'icons/electrum.ico', icon=home+'icons/electrum.ico',
# console=False) console=False)
# coll = COLLECT( coll = COLLECT(
# exe_dependent, exe_dependent,
# a.binaries, a.binaries,
# a.zipfiles, a.zipfiles,
# a.datas, a.datas,
# strip=None, strip=None,
# upx=True, upx=True,
# debug=False, debug=False,
# icon=home+'icons/electrum.ico', icon=home+'icons/electrum.ico',
# console=False, console=False,
# name=os.path.join('dist', 'electrum')) name=os.path.join('dist', 'electrum'))

View File

@ -1,28 +0,0 @@
#!/bin/bash
TREZOR_GIT_URL=https://github.com/trezor/python-trezor.git
KEEPKEY_GIT_URL=https://github.com/keepkey/python-keepkey.git
BTCHIP_GIT_URL=https://github.com/LedgerHQ/btchip-python.git
BRANCH=master
PYTHON_VERSION=3.5.4
# These settings probably don't need any change
export WINEPREFIX=/opt/wine64
PYHOME=c:/python$PYTHON_VERSION
PYTHON="wine $PYHOME/python.exe -OO -B"
# Let's begin!
cd `dirname $0`
set -e
cd tmp
$PYTHON -m pip install setuptools --upgrade
$PYTHON -m pip install cython --upgrade
$PYTHON -m pip install trezor==0.7.16 --upgrade
$PYTHON -m pip install keepkey==4.0.0 --upgrade
$PYTHON -m pip install btchip-python==0.1.23 --upgrade

View File

@ -1,24 +0,0 @@
#!/bin/bash
PYTHON_VERSION=3.5.4
PYINSTALLER_GIT_URL=https://github.com/ecdsa/pyinstaller.git
BRANCH=fix_2952
export WINEPREFIX=/opt/wine64
PYHOME=c:/python$PYTHON_VERSION
PYTHON="wine $PYHOME/python.exe -OO -B"
cd `dirname $0`
set -e
cd tmp
if [ ! -d "pyinstaller" ]; then
git clone -b $BRANCH $PYINSTALLER_GIT_URL pyinstaller
fi
cd pyinstaller
git pull
git checkout $BRANCH
$PYTHON setup.py install
cd ..
wine "C:/python$PYTHON_VERSION/scripts/pyinstaller.exe" -v

View File

@ -1,8 +1,18 @@
#!/bin/bash #!/bin/bash
# Please update these carefully, some versions won't work under Wine # Please update these carefully, some versions won't work under Wine
NSIS_URL=https://prdownloads.sourceforge.net/nsis/nsis-3.02.1-setup.exe?download NSIS_FILENAME=nsis-3.02.1-setup.exe
NSIS_URL=https://prdownloads.sourceforge.net/nsis/$NSIS_FILENAME?download
NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e
ZBAR_FILENAME=zbarw-20121031-setup.exe
ZBAR_URL=https://sourceforge.net/projects/zbarw/files/$ZBAR_FILENAME/download
ZBAR_SHA256=177e32b272fa76528a3af486b74e9cb356707be1c5ace4ed3fcee9723e2c2c02
LIBUSB_FILENAME=libusb-1.0.21.7z
LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.21/$LIBUSB_FILENAME?download
LIBUSB_SHA256=acdde63a40b1477898aee6153f9d91d1a2e8a5d93f832ca8ab876498f3a6d2b8
PYTHON_VERSION=3.5.4 PYTHON_VERSION=3.5.4
## These settings probably don't need change ## These settings probably don't need change
@ -21,23 +31,31 @@ verify_signature() {
return 0 return 0
else else
echo "$out" >&2 echo "$out" >&2
exit 0 exit 1
fi fi
} }
verify_hash() { verify_hash() {
local file=$1 expected_hash=$2 out= local file=$1 expected_hash=$2
actual_hash=$(sha256sum $file | awk '{print $1}') actual_hash=$(sha256sum $file | awk '{print $1}')
if [ "$actual_hash" == "$expected_hash" ]; then if [ "$actual_hash" == "$expected_hash" ]; then
return 0 return 0
else else
echo "$file $actual_hash (unexpected hash)" >&2 echo "$file $actual_hash (unexpected hash)" >&2
exit 0 rm "$file"
exit 1
fi
}
download_if_not_exist() {
local file_name=$1 url=$2
if [ ! -e $file_name ] ; then
wget -O $PWD/$file_name "$url"
fi fi
} }
# Let's begin! # Let's begin!
cd `dirname $0` here=$(dirname $(readlink -e $0))
set -e set -e
# Clean up Wine environment # Clean up Wine environment
@ -47,22 +65,21 @@ echo "done"
wine 'wineboot' wine 'wineboot'
echo "Cleaning tmp" mkdir -p /tmp/electrum-build
rm -rf tmp
mkdir -p tmp
echo "done"
cd tmp cd /tmp/electrum-build
# Install Python # Install Python
# note: you might need "sudo apt-get install dirmngr" for the following # note: you might need "sudo apt-get install dirmngr" for the following
# keys from https://www.python.org/downloads/#pubkeys # keys from https://www.python.org/downloads/#pubkeys
KEYRING_PYTHON_DEV=keyring-electrum-build-python-dev.gpg KEYLIST_PYTHON_DEV="531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5"
gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --recv-keys 531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5 KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg"
KEYSERVER_PYTHON_DEV="hkp://keys.gnupg.net"
gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --keyserver $KEYSERVER_PYTHON_DEV --recv-keys $KEYLIST_PYTHON_DEV
for msifile in core dev exe lib pip tools; do for msifile in core dev exe lib pip tools; do
echo "Installing $msifile..." echo "Installing $msifile..."
wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" wget -nc "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi"
wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc" wget -nc "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc"
verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV
wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION
done done
@ -70,36 +87,35 @@ done
# upgrade pip # upgrade pip
$PYTHON -m pip install pip --upgrade $PYTHON -m pip install pip --upgrade
# Install PyWin32 # Install pywin32-ctypes (needed by pyinstaller)
$PYTHON -m pip install pypiwin32 $PYTHON -m pip install pywin32-ctypes==0.1.2
# Install PyQt
$PYTHON -m pip install PyQt5
## Install pyinstaller
#$PYTHON -m pip install pyinstaller==3.3
# Install ZBar
#wget -q -O zbar.exe "https://sourceforge.net/projects/zbar/files/zbar/0.10/zbar-0.10-setup.exe/download"
#wine zbar.exe
# install Cryptodome
$PYTHON -m pip install pycryptodomex
# install PySocks # install PySocks
$PYTHON -m pip install win_inet_pton $PYTHON -m pip install win_inet_pton==1.0.1
# install websocket (python2) $PYTHON -m pip install -r $here/../deterministic-build/requirements-binaries.txt
$PYTHON -m pip install websocket-client
# Install PyInstaller
$PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip
# Install ZBar
download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL"
verify_hash $ZBAR_FILENAME "$ZBAR_SHA256"
wine "$PWD/$ZBAR_FILENAME" /S
# Upgrade setuptools (so Electrum can be installed later) # Upgrade setuptools (so Electrum can be installed later)
$PYTHON -m pip install setuptools --upgrade $PYTHON -m pip install setuptools --upgrade
# Install NSIS installer # Install NSIS installer
wget -q -O nsis.exe "$NSIS_URL" download_if_not_exist $NSIS_FILENAME "$NSIS_URL"
verify_hash nsis.exe $NSIS_SHA256 verify_hash $NSIS_FILENAME "$NSIS_SHA256"
wine nsis.exe /S wine "$PWD/$NSIS_FILENAME" /S
download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL"
verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256"
7z x -olibusb $LIBUSB_FILENAME -aos
cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/python$PYTHON_VERSION/
# Install UPX # Install UPX
#wget -O upx.zip "https://downloads.sourceforge.net/project/upx/upx/3.08/upx308w.zip" #wget -O upx.zip "https://downloads.sourceforge.net/project/upx/upx/3.08/upx308w.zip"
@ -109,5 +125,4 @@ wine nsis.exe /S
# add dlls needed for pyinstaller: # add dlls needed for pyinstaller:
cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/ cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/
echo "Wine is configured."
echo "Wine is configured. Please run prepare-pyinstaller.sh"

View File

@ -0,0 +1,5 @@
pycryptodomex==3.4.12
PyQt5==5.10
sip==4.19.7
six==1.11.0
websocket-client==0.46.0

View File

@ -0,0 +1,18 @@
btchip-python==0.1.24
certifi==2018.1.18
chardet==3.0.4
click==6.7
Cython==0.27.3
ecdsa==0.13
hidapi==0.7.99.post21
idna==2.6
keepkey==4.0.2
libusb1==1.6.4
mnemonic==0.18
pbkdf2==1.3
protobuf==3.5.1
pyblake2==1.1.0
requests==2.18.4
six==1.11.0
trezor==0.9.0
urllib3==1.22

View File

@ -1,13 +1,13 @@
certifi==2017.11.5 certifi==2018.1.18
chardet==3.0.4 chardet==3.0.4
dnspython==1.15.0 dnspython==1.15.0
ecdsa==0.13 ecdsa==0.13
idna==2.6 idna==2.6
jsonrpclib-pelix==0.3.1 jsonrpclib-pelix==0.3.1
pbkdf2==1.3 pbkdf2==1.3
protobuf==3.5.0.post1 protobuf==3.5.1
pyaes==1.6.1 pyaes==1.6.1
PySocks==1.6.7 PySocks==1.6.8
qrcode==5.3 qrcode==5.3
requests==2.18.4 requests==2.18.4
six==1.11.0 six==1.11.0

View File

@ -6,17 +6,17 @@ contrib=$(dirname "$0")
which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; } which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; }
rm $venv_dir -rf for i in '' '-hw' '-binaries'; do
virtualenv $venv_dir rm "$venv_dir" -rf
virtualenv -p $(which python3) $venv_dir
source $venv_dir/bin/activate source $venv_dir/bin/activate
echo "Installing dependencies" echo "Installing $i dependencies"
pushd $contrib/.. python -m pip install -r $contrib/requirements/requirements${i}.txt --upgrade
python setup.py install
popd
pip freeze | sed '/^Electrum/ d' > $contrib/requirements.txt pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements${i}.txt
done
echo "Updated requirements" echo "Done. Updated requirements"

View File

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

View File

@ -0,0 +1,3 @@
PyQt5
pycryptodomex
websocket-client

View File

@ -0,0 +1,4 @@
Cython>=0.27
trezor>=0.9.0
keepkey
btchip-python

View File

@ -0,0 +1,9 @@
pyaes>=0.1a1
ecdsa>=0.9
pbkdf2
requests
qrcode
protobuf
dnspython
jsonrpclib-pelix
PySocks>=1.6.6

View File

@ -9,6 +9,8 @@
# python-qt and its dependencies will still need to be installed with # python-qt and its dependencies will still need to be installed with
# your package manager. # your package manager.
PYTHON_VER="$(python3 -c 'import sys; print(sys.version[:3])')"
if [ -e ./env/bin/activate ]; then if [ -e ./env/bin/activate ]; then
source ./env/bin/activate source ./env/bin/activate
else else
@ -17,7 +19,7 @@ else
python3 setup.py install python3 setup.py install
fi fi
export PYTHONPATH="/usr/local/lib/python3.5/site-packages:$PYTHONPATH" export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH"
./electrum "$@" ./electrum "$@"

View File

@ -90,10 +90,12 @@ if is_local or is_android or is_macOS:
imp.load_module('electrum_plugins', *imp.find_module('plugins')) imp.load_module('electrum_plugins', *imp.find_module('plugins'))
from electrum import bitcoin
from electrum import bitcoin, util
from electrum import constants
from electrum import SimpleConfig, Network from electrum import SimpleConfig, Network
from electrum.wallet import Wallet, Imported_Wallet from electrum.wallet import Wallet, Imported_Wallet
from electrum.storage import WalletStorage from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption
from electrum.util import print_msg, print_stderr, json_encode, json_decode from electrum.util import print_msg, print_stderr, json_encode, json_decode
from electrum.util import set_verbosity, InvalidPassword from electrum.util import set_verbosity, InvalidPassword
from electrum.commands import get_parser, known_commands, Commands, config_variables from electrum.commands import get_parser, known_commands, Commands, config_variables
@ -163,6 +165,8 @@ def run_non_RPC(config):
print_msg("Recovering wallet...") print_msg("Recovering wallet...")
wallet.synchronize() wallet.synchronize()
wallet.wait_until_synchronized() wallet.wait_until_synchronized()
wallet.stop_threads()
# note: we don't wait for SPV
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet" msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
else: else:
msg = "This wallet was restored offline. It may contain more addresses than displayed." msg = "This wallet was restored offline. It may contain more addresses than displayed."
@ -195,7 +199,10 @@ def init_daemon(config_options):
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option") print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
sys.exit(0) sys.exit(0)
if storage.is_encrypted(): if storage.is_encrypted():
if config.get('password'): if storage.is_encrypted_with_hw_device():
plugins = init_plugins(config, 'cmdline')
password = get_password_for_hw_device_encrypted_storage(plugins)
elif config.get('password'):
password = config.get('password') password = config.get('password')
else: else:
password = prompt_password('Password:', False) password = prompt_password('Password:', False)
@ -222,7 +229,7 @@ def init_cmdline(config_options, server):
if cmdname in ['payto', 'paytomany'] and config.get('broadcast'): if cmdname in ['payto', 'paytomany'] and config.get('broadcast'):
cmd.requires_network = True cmd.requires_network = True
# instanciate wallet for command-line # instantiate wallet for command-line
storage = WalletStorage(config.get_wallet_path()) storage = WalletStorage(config.get_wallet_path())
if cmd.requires_wallet and not storage.file_exists(): if cmd.requires_wallet and not storage.file_exists():
@ -239,7 +246,10 @@ def init_cmdline(config_options, server):
# commands needing password # commands needing password
if (cmd.requires_wallet and storage.is_encrypted() and server is None)\ if (cmd.requires_wallet and storage.is_encrypted() and server is None)\
or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())): or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())):
if config.get('password'): if storage.is_encrypted_with_hw_device():
# this case is handled later in the control flow
password = None
elif config.get('password'):
password = config.get('password') password = config.get('password')
else: else:
password = prompt_password('Password:', False) password = prompt_password('Password:', False)
@ -258,19 +268,57 @@ def init_cmdline(config_options, server):
return cmd, password return cmd, password
def run_offline_command(config, config_options): def get_connected_hw_devices(plugins):
support = plugins.get_hardware_support()
if not support:
print_msg('No hardware wallet support found on your system.')
sys.exit(1)
# scan devices
devices = []
devmgr = plugins.device_manager
for name, description, plugin in support:
try:
u = devmgr.unpaired_device_infos(None, plugin)
except:
devmgr.print_error("error", name)
continue
devices += list(map(lambda x: (name, x), u))
return devices
def get_password_for_hw_device_encrypted_storage(plugins):
devices = get_connected_hw_devices(plugins)
if len(devices) == 0:
print_msg("Error: No connected hw device found. Can not decrypt this wallet.")
sys.exit(1)
elif len(devices) > 1:
print_msg("Warning: multiple hardware devices detected. "
"The first one will be used to decrypt the wallet.")
# FIXME we use the "first" device, in case of multiple ones
name, device_info = devices[0]
plugin = plugins.get_plugin(name)
derivation = get_derivation_used_for_hw_device_encryption()
xpub = plugin.get_xpub(device_info.device.id_, derivation, 'standard', plugin.handler)
password = keystore.Xpub.get_pubkey_from_xpub(xpub, ())
return password
def run_offline_command(config, config_options, plugins):
cmdname = config.get('cmd') cmdname = config.get('cmd')
cmd = known_commands[cmdname] cmd = known_commands[cmdname]
password = config_options.get('password') password = config_options.get('password')
if cmd.requires_wallet: if cmd.requires_wallet:
storage = WalletStorage(config.get_wallet_path()) storage = WalletStorage(config.get_wallet_path())
if storage.is_encrypted(): if storage.is_encrypted():
if storage.is_encrypted_with_hw_device():
password = get_password_for_hw_device_encrypted_storage(plugins)
config_options['password'] = password
storage.decrypt(password) storage.decrypt(password)
wallet = Wallet(storage) wallet = Wallet(storage)
else: else:
wallet = None wallet = None
# check password # check password
if cmd.requires_password and storage.get('use_encryption'): if cmd.requires_password and wallet.has_password():
try: try:
seed = wallet.check_password(password) seed = wallet.check_password(password)
except InvalidPassword: except InvalidPassword:
@ -281,7 +329,8 @@ def run_offline_command(config, config_options):
# arguments passed to function # arguments passed to function
args = [config.get(x) for x in cmd.params] args = [config.get(x) for x in cmd.params]
# decode json arguments # decode json arguments
args = list(map(json_decode, args)) if cmdname not in ('setconfig',):
args = list(map(json_decode, args))
# options # options
kwargs = {} kwargs = {}
for x in cmd.options: for x in cmd.options:
@ -298,8 +347,10 @@ def init_plugins(config, gui_name):
from electrum.plugins import Plugins from electrum.plugins import Plugins
return Plugins(config, is_local or is_android, gui_name) return Plugins(config, is_local or is_android, gui_name)
if __name__ == '__main__':
if __name__ == '__main__':
# The hook will only be used in the Qt GUI right now
util.setup_thread_excepthook()
# on osx, delete Process Serial Number arg generated for apps launched in Finder # on osx, delete Process Serial Number arg generated for apps launched in Finder
sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv)) sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv))
@ -364,7 +415,7 @@ if __name__ == '__main__':
cmdname = config.get('cmd') cmdname = config.get('cmd')
if config.get('testnet'): if config.get('testnet'):
bitcoin.NetworkConstants.set_testnet() constants.set_testnet()
# run non-RPC commands separately # run non-RPC commands separately
if cmdname in ['create', 'restore']: if cmdname in ['create', 'restore']:
@ -430,8 +481,8 @@ if __name__ == '__main__':
print_msg("Daemon not running; try 'electrum daemon start'") print_msg("Daemon not running; try 'electrum daemon start'")
sys.exit(1) sys.exit(1)
else: else:
init_plugins(config, 'cmdline') plugins = init_plugins(config, 'cmdline')
result = run_offline_command(config, config_options) result = run_offline_command(config, config_options, plugins)
# print result # print result
if isinstance(result, str): if isinstance(result, str):
print_msg(result) print_msg(result)

80
gui/kivy/Readme.md Normal file
View File

@ -0,0 +1,80 @@
# Kivy GUI
The Kivy GUI is used with Electrum on Android devices. To generate an APK file, follow these instructions.
## 1. Install python-for-android (p4a)
p4a is used to package Electrum, Python, SDL and a bootstrap Java app into an APK file.
We patched p4a to add some functionality we need for Electrum. Until those changes are
merged into p4a, you need to merge them locally (into the master branch):
2. [kivy/python-for-android#1217](https://github.com/kivy/python-for-android/pull/1217)
Something like this should work:
```sh
cd /opt
git clone https://github.com/kivy/python-for-android
cd python-for-android
git remote add agilewalker https://github.com/agilewalker/python-for-android
git checkout a036f4442b6a23
git fetch agilewalker
git merge agilewalker/master
```
## 2. Install buildozer
2.1 Buildozer is a frontend to p4a. Luckily we don't need to patch it:
```sh
cd /opt
git clone https://github.com/kivy/buildozer
cd buildozer
sudo python3 setup.py install
```
2.2 Download the [Crystax NDK](https://www.crystax.net/en/download) manually.
Extract into `/opt/crystax-ndk-10.3.2`
## 3. Update the Android SDK build tools
### Method 1: Using the GUI
Start the Android SDK manager in GUI mode:
~/.buildozer/android/platform/android-sdk-20/tools/android
Check the latest SDK available and install it.
Close the SDK manager.
Reopen the SDK manager, scroll to the bottom and install the latest build tools (probably v27)
Install "Android Support Library Repository" from the SDK manager.
### Method 2: Using the command line:
Repeat the following command until there is nothing to install:
~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t tools,platform-tools
Install Build Tools, android API 19 and Android Support Library:
~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t build-tools-27.0.3,android-19,extra-android-m2repository
## 5. Create the UI Atlas
In the `gui/kivy` directory of Electrum, run `make theming`.
## 6. Download Electrum dependencies
Run `contrib/make_packages`.
## 7. Build the APK
Run `contrib/make_apk`.
# FAQ
## Why do I get errors like `package me.dm7.barcodescanner.zxing does not exist` while compiling?
Update your Android build tools to version 27 like described above.
## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling?
Make sure that your p4a installation includes commit a3cc78a6d1a107cd3b6bd28db8b80f89e3ecddd2.
Also make sure you have recent SDK tools and platform-tools
## I changed something but I don't see any differences on the phone. What did I do wrong?
You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}`

View File

@ -1,24 +0,0 @@
Before compiling, create packages: `contrib/make_packages`
Commands::
`make theming` to make a atlas out of a list of pngs
`make apk` to make a apk
If something in included modules like kivy or any other module changes
then you need to rebuild the distribution. To do so:
rm -rf .buildozer/android/platform/python-for-android/dist
how to build with ssl:
rm -rf .buildozer/android/platform/build/
./contrib/make_apk
pushd /opt/electrum/.buildozer/android/platform/build/build/libs_collections/Electrum/armeabi-v7a
cp libssl1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libssl.so
cp libcrypto1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libcrypto.so
popd
./contrib/make_apk

View File

@ -1,21 +1,22 @@
import gettext import gettext
class _(str): class _(str):
observers = set() observers = set()
lang = None lang = None
def __new__(cls, s, *args, **kwargs): def __new__(cls, s):
if _.lang is None: if _.lang is None:
_.switch_lang('en') _.switch_lang('en')
t = _.translate(s, *args, **kwargs) t = _.translate(s)
o = super(_, cls).__new__(cls, t) o = super(_, cls).__new__(cls, t)
o.source_text = s o.source_text = s
return o return o
@staticmethod @staticmethod
def translate(s, *args, **kwargs): def translate(s, *args, **kwargs):
return _.lang(s).format(args, kwargs) return _.lang(s)
@staticmethod @staticmethod
def bind(label): def bind(label):

View File

@ -239,7 +239,7 @@
self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu() self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu()
canvas.before: canvas.before:
Color: Color:
rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.3, 0.3, 0.3, 1) rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.15, 0.15, 0.17, 1)
Rectangle: Rectangle:
size: self.size size: self.size
pos: self.pos pos: self.pos
@ -397,7 +397,7 @@
slide: 1 slide: 1
CleanHeader: CleanHeader:
id: history_tab id: history_tab
text: _('History') text: _('Balance')
slide: 2 slide: 2
CleanHeader: CleanHeader:
id: receive_tab id: receive_tab

View File

@ -82,6 +82,10 @@ class ElectrumWindow(App):
server_port = StringProperty('') server_port = StringProperty('')
num_chains = NumericProperty(0) num_chains = NumericProperty(0)
blockchain_name = StringProperty('') blockchain_name = StringProperty('')
fee_status = StringProperty('Fee')
balance = StringProperty('')
fiat_balance = StringProperty('')
is_fiat = BooleanProperty(False)
blockchain_checkpoint = NumericProperty(0) blockchain_checkpoint = NumericProperty(0)
auto_connect = BooleanProperty(False) auto_connect = BooleanProperty(False)
@ -95,8 +99,8 @@ class ElectrumWindow(App):
from .uix.dialogs.choice_dialog import ChoiceDialog from .uix.dialogs.choice_dialog import ChoiceDialog
protocol = 's' protocol = 's'
def cb2(host): def cb2(host):
from electrum.bitcoin import NetworkConstants from electrum import constants
pp = servers.get(host, NetworkConstants.DEFAULT_PORTS) pp = servers.get(host, constants.net.DEFAULT_PORTS)
port = pp.get(protocol, '') port = pp.get(protocol, '')
popup.ids.host.text = host popup.ids.host.text = host
popup.ids.port.text = port popup.ids.port.text = port
@ -175,8 +179,10 @@ class ElectrumWindow(App):
def btc_to_fiat(self, amount_str): def btc_to_fiat(self, amount_str):
if not amount_str: if not amount_str:
return '' return ''
if not self.fx.is_enabled():
return ''
rate = self.fx.exchange_rate() rate = self.fx.exchange_rate()
if not rate: if rate.is_nan():
return '' return ''
fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8) fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.') return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')
@ -185,7 +191,7 @@ class ElectrumWindow(App):
if not fiat_amount: if not fiat_amount:
return '' return ''
rate = self.fx.exchange_rate() rate = self.fx.exchange_rate()
if not rate: if rate.is_nan():
return '' return ''
satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate)) satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
return format_satoshis_plain(satoshis, self.decimal_point()) return format_satoshis_plain(satoshis, self.decimal_point())
@ -271,6 +277,7 @@ class ElectrumWindow(App):
# cached dialogs # cached dialogs
self._settings_dialog = None self._settings_dialog = None
self._password_dialog = None self._password_dialog = None
self.fee_status = self.electrum_config.get_fee_status()
def wallet_name(self): def wallet_name(self):
return os.path.basename(self.wallet.storage.path) if self.wallet else ' ' return os.path.basename(self.wallet.storage.path) if self.wallet else ' '
@ -457,6 +464,7 @@ class ElectrumWindow(App):
if self.network: if self.network:
interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces'] interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces']
self.network.register_callback(self.on_network_event, interests) self.network.register_callback(self.on_network_event, interests)
self.network.register_callback(self.on_fee, ['fee'])
self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history']) self.network.register_callback(self.on_history, ['on_history'])
# URI passed in config # URI passed in config
@ -631,17 +639,17 @@ class ElectrumWindow(App):
if not self.wallet.up_to_date or server_height == 0: if not self.wallet.up_to_date or server_height == 0:
status = _("Synchronizing...") status = _("Synchronizing...")
elif server_lag > 1: elif server_lag > 1:
status = _("Server lagging (%d blocks)"%server_lag) status = _("Server lagging")
else: else:
c, u, x = self.wallet.get_balance() status = ''
text = self.format_amount(c+x+u)
status = str(text.strip() + ' ' + self.base_unit)
else: else:
status = _("Disconnected") status = _("Disconnected")
self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '')
n = self.wallet.basename() # balance
self.status = '[size=15dp]%s[/size]\n%s' %(n, status) c, u, x = self.wallet.get_balance()
#fiat_balance = self.fx.format_amount_and_units(c+u+x) or '' text = self.format_amount(c+x+u)
self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy
def get_max_amount(self): def get_max_amount(self):
inputs = self.wallet.get_spendable_coins(None, self.electrum_config) inputs = self.wallet.get_spendable_coins(None, self.electrum_config)
@ -684,9 +692,6 @@ class ElectrumWindow(App):
def on_resume(self): def on_resume(self):
if self.nfcscanner: if self.nfcscanner:
self.nfcscanner.nfc_enable() self.nfcscanner.nfc_enable()
# workaround p4a bug:
# show an empty info bubble, to refresh the display
self.show_info_bubble('', duration=0.1, pos=(0,0), width=1, arrow_pos=None)
def on_size(self, instance, value): def on_size(self, instance, value):
width, height = value width, height = value
@ -831,6 +836,16 @@ class ElectrumWindow(App):
popup = AmountDialog(show_max, amount, cb) popup = AmountDialog(show_max, amount, cb)
popup.open() popup.open()
def fee_dialog(self, label, dt):
from .uix.dialogs.fee_dialog import FeeDialog
def cb():
self.fee_status = self.electrum_config.get_fee_status()
fee_dialog = FeeDialog(self, self.electrum_config, cb)
fee_dialog.open()
def on_fee(self, event, *arg):
self.fee_status = self.electrum_config.get_fee_status()
def protected(self, msg, f, args): def protected(self, msg, f, args):
if self.wallet.has_password(): if self.wallet.has_password():
self.password_dialog(msg, f, args) self.password_dialog(msg, f, args)
@ -846,7 +861,7 @@ class ElectrumWindow(App):
def _delete_wallet(self, b): def _delete_wallet(self, b):
if b: if b:
basename = os.path.basename(self.wallet.storage.path) basename = os.path.basename(self.wallet.storage.path)
self.protected(_("Enter your PIN code to confirm deletion of %s") % basename, self.__delete_wallet, ()) self.protected(_("Enter your PIN code to confirm deletion of {}").format(basename), self.__delete_wallet, ())
def __delete_wallet(self, pw): def __delete_wallet(self, pw):
wallet_path = self.get_wallet_path() wallet_path = self.get_wallet_path()
@ -928,6 +943,10 @@ class ElectrumWindow(App):
return return
if not self.wallet.can_export(): if not self.wallet.can_export():
return return
key = str(self.wallet.export_private_key(addr, password)[0]) try:
pk_label.data = key key = str(self.wallet.export_private_key(addr, password)[0])
pk_label.data = key
except InvalidPassword:
self.show_error("Invalid PIN")
return
self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label)) self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))

View File

@ -31,7 +31,7 @@ version.filename = %(source.dir)s/contrib/versions.py
#version = 1.9.8 #version = 1.9.8
# (list) Application requirements # (list) Application requirements
requirements = python3crystax, android, openssl, plyer, kivy==master requirements = python3crystax==3.6, android, openssl, plyer, kivy==master
# (str) Presplash of the application # (str) Presplash of the application
#presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png #presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png

View File

@ -13,21 +13,35 @@ Builder.load_string('''
anchor_x: 'center' anchor_x: 'center'
BoxLayout: BoxLayout:
orientation: 'vertical' orientation: 'vertical'
size_hint: 0.8, 1 size_hint: 0.9, 1
Widget:
size_hint: 1, 0.2
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: '80dp' height: '80dp'
Label: Button:
id: a background_color: 0, 0, 0, 0
btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else '' id: btc
fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else '' text: kb.amount + ' ' + app.base_unit
text1: ((self.fiat_text if kb.is_fiat else self.btc_text) if app.fiat_unit else self.btc_text) if self.btc_text else '' color: (0.7, 0.7, 1, 1) if kb.is_fiat else (1, 1, 1, 1)
text2: ((self.btc_text if kb.is_fiat else self.fiat_text) if app.fiat_unit else '') if self.btc_text else ''
text: self.text1 + "\\n" + "[color=#8888ff]" + self.text2 + "[/color]"
halign: 'right' halign: 'right'
size_hint: 1, None size_hint: 1, None
font_size: '22dp' font_size: '20dp'
height: '80dp' height: '48dp'
on_release:
kb.is_fiat = False
Button:
background_color: 0, 0, 0, 0
id: fiat
text: kb.fiat_amount + ' ' + app.fiat_unit
color: (1, 1, 1, 1) if kb.is_fiat else (0.7, 0.7, 1, 1)
halign: 'right'
size_hint: 1, None
font_size: '20dp'
height: '48dp'
disabled: not app.fx.is_enabled()
on_release:
kb.is_fiat = True
Widget: Widget:
size_hint: 1, 0.2 size_hint: 1, 0.2
GridLayout: GridLayout:
@ -65,6 +79,9 @@ Builder.load_string('''
text: '0' text: '0'
KButton: KButton:
text: '<' text: '<'
Widget:
size_hint: 1, None
height: '48dp'
Button: Button:
id: but_max id: but_max
opacity: 1 if root.show_max else 0 opacity: 1 if root.show_max else 0
@ -75,13 +92,6 @@ Builder.load_string('''
on_release: on_release:
kb.is_fiat = False kb.is_fiat = False
kb.amount = app.get_max_amount() kb.amount = app.get_max_amount()
Button:
id: button_fiat
size_hint: 1, None
height: '48dp'
text: (app.base_unit if not kb.is_fiat else app.fiat_unit) if app.fiat_unit else ''
on_release:
if app.fiat_unit: popup.toggle_fiat(kb)
Button: Button:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
@ -102,7 +112,7 @@ Builder.load_string('''
height: '48dp' height: '48dp'
text: _('OK') text: _('OK')
on_release: on_release:
root.callback(a.btc_text) root.callback(btc.text if kb.amount else '')
popup.dismiss() popup.dismiss()
''') ''')
@ -117,9 +127,6 @@ class AmountDialog(Factory.Popup):
if amount: if amount:
self.ids.kb.amount = amount self.ids.kb.amount = amount
def toggle_fiat(self, a):
a.is_fiat = not a.is_fiat
def update_amount(self, c): def update_amount(self, c):
kb = self.ids.kb kb = self.ids.kb
amount = kb.fiat_amount if kb.is_fiat else kb.amount amount = kb.fiat_amount if kb.is_fiat else kb.amount

View File

@ -3,7 +3,6 @@ from kivy.factory import Factory
from kivy.properties import ObjectProperty from kivy.properties import ObjectProperty
from kivy.lang import Builder from kivy.lang import Builder
from electrum.util import fee_levels
from electrum_gui.kivy.i18n import _ from electrum_gui.kivy.i18n import _
Builder.load_string(''' Builder.load_string('''
@ -29,7 +28,11 @@ Builder.load_string('''
text: _('New Fee') text: _('New Fee')
value: '' value: ''
Label: Label:
id: tooltip id: tooltip1
text: ''
size_hint_y: None
Label:
id: tooltip2
text: '' text: ''
size_hint_y: None size_hint_y: None
Slider: Slider:
@ -72,39 +75,39 @@ class BumpFeeDialog(Factory.Popup):
self.tx_size = size self.tx_size = size
self.callback = callback self.callback = callback
self.config = app.electrum_config self.config = app.electrum_config
self.fee_step = self.config.max_fee_rate() / 10 self.mempool = self.config.use_mempool_fees()
self.dynfees = self.config.get('dynamic_fees', True) and self.app.network self.dynfees = self.config.is_dynfee() and 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_fee.value = self.app.format_amount_and_units(self.init_fee)
self.update_slider() self.update_slider()
self.update_text() self.update_text()
def update_text(self): def update_text(self):
value = int(self.ids.slider.value) fee = self.get_fee()
self.ids.new_fee.value = self.app.format_amount_and_units(self.get_fee()) self.ids.new_fee.value = self.app.format_amount_and_units(fee)
if self.dynfees: pos = int(self.ids.slider.value)
value = int(self.ids.slider.value) fee_rate = self.get_fee_rate()
self.ids.tooltip.text = fee_levels[value] text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, fee_rate)
self.ids.tooltip1.text = text
self.ids.tooltip2.text = tooltip
def update_slider(self): def update_slider(self):
slider = self.ids.slider slider = self.ids.slider
maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool)
slider.range = (0, maxp)
slider.step = 1
slider.value = pos
def get_fee_rate(self):
pos = int(self.ids.slider.value)
if self.dynfees: if self.dynfees:
slider.range = (0, 4) fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(pos)
slider.step = 1
slider.value = 3
else: else:
slider.range = (1, 10) fee_rate = self.config.static_fee(pos)
slider.step = 1 return fee_rate
rate = self.init_fee*1000//self.tx_size
slider.value = min( rate * 2 // self.fee_step, 10)
def get_fee(self): def get_fee(self):
value = int(self.ids.slider.value) fee_rate = self.get_fee_rate()
if self.dynfees: return int(fee_rate * self.tx_size // 1000)
if self.config.has_fee_estimates():
dynfee = self.config.dynfee(value)
return int(dynfee * self.tx_size // 1000)
else:
return int(value*self.fee_step * self.tx_size // 1000)
def on_ok(self): def on_ok(self):
new_fee = self.get_fee() new_fee = self.get_fee()

View File

@ -3,7 +3,6 @@ from kivy.factory import Factory
from kivy.properties import ObjectProperty from kivy.properties import ObjectProperty
from kivy.lang import Builder from kivy.lang import Builder
from electrum.util import fee_levels
from electrum_gui.kivy.i18n import _ from electrum_gui.kivy.i18n import _
Builder.load_string(''' Builder.load_string('''
@ -12,29 +11,46 @@ Builder.load_string('''
title: _('Transaction Fees') title: _('Transaction Fees')
size_hint: 0.8, 0.8 size_hint: 0.8, 0.8
pos_hint: {'top':0.9} pos_hint: {'top':0.9}
method: 0
BoxLayout: BoxLayout:
orientation: 'vertical' orientation: 'vertical'
BoxLayout: BoxLayout:
orientation: 'horizontal' orientation: 'horizontal'
size_hint: 1, 0.5 size_hint: 1, 0.5
Label: Label:
id: fee_per_kb text: _('Method') + ':'
Button:
text: _('Mempool') if root.method == 2 else _('ETA') if root.method == 1 else _('Static')
background_color: (0,0,0,0)
bold: True
on_release:
root.method = (root.method + 1) % 3
root.update_slider()
root.update_text()
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: (_('Target') if root.method > 0 else _('Fee')) + ':'
Label:
id: fee_target
text: '' text: ''
Slider: Slider:
id: slider id: slider
range: 0, 4 range: 0, 4
step: 1 step: 1
on_value: root.on_slider(self.value) on_value: root.on_slider(self.value)
Widget:
size_hint: 1, 0.5
BoxLayout: BoxLayout:
orientation: 'horizontal' orientation: 'horizontal'
size_hint: 1, 0.5 size_hint: 1, 0.5
Label: TopLabel:
text: _('Dynamic Fees') id: fee_estimate
CheckBox: text: ''
id: dynfees font_size: '14dp'
on_active: root.on_checkbox(self.active)
Widget: Widget:
size_hint: 1, 1 size_hint: 1, 0.5
BoxLayout: BoxLayout:
orientation: 'horizontal' orientation: 'horizontal'
size_hint: 1, 0.5 size_hint: 1, 0.5
@ -60,53 +76,57 @@ class FeeDialog(Factory.Popup):
self.config = config self.config = config
self.fee_rate = self.config.fee_per_kb() self.fee_rate = self.config.fee_per_kb()
self.callback = callback self.callback = callback
self.dynfees = self.config.get('dynamic_fees', True) mempool = self.config.use_mempool_fees()
self.ids.dynfees.active = self.dynfees dynfees = self.config.is_dynfee()
self.method = (2 if mempool else 1) if dynfees else 0
self.update_slider() self.update_slider()
self.update_text() self.update_text()
def update_text(self): def update_text(self):
value = int(self.ids.slider.value) pos = int(self.ids.slider.value)
self.ids.fee_per_kb.text = self.get_fee_text(value) dynfees, mempool = self.get_method()
if self.method == 2:
fee_rate = self.config.depth_to_fee(pos)
target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate)
msg = 'In the current network conditions, in order to be positioned %s, a transaction will require a fee of %s.' % (target, estimate)
elif self.method == 1:
fee_rate = self.config.eta_to_fee(pos)
target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate)
msg = 'In the last few days, transactions that confirmed %s usually paid a fee of at least %s.' % (target.lower(), estimate)
else:
fee_rate = self.config.static_fee(pos)
target, estimate = self.config.get_fee_text(pos, dynfees, True, fee_rate)
msg = 'In the current network conditions, a transaction paying %s would be positioned %s.' % (target, estimate)
self.ids.fee_target.text = target
self.ids.fee_estimate.text = msg
def get_method(self):
dynfees = self.method > 0
mempool = self.method == 2
return dynfees, mempool
def update_slider(self): def update_slider(self):
slider = self.ids.slider slider = self.ids.slider
if self.dynfees: dynfees, mempool = self.get_method()
slider.range = (0, 4) maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool)
slider.step = 1 slider.range = (0, maxp)
slider.value = self.config.get('fee_level', 2) slider.step = 1
else: slider.value = pos
slider.range = (0, 9)
slider.step = 1
slider.value = self.config.static_fee_index(self.fee_rate)
def get_fee_text(self, value):
if self.ids.dynfees.active:
tooltip = fee_levels[value]
if self.config.has_fee_estimates():
dynfee = self.config.dynfee(value)
tooltip += '\n' + (self.app.format_amount_and_units(dynfee)) + '/kB'
else:
fee_rate = self.config.static_fee(value)
tooltip = self.app.format_amount_and_units(fee_rate) + '/kB'
if self.config.has_fee_estimates():
i = self.config.reverse_dynfee(fee_rate)
tooltip += '\n' + (_('low fee') if i < 0 else 'Within %d blocks'%i)
return tooltip
def on_ok(self): def on_ok(self):
value = int(self.ids.slider.value) value = int(self.ids.slider.value)
self.config.set_key('dynamic_fees', self.dynfees, False) dynfees, mempool = self.get_method()
if self.dynfees: self.config.set_key('dynamic_fees', dynfees, False)
self.config.set_key('fee_level', value, True) self.config.set_key('mempool_fees', mempool, False)
if dynfees:
if mempool:
self.config.set_key('depth_level', value, True)
else:
self.config.set_key('fee_level', value, True)
else: else:
self.config.set_key('fee_per_kb', self.config.static_fee(value), True) self.config.set_key('fee_per_kb', self.config.static_fee(value), True)
self.callback() self.callback()
def on_slider(self, value): def on_slider(self, value):
self.update_text() self.update_text()
def on_checkbox(self, b):
self.dynfees = b
self.update_slider()
self.update_text()

View File

@ -106,4 +106,6 @@ class FxDialog(Factory.Popup):
if ccy != self.fx.get_currency(): if ccy != self.fx.get_currency():
self.fx.set_currency(ccy) self.fx.set_currency(ccy)
self.app.fiat_unit = ccy self.app.fiat_unit = ccy
else:
self.app.is_fiat = False
Clock.schedule_once(lambda dt: self.add_exchanges()) Clock.schedule_once(lambda dt: self.add_exchanges())

View File

@ -135,7 +135,7 @@ Builder.load_string('''
height: self.minimum_height height: self.minimum_height
Label: Label:
color: root.text_color color: root.text_color
text: _('From %d cosigners')%n.value text: _('From {} cosigners').format(n.value)
Slider: Slider:
id: n id: n
range: 2, 5 range: 2, 5
@ -143,7 +143,7 @@ Builder.load_string('''
value: 2 value: 2
Label: Label:
color: root.text_color color: root.text_color
text: _('Require %d signatures')%m.value text: _('Require {} signatures').format(m.value)
Slider: Slider:
id: m id: m
range: 1, n.value range: 1, n.value
@ -613,7 +613,7 @@ class RestoreSeedDialog(WizardDialog):
for c in line.children: for c in line.children:
if isinstance(c, Button): if isinstance(c, Button):
if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
c.disabled = (c.text.lower() not in p) and last_word c.disabled = (c.text.lower() not in p) and bool(last_word)
elif c.text == ' ': elif c.text == ' ':
c.disabled = not enable_space c.disabled = not enable_space
@ -807,7 +807,7 @@ class InstallWizard(BaseWizard, Widget):
popup.init(message, callback) popup.init(message, callback)
popup.open() popup.open()
def request_password(self, run_next): def request_password(self, run_next, force_disable_encrypt_cb=False):
def callback(pin): def callback(pin):
if pin: if pin:
self.run('confirm_password', pin, run_next) self.run('confirm_password', pin, run_next)

View File

@ -8,7 +8,6 @@ from electrum.i18n import languages
from electrum_gui.kivy.i18n import _ from electrum_gui.kivy.i18n import _
from electrum.plugins import run_hook from electrum.plugins import run_hook
from electrum import coinchooser from electrum import coinchooser
from electrum.util import fee_levels
from .choice_dialog import ChoiceDialog from .choice_dialog import ChoiceDialog
@ -49,12 +48,6 @@ Builder.load_string('''
description: _("Base unit for Zclassic amounts.") description: _("Base unit for Zclassic amounts.")
action: partial(root.unit_dialog, self) action: partial(root.unit_dialog, self)
CardSeparator CardSeparator
SettingsItem:
status: root.fee_status()
title: _('Fees') + ': ' + self.status
description: _("Fees paid to the Zclassic miners.")
action: partial(root.fee_dialog, self)
CardSeparator
SettingsItem: SettingsItem:
status: root.fx_status() status: root.fx_status()
title: _('Fiat Currency') + ': ' + self.status title: _('Fiat Currency') + ': ' + self.status
@ -113,7 +106,6 @@ class SettingsDialog(Factory.Popup):
layout.bind(minimum_height=layout.setter('height')) layout.bind(minimum_height=layout.setter('height'))
# cached dialogs # cached dialogs
self._fx_dialog = None self._fx_dialog = None
self._fee_dialog = None
self._proxy_dialog = None self._proxy_dialog = None
self._language_dialog = None self._language_dialog = None
self._unit_dialog = None self._unit_dialog = None
@ -204,18 +196,7 @@ class SettingsDialog(Factory.Popup):
d.open() d.open()
def fee_status(self): def fee_status(self):
if self.config.get('dynamic_fees', True): return self.config.get_fee_status()
return fee_levels[self.config.get('fee_level', 2)]
else:
return self.app.format_amount_and_units(self.config.fee_per_kb()) + '/kB'
def fee_dialog(self, label, dt):
if self._fee_dialog is None:
from .fee_dialog import FeeDialog
def cb():
label.status = self.fee_status()
self._fee_dialog = FeeDialog(self.app, self.config, cb)
self._fee_dialog.open()
def boolean_dialog(self, name, title, message, dt): def boolean_dialog(self, name, title, message, dt):
from .checkbox_dialog import CheckBoxDialog from .checkbox_dialog import CheckBoxDialog

View File

@ -20,6 +20,7 @@ Builder.load_string('''
can_rbf: False can_rbf: False
fee_str: '' fee_str: ''
date_str: '' date_str: ''
date_label:''
amount_str: '' amount_str: ''
tx_hash: '' tx_hash: ''
status_str: '' status_str: ''
@ -46,7 +47,7 @@ Builder.load_string('''
text: _('Description') if root.description else '' text: _('Description') if root.description else ''
value: root.description value: root.description
BoxLabel: BoxLabel:
text: _('Date') if root.date_str else '' text: root.date_label
value: root.date_str value: root.date_str
BoxLabel: BoxLabel:
text: _('Amount sent') if root.is_mine else _('Amount received') text: _('Amount sent') if root.is_mine else _('Amount received')
@ -110,10 +111,13 @@ class TxDialog(Factory.Popup):
tx_hash, self.status_str, self.description, self.can_broadcast, self.can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx) tx_hash, self.status_str, self.description, self.can_broadcast, self.can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx)
self.tx_hash = tx_hash or '' self.tx_hash = tx_hash or ''
if timestamp: if timestamp:
self.date_label = _('Date')
self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
elif exp_n: elif exp_n:
self.date_str = _('Within %d blocks') % exp_n if exp_n > 0 else _('unknown (low fee)') self.date_label = _('Mempool depth')
self.date_str = _('{} from tip').format('%.2f MB'%(exp_n/1000000))
else: else:
self.date_label = ''
self.date_str = '' self.date_str = ''
if amount is None: if amount is None:

View File

@ -87,9 +87,9 @@ class CScreen(Factory.Screen):
self.add_widget(self.context_menu) self.add_widget(self.context_menu)
# note: this list needs to be kept in sync with another in qt
TX_ICONS = [ TX_ICONS = [
"close", "unconfirmed",
"close",
"close", "close",
"unconfirmed", "unconfirmed",
"close", "close",
@ -143,14 +143,13 @@ class HistoryScreen(CScreen):
ri.icon = icon ri.icon = icon
ri.date = status_str ri.date = status_str
ri.message = label ri.message = label
ri.value = value or 0
ri.amount = self.app.format_amount(value, True) if value is not None else '--'
ri.confirmations = conf ri.confirmations = conf
if self.app.fiat_unit and date: if value is not None:
rate = self.app.fx.history_rate(date) ri.is_mine = value < 0
if rate: if value < 0: value = - value
s = self.app.fx.value_str(value, rate) ri.amount = self.app.format_amount_and_units(value)
ri.quote_text = '' if s is None else s + ' ' + self.app.fiat_unit if self.app.fiat_unit and date:
ri.quote_text = self.app.fx.historical_value_str(value, date) + ' ' + self.app.fx.ccy
return ri return ri
def update(self, see_all=False): def update(self, see_all=False):
@ -162,13 +161,8 @@ class HistoryScreen(CScreen):
count = 0 count = 0
for item in history: for item in history:
ri = self.get_card(*item) ri = self.get_card(*item)
count += 1
history_card.add_widget(ri) history_card.add_widget(ri)
if count == 0:
msg = _('This screen shows your list of transactions. It is currently empty.')
history_card.add_widget(EmptyLabel(text=msg))
class SendScreen(CScreen): class SendScreen(CScreen):
@ -379,6 +373,8 @@ class ReceiveScreen(CScreen):
def save_request(self): def save_request(self):
addr = self.screen.address addr = self.screen.address
if not addr:
return
amount = self.screen.amount amount = self.screen.amount
message = self.screen.message message = self.screen.message
amount = self.app.get_amount(amount) if amount else 0 amount = self.app.get_amount(amount) if amount else 0
@ -521,7 +517,12 @@ class AddressScreen(CScreen):
def update(self): def update(self):
self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)] self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)]
wallet = self.app.wallet wallet = self.app.wallet
_list = wallet.get_change_addresses() if self.screen.show_change else wallet.get_receiving_addresses() if self.screen.show_change == 0:
_list = wallet.get_receiving_addresses()
elif self.screen.show_change == 1:
_list = wallet.get_change_addresses()
else:
_list = wallet.get_addresses()
search = self.screen.message search = self.screen.message
container = self.screen.ids.search_container container = self.screen.ids.search_container
container.clear_widgets() container.clear_widgets()

View File

@ -24,33 +24,18 @@
shorten: True shorten: True
Widget Widget
AddressLabel: AddressLabel:
text: root.memo text: (root.amount if root.status == 'Funded' else root.status) + ' ' + root.memo
color: .699, .699, .699, 1 color: .699, .699, .699, 1
font_size: '13sp' font_size: '13sp'
shorten: True shorten: True
Widget Widget
BoxLayout:
spacing: '8dp'
height: '32dp'
orientation: 'vertical'
Widget
AddressLabel:
text: root.amount
halign: 'right'
font_size: '15sp'
Widget
AddressLabel:
text: root.status
halign: 'right'
font_size: '13sp'
color: .699, .699, .699, 1
AddressScreen: AddressScreen:
id: addr_screen id: addr_screen
name: 'address' name: 'address'
message: '' message: ''
pr_status: 'Pending' pr_status: 'Pending'
show_change: False show_change: 0
show_used: 0 show_used: 0
on_message: on_message:
self.parent.update() self.parent.update()
@ -70,9 +55,9 @@ AddressScreen:
spacing: '5dp' spacing: '5dp'
AddressButton: AddressButton:
id: search id: search
text: _('Change') if root.show_change else _('Receiving') text: {0:_('Receiving'), 1:_('Change'), 2:_('All')}[root.show_change]
on_release: on_release:
root.show_change = not root.show_change root.show_change = (root.show_change + 1) % 3
Clock.schedule_once(lambda dt: app.address_screen.update()) Clock.schedule_once(lambda dt: app.address_screen.update())
AddressFilter: AddressFilter:
opacity: 1 opacity: 1
@ -103,4 +88,3 @@ AddressScreen:
id: search_container id: search_container
size_hint_y: None size_hint_y: None
height: self.minimum_height height: self.minimum_height
spacing: '2dp'

View File

@ -19,56 +19,57 @@
<HistoryItem@CardItem> <HistoryItem@CardItem>
icon: 'atlas://gui/kivy/theming/light/important' icon: 'atlas://gui/kivy/theming/light/important'
message: '' message: ''
value: 0 is_mine: True
amount: '--' amount: '--'
amount_color: '#FF6657' if self.value < 0 else '#2EA442' action: _('Sent') if self.is_mine else _('Received')
amount_color: '#FF6657' if self.is_mine else '#2EA442'
confirmations: 0 confirmations: 0
date: '' date: ''
quote_text: '' quote_text: ''
spacing: '9dp'
Image: Image:
id: icon id: icon
source: root.icon source: root.icon
size_hint: None, 1 size_hint: None, 1
width: self.height *.54 allow_stretch: True
width: self.height*1.5
mipmap: True mipmap: True
BoxLayout: BoxLayout:
orientation: 'vertical' orientation: 'vertical'
Widget Widget
CardLabel: CardLabel:
text: root.date text:
font_size: '14sp' u'[color={color}]{s}[/color]'.format(s='<<' if root.is_mine else '>>', color=root.amount_color)\
+ ' ' + root.action + ' ' + (root.quote_text if app.is_fiat else root.amount)
font_size: '15sp'
CardLabel: CardLabel:
color: .699, .699, .699, 1 color: .699, .699, .699, 1
font_size: '13sp' font_size: '14sp'
shorten: True shorten: True
text: root.message text: root.date + ' ' + root.message
Widget Widget
CardLabel:
halign: 'right'
font_size: '15sp'
size_hint: None, 1
width: '110sp'
markup: True
font_name: font_light
text:
u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\
u'[color=#B2B3B3][size=13sp]{qt}[/size]'\
u'[/color]'.format(amount_color=root.amount_color,\
amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\
unit=app.base_unit)
HistoryScreen: HistoryScreen:
name: 'history' name: 'history'
content: content content: content
ScrollView: BoxLayout:
id: content orientation: 'vertical'
do_scroll_x: False Button:
GridLayout background_color: 0, 0, 0, 0
id: history_container text: app.fiat_balance if app.is_fiat else app.balance
cols: 1 markup: True
size_hint: 1, None color: .9, .9, .9, 1
height: self.minimum_height font_size: '30dp'
padding: '12dp' bold: True
spacing: '2dp' size_hint: 1, 0.25
on_release: app.is_fiat = not app.is_fiat if app.fx.is_enabled() else False
ScrollView:
id: content
do_scroll_x: False
size_hint: 1, 0.75
GridLayout
id: history_container
cols: 1
size_hint: 1, None
height: self.minimum_height

View File

@ -11,7 +11,7 @@ Popup:
height: self.minimum_height height: self.minimum_height
padding: '10dp' padding: '10dp'
SettingsItem: SettingsItem:
value: _("%d connections.")% app.num_nodes if app.num_nodes else _("Not connected") value: _("{} connections.").format(app.num_nodes) if app.num_nodes else _("Not connected")
title: _("Status") + ': ' + self.value title: _("Status") + ': ' + self.value
description: _("Connections with Electrum servers") description: _("Connections with Electrum servers")
action: lambda x: None action: lambda x: None
@ -46,7 +46,7 @@ Popup:
CardSeparator CardSeparator
SettingsItem: SettingsItem:
title: _('Fork detected at block %d')%app.blockchain_checkpoint if app.num_chains>1 else _('No fork detected') title: _('Fork detected at block {}').format(app.blockchain_checkpoint) if app.num_chains>1 else _('No fork detected')
fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name
description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain') description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain')
action: app.choose_blockchain_dialog action: app.choose_blockchain_dialog

View File

@ -71,6 +71,24 @@ SendScreen:
text: s.message if s.message else (_('No Description') if root.is_pr else _('Description')) text: s.message if s.message else (_('No Description') if root.is_pr else _('Description'))
disabled: root.is_pr disabled: root.is_pr
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
CardSeparator:
opacity: int(not root.is_pr)
color: blue_bottom.foreground_color
BoxLayout:
size_hint: 1, None
height: blue_bottom.item_height
spacing: '5dp'
Image:
source: 'atlas://gui/kivy/theming/light/star_big_inactive'
opacity: 0.7
size_hint: None, None
size: '22dp', '22dp'
pos_hint: {'center_y': .5}
BlueButton:
id: fee_e
default_text: _('Fee')
text: app.fee_status
on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True))
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'

View File

@ -25,6 +25,7 @@
import signal import signal
import sys import sys
import traceback
try: try:
@ -94,6 +95,8 @@ class ElectrumGui:
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
if hasattr(QGuiApplication, 'setDesktopFileName'):
QGuiApplication.setDesktopFileName('electrum.desktop')
self.config = config self.config = config
self.daemon = daemon self.daemon = daemon
self.plugins = plugins self.plugins = plugins
@ -190,8 +193,10 @@ class ElectrumGui:
else: else:
try: try:
wallet = self.daemon.load_wallet(path, None) wallet = self.daemon.load_wallet(path, None)
except BaseException as e: except BaseException as e:
d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e)) traceback.print_exc(file=sys.stdout)
d = QMessageBox(QMessageBox.Warning, _('Error'),
_('Cannot load wallet:') + '\n' + str(e))
d.exec_() d.exec_()
return return
if not wallet: if not wallet:
@ -208,7 +213,14 @@ class ElectrumGui:
return return
wallet.start_threads(self.daemon.network) wallet.start_threads(self.daemon.network)
self.daemon.add_wallet(wallet) self.daemon.add_wallet(wallet)
w = self.create_window_for_wallet(wallet) try:
w = self.create_window_for_wallet(wallet)
except BaseException as e:
traceback.print_exc(file=sys.stdout)
d = QMessageBox(QMessageBox.Warning, _('Error'),
_('Cannot create window for wallet:') + '\n' + str(e))
d.exec_()
return
if uri: if uri:
w.pay_to_URI(uri) w.pay_to_URI(uri)
w.bring_to_top() w.bring_to_top()
@ -241,8 +253,7 @@ class ElectrumGui:
return return
except GoBack: except GoBack:
return return
except: except BaseException as e:
import traceback
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
return return
self.timer.start() self.timer.start()

View File

@ -24,47 +24,56 @@
# SOFTWARE. # SOFTWARE.
import webbrowser import webbrowser
from .util import *
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import block_explorer_URL from electrum.util import block_explorer_URL
from electrum.plugins import run_hook from electrum.plugins import run_hook
from electrum.bitcoin import is_address from electrum.bitcoin import is_address
from .util import *
class AddressList(MyTreeWidget): class AddressList(MyTreeWidget):
filter_columns = [0, 1, 2] # Address, Label, Balance filter_columns = [0, 1, 2, 3] # Type, Address, Label, Balance
def __init__(self, parent=None): def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [], 1) MyTreeWidget.__init__(self, parent, self.create_menu, [], 2)
self.refresh_headers() self.refresh_headers()
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.show_change = False self.setSortingEnabled(True)
self.show_change = 0
self.show_used = 0 self.show_used = 0
self.change_button = QComboBox(self) self.change_button = QComboBox(self)
self.change_button.currentIndexChanged.connect(self.toggle_change) self.change_button.currentIndexChanged.connect(self.toggle_change)
for t in [_('Receiving'), _('Change')]: for t in [_('All'), _('Receiving'), _('Change')]:
self.change_button.addItem(t) self.change_button.addItem(t)
self.used_button = QComboBox(self) self.used_button = QComboBox(self)
self.used_button.currentIndexChanged.connect(self.toggle_used) self.used_button.currentIndexChanged.connect(self.toggle_used)
for t in [_('All'), _('Unused'), _('Funded'), _('Used')]: for t in [_('All'), _('Unused'), _('Funded'), _('Used')]:
self.used_button.addItem(t) self.used_button.addItem(t)
def get_list_header(self): def get_toolbar_buttons(self):
return QLabel(_("Filter ")), self.change_button, self.used_button return QLabel(_("Filter:")), self.change_button, self.used_button
def on_hide_toolbar(self):
self.show_change = 0
self.show_used = 0
self.update()
def save_toolbar_state(self, state, config):
config.set_key('show_toolbar_addresses', state)
def refresh_headers(self): def refresh_headers(self):
headers = [ _('Address'), _('Label'), _('Balance')] headers = [_('Type'), _('Address'), _('Label'), _('Balance')]
fx = self.parent.fx fx = self.parent.fx
if fx and fx.get_fiat_address_config(): if fx and fx.get_fiat_address_config():
headers.extend([_(fx.get_currency()+' Balance')]) headers.extend([_(fx.get_currency()+' Balance')])
headers.extend([_('Tx')]) headers.extend([_('Tx')])
self.update_headers(headers) self.update_headers(headers)
def toggle_change(self, show): def toggle_change(self, state):
show = bool(show) if state == self.show_change:
if show == self.show_change:
return return
self.show_change = show self.show_change = state
self.update() self.update()
def toggle_used(self, state): def toggle_used(self, state):
@ -77,10 +86,15 @@ class AddressList(MyTreeWidget):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
item = self.currentItem() item = self.currentItem()
current_address = item.data(0, Qt.UserRole) if item else None current_address = item.data(0, Qt.UserRole) if item else None
addr_list = self.wallet.get_change_addresses() if self.show_change else self.wallet.get_receiving_addresses() if self.show_change == 1:
addr_list = self.wallet.get_receiving_addresses()
elif self.show_change == 2:
addr_list = self.wallet.get_change_addresses()
else:
addr_list = self.wallet.get_addresses()
self.clear() self.clear()
for address in addr_list: for address in addr_list:
num = len(self.wallet.history.get(address,[])) num = len(self.wallet.get_address_history(address))
is_used = self.wallet.is_used(address) is_used = self.wallet.is_used(address)
label = self.wallet.labels.get(address, '') label = self.wallet.labels.get(address, '')
c, u, x = self.wallet.get_addr_balance(address) c, u, x = self.wallet.get_addr_balance(address)
@ -91,23 +105,29 @@ class AddressList(MyTreeWidget):
continue continue
if self.show_used == 3 and not is_used: if self.show_used == 3 and not is_used:
continue continue
balance_text = self.parent.format_amount(balance) balance_text = self.parent.format_amount(balance, whitespaces=True)
fx = self.parent.fx fx = self.parent.fx
if fx and fx.get_fiat_address_config(): if fx and fx.get_fiat_address_config():
rate = fx.exchange_rate() rate = fx.exchange_rate()
fiat_balance = fx.value_str(balance, rate) fiat_balance = fx.value_str(balance, rate)
address_item = QTreeWidgetItem([address, label, balance_text, fiat_balance, "%d"%num]) address_item = SortableTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num])
address_item.setTextAlignment(3, Qt.AlignRight) address_item.setTextAlignment(4, Qt.AlignRight)
address_item.setFont(4, QFont(MONOSPACE_FONT))
else: else:
address_item = QTreeWidgetItem([address, label, balance_text, "%d"%num]) address_item = SortableTreeWidgetItem(['', address, label, balance_text, "%d"%num])
address_item.setTextAlignment(2, Qt.AlignRight) address_item.setFont(3, QFont(MONOSPACE_FONT))
address_item.setFont(0, QFont(MONOSPACE_FONT)) if self.wallet.is_change(address):
address_item.setData(0, Qt.UserRole, address) address_item.setText(0, _('change'))
address_item.setData(0, Qt.UserRole+1, True) # label can be edited address_item.setBackground(0, ColorScheme.YELLOW.as_color(True))
else:
address_item.setText(0, _('receiving'))
address_item.setBackground(0, ColorScheme.GREEN.as_color(True))
address_item.setFont(1, QFont(MONOSPACE_FONT))
address_item.setData(1, Qt.UserRole, address)
if self.wallet.is_frozen(address): if self.wallet.is_frozen(address):
address_item.setBackground(0, ColorScheme.BLUE.as_color(True)) address_item.setBackground(1, ColorScheme.BLUE.as_color(True))
if self.wallet.is_beyond_limit(address, self.show_change): if self.wallet.is_beyond_limit(address):
address_item.setBackground(0, ColorScheme.RED.as_color(True)) address_item.setBackground(1, ColorScheme.RED.as_color(True))
self.addChild(address_item) self.addChild(address_item)
if address == current_address: if address == current_address:
self.setCurrentItem(address_item) self.setCurrentItem(address_item)
@ -118,7 +138,7 @@ class AddressList(MyTreeWidget):
can_delete = self.wallet.can_delete_address() can_delete = self.wallet.can_delete_address()
selected = self.selectedItems() selected = self.selectedItems()
multi_select = len(selected) > 1 multi_select = len(selected) > 1
addrs = [item.text(0) for item in selected] addrs = [item.text(1) for item in selected]
if not addrs: if not addrs:
return return
if not multi_select: if not multi_select:
@ -135,10 +155,10 @@ class AddressList(MyTreeWidget):
if not multi_select: if not multi_select:
column_title = self.headerItem().text(col) column_title = self.headerItem().text(col)
copy_text = item.text(col) copy_text = item.text(col)
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(copy_text)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text))
menu.addAction(_('Details'), lambda: self.parent.show_address(addr)) menu.addAction(_('Details'), lambda: self.parent.show_address(addr))
if col in self.editable_columns: if col in self.editable_columns:
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, col)) menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, col))
menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr))
if self.wallet.can_export(): if self.wallet.can_export():
menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr))

View File

@ -106,12 +106,7 @@ class BTCAmountEdit(AmountEdit):
class FeerateEdit(BTCAmountEdit): class FeerateEdit(BTCAmountEdit):
def _base_unit(self): def _base_unit(self):
p = self.decimal_point() return 'zat/byte'
if p == 2:
return 'mZCL/kB'
if p == 0:
return 'zat/byte'
raise Exception('Unknown base unit')
def get_amount(self): def get_amount(self):
sat_per_byte_amount = BTCAmountEdit.get_amount(self) sat_per_byte_amount = BTCAmountEdit.get_amount(self)

View File

@ -203,7 +203,8 @@ class Console(QtWidgets.QPlainTextEdit):
self.skip = not self.skip self.skip = not self.skip
if type(self.namespace.get(command)) == type(lambda:None): if type(self.namespace.get(command)) == type(lambda:None):
self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command)) self.appendPlainText("'{}' is a function. Type '{}()' to use it in the Python console."
.format(command, command))
self.newPrompt() self.newPrompt()
return return

View File

@ -32,7 +32,7 @@ from PyQt5.QtGui import *
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem) QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem)
from .util import MyTreeWidget from .util import MyTreeWidget, import_meta_gui, export_meta_gui
class ContactList(MyTreeWidget): class ContactList(MyTreeWidget):
@ -53,12 +53,10 @@ class ContactList(MyTreeWidget):
self.parent.set_contact(item.text(0), item.text(1)) self.parent.set_contact(item.text(0), item.text(1))
def import_contacts(self): def import_contacts(self):
wallet_folder = self.parent.get_wallet_folder() import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update)
filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
if not filename: def export_contacts(self):
return export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file)
self.parent.contacts.import_file(filename)
self.on_update()
def create_menu(self, position): def create_menu(self, position):
menu = QMenu() menu = QMenu()
@ -66,16 +64,17 @@ class ContactList(MyTreeWidget):
if not selected: if not selected:
menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog()) menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog())
menu.addAction(_("Import file"), lambda: self.import_contacts()) menu.addAction(_("Import file"), lambda: self.import_contacts())
menu.addAction(_("Export file"), lambda: self.export_contacts())
else: else:
names = [item.text(0) for item in selected] names = [item.text(0) for item in selected]
keys = [item.text(1) for item in selected] keys = [item.text(1) for item in selected]
column = self.currentColumn() column = self.currentColumn()
column_title = self.headerItem().text(column) column_title = self.headerItem().text(column)
column_data = '\n'.join([item.text(column) for item in selected]) column_data = '\n'.join([item.text(column) for item in selected])
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
if column in self.editable_columns: if column in self.editable_columns:
item = self.currentItem() item = self.currentItem()
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column))
menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys)) menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys))
menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys)) menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys))
URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)] URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)]

206
gui/qt/exception_window.py Normal file
View File

@ -0,0 +1,206 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import json
import locale
import platform
import traceback
import os
import sys
import subprocess
import requests
from PyQt5.QtCore import QObject
import PyQt5.QtCore as QtCore
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import *
from electrum.i18n import _
from electrum import ELECTRUM_VERSION, bitcoin, constants
issue_template = """<h2>Traceback</h2>
<pre>
{traceback}
</pre>
<h2>Additional information</h2>
<ul>
<li>Electrum version: {app_version}</li>
<li>Operating system: {os}</li>
<li>Wallet type: {wallet_type}</li>
<li>Locale: {locale}</li>
</ul>
"""
report_server = "https://crashhub.electrum.org/crash"
class Exception_Window(QWidget):
_active_window = None
def __init__(self, main_window, exctype, value, tb):
self.exc_args = (exctype, value, tb)
self.main_window = main_window
QWidget.__init__(self)
self.setWindowTitle('Electrum - ' + _('An Error Occured'))
self.setMinimumSize(600, 300)
main_box = QVBoxLayout()
heading = QLabel('<h2>' + _('Sorry!') + '</h2>')
main_box.addWidget(heading)
main_box.addWidget(QLabel(_('Something went wrong while executing Electrum.')))
main_box.addWidget(QLabel(
_('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug '
'information:')))
collapse_info = QPushButton(_("Show report contents"))
collapse_info.clicked.connect(lambda: QMessageBox.about(self, "Report contents", self.get_report_string()))
main_box.addWidget(collapse_info)
main_box.addWidget(QLabel(_("Please briefly describe what led to the error (optional):")))
self.description_textfield = QTextEdit()
self.description_textfield.setFixedHeight(50)
main_box.addWidget(self.description_textfield)
main_box.addWidget(QLabel(_("Do you want to send this report?")))
buttons = QHBoxLayout()
report_button = QPushButton(_('Send Bug Report'))
report_button.clicked.connect(self.send_report)
report_button.setIcon(QIcon(":icons/tab_send.png"))
buttons.addWidget(report_button)
never_button = QPushButton(_('Never'))
never_button.clicked.connect(self.show_never)
buttons.addWidget(never_button)
close_button = QPushButton(_('Not Now'))
close_button.clicked.connect(self.close)
buttons.addWidget(close_button)
main_box.addLayout(buttons)
self.setLayout(main_box)
self.show()
def send_report(self):
if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in report_server:
# Gah! Some kind of altcoin wants to send us crash reports.
self.main_window.show_critical(_("Please report this issue manually."))
return
report = self.get_traceback_info()
report.update(self.get_additional_info())
report = json.dumps(report)
try:
response = requests.post(report_server, data=report, timeout=20)
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' +
str(e) + '\n' +
_("Please report this issue manually."))
return
else:
QMessageBox.about(self, "Crash report", response.text)
self.close()
def on_close(self):
Exception_Window._active_window = None
sys.__excepthook__(*self.exc_args)
self.close()
def show_never(self):
self.main_window.config.set_key("show_crash_reporter", False)
self.close()
def closeEvent(self, event):
self.on_close()
event.accept()
def get_traceback_info(self):
exc_string = str(self.exc_args[1])
stack = traceback.extract_tb(self.exc_args[2])
readable_trace = "".join(traceback.format_list(stack))
id = {
"file": stack[-1].filename,
"name": stack[-1].name,
"type": self.exc_args[0].__name__
}
return {
"exc_string": exc_string,
"stack": readable_trace,
"id": id
}
def get_additional_info(self):
args = {
"app_version": ELECTRUM_VERSION,
"os": platform.platform(),
"wallet_type": "unknown",
"locale": locale.getdefaultlocale()[0],
"description": self.description_textfield.toPlainText()
}
try:
args["wallet_type"] = self.main_window.wallet.wallet_type
except:
# Maybe the wallet isn't loaded yet
pass
try:
args["app_version"] = self.get_git_version()
except:
# This is probably not running from source
pass
return args
def get_report_string(self):
info = self.get_additional_info()
info["traceback"] = "".join(traceback.format_exception(*self.exc_args))
return issue_template.format(**info)
@staticmethod
def get_git_version():
dir = os.path.dirname(os.path.realpath(sys.argv[0]))
version = subprocess.check_output(['git', 'describe', '--always'], cwd=dir)
return str(version, "utf8").strip()
def _show_window(*args):
if not Exception_Window._active_window:
Exception_Window._active_window = Exception_Window(*args)
class Exception_Hook(QObject):
_report_exception = QtCore.pyqtSignal(object, object, object, object)
def __init__(self, main_window, *args, **kwargs):
super(Exception_Hook, self).__init__(*args, **kwargs)
if not main_window.config.get("show_crash_reporter", default=True):
return
self.main_window = main_window
sys.excepthook = self.handler
self._report_exception.connect(_show_window)
def handler(self, *args):
self._report_exception.emit(self.main_window, *args)

View File

@ -1,6 +1,4 @@
from electrum.i18n import _ from electrum.i18n import _
from PyQt5.QtGui import * from PyQt5.QtGui import *
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtWidgets import QSlider, QToolTip from PyQt5.QtWidgets import QSlider, QToolTip
@ -18,47 +16,43 @@ class FeeSlider(QSlider):
self.lock = threading.RLock() self.lock = threading.RLock()
self.update() self.update()
self.valueChanged.connect(self.moved) self.valueChanged.connect(self.moved)
self._active = True
def moved(self, pos): def moved(self, pos):
with self.lock: with self.lock:
fee_rate = self.config.dynfee(pos) if self.dyn else self.config.static_fee(pos) if self.dyn:
fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos)
else:
fee_rate = self.config.static_fee(pos)
tooltip = self.get_tooltip(pos, fee_rate) tooltip = self.get_tooltip(pos, fee_rate)
QToolTip.showText(QCursor.pos(), tooltip, self) QToolTip.showText(QCursor.pos(), tooltip, self)
self.setToolTip(tooltip) self.setToolTip(tooltip)
self.callback(self.dyn, pos, fee_rate) self.callback(self.dyn, pos, fee_rate)
def get_tooltip(self, pos, fee_rate): def get_tooltip(self, pos, fee_rate):
from electrum.util import fee_levels mempool = self.config.use_mempool_fees()
rate_str = self.window.format_fee_rate(fee_rate) if fee_rate else _('unknown') target, estimate = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate)
if self.dyn: if self.dyn:
tooltip = fee_levels[pos] + '\n' + rate_str return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate
else: else:
tooltip = 'Fixed rate: ' + rate_str return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate
if self.config.has_fee_estimates():
i = self.config.reverse_dynfee(fee_rate)
tooltip += '\n' + (_('Low fee') if i < 0 else 'Within %d blocks'%i)
return tooltip
def update(self): def update(self):
with self.lock: with self.lock:
self.dyn = self.config.is_dynfee() self.dyn = self.config.is_dynfee()
if self.dyn: mempool = self.config.use_mempool_fees()
pos = self.config.get('fee_level', 2) maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool)
fee_rate = self.config.dynfee(pos) self.setRange(0, maxp)
self.setRange(0, 4) self.setValue(pos)
self.setValue(pos)
else:
fee_rate = self.config.fee_per_kb()
pos = self.config.static_fee_index(fee_rate)
self.setRange(0, 9)
self.setValue(pos)
tooltip = self.get_tooltip(pos, fee_rate) tooltip = self.get_tooltip(pos, fee_rate)
self.setToolTip(tooltip) self.setToolTip(tooltip)
def activate(self): def activate(self):
self._active = True
self.setStyleSheet('') self.setStyleSheet('')
def deactivate(self): def deactivate(self):
self._active = False
# TODO it would be nice to find a platform-independent solution # TODO it would be nice to find a platform-independent solution
# that makes the slider look as if it was disabled # that makes the slider look as if it was disabled
self.setStyleSheet( self.setStyleSheet(
@ -79,3 +73,6 @@ class FeeSlider(QSlider):
} }
""" """
) )
def is_active(self):
return self._active

View File

@ -24,19 +24,24 @@
# SOFTWARE. # SOFTWARE.
import webbrowser import webbrowser
import datetime
from electrum.wallet import AddTransactionException, TX_HEIGHT_LOCAL
from .util import * from .util import *
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import block_explorer_URL from electrum.util import block_explorer_URL, profiler
from electrum.util import timestamp_to_datetime, profiler
try:
from electrum.plot import plot_history, NothingToPlotException
except:
plot_history = None
# note: this list needs to be kept in sync with another in kivy
TX_ICONS = [ TX_ICONS = [
"warning.png", "unconfirmed.png",
"warning.png",
"warning.png", "warning.png",
"unconfirmed.png", "unconfirmed.png",
"unconfirmed.png", "offline_tx.png",
"clock1.png", "clock1.png",
"clock2.png", "clock2.png",
"clock3.png", "clock3.png",
@ -46,51 +51,204 @@ TX_ICONS = [
] ]
class HistoryList(MyTreeWidget): class HistoryList(MyTreeWidget, AcceptFileDragDrop):
filter_columns = [2, 3, 4] # Date, Description, Amount filter_columns = [2, 3, 4] # Date, Description, Amount
def __init__(self, parent=None): def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3) MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
AcceptFileDragDrop.__init__(self, ".txn")
self.refresh_headers() self.refresh_headers()
self.setColumnHidden(1, True) self.setColumnHidden(1, True)
self.setSortingEnabled(True)
self.sortByColumn(0, Qt.AscendingOrder)
self.start_timestamp = None
self.end_timestamp = None
self.years = []
self.create_toolbar_buttons()
def format_date(self, d):
return str(datetime.date(d.year, d.month, d.day)) if d else _('None')
def refresh_headers(self): def refresh_headers(self):
headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')] headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')]
fx = self.parent.fx fx = self.parent.fx
if fx and fx.show_history(): if fx and fx.show_history():
headers.extend(['%s '%fx.ccy + _('Amount'), '%s '%fx.ccy + _('Balance')]) headers.extend(['%s '%fx.ccy + _('Value')])
self.editable_columns |= {6}
if fx.get_history_capital_gains_config():
headers.extend(['%s '%fx.ccy + _('Acquisition price')])
headers.extend(['%s '%fx.ccy + _('Capital Gains')])
else:
self.editable_columns -= {6}
self.update_headers(headers) self.update_headers(headers)
def get_domain(self): def get_domain(self):
'''Replaced in address_dialog.py''' '''Replaced in address_dialog.py'''
return self.wallet.get_addresses() return self.wallet.get_addresses()
def on_combo(self, x):
s = self.period_combo.itemText(x)
x = s == _('Custom')
self.start_button.setEnabled(x)
self.end_button.setEnabled(x)
if s == _('All'):
self.start_timestamp = None
self.end_timestamp = None
self.start_button.setText("-")
self.end_button.setText("-")
else:
try:
year = int(s)
except:
return
start_date = datetime.datetime(year, 1, 1)
end_date = datetime.datetime(year+1, 1, 1)
self.start_timestamp = time.mktime(start_date.timetuple())
self.end_timestamp = time.mktime(end_date.timetuple())
self.start_button.setText(_('From') + ' ' + self.format_date(start_date))
self.end_button.setText(_('To') + ' ' + self.format_date(end_date))
self.update()
def create_toolbar_buttons(self):
self.period_combo = QComboBox()
self.start_button = QPushButton('-')
self.start_button.pressed.connect(self.select_start_date)
self.start_button.setEnabled(False)
self.end_button = QPushButton('-')
self.end_button.pressed.connect(self.select_end_date)
self.end_button.setEnabled(False)
self.period_combo.addItems([_('All'), _('Custom')])
self.period_combo.activated.connect(self.on_combo)
def get_toolbar_buttons(self):
return self.period_combo, self.start_button, self.end_button
def on_hide_toolbar(self):
self.start_timestamp = None
self.end_timestamp = None
self.update()
def save_toolbar_state(self, state, config):
config.set_key('show_toolbar_history', state)
def select_start_date(self):
self.start_timestamp = self.select_date(self.start_button)
self.update()
def select_end_date(self):
self.end_timestamp = self.select_date(self.end_button)
self.update()
def select_date(self, button):
d = WindowModalDialog(self, _("Select date"))
d.setMinimumSize(600, 150)
d.date = None
vbox = QVBoxLayout()
def on_date(date):
d.date = date
cal = QCalendarWidget()
cal.setGridVisible(True)
cal.clicked[QDate].connect(on_date)
vbox.addWidget(cal)
vbox.addLayout(Buttons(OkButton(d), CancelButton(d)))
d.setLayout(vbox)
if d.exec_():
if d.date is None:
return None
date = d.date.toPyDate()
button.setText(self.format_date(date))
return time.mktime(date.timetuple())
def show_summary(self):
h = self.summary
start_date = h.get('start_date')
end_date = h.get('end_date')
format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit()
d = WindowModalDialog(self, _("Summary"))
d.setMinimumSize(600, 150)
vbox = QVBoxLayout()
grid = QGridLayout()
grid.addWidget(QLabel(_("Start")), 0, 0)
grid.addWidget(QLabel(self.format_date(start_date)), 0, 1)
grid.addWidget(QLabel(_("End")), 1, 0)
grid.addWidget(QLabel(self.format_date(end_date)), 1, 1)
grid.addWidget(QLabel(_("Initial balance")), 2, 0)
grid.addWidget(QLabel(format_amount(h['start_balance'])), 2, 1)
grid.addWidget(QLabel(str(h.get('start_fiat_balance'))), 2, 2)
grid.addWidget(QLabel(_("Final balance")), 4, 0)
grid.addWidget(QLabel(format_amount(h['end_balance'])), 4, 1)
grid.addWidget(QLabel(str(h.get('end_fiat_balance'))), 4, 2)
grid.addWidget(QLabel(_("Income")), 5, 0)
grid.addWidget(QLabel(format_amount(h.get('income'))), 5, 1)
grid.addWidget(QLabel(str(h.get('fiat_income'))), 5, 2)
grid.addWidget(QLabel(_("Expenditures")), 6, 0)
grid.addWidget(QLabel(format_amount(h.get('expenditures'))), 6, 1)
grid.addWidget(QLabel(str(h.get('fiat_expenditures'))), 6, 2)
grid.addWidget(QLabel(_("Capital gains")), 7, 0)
grid.addWidget(QLabel(str(h.get('capital_gains'))), 7, 2)
grid.addWidget(QLabel(_("Unrealized gains")), 8, 0)
grid.addWidget(QLabel(str(h.get('unrealized_gains', ''))), 8, 2)
vbox.addLayout(grid)
vbox.addLayout(Buttons(CloseButton(d)))
d.setLayout(vbox)
d.exec_()
def plot_history_dialog(self):
if plot_history is None:
self.parent.show_message(
_("Can't plot history.") + '\n' +
_("Perhaps some dependencies are missing...") + " (matplotlib?)")
return
try:
plt = plot_history(self.transactions)
plt.show()
except NothingToPlotException as e:
self.parent.show_message(str(e))
@profiler @profiler
def on_update(self): def on_update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
h = self.wallet.get_history(self.get_domain()) fx = self.parent.fx
r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=self.start_timestamp, to_timestamp=self.end_timestamp, fx=fx)
self.transactions = r['transactions']
self.summary = r['summary']
if not self.years and self.transactions:
from datetime import date
start_date = self.transactions[0].get('date') or date.today()
end_date = self.transactions[-1].get('date') or date.today()
self.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
self.period_combo.insertItems(1, self.years)
item = self.currentItem() item = self.currentItem()
current_tx = item.data(0, Qt.UserRole) if item else None current_tx = item.data(0, Qt.UserRole) if item else None
self.clear() self.clear()
fx = self.parent.fx
if fx: fx.history_used_spot = False if fx: fx.history_used_spot = False
for h_item in h: for tx_item in self.transactions:
tx_hash, height, conf, timestamp, value, balance = h_item tx_hash = tx_item['txid']
height = tx_item['height']
conf = tx_item['confirmations']
timestamp = tx_item['timestamp']
value = tx_item['value'].value
balance = tx_item['balance'].value
label = tx_item['label']
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp) status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
has_invoice = self.wallet.invoices.paid.get(tx_hash) has_invoice = self.wallet.invoices.paid.get(tx_hash)
icon = QIcon(":icons/" + TX_ICONS[status]) icon = QIcon(":icons/" + TX_ICONS[status])
v_str = self.parent.format_amount(value, True, whitespaces=True) v_str = self.parent.format_amount(value, True, whitespaces=True)
balance_str = self.parent.format_amount(balance, whitespaces=True) balance_str = self.parent.format_amount(balance, whitespaces=True)
label = self.wallet.get_label(tx_hash)
entry = ['', tx_hash, status_str, label, v_str, balance_str] entry = ['', tx_hash, status_str, label, v_str, balance_str]
if fx and fx.show_history(): fiat_value = None
date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) if value is not None and fx and fx.show_history():
for amount in [value, balance]: fiat_value = tx_item['fiat_value'].value
text = fx.historical_value_str(amount, date) value_str = fx.format_fiat(fiat_value)
entry.append(text) entry.append(value_str)
item = QTreeWidgetItem(entry) # fixme: should use is_mine
if value < 0:
entry.append(fx.format_fiat(tx_item['acquisition_price'].value))
entry.append(fx.format_fiat(tx_item['capital_gain'].value))
item = SortableTreeWidgetItem(entry)
item.setIcon(0, icon) item.setIcon(0, icon)
item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else ""))
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
if has_invoice: if has_invoice:
item.setIcon(3, QIcon(":icons/seal")) item.setIcon(3, QIcon(":icons/seal"))
for i in range(len(entry)): for i in range(len(entry)):
@ -101,12 +259,27 @@ class HistoryList(MyTreeWidget):
if value and value < 0: if value and value < 0:
item.setForeground(3, QBrush(QColor("#BC1E1E"))) item.setForeground(3, QBrush(QColor("#BC1E1E")))
item.setForeground(4, QBrush(QColor("#BC1E1E"))) item.setForeground(4, QBrush(QColor("#BC1E1E")))
if fiat_value and not tx_item['fiat_default']:
item.setForeground(6, QBrush(QColor("#1E1EFF")))
if tx_hash: if tx_hash:
item.setData(0, Qt.UserRole, tx_hash) item.setData(0, Qt.UserRole, tx_hash)
self.insertTopLevelItem(0, item) self.insertTopLevelItem(0, item)
if current_tx == tx_hash: if current_tx == tx_hash:
self.setCurrentItem(item) self.setCurrentItem(item)
def on_edited(self, item, column, prior):
'''Called only when the text actually changes'''
key = item.data(0, Qt.UserRole)
text = item.text(column)
# fixme
if column == 3:
self.parent.wallet.set_label(key, text)
self.update_labels()
self.parent.update_completions()
elif column == 6:
self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text)
self.on_update()
def on_doubleclick(self, item, column): def on_doubleclick(self, item, column):
if self.permit_edit(item, column): if self.permit_edit(item, column):
super(HistoryList, self).on_doubleclick(item, column) super(HistoryList, self).on_doubleclick(item, column)
@ -131,6 +304,7 @@ class HistoryList(MyTreeWidget):
if items: if items:
item = items[0] item = items[0]
item.setIcon(0, icon) item.setIcon(0, icon)
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
item.setText(2, status_str) item.setText(2, status_str)
def create_menu(self, position): def create_menu(self, position):
@ -148,20 +322,18 @@ class HistoryList(MyTreeWidget):
else: else:
column_title = self.headerItem().text(column) column_title = self.headerItem().text(column)
column_data = item.text(column) column_data = item.text(column)
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
height, conf, timestamp = self.wallet.get_tx_height(tx_hash) height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
tx = self.wallet.transactions.get(tx_hash) tx = self.wallet.transactions.get(tx_hash)
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
is_unconfirmed = height <= 0 is_unconfirmed = height <= 0
pr_key = self.wallet.invoices.paid.get(tx_hash) pr_key = self.wallet.invoices.paid.get(tx_hash)
menu = QMenu() menu = QMenu()
if height == TX_HEIGHT_LOCAL:
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
if column in self.editable_columns: menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) for c in self.editable_columns:
menu.addAction(_("Edit {}").format(self.headerItem().text(c)), lambda: self.editItem(item, c))
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
if is_unconfirmed and tx: if is_unconfirmed and tx:
rbf = is_mine and not tx.is_final() rbf = is_mine and not tx.is_final()
@ -176,3 +348,73 @@ class HistoryList(MyTreeWidget):
if tx_URL: if tx_URL:
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def remove_local_tx(self, delete_tx):
to_delete = {delete_tx}
to_delete |= self.wallet.get_depending_transactions(delete_tx)
question = _("Are you sure you want to remove this transaction?")
if len(to_delete) > 1:
question = _(
"Are you sure you want to remove this transaction and {} child transactions?".format(len(to_delete) - 1)
)
answer = QMessageBox.question(self.parent, _("Please confirm"), question, QMessageBox.Yes, QMessageBox.No)
if answer == QMessageBox.No:
return
for tx in to_delete:
self.wallet.remove_transaction(tx)
self.wallet.save_transactions(write=True)
# need to update at least: history_list, utxo_list, address_list
self.parent.need_update.set()
def onFileAdded(self, fn):
try:
with open(fn) as f:
tx = self.parent.tx_from_text(f.read())
self.parent.save_transaction_into_wallet(tx)
except IOError as e:
self.parent.show_error(e)
def export_history_dialog(self):
d = WindowModalDialog(self, _('Export History'))
d.setMinimumSize(400, 200)
vbox = QVBoxLayout(d)
defaultname = os.path.expanduser('~/electrum-history.csv')
select_msg = _('Select file to export your wallet transactions to')
hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
vbox.addLayout(hbox)
vbox.addStretch(1)
hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
vbox.addLayout(hbox)
#run_hook('export_history_dialog', self, hbox)
self.update()
if not d.exec_():
return
filename = filename_e.text()
if not filename:
return
try:
self.do_export_history(self.wallet, filename, csv_button.isChecked())
except (IOError, os.error) as reason:
export_error_label = _("Electrum was unable to produce a transaction export.")
self.parent.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
return
self.parent.show_message(_("Your wallet history has been successfully exported."))
def do_export_history(self, wallet, fileName, is_csv):
history = self.transactions
lines = []
for item in history:
if is_csv:
lines.append([item['txid'], item.get('label', ''), item['confirmations'], item['value'], item['date']])
else:
lines.append(item)
with open(fileName, "w+") as f:
if is_csv:
import csv
transaction = csv.writer(f, lineterminator='\n')
transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
for line in lines:
transaction.writerow(line)
else:
from electrum.util import json_encode
f.write(json_encode(history))

View File

@ -10,29 +10,25 @@ from PyQt5.QtWidgets import *
from electrum import Wallet, WalletStorage from electrum import Wallet, WalletStorage
from electrum.util import UserCancelled, InvalidPassword from electrum.util import UserCancelled, InvalidPassword
from electrum.base_wizard import BaseWizard from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET
from electrum.i18n import _ from electrum.i18n import _
from .seed_dialog import SeedLayout, KeysLayout from .seed_dialog import SeedLayout, KeysLayout
from .network_dialog import NetworkChoiceLayout from .network_dialog import NetworkChoiceLayout
from .util import * from .util import *
from .password_dialog import PasswordLayout, PW_NEW from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
class GoBack(Exception): class GoBack(Exception):
pass pass
MSG_GENERATING_WAIT = _("Generating your addresses, please wait...")
MSG_ENTER_ANYTHING = _("Please enter a seed phrase, a master key, a list of "
"Zclassic addresses, or a list of private keys")
MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or xprv):")
MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:")
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\ MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
+ _("Leave this field empty if you want to disable encryption.") + _("Leave this field empty if you want to disable encryption.")
MSG_RESTORE_PASSPHRASE = \ MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
_("Please enter your seed derivation passphrase. " + _("Your wallet file does not contain secrets, mostly just metadata. ") \
"Note: this is NOT your encryption password. " + _("It also contains your master public key that allows watching your addresses.") + '\n\n'\
"Leave this field empty if you did not use one or are unsure.") + _("Note: If you enable this setting, you will need your hardware device to open your wallet.")
class CosignWidget(QWidget): class CosignWidget(QWidget):
@ -196,12 +192,18 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
msg =_("This file does not exist.") + '\n' \ msg =_("This file does not exist.") + '\n' \
+ _("Press 'Next' to create this wallet, or choose another file.") + _("Press 'Next' to create this wallet, or choose another file.")
pw = False pw = False
elif self.storage.file_exists() and self.storage.is_encrypted():
msg = _("This file is encrypted.") + '\n' + _('Enter your password or choose another file.')
pw = True
else: else:
msg = _("Press 'Next' to open this wallet.") if self.storage.is_encrypted_with_user_pw():
pw = False msg = _("This file is encrypted with a password.") + '\n' \
+ _('Enter your password or choose another file.')
pw = True
elif self.storage.is_encrypted_with_hw_device():
msg = _("This file is encrypted using a hardware device.") + '\n' \
+ _("Press 'Next' to choose device to decrypt.")
pw = False
else:
msg = _("Press 'Next' to open this wallet.")
pw = False
else: else:
msg = _('Cannot read file') msg = _('Cannot read file')
pw = False pw = False
@ -227,23 +229,46 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
if not self.storage.file_exists(): if not self.storage.file_exists():
break break
if self.storage.file_exists() and self.storage.is_encrypted(): if self.storage.file_exists() and self.storage.is_encrypted():
password = self.pw_e.text() if self.storage.is_encrypted_with_user_pw():
try: password = self.pw_e.text()
self.storage.decrypt(password) try:
break self.storage.decrypt(password)
except InvalidPassword as e: break
QMessageBox.information(None, _('Error'), str(e)) except InvalidPassword as e:
continue QMessageBox.information(None, _('Error'), str(e))
except BaseException as e: continue
traceback.print_exc(file=sys.stdout) except BaseException as e:
QMessageBox.information(None, _('Error'), str(e)) traceback.print_exc(file=sys.stdout)
return QMessageBox.information(None, _('Error'), str(e))
return
elif self.storage.is_encrypted_with_hw_device():
try:
self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET)
except InvalidPassword as e:
# FIXME if we get here because of mistyped passphrase
# then that passphrase gets "cached"
QMessageBox.information(
None, _('Error'),
_('Failed to decrypt using this hardware device.') + '\n' +
_('If you use a passphrase, make sure it is correct.'))
self.stack = []
return self.run_and_get_wallet()
except BaseException as e:
traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e))
return
if self.storage.is_past_initial_decryption():
break
else:
return
else:
raise Exception('Unexpected encryption version')
path = self.storage.path path = self.storage.path
if self.storage.requires_split(): if self.storage.requires_split():
self.hide() self.hide()
msg = _("The wallet '%s' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n" msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n"
"Do you want to split your wallet into multiple files?"%path) "Do you want to split your wallet into multiple files?").format(path)
if not self.question(msg): if not self.question(msg):
return return
file_list = '\n'.join(self.storage.split_accounts()) file_list = '\n'.join(self.storage.split_accounts())
@ -261,10 +286,10 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
action = self.storage.get_action() action = self.storage.get_action()
if action and action != 'new': if action and action != 'new':
self.hide() self.hide()
msg = _("The file '%s' contains an incompletely created wallet.\n" msg = _("The file '{}' contains an incompletely created wallet.\n"
"Do you want to complete its creation now?") % path "Do you want to complete its creation now?").format(path)
if not self.question(msg): if not self.question(msg):
if self.question(_("Do you want to delete '%s'?") % path): if self.question(_("Do you want to delete '{}'?").format(path)):
os.remove(path) os.remove(path)
self.show_warning(_('The file was removed')) self.show_warning(_('The file was removed'))
return return
@ -386,17 +411,25 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
self.exec_layout(slayout) self.exec_layout(slayout)
return slayout.is_ext return slayout.is_ext
def pw_layout(self, msg, kind): def pw_layout(self, msg, kind, force_disable_encrypt_cb):
playout = PasswordLayout(None, msg, kind, self.next_button) playout = PasswordLayout(None, msg, kind, self.next_button,
force_disable_encrypt_cb=force_disable_encrypt_cb)
playout.encrypt_cb.setChecked(True) playout.encrypt_cb.setChecked(True)
self.exec_layout(playout.layout()) self.exec_layout(playout.layout())
return playout.new_password(), playout.encrypt_cb.isChecked() return playout.new_password(), playout.encrypt_cb.isChecked()
@wizard_dialog @wizard_dialog
def request_password(self, run_next): def request_password(self, run_next, force_disable_encrypt_cb=False):
"""Request the user enter a new password and confirm it. Return """Request the user enter a new password and confirm it. Return
the password or None for no password.""" the password or None for no password."""
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW) return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb)
@wizard_dialog
def request_storage_encryption(self, run_next):
playout = PasswordLayoutForHW(None, MSG_HW_STORAGE_ENCRYPTION, PW_NEW, self.next_button)
playout.encrypt_cb.setChecked(True)
self.exec_layout(playout.layout())
return playout.encrypt_cb.isChecked()
def show_restore(self, wallet, network): def show_restore(self, wallet, network):
# FIXME: these messages are shown after the install wizard is # FIXME: these messages are shown after the install wizard is
@ -437,7 +470,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
self.accept_signal.emit() self.accept_signal.emit()
def waiting_dialog(self, task, msg): def waiting_dialog(self, task, msg):
self.please_wait.setText(MSG_GENERATING_WAIT) self.please_wait.setText(msg)
self.refresh_gui() self.refresh_gui()
t = threading.Thread(target = task) t = threading.Thread(target = task)
t.start() t.start()

View File

@ -23,10 +23,11 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
from .util import *
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time from electrum.util import format_time
from .util import *
class InvoiceList(MyTreeWidget): class InvoiceList(MyTreeWidget):
filter_columns = [0, 1, 2, 3] # Date, Requested By, Description, Amount filter_columns = [0, 1, 2, 3] # Date, Requested By, Description, Amount
@ -57,12 +58,10 @@ class InvoiceList(MyTreeWidget):
self.parent.invoices_label.setVisible(len(inv_list)) self.parent.invoices_label.setVisible(len(inv_list))
def import_invoices(self): def import_invoices(self):
wallet_folder = self.parent.get_wallet_folder() import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update)
filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
if not filename: def export_invoices(self):
return export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
self.parent.invoices.import_file(filename)
self.on_update()
def create_menu(self, position): def create_menu(self, position):
menu = QMenu() menu = QMenu()
@ -76,7 +75,7 @@ class InvoiceList(MyTreeWidget):
pr = self.parent.invoices.get(key) pr = self.parent.invoices.get(key)
status = self.parent.invoices.get_status(key) status = self.parent.invoices.get_status(key)
if column_data: if column_data:
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key)) menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
if status == PR_UNPAID: if status == PR_UNPAID:
menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(key)) menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(key))

View File

@ -32,34 +32,32 @@ from decimal import Decimal
import base64 import base64
from functools import partial from functools import partial
from PyQt5.QtCore import Qt
from PyQt5.QtGui import * from PyQt5.QtGui import *
from PyQt5.QtCore import *
import PyQt5.QtCore as QtCore
from .exception_window import Exception_Hook
from PyQt5.QtWidgets import * from PyQt5.QtWidgets import *
from electrum.util import bh2u, bfh
from electrum import keystore, simple_config from electrum import keystore, simple_config
from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS, NetworkConstants from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS
from electrum import constants
from electrum.plugins import run_hook from electrum.plugins import run_hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import (format_time, format_satoshis, PrintError, from electrum.util import (format_time, format_satoshis, PrintError,
format_satoshis_plain, NotEnoughFunds, format_satoshis_plain, NotEnoughFunds,
UserCancelled, NoDynamicFeeEstimates) UserCancelled, NoDynamicFeeEstimates, profiler,
export_meta, import_meta, bh2u, bfh)
from electrum import Transaction from electrum import Transaction
from electrum import util, bitcoin, commands, coinchooser from electrum import util, bitcoin, commands, coinchooser
from electrum import paymentrequest from electrum import paymentrequest
from electrum.wallet import Multisig_Wallet from electrum.wallet import Multisig_Wallet, AddTransactionException
try:
from electrum.plot import plot_history
except:
plot_history = None
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
from .qrcodewidget import QRCodeWidget, QRDialog from .qrcodewidget import QRCodeWidget, QRDialog
from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
from .transaction_dialog import show_transaction from .transaction_dialog import show_transaction
from .fee_slider import FeeSlider from .fee_slider import FeeSlider
from .util import * from .util import *
@ -102,6 +100,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.gui_object = gui_object self.gui_object = gui_object
self.config = config = gui_object.config self.config = config = gui_object.config
self.setup_exception_hook()
self.network = gui_object.daemon.network self.network = gui_object.daemon.network
self.fx = gui_object.daemon.fx self.fx = gui_object.daemon.fx
self.invoices = wallet.invoices self.invoices = wallet.invoices
@ -123,9 +124,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.create_status_bar() self.create_status_bar()
self.need_update = threading.Event() self.need_update = threading.Event()
self.decimal_point = config.get('decimal_point', 8) self.decimal_point = config.get('decimal_point', 5)
self.fee_unit = config.get('fee_unit', 0) self.num_zeros = int(config.get('num_zeros',0))
self.num_zeros = int(config.get('num_zeros', 0))
self.completions = QStringListModel() self.completions = QStringListModel()
@ -203,6 +203,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def on_history(self, b): def on_history(self, b):
self.new_fx_history_signal.emit() self.new_fx_history_signal.emit()
def setup_exception_hook(self):
Exception_Hook(self)
def on_fx_history(self): def on_fx_history(self):
self.history_list.refresh_headers() self.history_list.refresh_headers()
self.history_list.update() self.history_list.update()
@ -283,7 +286,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.need_update.set() self.need_update.set()
self.gui_object.network_updated_signal_obj.network_updated_signal \ self.gui_object.network_updated_signal_obj.network_updated_signal \
.emit(event, args) .emit(event, args)
elif event == 'new_transaction': elif event == 'new_transaction':
self.tx_notifications.append(args[0]) self.tx_notifications.append(args[0])
self.notify_transactions_signal.emit() self.notify_transactions_signal.emit()
@ -305,6 +307,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.config.is_dynfee(): if self.config.is_dynfee():
self.fee_slider.update() self.fee_slider.update()
self.do_update_fee() self.do_update_fee()
elif event == 'fee_histogram':
if self.config.is_dynfee():
self.fee_slider.update()
self.do_update_fee()
# todo: update only unconfirmed tx
self.history_list.update()
else: else:
self.print_error("unexpected network_qt signal:", event, args) self.print_error("unexpected network_qt signal:", event, args)
@ -325,6 +333,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.print_error('close_wallet', self.wallet.storage.path) self.print_error('close_wallet', self.wallet.storage.path)
run_hook('close_wallet', self.wallet) run_hook('close_wallet', self.wallet)
@profiler
def load_wallet(self, wallet): def load_wallet(self, wallet):
wallet.thread = TaskThread(self, self.on_error) wallet.thread = TaskThread(self, self.on_error)
self.wallet = wallet self.wallet = wallet
@ -363,7 +372,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.setGeometry(100, 100, 840, 400) self.setGeometry(100, 100, 840, 400)
def watching_only_changed(self): def watching_only_changed(self):
name = "[TESTNET] Zclassic Electrum" if NetworkConstants.TESTNET else "Zclassic Electrum" name = "Zclassic Electrum TESTNET" if constants.net.TESTNET else "Zclassic Electrum"
title = '%s %s - %s' % (name, self.wallet.electrum_version, title = '%s %s - %s' % (name, self.wallet.electrum_version,
self.wallet.basename()) self.wallet.basename())
extra = [self.wallet.storage.get('wallet_type', '?')] extra = [self.wallet.storage.get('wallet_type', '?')]
@ -372,7 +381,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
extra.append(_('watching only')) extra.append(_('watching only'))
title += ' [%s]'% ', '.join(extra) title += ' [%s]'% ', '.join(extra)
self.setWindowTitle(title) self.setWindowTitle(title)
self.password_menu.setEnabled(self.wallet.can_change_password()) self.password_menu.setEnabled(self.wallet.may_have_password())
self.import_privkey_menu.setVisible(self.wallet.can_import_privkey()) self.import_privkey_menu.setVisible(self.wallet.can_import_privkey())
self.import_address_menu.setVisible(self.wallet.can_import_address()) self.import_address_menu.setVisible(self.wallet.can_import_address())
self.export_menu.setEnabled(self.wallet.can_export()) self.export_menu.setEnabled(self.wallet.can_export())
@ -467,17 +476,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.import_address_menu = wallet_menu.addAction(_("Import Addresses"), self.import_addresses) self.import_address_menu = wallet_menu.addAction(_("Import Addresses"), self.import_addresses)
wallet_menu.addSeparator() wallet_menu.addSeparator()
addresses_menu = wallet_menu.addMenu(_("&Addresses"))
addresses_menu.addAction(_("&Filter"), lambda: self.address_list.toggle_toolbar(self.config))
labels_menu = wallet_menu.addMenu(_("&Labels")) labels_menu = wallet_menu.addMenu(_("&Labels"))
labels_menu.addAction(_("&Import"), self.do_import_labels) labels_menu.addAction(_("&Import"), self.do_import_labels)
labels_menu.addAction(_("&Export"), self.do_export_labels) labels_menu.addAction(_("&Export"), self.do_export_labels)
history_menu = wallet_menu.addMenu(_("&History"))
history_menu.addAction(_("&Filter"), lambda: self.history_list.toggle_toolbar(self.config))
history_menu.addAction(_("&Summary"), self.history_list.show_summary)
history_menu.addAction(_("&Plot"), self.history_list.plot_history_dialog)
history_menu.addAction(_("&Export"), self.history_list.export_history_dialog)
contacts_menu = wallet_menu.addMenu(_("Contacts")) contacts_menu = wallet_menu.addMenu(_("Contacts"))
contacts_menu.addAction(_("&New"), self.new_contact_dialog) contacts_menu.addAction(_("&New"), self.new_contact_dialog)
contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts()) contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts())
contacts_menu.addAction(_("Export"), lambda: self.contact_list.export_contacts())
invoices_menu = wallet_menu.addMenu(_("Invoices")) invoices_menu = wallet_menu.addMenu(_("Invoices"))
invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices()) invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices())
hist_menu = wallet_menu.addMenu(_("&History")) invoices_menu.addAction(_("Export"), lambda: self.invoice_list.export_invoices())
hist_menu.addAction("Plot", self.plot_history_dialog).setEnabled(plot_history is not None)
hist_menu.addAction("Export", self.export_history_dialog)
wallet_menu.addSeparator() wallet_menu.addSeparator()
wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F")) wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F"))
@ -559,24 +574,24 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
return return
self.print_error("Notifying GUI") self.print_error("Notifying GUI")
if len(self.tx_notifications) > 0: if len(self.tx_notifications) > 0:
# Combine the transactions if there are more then three # Combine the transactions if there are at least three
tx_amount = len(self.tx_notifications) num_txns = len(self.tx_notifications)
if(tx_amount >= 3): if num_txns >= 3:
total_amount = 0 total_amount = 0
for tx in self.tx_notifications: for tx in self.tx_notifications:
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
if(v > 0): if v > 0:
total_amount += v total_amount += v
self.notify(_("%(txs)s new transactions received: Total amount received in the new transactions %(amount)s") \ self.notify(_("{} new transactions received: Total amount received in the new transactions {}")
% { 'txs' : tx_amount, 'amount' : self.format_amount_and_units(total_amount)}) .format(num_txns, self.format_amount_and_units(total_amount)))
self.tx_notifications = [] self.tx_notifications = []
else: else:
for tx in self.tx_notifications: for tx in self.tx_notifications:
if tx: if tx:
self.tx_notifications.remove(tx) self.tx_notifications.remove(tx)
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
if(v > 0): if v > 0:
self.notify(_("Inbound Transaction - %(amount)s") % { 'amount' : self.format_amount_and_units(v)}) self.notify(_("New Transaction: {}").format(self.format_amount_and_units(v)))
def notify(self, message): def notify(self, message):
if self.tray: if self.tray:
@ -625,16 +640,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def format_amount_and_units(self, amount): def format_amount_and_units(self, amount):
text = self.format_amount(amount) + ' '+ self.base_unit() text = self.format_amount(amount) + ' '+ self.base_unit()
x = self.fx.format_amount_and_units(amount) x = self.fx.format_amount_and_units(amount) if self.fx else None
if text and x: if text and x:
text += ' (%s)'%x text += ' (%s)'%x
return text return text
def format_fee_rate(self, fee_rate): def format_fee_rate(self, fee_rate):
if self.fee_unit == 0: return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' zat/byte'
return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' zat/byte'
else:
return self.format_amount(fee_rate) + ' ' + self.base_unit() + '/kB'
def get_decimal_point(self): def get_decimal_point(self):
return self.decimal_point return self.decimal_point
@ -704,7 +716,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
text = _("Synchronizing...") text = _("Synchronizing...")
icon = QIcon(":icons/status_waiting.png") icon = QIcon(":icons/status_waiting.png")
elif server_lag > 1: elif server_lag > 1:
text = _("Server is lagging (%d blocks)"%server_lag) text = _("Server is lagging ({} blocks)").format(server_lag)
icon = QIcon(":icons/status_lagging.png") icon = QIcon(":icons/status_lagging.png")
else: else:
c, u, x = self.wallet.get_balance() c, u, x = self.wallet.get_balance()
@ -749,7 +761,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
from .history_list import HistoryList from .history_list import HistoryList
self.history_list = l = HistoryList(self) self.history_list = l = HistoryList(self)
l.searchable_list = l l.searchable_list = l
return l toolbar = l.create_toolbar(self.config)
toolbar_shown = self.config.get('show_toolbar_history', False)
l.show_toolbar(toolbar_shown)
return self.create_list_tab(l, toolbar)
def show_address(self, addr): def show_address(self, addr):
from . import address_dialog from . import address_dialog
@ -883,14 +898,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if alias_addr: if alias_addr:
if self.wallet.is_mine(alias_addr): if self.wallet.is_mine(alias_addr):
msg = _('This payment request will be signed.') + '\n' + _('Please enter your password') msg = _('This payment request will be signed.') + '\n' + _('Please enter your password')
password = self.password_dialog(msg) password = None
if password: if self.wallet.has_keystore_encryption():
try: password = self.password_dialog(msg)
self.wallet.sign_payment_request(addr, alias, alias_addr, password) if not password:
except Exception as e:
self.show_error(str(e))
return return
else: try:
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
except Exception as e:
self.show_error(str(e))
return return
else: else:
return return
@ -905,11 +921,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
i = self.expires_combo.currentIndex() i = self.expires_combo.currentIndex()
expiration = list(map(lambda x: x[1], expiration_values))[i] expiration = list(map(lambda x: x[1], expiration_values))[i]
req = self.wallet.make_payment_request(addr, amount, message, expiration) req = self.wallet.make_payment_request(addr, amount, message, expiration)
self.wallet.add_payment_request(req, self.config) try:
self.sign_payment_request(addr) self.wallet.add_payment_request(req, self.config)
self.request_list.update() except Exception as e:
self.address_list.update() traceback.print_exc(file=sys.stderr)
self.save_request_button.setEnabled(False) self.show_error(_('Error adding payment request') + ':\n' + str(e))
else:
self.sign_payment_request(addr)
self.save_request_button.setEnabled(False)
finally:
self.request_list.update()
self.address_list.update()
def view_and_paste(self, title, msg, data): def view_and_paste(self, title, msg, data):
dialog = WindowModalDialog(self, title) dialog = WindowModalDialog(self, title)
@ -1005,6 +1027,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.qr_window and self.qr_window.isVisible(): if self.qr_window and self.qr_window.isVisible():
self.qr_window.set_content(addr, amount, message, uri) self.qr_window.set_content(addr, amount, message, uri)
def set_feerounding_text(self, num_satoshis_added):
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
.format(num_satoshis_added))
def create_send_tab(self): def create_send_tab(self):
# A 4-column grid layout. All the stretch is in the last column. # A 4-column grid layout. All the stretch is in the last column.
# The exchange rate plugin adds a fiat widget in column 2 # The exchange rate plugin adds a fiat widget in column 2
@ -1070,12 +1096,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def fee_cb(dyn, pos, fee_rate): def fee_cb(dyn, pos, fee_rate):
if dyn: if dyn:
self.config.set_key('fee_level', pos, False) if self.config.use_mempool_fees():
self.config.set_key('depth_level', pos, False)
else:
self.config.set_key('fee_level', pos, False)
else: else:
self.config.set_key('fee_per_kb', fee_rate, False) self.config.set_key('fee_per_kb', fee_rate, False)
if fee_rate: if fee_rate:
self.feerate_e.setAmount(fee_rate // 1000) self.feerate_e.setAmount(fee_rate // 1000)
else:
self.feerate_e.setAmount(None)
self.fee_e.setModified(False) self.fee_e.setModified(False)
self.fee_slider.activate() self.fee_slider.activate()
@ -1108,7 +1139,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.size_e.setFixedWidth(140) self.size_e.setFixedWidth(140)
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
self.feerate_e = FeerateEdit(lambda: 2 if self.fee_unit else 0) self.feerate_e = FeerateEdit(lambda: 0)
self.feerate_e.setAmount(self.config.fee_per_byte())
self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False)) self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False))
self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True)) self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True))
@ -1116,6 +1148,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.fee_e.textEdited.connect(partial(on_fee_or_feerate, self.fee_e, False)) self.fee_e.textEdited.connect(partial(on_fee_or_feerate, self.fee_e, False))
self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True)) self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True))
def feerounding_onclick():
text = (self.feerounding_text + '\n\n' +
_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +
_('At most 100 satoshis might be lost due to this rounding.') + ' ' +
_("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' +
_('Also, dust is not kept as change, but added to the fee.'))
QMessageBox.information(self, 'Fee rounding', text)
self.feerounding_icon = QPushButton(QIcon(':icons/info.png'), '')
self.feerounding_icon.setFixedWidth(20)
self.feerounding_icon.setFlat(True)
self.feerounding_icon.clicked.connect(feerounding_onclick)
self.feerounding_icon.setVisible(False)
self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e) self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e)
vbox_feelabel = QVBoxLayout() vbox_feelabel = QVBoxLayout()
@ -1129,14 +1175,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
hbox.addWidget(self.feerate_e) hbox.addWidget(self.feerate_e)
hbox.addWidget(self.size_e) hbox.addWidget(self.size_e)
hbox.addWidget(self.fee_e) hbox.addWidget(self.fee_e)
hbox.addWidget(self.feerounding_icon, Qt.AlignLeft)
hbox.addStretch(1)
vbox_feecontrol = QVBoxLayout() vbox_feecontrol = QVBoxLayout()
vbox_feecontrol.addWidget(self.fee_adv_controls) vbox_feecontrol.addWidget(self.fee_adv_controls)
vbox_feecontrol.addWidget(self.fee_slider) vbox_feecontrol.addWidget(self.fee_slider)
grid.addLayout(vbox_feecontrol, 5, 1, 1, 3) grid.addLayout(vbox_feecontrol, 5, 1, 1, -1)
if not self.config.get('show_fee', True): if not self.config.get('show_fee', False):
self.fee_adv_controls.setVisible(False) self.fee_adv_controls.setVisible(False)
self.preview_button = EnterButton(_("Preview"), self.do_preview) self.preview_button = EnterButton(_("Preview"), self.do_preview)
@ -1233,9 +1281,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
'''Recalculate the fee. If the fee was manually input, retain it, but '''Recalculate the fee. If the fee was manually input, retain it, but
still build the TX to see if there are enough funds. still build the TX to see if there are enough funds.
''' '''
if not self.config.get('offline') and self.config.is_dynfee() and not self.config.has_fee_estimates():
self.statusBar().showMessage(_('Waiting for fee estimates...'))
return False
freeze_fee = self.is_send_fee_frozen() freeze_fee = self.is_send_fee_frozen()
freeze_feerate = self.is_send_feerate_frozen() freeze_feerate = self.is_send_feerate_frozen()
amount = '!' if self.is_max else self.amount_e.get_amount() amount = '!' if self.is_max else self.amount_e.get_amount()
@ -1258,15 +1303,22 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
try: try:
tx = make_tx(fee_estimator) tx = make_tx(fee_estimator)
self.not_enough_funds = False self.not_enough_funds = False
except NotEnoughFunds: except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
self.not_enough_funds = True
if not freeze_fee: if not freeze_fee:
self.fee_e.setAmount(None) self.fee_e.setAmount(None)
return if not freeze_feerate:
except NoDynamicFeeEstimates: self.feerate_e.setAmount(None)
tx = make_tx(0) self.feerounding_icon.setVisible(False)
size = tx.estimated_size()
self.size_e.setAmount(size) if isinstance(e, NotEnoughFunds):
self.not_enough_funds = True
elif isinstance(e, NoDynamicFeeEstimates):
try:
tx = make_tx(0)
size = tx.estimated_size()
self.size_e.setAmount(size)
except BaseException:
pass
return return
except BaseException: except BaseException:
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
@ -1276,12 +1328,32 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.size_e.setAmount(size) self.size_e.setAmount(size)
fee = tx.get_fee() fee = tx.get_fee()
if not freeze_fee: fee = None if self.not_enough_funds else fee
fee = None if self.not_enough_funds else fee
self.fee_e.setAmount(fee) # Displayed fee/fee_rate values are set according to user input.
if not freeze_feerate: # Due to rounding or dropping dust in CoinChooser,
fee_rate = fee // size if fee is not None else None # actual fees often differ somewhat.
self.feerate_e.setAmount(fee_rate) if freeze_feerate or self.fee_slider.is_active():
displayed_feerate = self.feerate_e.get_amount()
displayed_feerate = displayed_feerate // 1000 if displayed_feerate else 0
displayed_fee = displayed_feerate * size
self.fee_e.setAmount(displayed_fee)
else:
if freeze_fee:
displayed_fee = self.fee_e.get_amount()
else:
# fallback to actual fee if nothing is frozen
displayed_fee = fee
self.fee_e.setAmount(displayed_fee)
displayed_fee = displayed_fee if displayed_fee else 0
displayed_feerate = displayed_fee // size if displayed_fee is not None else None
self.feerate_e.setAmount(displayed_feerate)
# show/hide fee rounding icon
feerounding = (fee - displayed_fee) if fee else 0
self.set_feerounding_text(feerounding)
self.feerounding_icon.setToolTip(self.feerounding_text)
self.feerounding_icon.setVisible(bool(feerounding))
if self.is_max: if self.is_max:
amount = tx.output_value() amount = tx.output_value()
@ -1331,7 +1403,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def request_password(self, *args, **kwargs): def request_password(self, *args, **kwargs):
parent = self.top_level_window() parent = self.top_level_window()
password = None password = None
while self.wallet.has_password(): while self.wallet.has_keystore_encryption():
password = self.password_dialog(parent=parent) password = self.password_dialog(parent=parent)
if password is None: if password is None:
# User cancelled password input # User cancelled password input
@ -1360,7 +1432,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
fee_estimator = self.fee_e.get_amount() fee_estimator = self.fee_e.get_amount()
elif self.is_send_feerate_frozen(): elif self.is_send_feerate_frozen():
amount = self.feerate_e.get_amount() amount = self.feerate_e.get_amount()
amount = 0 if amount is None else float(amount) amount = 0 if amount is None else amount
fee_estimator = partial( fee_estimator = partial(
simple_config.SimpleConfig.estimate_fee_for_feerate, amount) simple_config.SimpleConfig.estimate_fee_for_feerate, amount)
else: else:
@ -1369,7 +1441,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def read_send_tab(self): def read_send_tab(self):
if self.payment_request and self.payment_request.has_expired(): if self.payment_request and self.payment_request.has_expired():
self.show_error(_('Payment request has expired.')) self.show_error(_('Payment request has expired'))
return return
label = self.message_e.text() label = self.message_e.text()
@ -1384,7 +1456,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.payto_e.is_alias and self.payto_e.validated is False: if self.payto_e.is_alias and self.payto_e.validated is False:
alias = self.payto_e.toPlainText() alias = self.payto_e.toPlainText()
msg = _('WARNING: the alias "%s" could not be validated via an additional security check, DNSSEC, and thus may not be correct.'%alias) + '\n' msg = _('WARNING: the alias "{}" could not be validated via an additional '
'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n'
msg += _('Do you wish to continue?') msg += _('Do you wish to continue?')
if not self.question(msg): if not self.question(msg):
return return
@ -1439,13 +1512,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
tx.set_rbf(True) tx.set_rbf(True)
if fee < self.wallet.relayfee() * tx.estimated_size() / 1000: if fee < self.wallet.relayfee() * tx.estimated_size() / 1000:
self.show_error(_("This transaction requires a higher fee, or it will not be propagated by the network")) self.show_error('\n'.join([
_("This transaction requires a higher fee, or it will not be propagated by your current server"),
_("Try to raise your transaction fee, or use a server with a lower relay fee.")
]))
return return
if preview: if preview:
self.show_transaction(tx, tx_desc) self.show_transaction(tx, tx_desc)
return return
if not self.network:
self.show_error(_("You can't broadcast a transaction without a live network connection."))
return
# confirmation dialog # confirmation dialog
msg = [ msg = [
_("Amount to be sent") + ": " + self.format_amount_and_units(amount), _("Amount to be sent") + ": " + self.format_amount_and_units(amount),
@ -1457,11 +1537,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
x_fee_address, x_fee_amount = x_fee x_fee_address, x_fee_amount = x_fee
msg.append( _("Additional fees") + ": " + self.format_amount_and_units(x_fee_amount) ) msg.append( _("Additional fees") + ": " + self.format_amount_and_units(x_fee_amount) )
confirm_rate = 2 * self.config.max_fee_rate() confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
if fee > confirm_rate * tx.estimated_size() / 1000: if fee > confirm_rate * tx.estimated_size() / 1000:
msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")) msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
if self.wallet.has_password(): if self.wallet.has_keystore_encryption():
msg.append("") msg.append("")
msg.append(_("Enter your password to proceed")) msg.append(_("Enter your password to proceed"))
password = self.password_dialog('\n'.join(msg)) password = self.password_dialog('\n'.join(msg))
@ -1514,7 +1594,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
pr = self.payment_request pr = self.payment_request
if pr and pr.has_expired(): if pr and pr.has_expired():
self.payment_request = None self.payment_request = None
return False, _("Payment request has expired.") return False, _("Payment request has expired")
status, msg = self.network.broadcast(tx) status, msg = self.network.broadcast(tx)
if pr and status is True: if pr and status is True:
self.invoices.set_paid(pr, tx.txid()) self.invoices.set_paid(pr, tx.txid())
@ -1606,7 +1686,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.payment_request_error_signal.emit() self.payment_request_error_signal.emit()
def pay_to_URI(self, URI): def pay_to_URI(self, URI):
if not URI or not isinstance(URI, str): if not URI:
return return
try: try:
out = util.parse_URI(URI, self.on_pr) out = util.parse_URI(URI, self.on_pr)
@ -1646,7 +1726,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
e.setText('') e.setText('')
e.setFrozen(False) e.setFrozen(False)
self.fee_slider.activate() self.fee_slider.activate()
self.feerate_e.setAmount(self.config.fee_per_byte())
self.size_e.setAmount(0) self.size_e.setAmount(0)
self.feerounding_icon.setVisible(False)
self.set_pay_from([]) self.set_pay_from([])
self.tx_external_keypairs = {} self.tx_external_keypairs = {}
self.update_status() self.update_status()
@ -1658,26 +1740,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.utxo_list.update() self.utxo_list.update()
self.update_fee() self.update_fee()
def create_list_tab(self, l, list_header=None): def create_list_tab(self, l, toolbar=None):
w = QWidget() w = QWidget()
w.searchable_list = l w.searchable_list = l
vbox = QVBoxLayout() vbox = QVBoxLayout()
w.setLayout(vbox) w.setLayout(vbox)
vbox.setContentsMargins(0, 0, 0, 0) vbox.setContentsMargins(0, 0, 0, 0)
vbox.setSpacing(0) vbox.setSpacing(0)
if list_header: if toolbar:
hbox = QHBoxLayout() vbox.addLayout(toolbar)
for b in list_header:
hbox.addWidget(b)
hbox.addStretch()
vbox.addLayout(hbox)
vbox.addWidget(l) vbox.addWidget(l)
return w return w
def create_addresses_tab(self): def create_addresses_tab(self):
from .address_list import AddressList from .address_list import AddressList
self.address_list = l = AddressList(self) self.address_list = l = AddressList(self)
return self.create_list_tab(l, l.get_list_header()) toolbar = l.create_toolbar(self.config)
toolbar_shown = self.config.get('show_toolbar_addresses', False)
l.show_toolbar(toolbar_shown)
return self.create_list_tab(l, toolbar)
def create_utxo_tab(self): def create_utxo_tab(self):
from .utxo_list import UTXOList from .utxo_list import UTXOList
@ -1741,8 +1822,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
return True return True
def delete_contacts(self, labels): def delete_contacts(self, labels):
if not self.question(_("Remove %s from your list of contacts?") if not self.question(_("Remove {} from your list of contacts?")
% " + ".join(labels)): .format(" + ".join(labels))):
return return
for label in labels: for label in labels:
self.contacts.pop(label) self.contacts.pop(label)
@ -1862,17 +1943,37 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def update_buttons_on_seed(self): def update_buttons_on_seed(self):
self.seed_button.setVisible(self.wallet.has_seed()) self.seed_button.setVisible(self.wallet.has_seed())
self.password_button.setVisible(self.wallet.can_change_password()) self.password_button.setVisible(self.wallet.may_have_password())
self.send_button.setVisible(not self.wallet.is_watching_only()) self.send_button.setVisible(not self.wallet.is_watching_only())
def change_password_dialog(self): def change_password_dialog(self):
from .password_dialog import ChangePasswordDialog from electrum.storage import STO_EV_XPUB_PW
d = ChangePasswordDialog(self, self.wallet) if self.wallet.get_available_storage_encryption_version() == STO_EV_XPUB_PW:
ok, password, new_password, encrypt_file = d.run() from .password_dialog import ChangePasswordDialogForHW
d = ChangePasswordDialogForHW(self, self.wallet)
ok, encrypt_file = d.run()
if not ok:
return
try:
hw_dev_pw = self.wallet.keystore.get_password_for_storage_encryption()
except UserCancelled:
return
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.show_error(str(e))
return
old_password = hw_dev_pw if self.wallet.has_password() else None
new_password = hw_dev_pw if encrypt_file else None
else:
from .password_dialog import ChangePasswordDialogForSW
d = ChangePasswordDialogForSW(self, self.wallet)
ok, old_password, new_password, encrypt_file = d.run()
if not ok: if not ok:
return return
try: try:
self.wallet.update_password(password, new_password, encrypt_file) self.wallet.update_password(old_password, new_password, encrypt_file)
except BaseException as e: except BaseException as e:
self.show_error(str(e)) self.show_error(str(e))
return return
@ -1880,11 +1981,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
self.show_error(_('Failed to update password')) self.show_error(_('Failed to update password'))
return return
msg = _('Password was updated successfully') if new_password else _('Password is disabled, this wallet is not protected') msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected')
self.show_message(msg, title=_("Success")) self.show_message(msg, title=_("Success"))
self.update_lock_icon() self.update_lock_icon()
def toggle_search(self): def toggle_search(self):
tab = self.tabs.currentWidget()
#if hasattr(tab, 'searchable_list'):
# tab.searchable_list.toggle_toolbar()
#return
self.search_box.setHidden(not self.search_box.isHidden()) self.search_box.setHidden(not self.search_box.isHidden())
if not self.search_box.isHidden(): if not self.search_box.isHidden():
self.search_box.setFocus(1) self.search_box.setFocus(1)
@ -2017,8 +2122,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
rds_e = ShowQRTextEdit(text=redeem_script) rds_e = ShowQRTextEdit(text=redeem_script)
rds_e.addCopyButton(self.app) rds_e.addCopyButton(self.app)
vbox.addWidget(rds_e) vbox.addWidget(rds_e)
if xtype in ['p2wpkh', 'p2wsh', 'p2wpkh-p2sh', 'p2wsh-p2sh']:
vbox.addWidget(WWLabel(_("Warning: the format of private keys associated to segwit addresses may not be compatible with other wallets")))
vbox.addLayout(Buttons(CloseButton(d))) vbox.addLayout(Buttons(CloseButton(d)))
d.setLayout(vbox) d.setLayout(vbox)
d.exec_() d.exec_()
@ -2036,6 +2139,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if not bitcoin.is_address(address): if not bitcoin.is_address(address):
self.show_message(_('Invalid Zclassic address.')) self.show_message(_('Invalid Zclassic address.'))
return return
if self.wallet.is_watching_only():
self.show_message(_('This is a watching-only wallet.'))
return
if not self.wallet.is_mine(address): if not self.wallet.is_mine(address):
self.show_message(_('Address not in wallet.')) self.show_message(_('Address not in wallet.'))
return return
@ -2047,7 +2153,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
task = partial(self.wallet.sign_message, address, message, password) task = partial(self.wallet.sign_message, address, message, password)
def show_signed_message(sig): def show_signed_message(sig):
signature.setText(base64.b64encode(sig).decode('ascii')) try:
signature.setText(base64.b64encode(sig).decode('ascii'))
except RuntimeError:
# (signature) wrapped C/C++ object has been deleted
pass
self.wallet.thread.add(task, on_success=show_signed_message) self.wallet.thread.add(task, on_success=show_signed_message)
def do_verify(self, address, message, signature): def do_verify(self, address, message, signature):
@ -2106,9 +2217,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
@protected @protected
def do_decrypt(self, message_e, pubkey_e, encrypted_e, password): def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
if self.wallet.is_watching_only():
self.show_message(_('This is a watching-only wallet.'))
return
cyphertext = encrypted_e.toPlainText() cyphertext = encrypted_e.toPlainText()
task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password) task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password)
self.wallet.thread.add(task, on_success=lambda text: message_e.setText(text.decode('utf-8')))
def setText(text):
try:
message_e.setText(text.decode('utf-8'))
except RuntimeError:
# (message_e) wrapped C/C++ object has been deleted
pass
self.wallet.thread.add(task, on_success=setText)
def do_encrypt(self, message_e, pubkey_e, encrypted_e): def do_encrypt(self, message_e, pubkey_e, encrypted_e):
message = message_e.toPlainText() message = message_e.toPlainText()
@ -2207,25 +2329,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
return self.tx_from_text(file_content) return self.tx_from_text(file_content)
def do_process_from_text(self): def do_process_from_text(self):
from electrum.transaction import SerializationError
text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction")) text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
if not text: if not text:
return return
try: tx = self.tx_from_text(text)
tx = self.tx_from_text(text) if tx:
if tx: self.show_transaction(tx)
self.show_transaction(tx)
except SerializationError as e:
self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
def do_process_from_file(self): def do_process_from_file(self):
from electrum.transaction import SerializationError tx = self.read_tx_from_file()
try: if tx:
tx = self.read_tx_from_file() self.show_transaction(tx)
if tx:
self.show_transaction(tx)
except SerializationError as e:
self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
def do_process_from_txid(self): def do_process_from_txid(self):
from electrum import transaction from electrum import transaction
@ -2251,7 +2365,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
_('It can not be "backed up" by simply exporting these private keys.')) _('It can not be "backed up" by simply exporting these private keys.'))
d = WindowModalDialog(self, _('Private keys')) d = WindowModalDialog(self, _('Private keys'))
d.setMinimumSize(850, 300) d.setMinimumSize(980, 300)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
@ -2344,102 +2458,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
f.write(json.dumps(pklist, indent = 4)) f.write(json.dumps(pklist, indent = 4))
def do_import_labels(self): def do_import_labels(self):
labelsFile = self.getOpenFileName(_("Open Labels File"), "*.json") def import_labels(path):
if not labelsFile: return def _validate(data):
try: return data # TODO
with open(labelsFile, 'r') as f:
data = f.read() def import_labels_assign(data):
for key, value in json.loads(data).items(): for key, value in data.items():
self.wallet.set_label(key, value) self.wallet.set_label(key, value)
self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile)) import_meta(path, _validate, import_labels_assign)
except (IOError, os.error) as reason:
self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason)) def on_import():
self.address_list.update() self.need_update.set()
self.history_list.update() import_meta_gui(self, _('labels'), import_labels, on_import)
def do_export_labels(self): def do_export_labels(self):
labels = self.wallet.labels def export_labels(filename):
try: export_meta(self.wallet.labels, filename)
fileName = self.getSaveFileName(_("Select destination file for your labels:"), 'electrum_labels.json', "*.json") export_meta_gui(self, _('labels'), export_labels)
if fileName:
with open(fileName, 'w+') as f:
json.dump(labels, f, indent=4, sort_keys=True)
self.show_message(_("Your labels were exported to") + " '%s'" % str(fileName))
except (IOError, os.error) as reason:
self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason))
def export_history_dialog(self):
d = WindowModalDialog(self, _('Export History'))
d.setMinimumSize(400, 200)
vbox = QVBoxLayout(d)
defaultname = os.path.expanduser('~/electrum-history.csv')
select_msg = _('Select destination file for your wallet transaction history:')
hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
vbox.addLayout(hbox)
vbox.addStretch(1)
hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
vbox.addLayout(hbox)
run_hook('export_history_dialog', self, hbox)
self.update()
if not d.exec_():
return
filename = filename_e.text()
if not filename:
return
try:
self.do_export_history(self.wallet, filename, csv_button.isChecked())
except (IOError, os.error) as reason:
export_error_label = _("Electrum was unable to produce a transaction export.")
self.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
return
self.show_message(_("Your wallet history has been successfully exported."))
def plot_history_dialog(self):
if plot_history is None:
return
wallet = self.wallet
history = wallet.get_history()
if len(history) > 0:
plt = plot_history(self.wallet, history)
plt.show()
def do_export_history(self, wallet, fileName, is_csv):
history = wallet.get_history()
lines = []
for item in history:
tx_hash, height, confirmations, timestamp, value, balance = item
if height>0:
if timestamp is not None:
time_string = format_time(timestamp)
else:
time_string = _("unverified")
else:
time_string = _("unconfirmed")
if value is not None:
value_string = format_satoshis(value, True)
else:
value_string = '--'
if tx_hash:
label = wallet.get_label(tx_hash)
else:
label = ""
if is_csv:
lines.append([tx_hash, label, confirmations, value_string, time_string])
else:
lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
with open(fileName, "w+") as f:
if is_csv:
transaction = csv.writer(f, lineterminator='\n')
transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
for line in lines:
transaction.writerow(line)
else:
import json
f.write(json.dumps(lines, indent = 4))
def sweep_key_dialog(self): def sweep_key_dialog(self):
d = WindowModalDialog(self, title=_('Sweep private keys')) d = WindowModalDialog(self, title=_('Sweep private keys'))
@ -2590,6 +2625,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
nz.valueChanged.connect(on_nz) nz.valueChanged.connect(on_nz)
gui_widgets.append((nz_label, nz)) gui_widgets.append((nz_label, nz))
msg = '\n'.join([
_('Time based: fee rate is based on average confirmation time estimates'),
_('Mempool based: fee rate is targetting a depth in the memory pool')
]
)
fee_type_label = HelpLabel(_('Fee estimation') + ':', msg)
fee_type_combo = QComboBox()
fee_type_combo.addItems([_('Static'), _('ETA'), _('Mempool')])
fee_type_combo.setCurrentIndex(1 if self.config.use_mempool_fees() else 0)
def on_fee_type(x):
self.config.set_key('mempool_fees', x==2)
self.config.set_key('dynamic_fees', x>0)
self.fee_slider.update()
fee_type_combo.currentIndexChanged.connect(on_fee_type)
fee_widgets.append((fee_type_label, fee_type_combo))
use_rbf_cb = QCheckBox(_('Use Replace-By-Fee')) use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
use_rbf_cb.setChecked(self.config.get('use_rbf', True)) use_rbf_cb.setChecked(self.config.get('use_rbf', True))
@ -2602,18 +2652,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
use_rbf_cb.stateChanged.connect(on_use_rbf) use_rbf_cb.stateChanged.connect(on_use_rbf)
fee_widgets.append((use_rbf_cb, None)) fee_widgets.append((use_rbf_cb, None))
self.fee_unit = self.config.get('fee_unit', 0)
fee_unit_label = HelpLabel(_('Fee Unit') + ':', '')
fee_unit_combo = QComboBox()
fee_unit_combo.addItems([_('zat/byte'), _('mZCL/kB')])
fee_unit_combo.setCurrentIndex(self.fee_unit)
def on_fee_unit(x):
self.fee_unit = x
self.config.set_key('fee_unit', x)
self.fee_slider.update()
fee_unit_combo.currentIndexChanged.connect(on_fee_unit)
fee_widgets.append((fee_unit_label, fee_unit_combo))
msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\ msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
+ _('The following alias providers are available:') + '\n'\ + _('The following alias providers are available:') + '\n'\
+ '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\ + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
@ -2672,7 +2710,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
unit_combo = QComboBox() unit_combo = QComboBox()
unit_combo.addItems(units) unit_combo.addItems(units)
unit_combo.setCurrentIndex(units.index(self.base_unit())) unit_combo.setCurrentIndex(units.index(self.base_unit()))
def on_unit(x): def on_unit(x, nz):
unit_result = units[unit_combo.currentIndex()] unit_result = units[unit_combo.currentIndex()]
if self.base_unit() == unit_result: if self.base_unit() == unit_result:
return return
@ -2687,13 +2725,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
else: else:
raise Exception('Unknown base unit') raise Exception('Unknown base unit')
self.config.set_key('decimal_point', self.decimal_point, True) self.config.set_key('decimal_point', self.decimal_point, True)
nz.setMaximum(self.decimal_point)
self.history_list.update() self.history_list.update()
self.request_list.update() self.request_list.update()
self.address_list.update() self.address_list.update()
for edit, amount in zip(edits, amounts): for edit, amount in zip(edits, amounts):
edit.setAmount(amount) edit.setAmount(amount)
self.update_status() self.update_status()
unit_combo.currentIndexChanged.connect(on_unit) unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
gui_widgets.append((unit_label, unit_combo)) gui_widgets.append((unit_label, unit_combo))
block_explorers = sorted(util.block_explorer_info().keys()) block_explorers = sorted(util.block_explorer_info().keys())
@ -2783,8 +2822,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
unconf_cb.stateChanged.connect(on_unconf) unconf_cb.stateChanged.connect(on_unconf)
tx_widgets.append((unconf_cb, None)) tx_widgets.append((unconf_cb, None))
def on_outrounding(x):
self.config.set_key('coin_chooser_output_rounding', bool(x))
enable_outrounding = self.config.get('coin_chooser_output_rounding', False)
outrounding_cb = QCheckBox(_('Enable output value rounding'))
outrounding_cb.setToolTip(
_('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
_('This might improve your privacy somewhat.') + '\n' +
_('If enabled, at most 100 zatoshis might be lost due to this, per transaction.'))
outrounding_cb.setChecked(enable_outrounding)
outrounding_cb.stateChanged.connect(on_outrounding)
tx_widgets.append((outrounding_cb, None))
# Fiat Currency # Fiat Currency
hist_checkbox = QCheckBox() hist_checkbox = QCheckBox()
hist_capgains_checkbox = QCheckBox()
fiat_address_checkbox = QCheckBox() fiat_address_checkbox = QCheckBox()
ccy_combo = QComboBox() ccy_combo = QComboBox()
ex_combo = QComboBox() ex_combo = QComboBox()
@ -2806,6 +2858,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if not self.fx: return if not self.fx: return
fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config()) fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config())
def update_history_capgains_cb():
if not self.fx: return
hist_capgains_checkbox.setChecked(self.fx.get_history_capital_gains_config())
hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked())
def update_exchanges(): def update_exchanges():
if not self.fx: return if not self.fx: return
b = self.fx.is_enabled() b = self.fx.is_enabled()
@ -2844,6 +2901,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.fx.is_enabled() and checked: if self.fx.is_enabled() and checked:
# reset timeout to get historical rates # reset timeout to get historical rates
self.fx.timeout = 0 self.fx.timeout = 0
update_history_capgains_cb()
def on_history_capgains(checked):
if not self.fx: return
self.fx.set_history_capital_gains_config(checked)
self.history_list.refresh_headers()
def on_fiat_address(checked): def on_fiat_address(checked):
if not self.fx: return if not self.fx: return
@ -2853,16 +2916,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
update_currencies() update_currencies()
update_history_cb() update_history_cb()
update_history_capgains_cb()
update_fiat_address_cb() update_fiat_address_cb()
update_exchanges() update_exchanges()
ccy_combo.currentIndexChanged.connect(on_currency) ccy_combo.currentIndexChanged.connect(on_currency)
hist_checkbox.stateChanged.connect(on_history) hist_checkbox.stateChanged.connect(on_history)
hist_capgains_checkbox.stateChanged.connect(on_history_capgains)
fiat_address_checkbox.stateChanged.connect(on_fiat_address) fiat_address_checkbox.stateChanged.connect(on_fiat_address)
ex_combo.currentIndexChanged.connect(on_exchange) ex_combo.currentIndexChanged.connect(on_exchange)
fiat_widgets = [] fiat_widgets = []
fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo)) fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox)) fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
fiat_widgets.append((QLabel(_('Show capital gains in history')), hist_capgains_checkbox))
fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox)) fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox))
fiat_widgets.append((QLabel(_('Source')), ex_combo)) fiat_widgets.append((QLabel(_('Source')), ex_combo))
@ -3017,6 +3083,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0) grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0)
grid.addWidget(output_amount, 2, 1) grid.addWidget(output_amount, 2, 1)
fee_e = BTCAmountEdit(self.get_decimal_point) fee_e = BTCAmountEdit(self.get_decimal_point)
# FIXME with dyn fees, without estimates, there are all kinds of crashes here
def f(x): def f(x):
a = max_fee - fee_e.get_amount() a = max_fee - fee_e.get_amount()
output_amount.setText((self.format_amount(a) + ' ' + self.base_unit()) if a else '') output_amount.setText((self.format_amount(a) + ' ' + self.base_unit()) if a else '')
@ -3081,3 +3148,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if is_final: if is_final:
new_tx.set_rbf(False) new_tx.set_rbf(False)
self.show_transaction(new_tx, tx_label) self.show_transaction(new_tx, tx_label)
def save_transaction_into_wallet(self, tx):
try:
if not self.wallet.add_transaction(tx.txid(), tx):
self.show_error(_("Transaction could not be saved.") + "\n" +
_("It conflicts with current history."))
return False
except AddTransactionException as e:
self.show_error(e)
return False
else:
self.wallet.save_transactions(write=True)
# need to update at least: history_list, utxo_list, address_list
self.need_update.set()
self.msg_box(QPixmap(":icons/offline_tx.png"), None, _('Success'), _("Transaction added to wallet history"))
return True

View File

@ -31,8 +31,9 @@ from PyQt5.QtWidgets import *
import PyQt5.QtCore as QtCore import PyQt5.QtCore as QtCore
from electrum.i18n import _ from electrum.i18n import _
from electrum.bitcoin import NetworkConstants from electrum import constants
from electrum.util import print_error from electrum.util import print_error
from electrum.network import serialize_server, deserialize_server
from .util import * from .util import *
@ -145,7 +146,7 @@ class ServerListWidget(QTreeWidget):
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def set_server(self, s): def set_server(self, s):
host, port, protocol = s.split(':') host, port, protocol = deserialize_server(s)
self.parent.server_host.setText(host) self.parent.server_host.setText(host)
self.parent.server_port.setText(port) self.parent.server_port.setText(port)
self.parent.set_server() self.parent.set_server()
@ -170,7 +171,7 @@ class ServerListWidget(QTreeWidget):
port = d.get(protocol) port = d.get(protocol)
if port: if port:
x = QTreeWidgetItem([_host, port]) x = QTreeWidgetItem([_host, port])
server = _host+':'+port+':'+protocol server = serialize_server(_host, port, protocol)
x.setData(1, Qt.UserRole, server) x.setData(1, Qt.UserRole, server)
self.addTopLevelItem(x) self.addTopLevelItem(x)
@ -395,7 +396,7 @@ class NetworkChoiceLayout(object):
def change_protocol(self, use_ssl): def change_protocol(self, use_ssl):
p = 's' if use_ssl else 't' p = 's' if use_ssl else 't'
host = self.server_host.text() host = self.server_host.text()
pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS) pp = self.servers.get(host, constants.net.DEFAULT_PORTS)
if p not in pp.keys(): if p not in pp.keys():
p = list(pp.keys())[0] p = list(pp.keys())[0]
port = pp[p] port = pp[p]
@ -411,7 +412,7 @@ class NetworkChoiceLayout(object):
def follow_server(self, server): def follow_server(self, server):
self.network.switch_to_interface(server) self.network.switch_to_interface(server)
host, port, protocol, proxy, auto_connect = self.network.get_parameters() host, port, protocol, proxy, auto_connect = self.network.get_parameters()
host, port, protocol = server.split(':') host, port, protocol = deserialize_server(server)
self.network.set_parameters(host, port, protocol, proxy, auto_connect) self.network.set_parameters(host, port, protocol, proxy, auto_connect)
self.update() self.update()
@ -420,7 +421,7 @@ class NetworkChoiceLayout(object):
self.change_server(str(x.text(0)), self.protocol) self.change_server(str(x.text(0)), self.protocol)
def change_server(self, host, protocol): def change_server(self, host, protocol):
pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS) pp = self.servers.get(host, constants.net.DEFAULT_PORTS)
if protocol and protocol not in protocol_letters: if protocol and protocol not in protocol_letters:
protocol = None protocol = None
if protocol: if protocol:
@ -444,7 +445,6 @@ class NetworkChoiceLayout(object):
host, port, protocol, proxy, auto_connect = self.network.get_parameters() host, port, protocol, proxy, auto_connect = self.network.get_parameters()
host = str(self.server_host.text()) host = str(self.server_host.text())
port = str(self.server_port.text()) port = str(self.server_port.text())
protocol = 't' if self.config.get('nossl') else 's'
auto_connect = self.autoconnect_cb.isChecked() auto_connect = self.autoconnect_cb.isChecked()
self.network.set_parameters(host, port, protocol, proxy, auto_connect) self.network.set_parameters(host, port, protocol, proxy, auto_connect)

View File

@ -57,7 +57,7 @@ class PasswordLayout(object):
titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")] titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")]
def __init__(self, wallet, msg, kind, OK_button): def __init__(self, wallet, msg, kind, OK_button, force_disable_encrypt_cb=False):
self.wallet = wallet self.wallet = wallet
self.pw = QLineEdit() self.pw = QLineEdit()
@ -126,7 +126,8 @@ class PasswordLayout(object):
def enable_OK(): def enable_OK():
ok = self.new_pw.text() == self.conf_pw.text() ok = self.new_pw.text() == self.conf_pw.text()
OK_button.setEnabled(ok) OK_button.setEnabled(ok)
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text())) self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text())
and not force_disable_encrypt_cb)
self.new_pw.textChanged.connect(enable_OK) self.new_pw.textChanged.connect(enable_OK)
self.conf_pw.textChanged.connect(enable_OK) self.conf_pw.textChanged.connect(enable_OK)
@ -163,11 +164,84 @@ class PasswordLayout(object):
return pw return pw
class ChangePasswordDialog(WindowModalDialog): class PasswordLayoutForHW(object):
def __init__(self, wallet, msg, kind, OK_button):
self.wallet = wallet
self.kind = kind
self.OK_button = OK_button
vbox = QVBoxLayout()
label = QLabel(msg + "\n")
label.setWordWrap(True)
grid = QGridLayout()
grid.setSpacing(8)
grid.setColumnMinimumWidth(0, 150)
grid.setColumnMinimumWidth(1, 100)
grid.setColumnStretch(1,1)
logo_grid = QGridLayout()
logo_grid.setSpacing(8)
logo_grid.setColumnMinimumWidth(0, 70)
logo_grid.setColumnStretch(1,1)
logo = QLabel()
logo.setAlignment(Qt.AlignCenter)
logo_grid.addWidget(logo, 0, 0)
logo_grid.addWidget(label, 0, 1, 1, 2)
vbox.addLayout(logo_grid)
if wallet and wallet.has_storage_encryption():
lockfile = ":icons/lock.png"
else:
lockfile = ":icons/unlock.png"
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
vbox.addLayout(grid)
self.encrypt_cb = QCheckBox(_('Encrypt wallet file'))
grid.addWidget(self.encrypt_cb, 1, 0, 1, 2)
self.vbox = vbox
def title(self):
return _("Toggle Encryption")
def layout(self):
return self.vbox
class ChangePasswordDialogBase(WindowModalDialog):
def __init__(self, parent, wallet): def __init__(self, parent, wallet):
WindowModalDialog.__init__(self, parent) WindowModalDialog.__init__(self, parent)
is_encrypted = wallet.storage.is_encrypted() is_encrypted = wallet.has_storage_encryption()
OK_button = OkButton(self)
self.create_password_layout(wallet, is_encrypted, OK_button)
self.setWindowTitle(self.playout.title())
vbox = QVBoxLayout(self)
vbox.addLayout(self.playout.layout())
vbox.addStretch(1)
vbox.addLayout(Buttons(CancelButton(self), OK_button))
self.playout.encrypt_cb.setChecked(is_encrypted)
def create_password_layout(self, wallet, is_encrypted, OK_button):
raise NotImplementedError()
class ChangePasswordDialogForSW(ChangePasswordDialogBase):
def __init__(self, parent, wallet):
ChangePasswordDialogBase.__init__(self, parent, wallet)
if not wallet.has_password():
self.playout.encrypt_cb.setChecked(True)
def create_password_layout(self, wallet, is_encrypted, OK_button):
if not wallet.has_password(): if not wallet.has_password():
msg = _('Your wallet is not protected.') msg = _('Your wallet is not protected.')
msg += ' ' + _('Use this dialog to add a password to your wallet.') msg += ' ' + _('Use this dialog to add a password to your wallet.')
@ -177,14 +251,9 @@ class ChangePasswordDialog(WindowModalDialog):
else: else:
msg = _('Your wallet is password protected and encrypted.') msg = _('Your wallet is password protected and encrypted.')
msg += ' ' + _('Use this dialog to change your password.') msg += ' ' + _('Use this dialog to change your password.')
OK_button = OkButton(self) self.playout = PasswordLayout(
self.playout = PasswordLayout(wallet, msg, PW_CHANGE, OK_button) wallet, msg, PW_CHANGE, OK_button,
self.setWindowTitle(self.playout.title()) force_disable_encrypt_cb=not wallet.can_have_keystore_encryption())
vbox = QVBoxLayout(self)
vbox.addLayout(self.playout.layout())
vbox.addStretch(1)
vbox.addLayout(Buttons(CancelButton(self), OK_button))
self.playout.encrypt_cb.setChecked(is_encrypted or not wallet.has_password())
def run(self): def run(self):
if not self.exec_(): if not self.exec_():
@ -192,6 +261,26 @@ class ChangePasswordDialog(WindowModalDialog):
return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked() return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked()
class ChangePasswordDialogForHW(ChangePasswordDialogBase):
def __init__(self, parent, wallet):
ChangePasswordDialogBase.__init__(self, parent, wallet)
def create_password_layout(self, wallet, is_encrypted, OK_button):
if not is_encrypted:
msg = _('Your wallet file is NOT encrypted.')
else:
msg = _('Your wallet file is encrypted.')
msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.')
msg += '\n' + _('Use this dialog to toggle encryption.')
self.playout = PasswordLayoutForHW(wallet, msg, PW_CHANGE, OK_button)
def run(self):
if not self.exec_():
return False, None
return True, self.playout.encrypt_cb.isChecked()
class PasswordDialog(WindowModalDialog): class PasswordDialog(WindowModalDialog):
def __init__(self, parent=None, msg=None): def __init__(self, parent=None, msg=None):

View File

@ -115,7 +115,7 @@ class RequestList(MyTreeWidget):
column_title = self.headerItem().text(column) column_title = self.headerItem().text(column)
column_data = item.text(column) column_data = item.text(column)
menu = QMenu(self) menu = QMenu(self)
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr))) menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr)))
menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr)) menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr)) menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))

View File

@ -25,6 +25,7 @@
import copy import copy
import datetime import datetime
import json import json
import traceback
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtGui import * from PyQt5.QtGui import *
@ -33,16 +34,27 @@ from PyQt5.QtWidgets import *
from electrum.bitcoin import base_encode from electrum.bitcoin import base_encode
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugins import run_hook from electrum.plugins import run_hook
from electrum import simple_config
from electrum.util import bfh from electrum.util import bfh
from electrum.wallet import AddTransactionException
from electrum.transaction import SerializationError
from .util import * from .util import *
dialogs = [] # Otherwise python randomly garbage collects the dialogs... dialogs = [] # Otherwise python randomly garbage collects the dialogs...
def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False): def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
d = TxDialog(tx, parent, desc, prompt_if_unsaved) try:
dialogs.append(d) d = TxDialog(tx, parent, desc, prompt_if_unsaved)
d.show() except SerializationError as e:
traceback.print_exc(file=sys.stderr)
parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
else:
dialogs.append(d)
d.show()
class TxDialog(QDialog, MessageBoxMixin): class TxDialog(QDialog, MessageBoxMixin):
@ -56,7 +68,10 @@ class TxDialog(QDialog, MessageBoxMixin):
# e.g. the FX plugin. If this happens during or after a long # e.g. the FX plugin. If this happens during or after a long
# sign operation the signatures are lost. # sign operation the signatures are lost.
self.tx = copy.deepcopy(tx) self.tx = copy.deepcopy(tx)
self.tx.deserialize() try:
self.tx.deserialize()
except BaseException as e:
raise SerializationError(e)
self.main_window = parent self.main_window = parent
self.wallet = parent.wallet self.wallet = parent.wallet
self.prompt_if_unsaved = prompt_if_unsaved self.prompt_if_unsaved = prompt_if_unsaved
@ -98,8 +113,17 @@ class TxDialog(QDialog, MessageBoxMixin):
self.broadcast_button = b = QPushButton(_("Broadcast")) self.broadcast_button = b = QPushButton(_("Broadcast"))
b.clicked.connect(self.do_broadcast) b.clicked.connect(self.do_broadcast)
self.save_button = b = QPushButton(_("Save")) self.save_button = QPushButton(_("Save"))
b.clicked.connect(self.save) save_button_disabled = not tx.is_complete()
self.save_button.setDisabled(save_button_disabled)
if save_button_disabled:
self.save_button.setToolTip(_("Please sign this transaction in order to save it"))
else:
self.save_button.setToolTip("")
self.save_button.clicked.connect(self.save)
self.export_button = b = QPushButton(_("Export"))
b.clicked.connect(self.export)
self.cancel_button = b = QPushButton(_("Close")) self.cancel_button = b = QPushButton(_("Close"))
b.clicked.connect(self.close) b.clicked.connect(self.close)
@ -112,9 +136,9 @@ class TxDialog(QDialog, MessageBoxMixin):
self.copy_button = CopyButton(lambda: str(self.tx), parent.app) self.copy_button = CopyButton(lambda: str(self.tx), parent.app)
# Action buttons # Action buttons
self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button] self.buttons = [self.sign_button, self.broadcast_button, self.save_button, self.cancel_button]
# Transaction sharing buttons # Transaction sharing buttons
self.sharing_buttons = [self.copy_button, self.qr_button, self.save_button] self.sharing_buttons = [self.copy_button, self.qr_button, self.export_button]
run_hook('transaction_dialog', self) run_hook('transaction_dialog', self)
@ -136,11 +160,14 @@ class TxDialog(QDialog, MessageBoxMixin):
def closeEvent(self, event): def closeEvent(self, event):
if (self.prompt_if_unsaved and not self.saved if (self.prompt_if_unsaved and not self.saved
and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))): and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
event.ignore() event.ignore()
else: else:
event.accept() event.accept()
dialogs.remove(self) try:
dialogs.remove(self)
except ValueError:
pass # was not in list already
def show_qr(self): def show_qr(self):
text = bfh(str(self.tx)) text = bfh(str(self.tx))
@ -155,6 +182,8 @@ class TxDialog(QDialog, MessageBoxMixin):
if success: if success:
self.prompt_if_unsaved = True self.prompt_if_unsaved = True
self.saved = False self.saved = False
self.save_button.setDisabled(False)
self.save_button.setToolTip("")
self.update() self.update()
self.main_window.pop_top_level_window(self) self.main_window.pop_top_level_window(self)
@ -163,12 +192,18 @@ class TxDialog(QDialog, MessageBoxMixin):
self.main_window.sign_tx(self.tx, sign_done) self.main_window.sign_tx(self.tx, sign_done)
def save(self): def save(self):
if self.main_window.save_transaction_into_wallet(self.tx):
self.save_button.setDisabled(True)
self.saved = True
def export(self):
name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn' name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn'
fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn") fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn")
if fileName: if fileName:
with open(fileName, "w+") as f: with open(fileName, "w+") as f:
f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n') f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n')
self.show_message(_("Transaction saved successfully")) self.show_message(_("Transaction exported successfully"))
self.saved = True self.saved = True
def update(self): def update(self):
@ -191,11 +226,11 @@ class TxDialog(QDialog, MessageBoxMixin):
if timestamp: if timestamp:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
self.date_label.setText(_("Date: %s")%time_str) self.date_label.setText(_("Date: {}").format(time_str))
self.date_label.show() self.date_label.show()
elif exp_n: elif exp_n:
text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)') text = '%.2f MB'%(exp_n/1000000)
self.date_label.setText(_('Expected confirmation time') + ': ' + text) self.date_label.setText(_('Position in mempool') + ': ' + text + ' ' + _('from tip'))
self.date_label.show() self.date_label.show()
else: else:
self.date_label.hide() self.date_label.hide()
@ -206,9 +241,13 @@ class TxDialog(QDialog, MessageBoxMixin):
else: else:
amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit
size_str = _("Size:") + ' %d bytes'% size size_str = _("Size:") + ' %d bytes'% size
fee_str = _("Fee") + ': %s'% (format_amount(fee) + ' ' + base_unit if fee is not None else _('Unknown')) fee_str = _("Fee") + ': %s' % (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
if fee is not None: if fee is not None:
fee_str += ' ( %s ) '% self.main_window.format_fee_rate(fee/size*1000) fee_rate = fee/size*1000
fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate)
confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
if fee_rate > confirm_rate:
fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!'
self.amount_label.setText(amount_str) self.amount_label.setText(amount_str)
self.fee_label.setText(fee_str) self.fee_label.setText(fee_str)
self.size_label.setText(size_str) self.size_label.setText(size_str)
@ -224,7 +263,7 @@ class TxDialog(QDialog, MessageBoxMixin):
rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True))) rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
rec.setToolTip(_("Wallet receive address")) rec.setToolTip(_("Wallet receive address"))
chg = QTextCharFormat() chg = QTextCharFormat()
chg.setBackground(QBrush(QColor("yellow"))) chg.setBackground(QBrush(ColorScheme.YELLOW.as_color(background=True)))
chg.setToolTip(_("Wallet change address")) chg.setToolTip(_("Wallet change address"))
def text_format(addr): def text_format(addr):
@ -254,7 +293,7 @@ class TxDialog(QDialog, MessageBoxMixin):
if _addr: if _addr:
addr = _addr addr = _addr
if addr is None: if addr is None:
addr = _('Unknown') addr = _('unknown')
cursor.insertText(addr, text_format(addr)) cursor.insertText(addr, text_format(addr))
if x.get('value'): if x.get('value'):
cursor.insertText(format_amount(x['value']), ext) cursor.insertText(format_amount(x['value']), ext)

View File

@ -6,11 +6,15 @@ import queue
from collections import namedtuple from collections import namedtuple
from functools import partial from functools import partial
from electrum.i18n import _
from PyQt5.QtGui import * from PyQt5.QtGui import *
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtWidgets import * from PyQt5.QtWidgets import *
from electrum.i18n import _
from electrum.util import FileImportFailed, FileExportFailed
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
if platform.system() == 'Windows': if platform.system() == 'Windows':
MONOSPACE_FONT = 'Lucida Console' MONOSPACE_FONT = 'Lucida Console'
elif platform.system() == 'Darwin': elif platform.system() == 'Darwin':
@ -21,8 +25,6 @@ else:
dialogs = [] dialogs = []
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
pr_icons = { pr_icons = {
PR_UNPAID:":icons/unpaid.png", PR_UNPAID:":icons/unpaid.png",
PR_PAID:":icons/confirmed.png", PR_PAID:":icons/confirmed.png",
@ -200,9 +202,14 @@ class MessageBoxMixin(object):
def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok, def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok,
defaultButton=QMessageBox.NoButton): defaultButton=QMessageBox.NoButton):
parent = parent or self.top_level_window() parent = parent or self.top_level_window()
d = QMessageBox(icon, title, str(text), buttons, parent) if type(icon) is QPixmap:
d = QMessageBox(QMessageBox.Information, title, str(text), buttons, parent)
d.setIconPixmap(icon)
else:
d = QMessageBox(icon, title, str(text), buttons, parent)
d.setWindowModality(Qt.WindowModal) d.setWindowModality(Qt.WindowModal)
d.setDefaultButton(defaultButton) d.setDefaultButton(defaultButton)
d.setTextInteractionFlags(Qt.TextSelectableByMouse)
return d.exec_() return d.exec_()
class WindowModalDialog(QDialog, MessageBoxMixin): class WindowModalDialog(QDialog, MessageBoxMixin):
@ -216,7 +223,7 @@ class WindowModalDialog(QDialog, MessageBoxMixin):
class WaitingDialog(WindowModalDialog): class WaitingDialog(WindowModalDialog):
'''Shows a please wait dialog whilst runnning a task. It is not '''Shows a please wait dialog whilst running a task. It is not
necessary to maintain a reference to this dialog.''' necessary to maintain a reference to this dialog.'''
def __init__(self, parent, message, task, on_success=None, on_error=None): def __init__(self, parent, message, task, on_success=None, on_error=None):
assert parent assert parent
@ -228,6 +235,7 @@ class WaitingDialog(WindowModalDialog):
self.accepted.connect(self.on_accepted) self.accepted.connect(self.on_accepted)
self.show() self.show()
self.thread = TaskThread(self) self.thread = TaskThread(self)
self.thread.finished.connect(self.deleteLater) # see #3956
self.thread.add(task, on_success, self.accept, on_error) self.thread.add(task, on_success, self.accept, on_error)
def wait(self): def wait(self):
@ -254,7 +262,7 @@ def line_dialog(parent, title, label, ok_label, default=None):
def text_dialog(parent, title, label, ok_label, default=None, allow_multi=False): def text_dialog(parent, title, label, ok_label, default=None, allow_multi=False):
from .qrtextedit import ScanQRTextEdit from .qrtextedit import ScanQRTextEdit
dialog = WindowModalDialog(parent, title) dialog = WindowModalDialog(parent, title)
dialog.setMinimumWidth(500) dialog.setMinimumWidth(600)
l = QVBoxLayout() l = QVBoxLayout()
dialog.setLayout(l) dialog.setLayout(l)
l.addWidget(QLabel(label)) l.addWidget(QLabel(label))
@ -389,13 +397,18 @@ class MyTreeWidget(QTreeWidget):
self.editor = None self.editor = None
self.pending_update = False self.pending_update = False
if editable_columns is None: if editable_columns is None:
editable_columns = [stretch_column] editable_columns = {stretch_column}
else:
editable_columns = set(editable_columns)
self.editable_columns = editable_columns self.editable_columns = editable_columns
self.setItemDelegate(ElectrumItemDelegate(self)) self.setItemDelegate(ElectrumItemDelegate(self))
self.itemDoubleClicked.connect(self.on_doubleclick) self.itemDoubleClicked.connect(self.on_doubleclick)
self.update_headers(headers) self.update_headers(headers)
self.current_filter = "" self.current_filter = ""
self.setRootIsDecorated(False) # remove left margin
self.toolbar_shown = False
def update_headers(self, headers): def update_headers(self, headers):
self.setColumnCount(len(headers)) self.setColumnCount(len(headers))
self.setHeaderLabels(headers) self.setHeaderLabels(headers)
@ -406,11 +419,15 @@ class MyTreeWidget(QTreeWidget):
def editItem(self, item, column): def editItem(self, item, column):
if column in self.editable_columns: if column in self.editable_columns:
self.editing_itemcol = (item, column, item.text(column)) try:
# Calling setFlags causes on_changed events for some reason self.editing_itemcol = (item, column, item.text(column))
item.setFlags(item.flags() | Qt.ItemIsEditable) # Calling setFlags causes on_changed events for some reason
QTreeWidget.editItem(self, item, column) item.setFlags(item.flags() | Qt.ItemIsEditable)
item.setFlags(item.flags() & ~Qt.ItemIsEditable) QTreeWidget.editItem(self, item, column)
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
except RuntimeError:
# (item) wrapped C/C++ object has been deleted
pass
def keyPressEvent(self, event): def keyPressEvent(self, event):
if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None: if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None:
@ -478,8 +495,12 @@ class MyTreeWidget(QTreeWidget):
self.pending_update = True self.pending_update = True
else: else:
self.setUpdatesEnabled(False) self.setUpdatesEnabled(False)
scroll_pos = self.verticalScrollBar().value()
self.on_update() self.on_update()
self.setUpdatesEnabled(True) self.setUpdatesEnabled(True)
# To paint the list before resetting the scroll position
self.parent.app.processEvents()
self.verticalScrollBar().setValue(scroll_pos)
if self.current_filter: if self.current_filter:
self.filter(self.current_filter) self.filter(self.current_filter)
@ -503,6 +524,37 @@ class MyTreeWidget(QTreeWidget):
item.setHidden(all([item.text(column).lower().find(p) == -1 item.setHidden(all([item.text(column).lower().find(p) == -1
for column in columns])) for column in columns]))
def create_toolbar(self, config=None):
hbox = QHBoxLayout()
buttons = self.get_toolbar_buttons()
for b in buttons:
b.setVisible(False)
hbox.addWidget(b)
hide_button = QPushButton('x')
hide_button.setVisible(False)
hide_button.pressed.connect(lambda: self.show_toolbar(False, config))
self.toolbar_buttons = buttons + (hide_button,)
hbox.addStretch()
hbox.addWidget(hide_button)
return hbox
def save_toolbar_state(self, state, config):
pass # implemented in subclasses
def show_toolbar(self, state, config=None):
if state == self.toolbar_shown:
return
self.toolbar_shown = state
if config:
self.save_toolbar_state(state, config)
for b in self.toolbar_buttons:
b.setVisible(state)
if not state:
self.on_hide_toolbar()
def toggle_toolbar(self, config=None):
self.show_toolbar(not self.toolbar_shown, config)
class ButtonsWidget(QWidget): class ButtonsWidget(QWidget):
@ -589,12 +641,12 @@ class TaskThread(QThread):
except BaseException: except BaseException:
self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error) self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
def on_done(self, result, cb_done, cb): def on_done(self, result, cb_done, cb_result):
# This runs in the parent's thread. # This runs in the parent's thread.
if cb_done: if cb_done:
cb_done() cb_done()
if cb: if cb_result:
cb(result) cb_result(result)
def stop(self): def stop(self):
self.tasks.put(None) self.tasks.put(None)
@ -621,6 +673,7 @@ class ColorScheme:
dark_scheme = False dark_scheme = False
GREEN = ColorSchemeItem("#117c11", "#8af296") GREEN = ColorSchemeItem("#117c11", "#8af296")
YELLOW = ColorSchemeItem("#ffff00", "#ffff00")
RED = ColorSchemeItem("#7c1111", "#f18c8c") RED = ColorSchemeItem("#7c1111", "#f18c8c")
BLUE = ColorSchemeItem("#123b7c", "#8cb3f2") BLUE = ColorSchemeItem("#123b7c", "#8cb3f2")
DEFAULT = ColorSchemeItem("black", "white") DEFAULT = ColorSchemeItem("black", "white")
@ -635,6 +688,97 @@ class ColorScheme:
if ColorScheme.has_dark_background(widget): if ColorScheme.has_dark_background(widget):
ColorScheme.dark_scheme = True ColorScheme.dark_scheme = True
class AcceptFileDragDrop:
def __init__(self, file_type=""):
assert isinstance(self, QWidget)
self.setAcceptDrops(True)
self.file_type = file_type
def validateEvent(self, event):
if not event.mimeData().hasUrls():
event.ignore()
return False
for url in event.mimeData().urls():
if not url.toLocalFile().endswith(self.file_type):
event.ignore()
return False
event.accept()
return True
def dragEnterEvent(self, event):
self.validateEvent(event)
def dragMoveEvent(self, event):
if self.validateEvent(event):
event.setDropAction(Qt.CopyAction)
def dropEvent(self, event):
if self.validateEvent(event):
for url in event.mimeData().urls():
self.onFileAdded(url.toLocalFile())
def onFileAdded(self, fn):
raise NotImplementedError()
def import_meta_gui(electrum_window, title, importer, on_success):
filter_ = "JSON (*.json);;All files (*)"
filename = electrum_window.getOpenFileName(_("Open {} file").format(title), filter_)
if not filename:
return
try:
importer(filename)
except FileImportFailed as e:
electrum_window.show_critical(str(e))
else:
electrum_window.show_message(_("Your {} were successfully imported").format(title))
on_success()
def export_meta_gui(electrum_window, title, exporter):
filter_ = "JSON (*.json);;All files (*)"
filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title),
'electrum_{}.json'.format(title), filter_)
if not filename:
return
try:
exporter(filename)
except FileExportFailed as e:
electrum_window.show_critical(str(e))
else:
electrum_window.show_message(_("Your {0} were exported to '{1}'")
.format(title, str(filename)))
def get_parent_main_window(widget):
"""Returns a reference to the ElectrumWindow this widget belongs to."""
from .main_window import ElectrumWindow
for _ in range(100):
if widget is None:
return None
if not isinstance(widget, ElectrumWindow):
widget = widget.parentWidget()
else:
return widget
return None
class SortableTreeWidgetItem(QTreeWidgetItem):
DataRole = Qt.UserRole + 1
def __lt__(self, other):
column = self.treeWidget().sortColumn()
if None not in [x.data(column, self.DataRole) for x in [self, other]]:
# We have set custom data to sort by
return self.data(column, self.DataRole) < other.data(column, self.DataRole)
try:
# Is the value something numeric?
return float(self.text(column)) < float(other.text(column))
except ValueError:
# If not, we will just do string comparison
return self.text(column) < other.text(column)
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication([]) app = QApplication([])
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))

View File

@ -32,6 +32,7 @@ class UTXOList(MyTreeWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1) MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1)
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSortingEnabled(True)
def get_name(self, x): def get_name(self, x):
return x.get('prevout_hash') + ":%d"%x.get('prevout_n') return x.get('prevout_hash') + ":%d"%x.get('prevout_n')
@ -46,9 +47,10 @@ class UTXOList(MyTreeWidget):
height = x.get('height') height = x.get('height')
name = self.get_name(x) name = self.get_name(x)
label = self.wallet.get_label(x.get('prevout_hash')) label = self.wallet.get_label(x.get('prevout_hash'))
amount = self.parent.format_amount(x['value']) amount = self.parent.format_amount(x['value'], whitespaces=True)
utxo_item = QTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]]) utxo_item = SortableTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]])
utxo_item.setFont(0, QFont(MONOSPACE_FONT)) utxo_item.setFont(0, QFont(MONOSPACE_FONT))
utxo_item.setFont(2, QFont(MONOSPACE_FONT))
utxo_item.setFont(4, QFont(MONOSPACE_FONT)) utxo_item.setFont(4, QFont(MONOSPACE_FONT))
utxo_item.setData(0, Qt.UserRole, name) utxo_item.setData(0, Qt.UserRole, name)
if self.wallet.is_frozen(address): if self.wallet.is_frozen(address):

View File

@ -14,6 +14,7 @@
<file>icons/electrum_light_icon.png</file> <file>icons/electrum_light_icon.png</file>
<file>icons/electrum_dark_icon.png</file> <file>icons/electrum_dark_icon.png</file>
<file>icons/file.png</file> <file>icons/file.png</file>
<file>icons/info.png</file>
<file>icons/keepkey.png</file> <file>icons/keepkey.png</file>
<file>icons/keepkey_unpaired.png</file> <file>icons/keepkey_unpaired.png</file>
<file>icons/key.png</file> <file>icons/key.png</file>
@ -22,6 +23,7 @@
<file>icons/lock.png</file> <file>icons/lock.png</file>
<file>icons/microphone.png</file> <file>icons/microphone.png</file>
<file>icons/network.png</file> <file>icons/network.png</file>
<file>icons/offline_tx.png</file>
<file>icons/qrcode.png</file> <file>icons/qrcode.png</file>
<file>icons/qrcode_white.png</file> <file>icons/qrcode_white.png</file>
<file>icons/preferences.png</file> <file>icons/preferences.png</file>

BIN
icons/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
icons/offline_tx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -24,12 +24,19 @@
# SOFTWARE. # SOFTWARE.
import os import os
import sys
import traceback
from . import bitcoin from . import bitcoin
from . import keystore from . import keystore
from .keystore import bip44_derivation from .keystore import bip44_derivation
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types
from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
from .i18n import _ from .i18n import _
from .util import UserCancelled
# hardware device setup purpose
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
class ScriptTypeNotSupported(Exception): pass class ScriptTypeNotSupported(Exception): pass
@ -123,7 +130,7 @@ class BaseWizard(object):
choices = [ choices = [
('choose_seed_type', _('Create a new seed')), ('choose_seed_type', _('Create a new seed')),
('restore_from_seed', _('I already have a seed')), ('restore_from_seed', _('I already have a seed')),
('restore_from_key', _('Use public or private keys')), ('restore_from_key', _('Use a master key')),
] ]
if not self.is_kivy: if not self.is_kivy:
choices.append(('choose_hw_device', _('Use a hardware device'))) choices.append(('choose_hw_device', _('Use a hardware device')))
@ -146,17 +153,22 @@ class BaseWizard(object):
is_valid=v, allow_multi=True) is_valid=v, allow_multi=True)
def on_import(self, text): def on_import(self, text):
# create a temporary wallet and exploit that modifications
# will be reflected on self.storage
if keystore.is_address_list(text): if keystore.is_address_list(text):
self.wallet = Imported_Wallet(self.storage) w = Imported_Wallet(self.storage)
for x in text.split(): for x in text.split():
self.wallet.import_address(x) w.import_address(x)
elif keystore.is_private_key_list(text): elif keystore.is_private_key_list(text):
k = keystore.Imported_KeyStore({}) k = keystore.Imported_KeyStore({})
self.storage.put('keystore', k.dump()) self.storage.put('keystore', k.dump())
self.wallet = Imported_Wallet(self.storage) w = Imported_Wallet(self.storage)
for x in text.split(): for x in text.split():
self.wallet.import_private_key(x, None) w.import_private_key(x, None)
self.terminate() self.keystores.append(w.keystore)
else:
return self.terminate()
return self.run('create_wallet')
def restore_from_key(self): def restore_from_key(self):
if self.wallet_type == 'standard': if self.wallet_type == 'standard':
@ -175,7 +187,7 @@ class BaseWizard(object):
k = keystore.from_master_key(text) k = keystore.from_master_key(text)
self.on_keystore(k) self.on_keystore(k)
def choose_hw_device(self): def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET):
title = _('Hardware Keystore') title = _('Hardware Keystore')
# check available plugins # check available plugins
support = self.plugins.get_hardware_support() support = self.plugins.get_hardware_support()
@ -184,7 +196,7 @@ class BaseWizard(object):
_('No hardware wallet support found on your system.'), _('No hardware wallet support found on your system.'),
_('Please install the relevant libraries (eg python-trezor for Trezor).'), _('Please install the relevant libraries (eg python-trezor for Trezor).'),
]) ])
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device()) self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
return return
# scan devices # scan devices
devices = [] devices = []
@ -204,34 +216,51 @@ class BaseWizard(object):
_('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ', _('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.'), _('On Linux, you might have to add a new permission to your udev rules.'),
]) ])
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device()) self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
return return
# select device # select device
self.devices = devices self.devices = devices
choices = [] choices = []
for name, info in devices: for name, info in devices:
state = _("initialized") if info.initialized else _("wiped") state = _("initialized") if info.initialized else _("wiped")
label = info.label or _("An unnamed %s")%name label = info.label or _("An unnamed {}").format(name)
descr = "%s [%s, %s]" % (label, name, state) descr = "%s [%s, %s]" % (label, name, state)
choices.append(((name, info), descr)) choices.append(((name, info), descr))
msg = _('Select a device') + ':' msg = _('Select a device') + ':'
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_device) self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose))
def on_device(self, name, device_info): def on_device(self, name, device_info, *, purpose):
self.plugin = self.plugins.get_plugin(name) self.plugin = self.plugins.get_plugin(name)
try: try:
self.plugin.setup_device(device_info, self) self.plugin.setup_device(device_info, self, purpose)
except OSError as e:
self.show_error(_('We encountered an error while connecting to your device:')
+ '\n' + str(e) + '\n'
+ _('To try to fix this, we will now re-pair with your device.') + '\n'
+ _('Please try again.'))
devmgr = self.plugins.device_manager
devmgr.unpair_id(device_info.device.id_)
self.choose_hw_device(purpose)
return
except BaseException as e: except BaseException as e:
self.show_error(str(e)) self.show_error(str(e))
self.choose_hw_device() self.choose_hw_device(purpose)
return return
if self.wallet_type=='multisig': if purpose == HWD_SETUP_NEW_WALLET:
# There is no general standard for HD multisig. if self.wallet_type=='multisig':
# This is partially compatible with BIP45; assumes index=0 # There is no general standard for HD multisig.
self.on_hw_derivation(name, device_info, "m/45'/0") # This is partially compatible with BIP45; assumes index=0
self.on_hw_derivation(name, device_info, "m/45'/0")
else:
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
self.derivation_dialog(f)
elif purpose == HWD_SETUP_DECRYPT_WALLET:
derivation = get_derivation_used_for_hw_device_encryption()
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self)
password = keystore.Xpub.get_pubkey_from_xpub(xpub, ())
self.storage.decrypt(password)
else: else:
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x)) raise Exception('unknown purpose: %s' % purpose)
self.derivation_dialog(f)
def derivation_dialog(self, f): def derivation_dialog(self, f):
default = bip44_derivation(0, bip43_purpose=44) default = bip44_derivation(0, bip43_purpose=44)
@ -364,13 +393,45 @@ class BaseWizard(object):
self.run('create_wallet') self.run('create_wallet')
def create_wallet(self): def create_wallet(self):
if any(k.may_have_password() for k in self.keystores): encrypt_keystore = any(k.may_have_password() for k in self.keystores)
self.request_password(run_next=self.on_password) # note: the following condition ("if") is duplicated logic from
# wallet.get_available_storage_encryption_version()
if self.wallet_type == 'standard' and isinstance(self.keystores[0], keystore.Hardware_KeyStore):
# offer encrypting with a pw derived from the hw device
k = self.keystores[0]
try:
k.handler = self.plugin.create_handler(self)
password = k.get_password_for_storage_encryption()
except UserCancelled:
devmgr = self.plugins.device_manager
devmgr.unpair_xpub(k.xpub)
self.choose_hw_device()
return
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.show_error(str(e))
return
self.request_storage_encryption(
run_next=lambda encrypt_storage: self.on_password(
password,
encrypt_storage=encrypt_storage,
storage_enc_version=STO_EV_XPUB_PW,
encrypt_keystore=False))
else: else:
self.on_password(None, False) # prompt the user to set an arbitrary password
self.request_password(
run_next=lambda password, encrypt_storage: self.on_password(
password,
encrypt_storage=encrypt_storage,
storage_enc_version=STO_EV_USER_PW,
encrypt_keystore=encrypt_keystore),
force_disable_encrypt_cb=not encrypt_keystore)
def on_password(self, password, encrypt): def on_password(self, password, *, encrypt_storage,
self.storage.set_password(password, encrypt) storage_enc_version=STO_EV_USER_PW, encrypt_keystore):
self.storage.set_keystore_encryption(bool(password) and encrypt_keystore)
if encrypt_storage:
self.storage.set_password(password, enc_version=storage_enc_version)
for k in self.keystores: for k in self.keystores:
if k.may_have_password(): if k.may_have_password():
k.update_password(None, password) k.update_password(None, password)
@ -386,6 +447,13 @@ class BaseWizard(object):
self.storage.write() self.storage.write()
self.wallet = Multisig_Wallet(self.storage) self.wallet = Multisig_Wallet(self.storage)
self.run('create_addresses') self.run('create_addresses')
elif self.wallet_type == 'imported':
if len(self.keystores) > 0:
keys = self.keystores[0].dump()
self.storage.put('keystore', keys)
self.wallet = Imported_Wallet(self.storage)
self.wallet.storage.write()
self.terminate()
def show_xpub_and_add_cosigners(self, xpub): def show_xpub_and_add_cosigners(self, xpub):
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
@ -444,5 +512,5 @@ class BaseWizard(object):
self.wallet.synchronize() self.wallet.synchronize()
self.wallet.storage.write() self.wallet.storage.write()
self.terminate() self.terminate()
msg = _("Electrum is generating your addresses, please wait.") msg = _("Electrum is generating your addresses, please wait...")
self.waiting_dialog(task, msg) self.waiting_dialog(task, msg)

View File

@ -37,89 +37,11 @@ from .util import bfh, bh2u, to_string
from . import version from . import version
from .util import print_error, InvalidPassword, assert_bytes, to_bytes, inv_dict from .util import print_error, InvalidPassword, assert_bytes, to_bytes, inv_dict
from . import segwit_addr from . import segwit_addr
from . import constants
def read_json(filename, default):
path = os.path.join(os.path.dirname(__file__), filename)
try:
with open(path, 'r') as f:
r = json.loads(f.read())
except:
r = default
return r
# Version numbers for BIP32 extended keys
# standard: xprv, xpub
# segwit in p2sh: yprv, ypub
# native segwit: zprv, zpub
XPRV_HEADERS = {
'standard': 0x0488ade4,
'p2wpkh-p2sh': 0x049d7878,
'p2wsh-p2sh': 0x295b005,
'p2wpkh': 0x4b2430c,
'p2wsh': 0x2aa7a99
}
XPUB_HEADERS = {
'standard': 0x0488b21e,
'p2wpkh-p2sh': 0x049d7cb2,
'p2wsh-p2sh': 0x295b43f,
'p2wpkh': 0x4b24746,
'p2wsh': 0x2aa7ed3
}
class NetworkConstants:
# https://github.com/z-classic/zclassic/blob/master/src/chainparams.cpp#L103
@classmethod
def set_mainnet(cls):
cls.TESTNET = False
cls.WIF_PREFIX = 0x80
cls.ADDRTYPE_P2PKH = [0x1C, 0xB8]
cls.ADDRTYPE_P2SH = [0x1C, 0xBD]
cls.ADDRTYPE_SHIELDED = [0x16, 0x9A]
cls.SEGWIT_HRP = "bc" #TODO zcl has no segwit
cls.GENESIS = "0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602"
cls.DEFAULT_PORTS = {'t': '50001', 's': '50002'}
cls.DEFAULT_SERVERS = read_json('servers.json', {})
cls.CHECKPOINTS = read_json('checkpoints.json', [])
cls.EQUIHASH_N = 200
cls.EQUIHASH_K = 9
cls.HEADERS_URL = "http://headers.zcl-electrum.com/blockchain_headers"
cls.CHUNK_SIZE = 200
# https://github.com/z-classic/zclassic/blob/master/src/chainparams.cpp#L234
@classmethod
def set_testnet(cls):
cls.TESTNET = True
cls.WIF_PREFIX = 0xef
cls.ADDRTYPE_P2PKH = [0x1D, 0x25]
cls.ADDRTYPE_P2SH = [0x1C, 0xBA]
cls.ADDRTYPE_SHIELDED = [0x16, 0xB6]
cls.SEGWIT_HRP = "tb" #TODO zcl has no segwit
cls.GENESIS = "03e1c4bb705c871bf9bfda3e74b7f8f86bff267993c215a89d5795e3708e5e1f"
cls.DEFAULT_PORTS = {'t': '51001', 's': '51002'}
cls.DEFAULT_SERVERS = read_json('servers_testnet.json', {})
cls.CHECKPOINTS = read_json('checkpoints_testnet.json', [])
cls.EQUIHASH_N = 200
cls.EQUIHASH_K = 9
#cls.HEADERS_URL = "http://35.224.186.7/blockchain_headers"
cls.CHUNK_SIZE = 200
NetworkConstants.set_mainnet()
################################## transactions ################################## transactions
FEE_STEP = 10000
DEFAULT_FEE_RATE = 10000
MAX_FEE_RATE = 300000
FEE_TARGETS = [25, 10, 5, 2]
COINBASE_MATURITY = 100 COINBASE_MATURITY = 100
COIN = 100000000 COIN = 100000000
@ -332,11 +254,11 @@ def ser_uint256(u):
return rs return rs
def sha256(x): def sha256(x):
if isinstance(x, str): x = to_bytes(x, 'utf8')
x = x.encode('utf8')
return bytes(hashlib.sha256(x).digest()) return bytes(hashlib.sha256(x).digest())
def Hash(x): def Hash(x):
x = to_bytes(x, 'utf8')
out = bytes(sha256(sha256(x))) out = bytes(sha256(sha256(x)))
return out return out
@ -429,16 +351,16 @@ def b58_address_to_hash160(addr):
def hash160_to_p2pkh(h160): def hash160_to_p2pkh(h160):
return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2PKH) return hash160_to_b58_address(h160, constants.net.ADDRTYPE_P2PKH)
def hash160_to_p2sh(h160): def hash160_to_p2sh(h160):
return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2SH) return hash160_to_b58_address(h160, constants.net.ADDRTYPE_P2SH)
def public_key_to_p2pkh(public_key): def public_key_to_p2pkh(public_key):
return hash160_to_p2pkh(hash_160(public_key)) return hash160_to_p2pkh(hash_160(public_key))
def hash_to_segwit_addr(h): def hash_to_segwit_addr(h):
return segwit_addr.encode(NetworkConstants.SEGWIT_HRP, 0, h) return segwit_addr.encode(constants.net.SEGWIT_HRP, 0, h)
def public_key_to_p2wpkh(public_key): def public_key_to_p2wpkh(public_key):
return hash_to_segwit_addr(hash_160(public_key)) return hash_to_segwit_addr(hash_160(public_key))
@ -484,7 +406,7 @@ def script_to_address(script):
return addr return addr
def address_to_script(addr): def address_to_script(addr):
witver, witprog = segwit_addr.decode(NetworkConstants.SEGWIT_HRP, addr) witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
if witprog is not None: if witprog is not None:
assert (0 <= witver <= 16) assert (0 <= witver <= 16)
OP_n = witver + 0x50 if witver > 0 else 0 OP_n = witver + 0x50 if witver > 0 else 0
@ -492,11 +414,11 @@ def address_to_script(addr):
script += push_script(bh2u(bytes(witprog))) script += push_script(bh2u(bytes(witprog)))
return script return script
addrtype, hash_160 = b58_address_to_hash160(addr) addrtype, hash_160 = b58_address_to_hash160(addr)
if addrtype == NetworkConstants.ADDRTYPE_P2PKH: if addrtype == constants.net.ADDRTYPE_P2PKH:
script = '76a9' # op_dup, op_hash_160 script = '76a9' # op_dup, op_hash_160
script += push_script(bh2u(hash_160)) script += push_script(bh2u(hash_160))
script += '88ac' # op_equalverify, op_checksig script += '88ac' # op_equalverify, op_checksig
elif addrtype == NetworkConstants.ADDRTYPE_P2SH: elif addrtype == constants.net.ADDRTYPE_P2SH:
script = 'a9' # op_hash_160 script = 'a9' # op_hash_160
script += push_script(bh2u(hash_160)) script += push_script(bh2u(hash_160))
script += '87' # op_equal script += '87' # op_equal
@ -600,9 +522,8 @@ def DecodeBase58Check(psz):
return key return key
# backwards compat
# extended key export format for segwit # extended WIF for segwit (used in 3.0.x; but still used internally)
SCRIPT_TYPES = { SCRIPT_TYPES = {
'p2pkh':0, 'p2pkh':0,
'p2wpkh':1, 'p2wpkh':1,
@ -613,26 +534,43 @@ SCRIPT_TYPES = {
} }
def serialize_privkey(secret, compressed, txin_type): def serialize_privkey(secret, compressed, txin_type, internal_use=False):
prefix = bytes([(SCRIPT_TYPES[txin_type]+NetworkConstants.WIF_PREFIX)&255]) if internal_use:
prefix = bytes([(SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255])
else:
prefix = bytes([constants.net.WIF_PREFIX])
suffix = b'\01' if compressed else b'' suffix = b'\01' if compressed else b''
vchIn = prefix + secret + suffix vchIn = prefix + secret + suffix
return EncodeBase58Check(vchIn) base58_wif = EncodeBase58Check(vchIn)
if internal_use:
return base58_wif
else:
return '{}:{}'.format(txin_type, base58_wif)
def deserialize_privkey(key): def deserialize_privkey(key):
# whether the pubkey is compressed should be visible from the keystore
vch = DecodeBase58Check(key)
if is_minikey(key): if is_minikey(key):
return 'p2pkh', minikey_to_private_key(key), True return 'p2pkh', minikey_to_private_key(key), True
elif vch:
txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - NetworkConstants.WIF_PREFIX] txin_type = None
assert len(vch) in [33, 34] if ':' in key:
compressed = len(vch) == 34 txin_type, key = key.split(sep=':', maxsplit=1)
return txin_type, vch[1:33], compressed assert txin_type in SCRIPT_TYPES
else: vch = DecodeBase58Check(key)
if not vch:
raise BaseException("cannot deserialize", key) raise BaseException("cannot deserialize", key)
if txin_type is None:
# keys exported in version 3.0.x encoded script type in first byte
txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - constants.net.WIF_PREFIX]
else:
assert vch[0] == constants.net.WIF_PREFIX
assert len(vch) in [33, 34]
compressed = len(vch) == 34
return txin_type, vch[1:33], compressed
def regenerate_key(pk): def regenerate_key(pk):
assert len(pk) == 32 assert len(pk) == 32
return EC_KEY(pk) return EC_KEY(pk)
@ -662,7 +600,7 @@ def address_from_private_key(sec):
def is_segwit_address(addr): def is_segwit_address(addr):
try: try:
witver, witprog = segwit_addr.decode(NetworkConstants.SEGWIT_HRP, addr) witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
except Exception as e: except Exception as e:
return False return False
return witprog is not None return witprog is not None
@ -672,7 +610,7 @@ def is_b58_address(addr):
addrtype, h = b58_address_to_hash160(addr) addrtype, h = b58_address_to_hash160(addr)
except Exception as e: except Exception as e:
return False return False
if addrtype not in [NetworkConstants.ADDRTYPE_P2PKH, NetworkConstants.ADDRTYPE_P2SH]: if addrtype not in [constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH]:
return False return False
return addr == hash160_to_b58_address(h, addrtype) return addr == hash160_to_b58_address(h, addrtype)
@ -735,8 +673,8 @@ def verify_message(address, sig, message):
return False return False
def encrypt_message(message, pubkey): def encrypt_message(message, pubkey, magic=b'BIE1'):
return EC_KEY.encrypt_message(message, bfh(pubkey)) return EC_KEY.encrypt_message(message, bfh(pubkey), magic)
def chunks(l, n): def chunks(l, n):
@ -881,7 +819,7 @@ class EC_KEY(object):
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac # ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac
@classmethod @classmethod
def encrypt_message(self, message, pubkey): def encrypt_message(self, message, pubkey, magic=b'BIE1'):
assert_bytes(message) assert_bytes(message)
pk = ser_to_point(pubkey) pk = ser_to_point(pubkey)
@ -895,20 +833,20 @@ class EC_KEY(object):
iv, key_e, key_m = key[0:16], key[16:32], key[32:] iv, key_e, key_m = key[0:16], key[16:32], key[32:]
ciphertext = aes_encrypt_with_iv(key_e, iv, message) ciphertext = aes_encrypt_with_iv(key_e, iv, message)
ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True)) ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True))
encrypted = b'BIE1' + ephemeral_pubkey + ciphertext encrypted = magic + ephemeral_pubkey + ciphertext
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest() mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
return base64.b64encode(encrypted + mac) return base64.b64encode(encrypted + mac)
def decrypt_message(self, encrypted): def decrypt_message(self, encrypted, magic=b'BIE1'):
encrypted = base64.b64decode(encrypted) encrypted = base64.b64decode(encrypted)
if len(encrypted) < 85: if len(encrypted) < 85:
raise Exception('invalid ciphertext: length') raise Exception('invalid ciphertext: length')
magic = encrypted[:4] magic_found = encrypted[:4]
ephemeral_pubkey = encrypted[4:37] ephemeral_pubkey = encrypted[4:37]
ciphertext = encrypted[37:-32] ciphertext = encrypted[37:-32]
mac = encrypted[-32:] mac = encrypted[-32:]
if magic != b'BIE1': if magic_found != magic:
raise Exception('invalid ciphertext: invalid magic bytes') raise Exception('invalid ciphertext: invalid magic bytes')
try: try:
ephemeral_pubkey = ser_to_point(ephemeral_pubkey) ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
@ -984,25 +922,35 @@ def _CKD_pub(cK, c, s):
return cK_n, c_n return cK_n, c_n
def xprv_header(xtype): def xprv_header(xtype, *, net=None):
return bfh("%08x" % XPRV_HEADERS[xtype]) if net is None:
net = constants.net
return bfh("%08x" % net.XPRV_HEADERS[xtype])
def xpub_header(xtype): def xpub_header(xtype, *, net=None):
return bfh("%08x" % XPUB_HEADERS[xtype]) if net is None:
net = constants.net
return bfh("%08x" % net.XPUB_HEADERS[xtype])
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4): def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4,
xprv = xprv_header(xtype) + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k child_number=b'\x00'*4, *, net=None):
xprv = xprv_header(xtype, net=net) \
+ bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
return EncodeBase58Check(xprv) return EncodeBase58Check(xprv)
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4): def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
xpub = xpub_header(xtype) + bytes([depth]) + fingerprint + child_number + c + cK child_number=b'\x00'*4, *, net=None):
xpub = xpub_header(xtype, net=net) \
+ bytes([depth]) + fingerprint + child_number + c + cK
return EncodeBase58Check(xpub) return EncodeBase58Check(xpub)
def deserialize_xkey(xkey, prv): def deserialize_xkey(xkey, prv, *, net=None):
if net is None:
net = constants.net
xkey = DecodeBase58Check(xkey) xkey = DecodeBase58Check(xkey)
if len(xkey) != 78: if len(xkey) != 78:
raise BaseException('Invalid length') raise BaseException('Invalid length')
@ -1011,7 +959,7 @@ def deserialize_xkey(xkey, prv):
child_number = xkey[9:13] child_number = xkey[9:13]
c = xkey[13:13+32] c = xkey[13:13+32]
header = int('0x' + bh2u(xkey[0:4]), 16) header = int('0x' + bh2u(xkey[0:4]), 16)
headers = XPRV_HEADERS if prv else XPUB_HEADERS headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
if header not in headers.values(): if header not in headers.values():
raise BaseException('Invalid xpub format', hex(header)) raise BaseException('Invalid xpub format', hex(header))
xtype = list(headers.keys())[list(headers.values()).index(header)] xtype = list(headers.keys())[list(headers.values()).index(header)]
@ -1020,11 +968,11 @@ def deserialize_xkey(xkey, prv):
return xtype, depth, fingerprint, child_number, c, K_or_k return xtype, depth, fingerprint, child_number, c, K_or_k
def deserialize_xpub(xkey): def deserialize_xpub(xkey, *, net=None):
return deserialize_xkey(xkey, False) return deserialize_xkey(xkey, False, net=net)
def deserialize_xprv(xkey): def deserialize_xprv(xkey, *, net=None):
return deserialize_xkey(xkey, True) return deserialize_xkey(xkey, True, net=net)
def xpub_type(x): def xpub_type(x):
return deserialize_xpub(x)[0] return deserialize_xpub(x)[0]

View File

@ -27,6 +27,7 @@ from io import BytesIO
from . import util from . import util
from . import bitcoin from . import bitcoin
from . import constants
from .bitcoin import * from .bitcoin import *
import base64 import base64
@ -84,7 +85,10 @@ def hash_header(header):
return '0' * 64 return '0' * 64
if header.get('prev_block_hash') is None: if header.get('prev_block_hash') is None:
header['prev_block_hash'] = '00'*64 header['prev_block_hash'] = '00'*64
return hash_to_str(Hash(serialize_header(header))) '''
TODO 32?
'''
return hash_encode(Hash(bfh(serialize_header(header))))
blockchains = {} blockchains = {}
@ -131,7 +135,7 @@ class Blockchain(util.PrintError):
self.config = config self.config = config
self.catch_up = None # interface catching up self.catch_up = None # interface catching up
self.checkpoint = checkpoint self.checkpoint = checkpoint
self.checkpoints = NetworkConstants.CHECKPOINTS self.checkpoints = constants.net.CHECKPOINTS
self.parent_id = parent_id self.parent_id = parent_id
self.lock = threading.Lock() self.lock = threading.Lock()
with self.lock: with self.lock:
@ -175,34 +179,34 @@ class Blockchain(util.PrintError):
def update_size(self): def update_size(self):
p = self.path() p = self.path()
self._size = os.path.getsize(p)//bitcoin.HEADER_SIZE if os.path.exists(p) else 0 self._size = os.path.getsize(p)//80 if os.path.exists(p) else 0
def verify_header(self, header, prev_header): def verify_header(self, header, prev_hash, target):
if prev_header: _hash = hash_header(header)
prev_hash = hash_header(prev_header) if prev_hash != header.get('prev_block_hash'):
if prev_hash != header.get('prev_block_hash'): raise BaseException("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
raise BaseException("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) if constants.net.TESTNET:
_powhash = sha256_header(header) return
target = self.bits_to_target(header['bits']) bits = self.target_to_bits(target)
if _powhash > target: if bits != header.get('bits'):
raise BaseException("insufficient proof of work: %s vs target %s" % (int('0x' + _powhash, 16), target)) raise BaseException("bits mismatch: %s vs %s" % (bits, header.get('bits')))
if int('0x' + _hash, 16) > target:
raise BaseException("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target))
nonce = uint256_from_bytes(str_to_hash(header.get('nonce'))) nonce = uint256_from_bytes(str_to_hash(header.get('nonce')))
n_solution = vector_from_bytes(base64.b64decode(header.get('n_solution').encode('utf8'))) n_solution = vector_from_bytes(base64.b64decode(header.get('n_solution').encode('utf8')))
if not is_gbp_valid(serialize_header(header), nonce, n_solution, if not is_gbp_valid(serialize_header(header), nonce, n_solution,
NetworkConstants.EQUIHASH_N, NetworkConstants.EQUIHASH_K): constants.net.EQUIHASH_N, constants.net.EQUIHASH_K):
raise BaseException("Equihash invalid") raise BaseException("Equihash invalid")
def verify_chunk(self, index, data): def verify_chunk(self, index, data):
num = len(data) // bitcoin.HEADER_SIZE num = len(data) // bitcoin.HEADER_SIZE
prev_header = None prev_hash = self.get_hash(index * constants.net.CHUNK_SIZE - 1)
if index != 0: target = self.get_target(index-1)
prev_header = self.read_header(index * NetworkConstants.CHUNK_SIZE - 1)
for i in range(num): for i in range(num):
raw_header = data[i*bitcoin.HEADER_SIZE:(i+1) * bitcoin.HEADER_SIZE] raw_header = data[i*bitcoin.HEADER_SIZE:(i+1) * bitcoin.HEADER_SIZE]
header = deserialize_header(raw_header, index*NetworkConstants.CHUNK_SIZE + i) header = deserialize_header(raw_header, index*constants.net.CHUNK_SIZE + i)
self.verify_header(header, prev_header) self.verify_header(header, prev_hash, target)
prev_header = header prev_hash = hash_header(header)
def path(self): def path(self):
d = util.get_headers_dir(self.config) d = util.get_headers_dir(self.config)
@ -211,11 +215,12 @@ class Blockchain(util.PrintError):
def save_chunk(self, index, chunk): def save_chunk(self, index, chunk):
filename = self.path() filename = self.path()
d = (index * NetworkConstants.CHUNK_SIZE - self.checkpoint) * bitcoin.HEADER_SIZE d = (index * constants.net.CHUNK_SIZE - self.checkpoint) * bitcoin.HEADER_SIZE
if d < 0: if d < 0:
chunk = chunk[-d:] chunk = chunk[-d:]
d = 0 d = 0
self.write(chunk, d) truncate = index >= len(self.checkpoints)
self.write(chunk, d, truncate)
self.swap_with_parent() self.swap_with_parent()
def swap_with_parent(self): def swap_with_parent(self):
@ -253,11 +258,11 @@ class Blockchain(util.PrintError):
blockchains[self.checkpoint] = self blockchains[self.checkpoint] = self
blockchains[parent.checkpoint] = parent blockchains[parent.checkpoint] = parent
def write(self, data, offset): def write(self, data, offset, truncate=True):
filename = self.path() filename = self.path()
with self.lock: with self.lock:
with open(filename, 'rb+') as f: with open(filename, 'rb+') as f:
if offset != self._size*bitcoin.HEADER_SIZE: if truncate and offset != self._size*bitcoin.HEADER_SIZE:
f.seek(offset) f.seek(offset)
f.truncate() f.truncate()
f.seek(offset) f.seek(offset)
@ -268,7 +273,7 @@ class Blockchain(util.PrintError):
def save_header(self, header): def save_header(self, header):
delta = header.get('block_height') - self.checkpoint delta = header.get('block_height') - self.checkpoint
data = serialize_header(header) data = bfh(serialize_header(header))
assert delta == self.size() assert delta == self.size()
assert len(data) == bitcoin.HEADER_SIZE assert len(data) == bitcoin.HEADER_SIZE
self.write(data, delta*bitcoin.HEADER_SIZE) self.write(data, delta*bitcoin.HEADER_SIZE)
@ -289,25 +294,52 @@ class Blockchain(util.PrintError):
with open(name, 'rb') as f: with open(name, 'rb') as f:
f.seek(delta * bitcoin.HEADER_SIZE) f.seek(delta * bitcoin.HEADER_SIZE)
h = f.read(bitcoin.HEADER_SIZE) h = f.read(bitcoin.HEADER_SIZE)
if h == bytes([0])*bitcoin.HEADER_SIZE:
return None
return deserialize_header(h, height) return deserialize_header(h, height)
def get_hash(self, height): def get_hash(self, height):
return self.hash_header(self.read_header(height)) if height == -1:
return '0000000000000000000000000000000000000000000000000000000000000000'
elif height == 0:
return constants.net.GENESIS
elif height < len(self.checkpoints) * constants.net.CHUNK_SIZE:
assert (height+1) % constants.net.CHUNK_SIZE == 0, height
index = height // constants.net.CHUNK_SIZE
h, t = self.checkpoints[index]
return h
else:
return hash_header(self.read_header(height))
def hash_header(self, header): def get_target(self, index):
return hash_header(header) # compute target from chunk x, used in chunk x+1
if constants.net.TESTNET:
return 0
if index == -1:
return MAX_TARGET
if index < len(self.checkpoints):
h, t = self.checkpoints[index]
return t
# new target
first = self.read_header(index * constants.net.CHUNK_SIZE)
last = self.read_header(index * constants.net.CHUNK_SIZE + (constants.net.CHUNK_SIZE - 1))
bits = last.get('bits')
target = self.bits_to_target(bits)
nActualTimespan = last.get('timestamp') - first.get('timestamp')
nTargetTimespan = 14 * 24 * 60 * 60
nActualTimespan = max(nActualTimespan, nTargetTimespan // 4)
nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)
new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan)
return new_target
def bits_to_target(self, bits): def bits_to_target(self, bits):
bitsN = (bits >> 24) & 0xff bitsN = (bits >> 24) & 0xff
# if not (bitsN >= 0x03 and bitsN <= 0x1d): if not (bitsN >= 0x03 and bitsN <= 0x1d):
# raise BaseException("First part of bits should be in [0x03, 0x1d]") raise BaseException("First part of bits should be in [0x03, 0x1d]")
bitsBase = bits & 0xffffff bitsBase = bits & 0xffffff
# if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff): if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff):
# raise BaseException("Second part of bits should be in [0x8000, 0x7fffff]") raise BaseException("Second part of bits should be in [0x8000, 0x7fffff]")
if bitsN <= 3: return bitsBase << (8 * (bitsN-3))
return bitsBase >> (8 * (3 - bitsN))
else:
return bitsBase << (8 * (bitsN - 3))
def target_to_bits(self, target): def target_to_bits(self, target):
c = ("%064x" % target)[2:] c = ("%064x" % target)[2:]
@ -320,38 +352,42 @@ class Blockchain(util.PrintError):
return bitsN << 24 | bitsBase return bitsN << 24 | bitsBase
def can_connect(self, header, check_height=True): def can_connect(self, header, check_height=True):
# import pdb; pdb.set_trace()
height = header['block_height'] height = header['block_height']
if check_height and self.height() != height - 1: if check_height and self.height() != height - 1:
self.print_error("cannot connect at height", height) self.print_error("cannot connect at height", height)
return False return False
if height == 0: if height == 0:
return hash_header(header) == NetworkConstants.GENESIS return hash_header(header) == constants.net.GENESIS
try: try:
prev_header = self.read_header(height - 1) prev_hash = self.get_hash(height - 1)
prev_hash = self.hash_header(prev_header)
except: except:
return False return False
if prev_hash != header.get('prev_block_hash'): if prev_hash != header.get('prev_block_hash'):
return False return False
target = self.get_target(height // constants.net.CHUNK_SIZE - 1)
try: try:
self.verify_header(header, prev_header) self.verify_header(header, prev_hash, target)
except BaseException as e: except BaseException as e:
import traceback
traceback.print_exc()
self.print_error('verify_header failed', str(e))
return False return False
return True return True
def connect_chunk(self, idx, hexdata): def connect_chunk(self, idx, hexdata):
try: try:
data = bytes.fromhex(hexdata) data = bfh(hexdata)
self.verify_chunk(idx, data) self.verify_chunk(idx, data)
self.print_error("validated chunk %d" % idx) #self.print_error("validated chunk %d" % idx)
self.save_chunk(idx, data) self.save_chunk(idx, data)
return True return True
except BaseException as e: except BaseException as e:
import traceback self.print_error('verify_chunk %d failed'%idx, str(e))
traceback.print_exc()
self.print_error('verify_chunk failed', str(e))
return False return False
def get_checkpoints(self):
# for each chunk, store the hash of the last block and the target after the chunk
cp = []
n = self.height() // 2016
for index in range(n):
h = self.get_hash((index+1) * 2016 -1)
target = self.get_target(index)
cp.append((h, target))
return cp

View File

@ -25,7 +25,7 @@
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from math import floor, log10 from math import floor, log10
from .bitcoin import sha256, COIN, TYPE_ADDRESS from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
from .transaction import Transaction from .transaction import Transaction
from .util import NotEnoughFunds, PrintError from .util import NotEnoughFunds, PrintError
@ -87,6 +87,8 @@ def strip_unneeded(bkts, sufficient_funds):
class CoinChooserBase(PrintError): class CoinChooserBase(PrintError):
enable_output_value_rounding = False
def keys(self, coins): def keys(self, coins):
raise NotImplementedError raise NotImplementedError
@ -135,7 +137,13 @@ class CoinChooserBase(PrintError):
zeroes = [trailing_zeroes(i) for i in output_amounts] zeroes = [trailing_zeroes(i) for i in output_amounts]
min_zeroes = min(zeroes) min_zeroes = min(zeroes)
max_zeroes = max(zeroes) max_zeroes = max(zeroes)
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
if n > 1:
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
else:
# if there is only one change output, this will ensure that we aim
# to have one that is exactly as precise as the most precise output
zeroes = [min_zeroes]
# Calculate change; randomize it a bit if using more than 1 output # Calculate change; randomize it a bit if using more than 1 output
remaining = change_amount remaining = change_amount
@ -150,8 +158,10 @@ class CoinChooserBase(PrintError):
n -= 1 n -= 1
# Last change output. Round down to maximum precision but lose # Last change output. Round down to maximum precision but lose
# no more than 100 satoshis to fees (2dp) # no more than 10**max_dp_to_round_for_privacy
N = pow(10, min(2, zeroes[0])) # 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]))
amount = (remaining // N) * N amount = (remaining // N) * N
amounts.append(amount) amounts.append(amount)
@ -230,6 +240,13 @@ class CoinChooserBase(PrintError):
tx.add_inputs([coin for b in buckets for coin in b.coins]) tx.add_inputs([coin for b in buckets for coin in b.coins])
tx_weight = get_tx_weight(buckets) tx_weight = get_tx_weight(buckets)
# change is sent back to sending address unless specified
if not change_addrs:
change_addrs = [tx.inputs()[0]['address']]
# note: this is not necessarily the final "first input address"
# because the inputs had not been sorted at this point
assert is_address(change_addrs[0])
# This takes a count of change outputs and returns a tx fee # This takes a count of change outputs and returns a tx fee
output_weight = 4 * Transaction.estimated_output_size(change_addrs[0]) output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
fee = lambda count: fee_estimator_w(tx_weight + count * output_weight) fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
@ -273,6 +290,8 @@ class CoinChooserRandom(CoinChooserBase):
candidates.add(tuple(sorted(permutation[:count + 1]))) candidates.add(tuple(sorted(permutation[:count + 1])))
break break
else: 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.
raise NotEnoughFunds() raise NotEnoughFunds()
candidates = [[buckets[n] for n in c] for c in candidates] candidates = [[buckets[n] for n in c] for c in candidates]
@ -284,7 +303,7 @@ class CoinChooserRandom(CoinChooserBase):
Any bucket can be: Any bucket can be:
1. "confirmed" if it only contains confirmed coins; else 1. "confirmed" if it only contains confirmed coins; else
2. "unconfirmed" if it does not contain coins with unconfirmed parents 2. "unconfirmed" if it does not contain coins with unconfirmed parents
3. "unconfirmed parent" otherwise 3. other: e.g. "unconfirmed parent" or "local"
This method tries to only use buckets of type 1, and if the coins there This method tries to only use buckets of type 1, and if the coins there
are not enough, tries to use the next type but while also selecting are not enough, tries to use the next type but while also selecting
@ -292,9 +311,9 @@ class CoinChooserRandom(CoinChooserBase):
""" """
conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0] conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0]
unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0] unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0]
unconf_par_buckets = [bkt for bkt in buckets if bkt.min_height == -1] other_buckets = [bkt for bkt in buckets if bkt.min_height < 0]
bucket_sets = [conf_buckets, unconf_buckets, unconf_par_buckets] bucket_sets = [conf_buckets, unconf_buckets, other_buckets]
already_selected_buckets = [] already_selected_buckets = []
for bkts_choose_from in bucket_sets: for bkts_choose_from in bucket_sets:
@ -368,4 +387,6 @@ def get_name(config):
def get_coin_chooser(config): def get_coin_chooser(config):
klass = COIN_CHOOSERS[get_name(config)] klass = COIN_CHOOSERS[get_name(config)]
return klass() coinchooser = klass()
coinchooser.enable_output_value_rounding = config.get('coin_chooser_output_rounding', False)
return coinchooser

View File

@ -34,7 +34,7 @@ from functools import wraps
from decimal import Decimal from decimal import Decimal
from .import util from .import util
from .util import bfh, bh2u, format_satoshis from .util import bfh, bh2u, format_satoshis, json_decode, print_error
from .import bitcoin from .import bitcoin
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
from .i18n import _ from .i18n import _
@ -82,7 +82,7 @@ def command(s):
password = kwargs.get('password') password = kwargs.get('password')
if c.requires_wallet and wallet is None: if c.requires_wallet and wallet is None:
raise BaseException("wallet not loaded. Use 'electrum daemon load_wallet'") raise BaseException("wallet not loaded. Use 'electrum daemon load_wallet'")
if c.requires_password and password is None and wallet.storage.get('use_encryption'): if c.requires_password and password is None and wallet.has_password():
return {'error': 'Password required' } return {'error': 'Password required' }
return func(*args, **kwargs) return func(*args, **kwargs)
return func_wrapper return func_wrapper
@ -138,6 +138,8 @@ class Commands:
@command('wp') @command('wp')
def password(self, password=None, new_password=None): def password(self, password=None, new_password=None):
"""Change wallet password. """ """Change wallet password. """
if self.wallet.storage.is_encrypted_with_hw_device() and new_password:
raise Exception("Can't change the password of a wallet encrypted with a hw device.")
b = self.wallet.storage.is_encrypted() b = self.wallet.storage.is_encrypted()
self.wallet.update_password(password, new_password, b) self.wallet.update_password(password, new_password, b)
self.wallet.storage.write() self.wallet.storage.write()
@ -151,10 +153,8 @@ class Commands:
@command('') @command('')
def setconfig(self, key, value): def setconfig(self, key, value):
"""Set a configuration variable. 'value' may be a string or a Python expression.""" """Set a configuration variable. 'value' may be a string or a Python expression."""
try: if key not in ('rpcuser', 'rpcpassword'):
value = ast.literal_eval(value) value = json_decode(value)
except:
pass
self.config.set_key(key, value) self.config.set_key(key, value)
return True return True
@ -177,7 +177,8 @@ class Commands:
"""Return the transaction history of any address. Note: This is a """Return the transaction history of any address. Note: This is a
walletless server query, results are not checked by SPV. walletless server query, results are not checked by SPV.
""" """
return self.network.synchronous_get(('blockchain.address.get_history', [address])) sh = bitcoin.address_to_scripthash(address)
return self.network.synchronous_get(('blockchain.scripthash.get_history', [sh]))
@command('w') @command('w')
def listunspent(self): def listunspent(self):
@ -194,7 +195,8 @@ class Commands:
"""Returns the UTXO list of any address. Note: This """Returns the UTXO list of any address. Note: This
is a walletless server query, results are not checked by SPV. is a walletless server query, results are not checked by SPV.
""" """
return self.network.synchronous_get(('blockchain.address.listunspent', [address])) sh = bitcoin.address_to_scripthash(address)
return self.network.synchronous_get(('blockchain.scripthash.listunspent', [sh]))
@command('') @command('')
def serialize(self, jsontx): def serialize(self, jsontx):
@ -316,20 +318,12 @@ class Commands:
"""Return the balance of any address. Note: This is a walletless """Return the balance of any address. Note: This is a walletless
server query, results are not checked by SPV. server query, results are not checked by SPV.
""" """
out = self.network.synchronous_get(('blockchain.address.get_balance', [address])) sh = bitcoin.address_to_scripthash(address)
out = self.network.synchronous_get(('blockchain.scripthash.get_balance', [sh]))
out["confirmed"] = str(Decimal(out["confirmed"])/COIN) out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN) out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
return out return out
@command('n')
def getproof(self, address):
"""Get Merkle branch of an address in the UTXO set"""
p = self.network.synchronous_get(('blockchain.address.get_proof', [address]))
out = []
for i,s in p:
out.append(i)
return out
@command('n') @command('n')
def getmerkle(self, txid, height): def getmerkle(self, txid, height):
"""Get Merkle branch of a transaction included in a block. Electrum """Get Merkle branch of a transaction included in a block. Electrum
@ -448,46 +442,20 @@ class Commands:
return tx.as_dict() return tx.as_dict()
@command('w') @command('w')
def history(self): def history(self, year=None, show_addresses=False, show_fiat=False):
"""Wallet history. Returns the transaction history of your wallet.""" """Wallet history. Returns the transaction history of your wallet."""
balance = 0 kwargs = {'show_addresses': show_addresses}
out = [] if year:
for item in self.wallet.get_history(): import time
tx_hash, height, conf, timestamp, value, balance = item start_date = datetime.datetime(year, 1, 1)
if timestamp: end_date = datetime.datetime(year+1, 1, 1)
date = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
else: kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
date = "----" if show_fiat:
label = self.wallet.get_label(tx_hash) from .exchange_rate import FxThread
tx = self.wallet.transactions.get(tx_hash) fx = FxThread(self.config, None)
tx.deserialize() kwargs['fx'] = fx
input_addresses = [] return self.wallet.get_full_history(**kwargs)
output_addresses = []
for x in tx.inputs():
if x['type'] == 'coinbase': continue
addr = x.get('address')
if addr == None: continue
if addr == "(pubkey)":
prevout_hash = x.get('prevout_hash')
prevout_n = x.get('prevout_n')
_addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n)
if _addr:
addr = _addr
input_addresses.append(addr)
for addr, v in tx.get_outputs():
output_addresses.append(addr)
out.append({
'txid': tx_hash,
'timestamp': timestamp,
'date': date,
'input_addresses': input_addresses,
'output_addresses': output_addresses,
'label': label,
'value': str(Decimal(value)/COIN) if value is not None else None,
'height': height,
'confirmations': conf
})
return out
@command('w') @command('w')
def setlabel(self, key, label): def setlabel(self, key, label):
@ -631,6 +599,15 @@ class Commands:
out = self.wallet.get_payment_request(addr, self.config) out = self.wallet.get_payment_request(addr, self.config)
return self._format_request(out) return self._format_request(out)
@command('w')
def addtransaction(self, tx):
""" Add a transaction to the wallet history """
tx = Transaction(tx)
if not self.wallet.add_transaction(tx.txid(), tx):
return False
self.wallet.save_transactions()
return tx.txid()
@command('wp') @command('wp')
def signrequest(self, address, password=None): def signrequest(self, address, password=None):
"Sign payment request with an OpenAlias" "Sign payment request with an OpenAlias"
@ -658,13 +635,15 @@ class Commands:
import urllib.request import urllib.request
headers = {'content-type':'application/json'} headers = {'content-type':'application/json'}
data = {'address':address, 'status':x.get('result')} data = {'address':address, 'status':x.get('result')}
serialized_data = util.to_bytes(json.dumps(data))
try: try:
req = urllib.request.Request(URL, json.dumps(data), headers) req = urllib.request.Request(URL, serialized_data, headers)
response_stream = urllib.request.urlopen(req, timeout=5) response_stream = urllib.request.urlopen(req, timeout=5)
util.print_error('Got Response for %s' % address) util.print_error('Got Response for %s' % address)
except BaseException as e: except BaseException as e:
util.print_error(str(e)) util.print_error(str(e))
self.network.send([('blockchain.address.subscribe', [address])], callback) h = self.network.addr_to_scripthash(address)
self.network.send([('blockchain.scripthash.subscribe', [h])], callback)
return True return True
@command('wn') @command('wn')
@ -672,6 +651,12 @@ class Commands:
""" return wallet synchronization status """ """ return wallet synchronization status """
return self.wallet.is_up_to_date() return self.wallet.is_up_to_date()
@command('n')
def getfeerate(self):
"""Return current optimal fee rate per kilobyte, according
to config settings (static/dynamic)"""
return self.config.fee_per_kb()
@command('') @command('')
def help(self): def help(self):
# for the python console # for the python console
@ -727,6 +712,9 @@ command_options = {
'pending': (None, "Show only pending requests."), 'pending': (None, "Show only pending requests."),
'expired': (None, "Show only expired requests."), 'expired': (None, "Show only expired requests."),
'paid': (None, "Show only paid requests."), 'paid': (None, "Show only paid requests."),
'show_addresses': (None, "Show input and output addresses"),
'show_fiat': (None, "Show fiat value of transactions"),
'year': (None, "Show history for a given year"),
} }
@ -737,6 +725,7 @@ arg_types = {
'num': int, 'num': int,
'nbits': int, 'nbits': int,
'imax': int, 'imax': int,
'year': int,
'entropy': int, 'entropy': int,
'tx': tx_from_str, 'tx': tx_from_str,
'pubkeys': json_loads, 'pubkeys': json_loads,
@ -800,7 +789,7 @@ def subparser_call(self, parser, namespace, values, option_string=None):
parser = self._name_parser_map[parser_name] parser = self._name_parser_map[parser_name]
except KeyError: except KeyError:
tup = parser_name, ', '.join(self._name_parser_map) tup = parser_name, ', '.join(self._name_parser_map)
msg = _('unknown parser %r (choices: %s)') % tup msg = _('unknown parser {!r} (choices: {})').format(*tup)
raise ArgumentError(self, msg) raise ArgumentError(self, msg)
# parse all the remaining options into the namespace # parse all the remaining options into the namespace
# store any unrecognized options on the object, so that the top # store any unrecognized options on the object, so that the top
@ -825,7 +814,6 @@ def add_global_options(parser):
group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory") group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path") group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet") group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
group.add_argument("--nossl", action="store_true", dest="nossl", default=False, help="Disable SSL")
def get_parser(): def get_parser():
# create main parser # create main parser

121
lib/constants.py Normal file
View File

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2018 The Electrum developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import json
def read_json(filename, default):
path = os.path.join(os.path.dirname(__file__), filename)
try:
with open(path, 'r') as f:
r = json.loads(f.read())
except:
r = default
return r
class BitcoinMainnet:
TESTNET = False
WIF_PREFIX = 0x80
ADDRTYPE_P2PKH = [0x1C, 0xB8]
ADDRTYPE_P2SH = [0x1C, 0xBD]
ADDRTYPE_SHIELDED = [0x16, 0x9A]
SEGWIT_HRP = "bc" # (No ZCL Segwit)
GENESIS = "0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602"
DEFAULT_PORTS = {'t': '50001', 's': '50002'}
DEFAULT_SERVERS = read_json('servers.json', {})
CHECKPOINTS = read_json('checkpoints.json', [])
EQUIHASH_N = 200
EQUIHASH_K = 9
CHUNK_SIZE = 200
HEADERS_URL = "http://headers.zcl-electrum.com/blockchain_headers"
XPRV_HEADERS = {
'standard': 0x0488ade4, # xprv
'p2wpkh-p2sh': 0x049d7878, # yprv
'p2wsh-p2sh': 0x0295b005, # Yprv
'p2wpkh': 0x04b2430c, # zprv
'p2wsh': 0x02aa7a99, # Zprv
}
XPUB_HEADERS = {
'standard': 0x0488b21e, # xpub
'p2wpkh-p2sh': 0x049d7cb2, # ypub
'p2wsh-p2sh': 0x0295b43f, # Ypub
'p2wpkh': 0x04b24746, # zpub
'p2wsh': 0x02aa7ed3, # Zpub
}
class BitcoinTestnet:
TESTNET = True
WIF_PREFIX = 0xef
ADDRTYPE_P2PKH = [0x1D, 0x25]
ADDRTYPE_P2SH = [0x1C, 0xBA]
ADDTYPE_SHIELDED = [0x16, 0xB6]
SEGWIT_HRP = "tb" # (ZCL has no Segwit)
GENESIS = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
DEFAULT_PORTS = {'t': '51001', 's': '51002'}
DEFAULT_SERVERS = read_json('servers_testnet.json', {})
CHECKPOINTS = read_json('checkpoints_testnet.json', [])
EQUIHASH_N = 200
EQUIHASH_K = 9
CHUNK_SIZE = 200
XPRV_HEADERS = {
'standard': 0x04358394, # tprv
'p2wpkh-p2sh': 0x044a4e28, # uprv
'p2wsh-p2sh': 0x024285b5, # Uprv
'p2wpkh': 0x045f18bc, # vprv
'p2wsh': 0x02575048, # Vprv
}
XPUB_HEADERS = {
'standard': 0x043587cf, # tpub
'p2wpkh-p2sh': 0x044a5262, # upub
'p2wsh-p2sh': 0x024285ef, # Upub
'p2wpkh': 0x045f1cf6, # vpub
'p2wsh': 0x02575483, # Vpub
}
# don't import net directly, import the module instead (so that net is singleton)
net = BitcoinMainnet
def set_mainnet():
global net
net = BitcoinMainnet
def set_testnet():
global net
net = BitcoinTestnet

View File

@ -23,9 +23,12 @@
import re import re
import dns import dns
import json import json
import traceback
import sys
from . import bitcoin from . import bitcoin
from . import dnssec from . import dnssec
from .util import export_meta, import_meta
class Contacts(dict): class Contacts(dict):
@ -48,14 +51,15 @@ class Contacts(dict):
self.storage.put('contacts', dict(self)) self.storage.put('contacts', dict(self))
def import_file(self, path): def import_file(self, path):
try: import_meta(path, self._validate, self.load_meta)
with open(path, 'r') as f:
d = self._validate(json.loads(f.read())) def load_meta(self, data):
except: self.update(data)
return
self.update(d)
self.save() self.save()
def export_file(self, filename):
export_meta(self, filename)
def __setitem__(self, key, value): def __setitem__(self, key, value):
dict.__setitem__(self, key, value) dict.__setitem__(self, key, value)
self.save() self.save()
@ -113,13 +117,13 @@ class Contacts(dict):
return None return None
def _validate(self, data): def _validate(self, data):
for k,v in list(data.items()): for k, v in list(data.items()):
if k == 'contacts': if k == 'contacts':
return self._validate(v) return self._validate(v)
if not bitcoin.is_address(k): if not bitcoin.is_address(k):
data.pop(k) data.pop(k)
else: else:
_type,_ = v _type, _ = v
if _type != 'address': if _type != 'address':
data.pop(k) data.pop(k)
return data return data

View File

@ -2,6 +2,7 @@
"CoinMarketCap": [ "CoinMarketCap": [
"USD", "USD",
"EUR", "EUR",
"CNY" "RUB",
"USD"
] ]
} }

View File

@ -25,6 +25,8 @@
import ast import ast
import os import os
import time import time
import traceback
import sys
# from jsonrpc import JSONRPCResponseManager # from jsonrpc import JSONRPCResponseManager
import jsonrpclib import jsonrpclib
@ -121,13 +123,12 @@ class Daemon(DaemonThread):
self.config = config self.config = config
if config.get('offline'): if config.get('offline'):
self.network = None self.network = None
self.fx = None
else: else:
self.network = Network(config) self.network = Network(config)
self.network.start() self.network.start()
self.fx = FxThread(config, self.network) self.fx = FxThread(config, self.network)
if self.network:
self.network.add_jobs([self.fx]) self.network.add_jobs([self.fx])
self.gui = None self.gui = None
self.wallets = {} self.wallets = {}
# Setup JSONRPC server # Setup JSONRPC server
@ -173,7 +174,7 @@ class Daemon(DaemonThread):
path = config.get_wallet_path() path = config.get_wallet_path()
wallet = self.load_wallet(path, config.get('password')) wallet = self.load_wallet(path, config.get('password'))
self.cmd_runner.wallet = wallet self.cmd_runner.wallet = wallet
response = True response = wallet is not None
elif sub == 'close_wallet': elif sub == 'close_wallet':
path = config.get_wallet_path() path = config.get_wallet_path()
if path in self.wallets: if path in self.wallets:
@ -301,4 +302,8 @@ class Daemon(DaemonThread):
gui_name = 'qt' gui_name = 'qt'
gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui']) gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui'])
self.gui = gui.ElectrumGui(config, self, plugins) self.gui = gui.ElectrumGui(config, self, plugins)
self.gui.main() try:
self.gui.main()
except BaseException as e:
traceback.print_exc(file=sys.stdout)
# app will exit now

View File

@ -2,9 +2,12 @@ from datetime import datetime
import inspect import inspect
import requests import requests
import sys import sys
import os
import json
from threading import Thread from threading import Thread
import time import time
import csv import csv
import decimal
from decimal import Decimal from decimal import Decimal
from .bitcoin import COIN from .bitcoin import COIN
@ -13,15 +16,12 @@ from .util import PrintError, ThreadJob
# See https://en.wikipedia.org/wiki/ISO_4217 # See https://en.wikipedia.org/wiki/ISO_4217
CCY_PRECISIONS = {} CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
''' 'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,
{'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, 'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3,
'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0, 'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0,
'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3, 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0, 'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
'''
class ExchangeBase(PrintError): class ExchangeBase(PrintError):
@ -34,7 +34,7 @@ class ExchangeBase(PrintError):
def get_json(self, site, get_string): def get_json(self, site, get_string):
# APIs must have https # APIs must have https
url = ''.join(['https://', site, get_string]) url = ''.join(['https://', site, get_string])
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}) response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}, timeout=10)
return response.json() return response.json()
def get_csv(self, site, get_string): def get_csv(self, site, get_string):
@ -60,28 +60,54 @@ class ExchangeBase(PrintError):
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
def get_historical_rates_safe(self, ccy): def read_historical_rates(self, ccy, cache_dir):
filename = os.path.join(cache_dir, self.name() + '_'+ ccy)
if os.path.exists(filename):
timestamp = os.stat(filename).st_mtime
try:
with open(filename, 'r') as f:
h = json.loads(f.read())
h['timestamp'] = timestamp
except:
h = None
else:
h = None
if h:
self.history[ccy] = h
self.on_history()
return h
def get_historical_rates_safe(self, ccy, cache_dir):
try: try:
self.print_error("requesting fx history for", ccy) self.print_error("requesting fx history for", ccy)
self.history[ccy] = self.historical_rates(ccy) h = self.request_history(ccy)
self.print_error("received fx history for", ccy) self.print_error("received fx history for", ccy)
self.on_history()
except BaseException as e: except BaseException as e:
self.print_error("failed fx history:", e) self.print_error("failed fx history:", e)
return
filename = os.path.join(cache_dir, self.name() + '_' + ccy)
with open(filename, 'w') as f:
f.write(json.dumps(h))
h['timestamp'] = time.time()
self.history[ccy] = h
self.on_history()
def get_historical_rates(self, ccy): def get_historical_rates(self, ccy, cache_dir):
result = self.history.get(ccy) if ccy not in self.history_ccys():
if not result and ccy in self.history_ccys(): return
t = Thread(target=self.get_historical_rates_safe, args=(ccy,)) h = self.history.get(ccy)
if h is None:
h = self.read_historical_rates(ccy, cache_dir)
if h is None or h['timestamp'] < time.time() - 24*3600:
t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir))
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
return result
def history_ccys(self): def history_ccys(self):
return [] return []
def historical_rate(self, ccy, d_t): def historical_rate(self, ccy, d_t):
return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d')) return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'), 'NaN')
def get_currencies(self): def get_currencies(self):
rates = self.get_rates('') rates = self.get_rates('')
@ -126,7 +152,9 @@ def get_exchanges_and_currencies():
exchange = klass(None, None) exchange = klass(None, None)
try: try:
d[name] = exchange.get_currencies() d[name] = exchange.get_currencies()
print(name, "ok")
except: except:
print(name, "error")
continue continue
with open(path, 'w') as f: with open(path, 'w') as f:
f.write(json.dumps(d, indent=4, sort_keys=True)) f.write(json.dumps(d, indent=4, sort_keys=True))
@ -157,7 +185,10 @@ class FxThread(ThreadJob):
self.history_used_spot = False self.history_used_spot = False
self.ccy_combo = None self.ccy_combo = None
self.hist_checkbox = None self.hist_checkbox = None
self.cache_dir = os.path.join(config.path, 'cache')
self.set_exchange(self.config_exchange()) self.set_exchange(self.config_exchange())
if not os.path.exists(self.cache_dir):
os.mkdir(self.cache_dir)
def get_currencies(self, h): def get_currencies(self, h):
d = get_exchanges_by_ccy(h) d = get_exchanges_by_ccy(h)
@ -170,13 +201,17 @@ class FxThread(ThreadJob):
def ccy_amount_str(self, amount, commas): def ccy_amount_str(self, amount, commas):
prec = CCY_PRECISIONS.get(self.ccy, 2) prec = CCY_PRECISIONS.get(self.ccy, 2)
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
return fmt_str.format(round(amount, prec)) try:
rounded_amount = round(amount, prec)
except decimal.InvalidOperation:
rounded_amount = amount
return fmt_str.format(rounded_amount)
def run(self): def run(self):
# This runs from the plugins thread which catches exceptions # This runs from the plugins thread which catches exceptions
if self.is_enabled(): if self.is_enabled():
if self.timeout ==0 and self.show_history(): if self.timeout ==0 and self.show_history():
self.exchange.get_historical_rates(self.ccy) self.exchange.get_historical_rates(self.ccy, self.cache_dir)
if self.timeout <= time.time(): if self.timeout <= time.time():
self.timeout = time.time() + 150 self.timeout = time.time() + 150
self.exchange.update(self.ccy) self.exchange.update(self.ccy)
@ -193,6 +228,12 @@ class FxThread(ThreadJob):
def set_history_config(self, b): def set_history_config(self, b):
self.config.set_key('history_rates', bool(b)) self.config.set_key('history_rates', bool(b))
def get_history_capital_gains_config(self):
return bool(self.config.get('history_rates_capital_gains', False))
def set_history_capital_gains_config(self, b):
self.config.set_key('history_rates_capital_gains', bool(b))
def get_fiat_address_config(self): def get_fiat_address_config(self):
return bool(self.config.get('fiat_address')) return bool(self.config.get('fiat_address'))
@ -224,45 +265,65 @@ class FxThread(ThreadJob):
# A new exchange means new fx quotes, initially empty. Force # A new exchange means new fx quotes, initially empty. Force
# a quote refresh # a quote refresh
self.timeout = 0 self.timeout = 0
self.exchange.read_historical_rates(self.ccy, self.cache_dir)
def on_quotes(self): def on_quotes(self):
self.network.trigger_callback('on_quotes') if self.network:
self.network.trigger_callback('on_quotes')
def on_history(self): def on_history(self):
self.network.trigger_callback('on_history') if self.network:
self.network.trigger_callback('on_history')
def exchange_rate(self): def exchange_rate(self):
'''Returns None, or the exchange rate as a Decimal''' '''Returns None, or the exchange rate as a Decimal'''
rate = self.exchange.quotes.get(self.ccy) rate = self.exchange.quotes.get(self.ccy)
if rate: if rate is None:
return Decimal(rate) return Decimal('NaN')
return Decimal(rate)
def format_amount(self, btc_balance):
rate = self.exchange_rate()
return '' if rate.is_nan() else "%s" % self.value_str(btc_balance, rate)
def format_amount_and_units(self, btc_balance): def format_amount_and_units(self, btc_balance):
rate = self.exchange_rate() rate = self.exchange_rate()
return '' if rate is None else "%s %s" % (self.value_str(btc_balance, rate), self.ccy) return '' if rate.is_nan() else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
def get_fiat_status_text(self, btc_balance, base_unit, decimal_point): def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):
rate = self.exchange_rate() rate = self.exchange_rate()
return _(" (No exchange rate available)") if rate is None else " 1 %s=%s %s" % (base_unit, return _(" (No FX rate available)") if rate.is_nan() else " 1 %s~%s %s" % (base_unit,
self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy) self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
def fiat_value(self, satoshis, rate):
return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate)
def value_str(self, satoshis, rate): def value_str(self, satoshis, rate):
if satoshis is None: # Can happen with incomplete history return self.format_fiat(self.fiat_value(satoshis, rate))
return _("Unknown")
if rate: def format_fiat(self, value):
value = Decimal(satoshis) / COIN * Decimal(rate) if value.is_nan():
return "%s" % (self.ccy_amount_str(value, True)) return _("No data")
return _("No data") return "%s" % (self.ccy_amount_str(value, True))
def history_rate(self, d_t): def history_rate(self, d_t):
if d_t is None:
return Decimal('NaN')
rate = self.exchange.historical_rate(self.ccy, d_t) rate = self.exchange.historical_rate(self.ccy, d_t)
# Frequently there is no rate for today, until tomorrow :) # Frequently there is no rate for today, until tomorrow :)
# Use spot quotes in that case # Use spot quotes in that case
if rate is None and (datetime.today().date() - d_t.date()).days <= 2: if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2:
rate = self.exchange.quotes.get(self.ccy) rate = self.exchange.quotes.get(self.ccy, 'NaN')
self.history_used_spot = True self.history_used_spot = True
return rate return Decimal(rate)
def historical_value_str(self, satoshis, d_t): def historical_value_str(self, satoshis, d_t):
rate = self.history_rate(d_t) return self.format_fiat(self.historical_value(satoshis, d_t))
return self.value_str(satoshis, rate)
def historical_value(self, satoshis, d_t):
return self.fiat_value(satoshis, self.history_rate(d_t))
def timestamp_rate(self, timestamp):
from electrum.util import timestamp_to_datetime
date = timestamp_to_datetime(timestamp)
return self.history_rate(date)

View File

@ -31,9 +31,7 @@ import threading
import time import time
import traceback import traceback
from .util import print_error, get_cert_path from .util import print_error
ca_path = get_cert_path()
from . import util from . import util
from . import x509 from . import x509

View File

@ -28,7 +28,7 @@ from unicodedata import normalize
from . import bitcoin from . import bitcoin
from .bitcoin import * from .bitcoin import *
from . import constants
from .util import PrintError, InvalidPassword, hfu from .util import PrintError, InvalidPassword, hfu
from .mnemonic import Mnemonic, load_wordlist from .mnemonic import Mnemonic, load_wordlist
from .plugins import run_hook from .plugins import run_hook
@ -45,6 +45,10 @@ class KeyStore(PrintError):
def can_import(self): def can_import(self):
return False return False
def may_have_password(self):
"""Returns whether the keystore can be encrypted with a password."""
raise NotImplementedError()
def get_tx_derivations(self, tx): def get_tx_derivations(self, tx):
keypairs = {} keypairs = {}
for txin in tx.inputs(): for txin in tx.inputs():
@ -116,9 +120,6 @@ class Imported_KeyStore(Software_KeyStore):
def is_deterministic(self): def is_deterministic(self):
return False return False
def can_change_password(self):
return True
def get_master_public_key(self): def get_master_public_key(self):
return None return None
@ -138,7 +139,10 @@ class Imported_KeyStore(Software_KeyStore):
def import_privkey(self, sec, password): def import_privkey(self, sec, password):
txin_type, privkey, compressed = deserialize_privkey(sec) txin_type, privkey, compressed = deserialize_privkey(sec)
pubkey = public_key_from_private_key(privkey, compressed) pubkey = public_key_from_private_key(privkey, compressed)
self.keypairs[pubkey] = pw_encode(sec, password) # re-serialize the key so the internal storage format is consistent
serialized_privkey = serialize_privkey(
privkey, compressed, txin_type, internal_use=True)
self.keypairs[pubkey] = pw_encode(serialized_privkey, password)
return txin_type, pubkey return txin_type, pubkey
def delete_imported_key(self, key): def delete_imported_key(self, key):
@ -196,9 +200,6 @@ class Deterministic_KeyStore(Software_KeyStore):
def is_watching_only(self): def is_watching_only(self):
return not self.has_seed() return not self.has_seed()
def can_change_password(self):
return not self.is_watching_only()
def add_seed(self, seed): def add_seed(self, seed):
if self.seed: if self.seed:
raise Exception("a seed exists") raise Exception("a seed exists")
@ -522,9 +523,13 @@ class Hardware_KeyStore(KeyStore, Xpub):
assert not self.has_seed() assert not self.has_seed()
return False return False
def can_change_password(self): def get_password_for_storage_encryption(self):
return False from .storage import get_derivation_used_for_hw_device_encryption
client = self.plugin.get_client(self)
derivation = get_derivation_used_for_hw_device_encryption()
xpub = client.get_xpub(derivation, "standard")
password = self.get_pubkey_from_xpub(xpub, ())
return password
def bip39_normalize_passphrase(passphrase): def bip39_normalize_passphrase(passphrase):
@ -683,7 +688,7 @@ is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
def bip44_derivation(account_id, bip43_purpose=44): def bip44_derivation(account_id, bip43_purpose=44):
coin = 1 if bitcoin.NetworkConstants.TESTNET else 147 # ZCL - SLIP0044 coin = 1 if constants.net.TESTNET else 147 # ZCL - SLIP0044
return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id)) return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
def from_seed(seed, passphrase, is_p2sh): def from_seed(seed, passphrase, is_p2sh):

View File

@ -37,9 +37,11 @@ import socks
from . import util from . import util
from . import bitcoin from . import bitcoin
from .bitcoin import * from .bitcoin import *
from . import constants
from .interface import Connection, Interface from .interface import Connection, Interface
from . import blockchain from . import blockchain
from .version import ELECTRUM_VERSION, PROTOCOL_VERSION from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
from .i18n import _
NODES_RETRY_INTERVAL = 60 NODES_RETRY_INTERVAL = 60
@ -59,7 +61,7 @@ def parse_servers(result):
for v in item[2]: for v in item[2]:
if re.match("[st]\d*", v): if re.match("[st]\d*", v):
protocol, port = v[0], v[1:] protocol, port = v[0], v[1:]
if port == '': port = NetworkConstants.DEFAULT_PORTS[protocol] if port == '': port = constants.net.DEFAULT_PORTS[protocol]
out[protocol] = port out[protocol] = port
elif re.match("v(.?)+", v): elif re.match("v(.?)+", v):
version = v[1:] version = v[1:]
@ -93,7 +95,7 @@ def filter_protocol(hostmap, protocol = 's'):
def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()): def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
if hostmap is None: if hostmap is None:
hostmap = NetworkConstants.DEFAULT_SERVERS hostmap = constants.net.DEFAULT_SERVERS
eligible = list(set(filter_protocol(hostmap, protocol)) - exclude_set) eligible = list(set(filter_protocol(hostmap, protocol)) - exclude_set)
return random.choice(eligible) if eligible else None return random.choice(eligible) if eligible else None
@ -137,7 +139,7 @@ def deserialize_proxy(s):
def deserialize_server(server_str): def deserialize_server(server_str):
host, port, protocol = str(server_str).split(':') host, port, protocol = str(server_str).rsplit(':', 2)
assert protocol in 'st' assert protocol in 'st'
int(port) # Throw if cannot be converted to int int(port) # Throw if cannot be converted to int
return host, port, protocol return host, port, protocol
@ -171,17 +173,17 @@ class Network(util.DaemonThread):
self.blockchain_index = config.get('blockchain_index', 0) self.blockchain_index = config.get('blockchain_index', 0)
if self.blockchain_index not in self.blockchains.keys(): if self.blockchain_index not in self.blockchains.keys():
self.blockchain_index = 0 self.blockchain_index = 0
self.protocol = 't' if self.config.get('nossl') else 's'
# Server for addresses and transactions # Server for addresses and transactions
self.default_server = self.config.get('server') self.default_server = self.config.get('server', None)
# Sanitize default server # Sanitize default server
try: if self.default_server:
host, port, protocol = deserialize_server(self.default_server) try:
assert protocol == self.protocol deserialize_server(self.default_server)
except: except:
self.default_server = None self.print_error('Warning: failed to parse server-string; falling back to random.')
self.default_server = None
if not self.default_server: if not self.default_server:
self.default_server = pick_random_server(protocol=self.protocol) self.default_server = pick_random_server()
self.lock = threading.Lock() self.lock = threading.Lock()
self.pending_sends = [] self.pending_sends = []
self.message_id = 0 self.message_id = 0
@ -220,7 +222,8 @@ class Network(util.DaemonThread):
self.connecting = set() self.connecting = set()
self.requested_chunks = set() self.requested_chunks = set()
self.socket_queue = queue.Queue() self.socket_queue = queue.Queue()
self.start_network(self.protocol, deserialize_proxy(self.config.get('proxy'))) self.start_network(deserialize_server(self.default_server)[2],
deserialize_proxy(self.config.get('proxy')))
def register_callback(self, callback, events): def register_callback(self, callback, events):
with self.lock: with self.lock:
@ -305,6 +308,9 @@ class Network(util.DaemonThread):
# Resend unanswered requests # Resend unanswered requests
requests = self.unanswered_requests.values() requests = self.unanswered_requests.values()
self.unanswered_requests = {} self.unanswered_requests = {}
if self.interface.ping_required():
params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
self.queue_request('server.version', params, self.interface)
for request in requests: for request in requests:
message_id = self.queue_request(request[0], request[1]) message_id = self.queue_request(request[0], request[1])
self.unanswered_requests[message_id] = request self.unanswered_requests[message_id] = request
@ -313,15 +319,14 @@ class Network(util.DaemonThread):
self.queue_request('server.peers.subscribe', []) self.queue_request('server.peers.subscribe', [])
self.request_fee_estimates() self.request_fee_estimates()
self.queue_request('blockchain.relayfee', []) self.queue_request('blockchain.relayfee', [])
if self.interface.ping_required():
params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
self.queue_request('server.version', params, self.interface)
for h in self.subscribed_addresses: for h in self.subscribed_addresses:
self.queue_request('blockchain.scripthash.subscribe', [h]) self.queue_request('blockchain.scripthash.subscribe', [h])
def request_fee_estimates(self): def request_fee_estimates(self):
from .simple_config import FEE_ETA_TARGETS
self.config.requested_fee_estimates() self.config.requested_fee_estimates()
for i in bitcoin.FEE_TARGETS: self.queue_request('mempool.get_fee_histogram', [])
for i in FEE_ETA_TARGETS:
self.queue_request('blockchain.estimatefee', [i]) self.queue_request('blockchain.estimatefee', [i])
def get_status_value(self, key): def get_status_value(self, key):
@ -331,6 +336,8 @@ class Network(util.DaemonThread):
value = self.banner value = self.banner
elif key == 'fee': elif key == 'fee':
value = self.config.fee_estimates value = self.config.fee_estimates
elif key == 'fee_histogram':
value = self.config.mempool_fees
elif key == 'updated': elif key == 'updated':
value = (self.get_local_height(), self.get_server_height()) value = (self.get_local_height(), self.get_server_height())
elif key == 'servers': elif key == 'servers':
@ -358,7 +365,7 @@ class Network(util.DaemonThread):
return list(self.interfaces.keys()) return list(self.interfaces.keys())
def get_servers(self): def get_servers(self):
out = NetworkConstants.DEFAULT_SERVERS out = constants.net.DEFAULT_SERVERS
if self.irc_servers: if self.irc_servers:
out.update(filter_version(self.irc_servers.copy())) out.update(filter_version(self.irc_servers.copy()))
else: else:
@ -542,6 +549,11 @@ class Network(util.DaemonThread):
elif method == 'server.donation_address': elif method == 'server.donation_address':
if error is None: if error is None:
self.donation_address = result self.donation_address = result
elif method == 'mempool.get_fee_histogram':
if error is None:
self.print_error('fee_histogram', result)
self.config.mempool_fees = result
self.notify('fee_histogram')
elif method == 'blockchain.estimatefee': elif method == 'blockchain.estimatefee':
if error is None and result > 0: if error is None and result > 0:
i = params[0] i = params[0]
@ -767,29 +779,33 @@ class Network(util.DaemonThread):
error = response.get('error') error = response.get('error')
result = response.get('result') result = response.get('result')
params = response.get('params') params = response.get('params')
blockchain = interface.blockchain
if result is None or params is None or error is not None: if result is None or params is None or error is not None:
interface.print_error(error or 'bad response') interface.print_error(error or 'bad response')
return return
index = params[0] index = params[0]
# Ignore unsolicited chunks # Ignore unsolicited chunks
if index not in self.requested_chunks: if index not in self.requested_chunks:
interface.print_error("received chunk %d (unsolicited)" % index)
return return
else:
interface.print_error("received chunk %d" % index)
self.requested_chunks.remove(index) self.requested_chunks.remove(index)
connect = interface.blockchain.connect_chunk(index, result) connect = blockchain.connect_chunk(index, result)
if not connect: if not connect:
self.connection_down(interface.server) self.connection_down(interface.server)
return return
# If not finished, get the next chunk # If not finished, get the next chunk
if interface.blockchain.height() < interface.tip: if index >= len(blockchain.checkpoints) and blockchain.height() < interface.tip:
self.request_chunk(interface, index+1) self.request_chunk(interface, index+1)
else: else:
interface.mode = 'default' interface.mode = 'default'
interface.print_error('catch up done', interface.blockchain.height()) interface.print_error('catch up done', blockchain.height())
interface.blockchain.catch_up = None blockchain.catch_up = None
self.notify('updated') self.notify('updated')
def request_header(self, interface, height): def request_header(self, interface, height):
interface.print_error("requesting header %d" % height) #interface.print_error("requesting header %d" % height)
self.queue_request('blockchain.block.get_header', [height], interface) self.queue_request('blockchain.block.get_header', [height], interface)
interface.request = height interface.request = height
interface.req_time = time.time() interface.req_time = time.time()
@ -802,11 +818,10 @@ class Network(util.DaemonThread):
self.connection_down(interface.server) self.connection_down(interface.server)
return return
height = header.get('block_height') height = header.get('block_height')
if int(interface.request) != height: if interface.request != height:
interface.print_error("unsolicited header",interface.request, height) interface.print_error("unsolicited header",interface.request, height)
self.connection_down(interface.server) self.connection_down(interface.server)
return return
interface.print_error("interface.mode %s" % interface.mode)
chain = blockchain.check_header(header) chain = blockchain.check_header(header)
if interface.mode == 'backward': if interface.mode == 'backward':
can_connect = blockchain.can_connect(header) can_connect = blockchain.can_connect(header)
@ -822,6 +837,7 @@ class Network(util.DaemonThread):
interface.blockchain = chain interface.blockchain = chain
interface.good = height interface.good = height
next_height = (interface.bad + interface.good) // 2 next_height = (interface.bad + interface.good) // 2
assert next_height >= self.max_checkpoint(), (interface.bad, interface.good)
else: else:
if height == 0: if height == 0:
self.connection_down(interface.server) self.connection_down(interface.server)
@ -830,7 +846,8 @@ class Network(util.DaemonThread):
interface.bad = height interface.bad = height
interface.bad_header = header interface.bad_header = header
delta = interface.tip - height delta = interface.tip - height
next_height = max(0, interface.tip - 2 * delta) next_height = max(self.max_checkpoint(), interface.tip - 2 * delta)
elif interface.mode == 'binary': elif interface.mode == 'binary':
if chain: if chain:
interface.good = height interface.good = height
@ -840,6 +857,7 @@ class Network(util.DaemonThread):
interface.bad_header = header interface.bad_header = header
if interface.bad != interface.good + 1: if interface.bad != interface.good + 1:
next_height = (interface.bad + interface.good) // 2 next_height = (interface.bad + interface.good) // 2
assert next_height >= self.max_checkpoint()
elif not interface.blockchain.can_connect(interface.bad_header, check_height=False): elif not interface.blockchain.can_connect(interface.bad_header, check_height=False):
self.connection_down(interface.server) self.connection_down(interface.server)
next_height = None next_height = None
@ -908,7 +926,7 @@ class Network(util.DaemonThread):
# If not finished, get the next header # If not finished, get the next header
if next_height: if next_height:
if interface.mode == 'catch_up' and interface.tip > next_height + 50: if interface.mode == 'catch_up' and interface.tip > next_height + 50:
self.request_chunk(interface, next_height // NetworkConstants.CHUNK_SIZE) self.request_chunk(interface, next_height // constants.net.CHUNK_SIZE)
else: else:
self.request_header(interface, next_height) self.request_header(interface, next_height)
else: else:
@ -949,36 +967,18 @@ class Network(util.DaemonThread):
def init_headers_file(self): def init_headers_file(self):
b = self.blockchains[0] b = self.blockchains[0]
print(b.get_hash(0), NetworkConstants.GENESIS)
if b.get_hash(0) == NetworkConstants.GENESIS:
self.downloading_headers = False
return
filename = b.path() filename = b.path()
def download_thread(): length = bitcoin.HEADER_SIZE * len(constants.net.CHECKPOINTS) * constants.net.CHUNK_SIZE
try: if not os.path.exists(filename) or os.path.getsize(filename) < length:
import urllib, socket with open(filename, 'wb') as f:
socket.setdefaulttimeout(30) if length>0:
self.print_error("downloading ", NetworkConstants.HEADERS_URL) f.seek(length-1)
urllib.request.urlretrieve(NetworkConstants.HEADERS_URL, filename) f.write(b'\x00')
self.print_error("done.") with b.lock:
except Exception: b.update_size()
import traceback
traceback.print_exc()
self.print_error("download failed. creating file", filename)
open(filename, 'wb+').close()
b = self.blockchains[0]
with b.lock: b.update_size()
self.downloading_headers = False
self.downloading_headers = True
t = threading.Thread(target = download_thread)
t.daemon = True
t.start()
def run(self): def run(self):
self.init_headers_file() self.init_headers_file()
while self.is_running() and self.downloading_headers:
time.sleep(1)
while self.is_running(): while self.is_running():
self.maintain_sockets() self.maintain_sockets()
self.wait_on_sockets() self.wait_on_sockets()
@ -990,10 +990,11 @@ class Network(util.DaemonThread):
def on_notify_header(self, interface, header): def on_notify_header(self, interface, header):
height = header.get('block_height') height = header.get('block_height')
if not height: if not height:
return return
if height < self.max_checkpoint():
self.connection_down(interface.server)
return
interface.tip_header = header interface.tip_header = header
interface.tip = height interface.tip = height
@ -1078,7 +1079,7 @@ class Network(util.DaemonThread):
try: try:
r = q.get(True, timeout) r = q.get(True, timeout)
except queue.Empty: except queue.Empty:
raise BaseException('Server did not answer') raise util.TimeoutException(_('Server did not answer'))
if r.get('error'): if r.get('error'):
raise BaseException(r.get('error')) raise BaseException(r.get('error'))
return r.get('result') return r.get('result')
@ -1092,3 +1093,12 @@ class Network(util.DaemonThread):
if out != tx_hash: if out != tx_hash:
return False, "error: " + out return False, "error: " + out
return True, out return True, out
def export_checkpoints(self, path):
# run manually from the console to generate checkpoints
cp = self.blockchain().get_checkpoints()
with open(path, 'w') as f:
f.write(json.dumps(cp, indent=4))
def max_checkpoint(self):
return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1)

View File

@ -39,7 +39,8 @@ except ImportError:
from . import bitcoin from . import bitcoin
from . import util from . import util
from .util import print_error, bh2u, bfh, get_cert_path from .util import print_error, bh2u, bfh
from .util import export_meta, import_meta
from . import transaction from . import transaction
from . import x509 from . import x509
from . import rsakey from . import rsakey
@ -49,7 +50,7 @@ from .bitcoin import TYPE_ADDRESS
REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'} REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'} ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
ca_path = get_cert_path() ca_path = requests.certs.where()
ca_list = None ca_list = None
ca_keyID = None ca_keyID = None
@ -467,24 +468,29 @@ class InvoiceStore(object):
continue continue
def import_file(self, path): def import_file(self, path):
try: def validate(data):
with open(path, 'r') as f: return data # TODO
d = json.loads(f.read()) import_meta(path, validate, self.on_import)
self.load(d)
except: def on_import(self, data):
traceback.print_exc(file=sys.stderr) self.load(data)
return
self.save() self.save()
def save(self): def export_file(self, filename):
l = {} export_meta(self.dump(), filename)
def dump(self):
d = {}
for k, pr in self.invoices.items(): for k, pr in self.invoices.items():
l[k] = { d[k] = {
'hex': bh2u(pr.raw), 'hex': bh2u(pr.raw),
'requestor': pr.requestor, 'requestor': pr.requestor,
'txid': pr.tx 'txid': pr.tx
} }
self.storage.put('invoices', l) return d
def save(self):
self.storage.put('invoices', self.dump())
def get_status(self, key): def get_status(self, key):
pr = self.get(key) pr = self.get(key)

View File

@ -14,17 +14,23 @@ from matplotlib.patches import Ellipse
from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker
def plot_history(wallet, history): class NothingToPlotException(Exception):
def __str__(self):
return _("Nothing to plot.")
def plot_history(history):
if len(history) == 0:
raise NothingToPlotException()
hist_in = defaultdict(int) hist_in = defaultdict(int)
hist_out = defaultdict(int) hist_out = defaultdict(int)
for item in history: for item in history:
tx_hash, height, confirmations, timestamp, value, balance = item if not item['confirmations']:
if not confirmations:
continue continue
if timestamp is None: if item['timestamp'] is None:
continue continue
value = value*1./COIN value = item['value'].value/COIN
date = datetime.datetime.fromtimestamp(timestamp) date = item['date']
datenum = int(md.date2num(datetime.date(date.year, date.month, 1))) datenum = int(md.date2num(datetime.date(date.year, date.month, 1)))
if value > 0: if value > 0:
hist_in[datenum] += value hist_in[datenum] += value
@ -43,12 +49,19 @@ def plot_history(wallet, history):
xfmt = md.DateFormatter('%Y-%m') xfmt = md.DateFormatter('%Y-%m')
ax.xaxis.set_major_formatter(xfmt) ax.xaxis.set_major_formatter(xfmt)
width = 20 width = 20
dates, values = zip(*sorted(hist_in.items()))
r1 = axarr[0].bar(dates, values, width, label='incoming') r1 = None
axarr[0].legend(loc='upper left') r2 = None
dates_values = list(zip(*sorted(hist_in.items())))
if dates_values and len(dates_values) == 2:
dates, values = dates_values
r1 = axarr[0].bar(dates, values, width, label='incoming')
axarr[0].legend(loc='upper left')
dates_values = list(zip(*sorted(hist_out.items()))) dates_values = list(zip(*sorted(hist_out.items())))
if dates_values and len(dates_values) == 2: if dates_values and len(dates_values) == 2:
dates, values = dates_values dates, values = dates_values
r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing') r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing')
axarr[1].legend(loc='upper left') axarr[1].legend(loc='upper left')
if r1 is None and r2 is None:
raise NothingToPlotException()
return plt return plt

View File

@ -312,6 +312,8 @@ class DeviceMgr(ThreadJob, PrintError):
# What we recognise. Each entry is a (vendor_id, product_id) # What we recognise. Each entry is a (vendor_id, product_id)
# pair. # pair.
self.recognised_hardware = set() self.recognised_hardware = set()
# Custom enumerate functions for devices we don't know about.
self.enumerate_func = set()
# For synchronization # For synchronization
self.lock = threading.RLock() self.lock = threading.RLock()
self.hid_lock = threading.RLock() self.hid_lock = threading.RLock()
@ -334,6 +336,9 @@ class DeviceMgr(ThreadJob, PrintError):
for pair in device_pairs: for pair in device_pairs:
self.recognised_hardware.add(pair) self.recognised_hardware.add(pair)
def register_enumerate_func(self, func):
self.enumerate_func.add(func)
def create_client(self, device, handler, plugin): def create_client(self, device, handler, plugin):
# Get from cache first # Get from cache first
client = self.client_lookup(device.id_) client = self.client_lookup(device.id_)
@ -362,15 +367,20 @@ class DeviceMgr(ThreadJob, PrintError):
if not xpub in self.xpub_ids: if not xpub in self.xpub_ids:
return return
_id = self.xpub_ids.pop(xpub) _id = self.xpub_ids.pop(xpub)
client = self.client_lookup(_id) self._close_client(_id)
self.clients.pop(client, None)
if client:
client.close()
def unpair_id(self, id_): def unpair_id(self, id_):
xpub = self.xpub_by_id(id_) xpub = self.xpub_by_id(id_)
if xpub: if xpub:
self.unpair_xpub(xpub) self.unpair_xpub(xpub)
else:
self._close_client(id_)
def _close_client(self, id_):
client = self.client_lookup(id_)
self.clients.pop(client, None)
if client:
client.close()
def pair_xpub(self, xpub, id_): def pair_xpub(self, xpub, id_):
with self.lock: with self.lock:
@ -442,11 +452,11 @@ class DeviceMgr(ThreadJob, PrintError):
# The user input has wrong PIN or passphrase, or cancelled input, # The user input has wrong PIN or passphrase, or cancelled input,
# or it is not pairable # or it is not pairable
raise DeviceUnpairableError( raise DeviceUnpairableError(
_('Electrum cannot pair with your %s.\n\n' _('Electrum cannot pair with your {}.\n\n'
'Before you request bitcoins to be sent to addresses in this ' 'Before you request bitcoins to be sent to addresses in this '
'wallet, ensure you can pair with your device, or that you have ' 'wallet, ensure you can pair with your device, or that you have '
'its seed (and passphrase, if any). Otherwise all bitcoins you ' 'its seed (and passphrase, if any). Otherwise all bitcoins you '
'receive will be unspendable.') % plugin.device) 'receive will be unspendable.').format(plugin.device))
def unpaired_device_infos(self, handler, plugin, devices=None): def unpaired_device_infos(self, handler, plugin, devices=None):
'''Returns a list of DeviceInfo objects: one for each connected, '''Returns a list of DeviceInfo objects: one for each connected,
@ -472,9 +482,9 @@ class DeviceMgr(ThreadJob, PrintError):
infos = self.unpaired_device_infos(handler, plugin, devices) infos = self.unpaired_device_infos(handler, plugin, devices)
if infos: if infos:
break break
msg = _('Please insert your %s. Verify the cable is ' msg = _('Please insert your {}. Verify the cable is '
'connected and that no other application is using it.\n\n' 'connected and that no other application is using it.\n\n'
'Try to connect again?') % plugin.device 'Try to connect again?').format(plugin.device)
if not handler.yes_no_question(msg): if not handler.yes_no_question(msg):
raise UserCancelled() raise UserCancelled()
devices = None devices = None
@ -484,7 +494,7 @@ class DeviceMgr(ThreadJob, PrintError):
for info in infos: for info in infos:
if info.label == keystore.label: if info.label == keystore.label:
return info return info
msg = _("Please select which %s device to use:") % plugin.device msg = _("Please select which {} device to use:").format(plugin.device)
descriptions = [info.label + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos] descriptions = [info.label + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos]
c = handler.query_choice(msg, descriptions) c = handler.query_choice(msg, descriptions)
if c is None: if c is None:
@ -492,7 +502,8 @@ class DeviceMgr(ThreadJob, PrintError):
info = infos[c] info = infos[c]
# save new label # save new label
keystore.set_label(info.label) keystore.set_label(info.label)
handler.win.wallet.save_keystore() if handler.win.wallet is not None:
handler.win.wallet.save_keystore()
return info return info
def scan_devices(self): def scan_devices(self):
@ -504,6 +515,7 @@ class DeviceMgr(ThreadJob, PrintError):
self.print_error("scanning devices...") self.print_error("scanning devices...")
with self.hid_lock: with self.hid_lock:
hid_list = hid.enumerate(0, 0) hid_list = hid.enumerate(0, 0)
# First see what's connected that we know about # First see what's connected that we know about
devices = [] devices = []
for d in hid_list: for d in hid_list:
@ -519,6 +531,10 @@ class DeviceMgr(ThreadJob, PrintError):
devices.append(Device(d['path'], interface_number, devices.append(Device(d['path'], interface_number,
id_, product_key, usage_page)) id_, product_key, usage_page))
# Let plugin handlers enumerate devices we don't know about
for f in self.enumerate_func:
devices.extend(f())
# Now find out what was disconnected # Now find out what was disconnected
pairs = [(dev.path, dev.id_) for dev in devices] pairs = [(dev.path, dev.id_) for dev in devices]
disconnected_ids = [] disconnected_ids = []

View File

@ -29,8 +29,8 @@ import ctypes
if sys.platform == 'darwin': if sys.platform == 'darwin':
name = 'libzbar.dylib' name = 'libzbar.dylib'
elif sys.platform == 'windows': elif sys.platform in ('windows', 'win32'):
name = 'libzbar.dll' name = 'libzbar-0.dll'
else: else:
name = 'libzbar.so.0' name = 'libzbar.so.0'
@ -40,7 +40,7 @@ except OSError:
libzbar = None libzbar = None
def scan_barcode(device='', timeout=-1, display=True, threaded=False): def scan_barcode(device='', timeout=-1, display=True, threaded=False, try_again=True):
if libzbar is None: if libzbar is None:
raise RuntimeError("Cannot start QR scanner; zbar not available.") raise RuntimeError("Cannot start QR scanner; zbar not available.")
libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p
@ -50,6 +50,10 @@ def scan_barcode(device='', timeout=-1, display=True, threaded=False):
proc = libzbar.zbar_processor_create(threaded) proc = libzbar.zbar_processor_create(threaded)
libzbar.zbar_processor_request_size(proc, 640, 480) libzbar.zbar_processor_request_size(proc, 640, 480)
if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0: if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0:
if try_again:
# workaround for a bug in "ZBar for Windows"
# libzbar.zbar_processor_init always seem to fail the first time around
return scan_barcode(device, timeout, display, threaded, try_again=False)
raise RuntimeError("Can not start QR scanner; initialization failed.") raise RuntimeError("Can not start QR scanner; initialization failed.")
libzbar.zbar_processor_set_visible(proc) libzbar.zbar_processor_set_visible(proc)
if libzbar.zbar_process_one(proc, timeout): if libzbar.zbar_process_one(proc, timeout):

View File

@ -1,231 +0,0 @@
{
"E-X.not.fyi": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"ELECTRUMX.not.fyi": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"ELEX01.blackpole.online": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"VPS.hsmiths.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"bitcoin.freedomnode.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"btc.smsys.me": {
"pruning": "-",
"s": "995",
"version": "1.1"
},
"currentlane.lovebitco.in": {
"pruning": "-",
"t": "50001",
"version": "1.1"
},
"daedalus.bauerj.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"de01.hamster.science": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"ecdsa.net": {
"pruning": "-",
"s": "110",
"t": "50001",
"version": "1.1"
},
"elec.luggs.co": {
"pruning": "-",
"s": "443",
"version": "1.1"
},
"electrum.akinbo.org": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.antumbra.se": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.be": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.coinucopia.io": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.cutie.ga": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.festivaldelhumor.org": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.hsmiths.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.qtornado.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum.vom-stausee.de": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrum3.hachre.de": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrumx.bot.nu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"electrumx.westeurope.cloudapp.azure.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"elx01.knas.systems": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"ex-btc.server-on.net": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"helicarrier.bauerj.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"mooo.not.fyi": {
"pruning": "-",
"s": "50012",
"t": "50011",
"version": "1.1"
},
"ndnd.selfhost.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"node.arihanc.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"node.xbt.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"node1.volatilevictory.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"noserver4u.de": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"qmebr.spdns.org": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"raspi.hsmiths.com": {
"pruning": "-",
"s": "51002",
"t": "51001",
"version": "1.1"
},
"s2.noip.pl": {
"pruning": "-",
"s": "50102",
"version": "1.1"
},
"s5.noip.pl": {
"pruning": "-",
"s": "50105",
"version": "1.1"
},
"songbird.bauerj.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"us.electrum.be": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
},
"us01.hamster.science": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.1"
}
}

View File

@ -1,4 +1,4 @@
{ {
"electrum.zclassic.community": {"s":"50002"}, "electrum.zclassic.community": {"s":"50002", "pruning": "-", "version": "1.1"},
"zclele.duckdns.org": {"s":"50002"} "zclele.duckdns.org": {"s":"50002", "pruning": "-", "version": "1.1"}
} }

View File

@ -1,3 +1,3 @@
{ {
"localhost": {"t":"51001", "s":"51002"} "localhost": {"t":"51001", "s":"51002", "pruning": "-", "version": "1.1"}
} }

View File

@ -5,11 +5,21 @@ import os
import stat import stat
from copy import deepcopy from copy import deepcopy
from .util import user_dir, print_error, print_stderr, PrintError
from .bitcoin import DEFAULT_FEE_RATE, MAX_FEE_RATE, FEE_TARGETS from .util import (user_dir, print_error, PrintError,
NoDynamicFeeEstimates, format_satoshis)
from .i18n import _
FEE_ETA_TARGETS = [25, 10, 5, 2]
FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000]
# satoshi per kbyte
FEERATE_MAX_DYNAMIC = 1500000
FEERATE_WARNING_HIGH_FEE = 600000
FEERATE_FALLBACK_STATIC_FEE = 150000
FEERATE_DEFAULT_RELAY = 1000
FEERATE_STATIC_VALUES = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
SYSTEM_CONFIG_PATH = "/etc/electrum.conf"
config = None config = None
@ -24,35 +34,37 @@ def set_config(c):
config = c config = c
FINAL_CONFIG_VERSION = 2
class SimpleConfig(PrintError): class SimpleConfig(PrintError):
""" """
The SimpleConfig class is responsible for handling operations involving The SimpleConfig class is responsible for handling operations involving
configuration files. configuration files.
There are 3 different sources of possible configuration values: There are two different sources of possible configuration values:
1. Command line options. 1. Command line options.
2. User configuration (in the user's config directory) 2. User configuration (in the user's config directory)
3. System configuration (in /etc/) They are taken in order (1. overrides config options set in 2.)
They are taken in order (1. overrides config options set in 2., that
override config set in 3.)
""" """
fee_rates = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
def __init__(self, options={}, read_system_config_function=None, def __init__(self, options=None, read_user_config_function=None,
read_user_config_function=None, read_user_dir_function=None): read_user_dir_function=None):
if options is None:
options = {}
# This lock needs to be acquired for updating and reading the config in # This lock needs to be acquired for updating and reading the config in
# a thread-safe way. # a thread-safe way.
self.lock = threading.RLock() self.lock = threading.RLock()
self.mempool_fees = {}
self.fee_estimates = {} self.fee_estimates = {}
self.fee_estimates_last_updated = {} self.fee_estimates_last_updated = {}
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
# The following two functions are there for dependency injection when # The following two functions are there for dependency injection when
# testing. # testing.
if read_system_config_function is None:
read_system_config_function = read_system_config
if read_user_config_function is None: if read_user_config_function is None:
read_user_config_function = read_user_config read_user_config_function = read_user_config
if read_user_dir_function is None: if read_user_dir_function is None:
@ -62,24 +74,30 @@ class SimpleConfig(PrintError):
# The command line options # The command line options
self.cmdline_options = deepcopy(options) self.cmdline_options = deepcopy(options)
# don't allow to be set on CLI:
# Portable wallets don't use a system config self.cmdline_options.pop('config_version', None)
if self.cmdline_options.get('portable', False):
self.system_config = {}
else:
self.system_config = read_system_config_function()
# Set self.path and read the user config # Set self.path and read the user config
self.user_config = {} # for self.get in electrum_path() self.user_config = {} # for self.get in electrum_path()
self.path = self.electrum_path() self.path = self.electrum_path()
self.user_config = read_user_config_function(self.path) self.user_config = read_user_config_function(self.path)
# Upgrade obsolete keys if not self.user_config:
self.fixup_keys({'auto_cycle': 'auto_connect'}) # avoid new config getting upgraded
self.user_config = {'config_version': FINAL_CONFIG_VERSION}
# config "upgrade" - CLI options
self.rename_config_keys(
self.cmdline_options, {'auto_cycle': 'auto_connect'}, True)
# config upgrade - user config
if self.requires_upgrade():
self.upgrade()
# Make a singleton instance of 'self' # Make a singleton instance of 'self'
set_config(self) set_config(self)
def electrum_path(self): def electrum_path(self):
# Read electrum_path from command line / system configuration # Read electrum_path from command line
# Otherwise use the user's default data directory. # Otherwise use the user's default data directory.
path = self.get('electrum_path') path = self.get('electrum_path')
if path is None: if path is None:
@ -101,45 +119,92 @@ class SimpleConfig(PrintError):
self.print_error("electrum directory", path) self.print_error("electrum directory", path)
return path return path
def fixup_config_keys(self, config, keypairs): def rename_config_keys(self, config, keypairs, deprecation_warning=False):
"""Migrate old key names to new ones"""
updated = False updated = False
for old_key, new_key in keypairs.items(): for old_key, new_key in keypairs.items():
if old_key in config: if old_key in config:
if not new_key in config: if new_key not in config:
config[new_key] = config[old_key] config[new_key] = config[old_key]
if deprecation_warning:
self.print_stderr('Note that the {} variable has been deprecated. '
'You should use {} instead.'.format(old_key, new_key))
del config[old_key] del config[old_key]
updated = True updated = True
return updated return updated
def fixup_keys(self, keypairs): def set_key(self, key, value, save=True):
'''Migrate old key names to new ones'''
self.fixup_config_keys(self.cmdline_options, keypairs)
self.fixup_config_keys(self.system_config, keypairs)
if self.fixup_config_keys(self.user_config, keypairs):
self.save_user_config()
def set_key(self, key, value, save = True):
if not self.is_modifiable(key): if not self.is_modifiable(key):
print_stderr("Warning: not changing config key '%s' set on the command line" % key) self.print_stderr("Warning: not changing config key '%s' set on the command line" % key)
return return
self._set_key_in_user_config(key, value, save)
def _set_key_in_user_config(self, key, value, save=True):
with self.lock: with self.lock:
self.user_config[key] = value if value is not None:
self.user_config[key] = value
else:
self.user_config.pop(key, None)
if save: if save:
self.save_user_config() self.save_user_config()
return
def get(self, key, default=None): def get(self, key, default=None):
with self.lock: with self.lock:
out = self.cmdline_options.get(key) out = self.cmdline_options.get(key)
if out is None: if out is None:
out = self.user_config.get(key) out = self.user_config.get(key, default)
if out is None:
out = self.system_config.get(key, default)
return out return out
def requires_upgrade(self):
return self.get_config_version() < FINAL_CONFIG_VERSION
def upgrade(self):
with self.lock:
self.print_error('upgrading config')
self.convert_version_2()
self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)
def convert_version_2(self):
if not self._is_upgrade_method_needed(1, 1):
return
self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})
try:
# change server string FROM host:port:proto TO host:port:s
server_str = self.user_config.get('server')
host, port, protocol = str(server_str).rsplit(':', 2)
assert protocol in ('s', 't')
int(port) # Throw if cannot be converted to int
server_str = '{}:{}:s'.format(host, port)
self._set_key_in_user_config('server', server_str)
except BaseException:
self._set_key_in_user_config('server', None)
self.set_key('config_version', 2)
def _is_upgrade_method_needed(self, min_version, max_version):
cur_version = self.get_config_version()
if cur_version > max_version:
return False
elif cur_version < min_version:
raise BaseException(
('config upgrade: unexpected version %d (should be %d-%d)'
% (cur_version, min_version, max_version)))
else:
return True
def get_config_version(self):
config_version = self.get('config_version', 1)
if config_version > FINAL_CONFIG_VERSION:
self.print_stderr('WARNING: config version ({}) is higher than ours ({})'
.format(config_version, FINAL_CONFIG_VERSION))
return config_version
def is_modifiable(self, key): def is_modifiable(self, key):
return not key in self.cmdline_options return key not in self.cmdline_options
def save_user_config(self): def save_user_config(self):
if not self.path: if not self.path:
@ -203,68 +268,197 @@ class SimpleConfig(PrintError):
path = wallet.storage.path path = wallet.storage.path
self.set_key('gui_last_wallet', path) self.set_key('gui_last_wallet', path)
def default_fee_rate(self): def impose_hard_limits_on_fee(func):
f = self.get('default_fee_rate', DEFAULT_FEE_RATE) def get_fee_within_limits(self, *args, **kwargs):
if f==0: fee = func(self, *args, **kwargs)
f = DEFAULT_FEE_RATE if fee is None:
return f return fee
fee = min(FEERATE_MAX_DYNAMIC, fee)
fee = max(FEERATE_DEFAULT_RELAY, fee)
return fee
return get_fee_within_limits
def max_fee_rate(self): @impose_hard_limits_on_fee
f = self.get('max_fee_rate', MAX_FEE_RATE) def eta_to_fee(self, i):
if f==0: """Returns fee in sat/kbyte."""
f = MAX_FEE_RATE
return f
def dynfee(self, i):
if i < 4: if i < 4:
j = FEE_TARGETS[i] j = FEE_ETA_TARGETS[i]
fee = self.fee_estimates.get(j) fee = self.fee_estimates.get(j)
else: else:
assert i == 4 assert i == 4
fee = self.fee_estimates.get(2) fee = self.fee_estimates.get(2)
if fee is not None: if fee is not None:
fee += fee/2 fee += fee/2
if fee is not None:
fee = min(5*MAX_FEE_RATE, fee)
return fee return fee
def reverse_dynfee(self, fee_per_kb): def fee_to_depth(self, target_fee):
depth = 0
for fee, s in self.mempool_fees:
depth += s
if fee <= target_fee:
break
else:
return 0
return depth
@impose_hard_limits_on_fee
def depth_to_fee(self, i):
"""Returns fee in zat/kbyte."""
target = self.depth_target(i)
depth = 0
for fee, s in self.mempool_fees:
depth += s
if depth > target:
break
else:
return 0
return fee * 1000
def depth_target(self, i):
return FEE_DEPTH_TARGETS[i]
def eta_target(self, i):
if i == len(FEE_ETA_TARGETS):
return 1
return FEE_ETA_TARGETS[i]
def fee_to_eta(self, fee_per_kb):
import operator import operator
l = list(self.fee_estimates.items()) + [(1, self.dynfee(4))] l = list(self.fee_estimates.items()) + [(1, self.eta_to_fee(4))]
dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l) dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l)
min_target, min_value = min(dist, key=operator.itemgetter(1)) min_target, min_value = min(dist, key=operator.itemgetter(1))
if fee_per_kb < self.fee_estimates.get(25)/2: if fee_per_kb < self.fee_estimates.get(25)/2:
min_target = -1 min_target = -1
return min_target return min_target
def depth_tooltip(self, depth):
return "%.1f MB from tip"%(depth/1000000)
def eta_tooltip(self, x):
if x < 0:
return _('Low fee')
elif x == 1:
return _('In the next block')
else:
return _('Within {} blocks').format(x)
def get_fee_status(self):
dyn = self.is_dynfee()
mempool = self.use_mempool_fees()
pos = self.get_depth_level() if mempool else self.get_fee_level()
fee_rate = self.fee_per_kb()
target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate)
return tooltip + ' [%s]'%target if dyn else target + ' [Static]'
def get_fee_text(self, pos, dyn, mempool, fee_rate):
"""Returns (text, tooltip) where
text is what we target: static fee / num blocks to confirm in / mempool depth
tooltip is the corresponding estimate (e.g. num blocks for a static fee)
"""
rate_str = (format_satoshis(fee_rate/1000, False, 0, 0, False) + ' sat/byte') if fee_rate is not None else 'unknown'
if dyn:
if mempool:
depth = self.depth_target(pos)
text = self.depth_tooltip(depth)
else:
eta = self.eta_target(pos)
text = self.eta_tooltip(eta)
tooltip = rate_str
else:
text = rate_str
if mempool and self.has_fee_mempool():
depth = self.fee_to_depth(fee_rate)
tooltip = self.depth_tooltip(depth)
elif not mempool and self.has_fee_etas():
eta = self.fee_to_eta(fee_rate)
tooltip = self.eta_tooltip(eta)
else:
tooltip = ''
return text, tooltip
def get_depth_level(self):
maxp = len(FEE_DEPTH_TARGETS) - 1
return min(maxp, self.get('depth_level', 2))
def get_fee_level(self):
maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
return min(maxp, self.get('fee_level', 2))
def get_fee_slider(self, dyn, mempool):
if dyn:
if mempool:
pos = self.get_depth_level()
maxp = len(FEE_DEPTH_TARGETS) - 1
fee_rate = self.depth_to_fee(pos)
else:
pos = self.get_fee_level()
maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
fee_rate = self.eta_to_fee(pos)
else:
fee_rate = self.fee_per_kb()
pos = self.static_fee_index(fee_rate)
maxp = 9
return maxp, pos, fee_rate
def static_fee(self, i): def static_fee(self, i):
return self.fee_rates[i] return FEERATE_STATIC_VALUES[i]
def static_fee_index(self, value): def static_fee_index(self, value):
dist = list(map(lambda x: abs(x - value), self.fee_rates)) dist = list(map(lambda x: abs(x - value), FEERATE_STATIC_VALUES))
return min(range(len(dist)), key=dist.__getitem__) return min(range(len(dist)), key=dist.__getitem__)
def has_fee_estimates(self): def has_fee_etas(self):
return len(self.fee_estimates)==4 return len(self.fee_estimates) == 4
def has_fee_mempool(self):
return bool(self.mempool_fees)
def has_dynamic_fees_ready(self):
if self.use_mempool_fees():
return self.has_fee_mempool()
else:
return self.has_fee_etas()
# 'dynamic' fees are disabled - we use 'static' (but adjustable) fees
def is_dynfee(self): def is_dynfee(self):
return self.get('dynamic_fees', False) return bool(self.get('dynamic_fees', True))
def use_mempool_fees(self):
return bool(self.get('mempool_fees', False))
def fee_per_kb(self): def fee_per_kb(self):
dyn = self.is_dynfee() """Returns sat/kvB fee to pay for a txn.
if dyn: Note: might return None.
fee_rate = self.dynfee(self.get('fee_level', 2)) """
if self.is_dynfee():
if self.use_mempool_fees():
fee_rate = self.depth_to_fee(self.get_depth_level())
else:
fee_rate = self.eta_to_fee(self.get_fee_level())
else: else:
fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2) fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE)
return fee_rate return fee_rate
def fee_per_byte(self):
"""Returns sat/vB fee to pay for a txn.
Note: might return None.
"""
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):
return self.estimate_fee_for_feerate(self.fee_per_kb(), size) 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 @classmethod
def estimate_fee_for_feerate(cls, fee_per_kb, size): def estimate_fee_for_feerate(cls, fee_per_kb, size):
return int(fee_per_kb * size / 1000.) # note: We only allow integer sat/byte values atm.
# The GUI for simplicity reasons only displays integer sat/byte,
# and for the sake of consistency, we thus only use integer sat/byte in
# the backend too.
fee_per_byte = int(fee_per_kb / 1000)
return int(fee_per_byte * size)
def update_fee_estimates(self, key, value): def update_fee_estimates(self, key, value):
self.fee_estimates[key] = value self.fee_estimates[key] = value
@ -275,11 +469,7 @@ class SimpleConfig(PrintError):
Returns True if an update should be requested. Returns True if an update should be requested.
""" """
now = time.time() now = time.time()
prev_updates = self.fee_estimates_last_updated.values() return now - self.last_time_fee_estimates_requested > 60
oldest_fee_time = min(prev_updates) if prev_updates else 0
stale_fees = now - oldest_fee_time > 7200
old_request = now - self.last_time_fee_estimates_requested > 60
return stale_fees and old_request
def requested_fee_estimates(self): def requested_fee_estimates(self):
self.last_time_fee_estimates_requested = time.time() self.last_time_fee_estimates_requested = time.time()
@ -291,21 +481,6 @@ class SimpleConfig(PrintError):
return device return device
def read_system_config(path=SYSTEM_CONFIG_PATH):
"""Parse and return the system config settings in /etc/electrum.conf."""
result = {}
if os.path.exists(path):
import configparser
p = configparser.ConfigParser()
try:
p.read(path)
for k, v in p.items('client'):
result[k] = v
except (configparser.NoSectionError, configparser.MissingSectionHeaderError):
pass
return result
def read_user_config(path): def read_user_config(path):
"""Parse and store the user config settings in electrum.conf into user_config[].""" """Parse and store the user config settings in electrum.conf into user_config[]."""
if not path: if not path:

View File

@ -33,7 +33,7 @@ import pbkdf2, hmac, hashlib
import base64 import base64
import zlib import zlib
from .util import PrintError, profiler from .util import PrintError, profiler, InvalidPassword
from .plugins import run_hook, plugin_loaders from .plugins import run_hook, plugin_loaders
from .keystore import bip44_derivation from .keystore import bip44_derivation
from . import bitcoin from . import bitcoin
@ -56,6 +56,13 @@ def multisig_type(wallet_type):
match = [int(x) for x in match.group(1, 2)] match = [int(x) for x in match.group(1, 2)]
return match return match
def get_derivation_used_for_hw_device_encryption():
return ("m"
"/4541509'" # ascii 'ELE' as decimal ("BIP43 purpose")
"/1112098098'") # ascii 'BIE2' as decimal
# storage encryption version
STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW = range(0, 3)
class WalletStorage(PrintError): class WalletStorage(PrintError):
@ -70,9 +77,11 @@ class WalletStorage(PrintError):
if self.file_exists(): if self.file_exists():
with open(self.path, "r") as f: with open(self.path, "r") as f:
self.raw = f.read() self.raw = f.read()
self._encryption_version = self._init_encryption_version()
if not self.is_encrypted(): if not self.is_encrypted():
self.load_data(self.raw) self.load_data(self.raw)
else: else:
self._encryption_version = STO_EV_PLAINTEXT
# avoid new wallets getting 'upgraded' # avoid new wallets getting 'upgraded'
self.put('seed_version', FINAL_SEED_VERSION) self.put('seed_version', FINAL_SEED_VERSION)
@ -106,11 +115,47 @@ class WalletStorage(PrintError):
if self.requires_upgrade(): if self.requires_upgrade():
self.upgrade() self.upgrade()
def is_past_initial_decryption(self):
"""Return if storage is in a usable state for normal operations.
The value is True exactly
if encryption is disabled completely (self.is_encrypted() == False),
or if encryption is enabled but the contents have already been decrypted.
"""
return bool(self.data)
def is_encrypted(self): def is_encrypted(self):
"""Return if storage encryption is currently enabled."""
return self.get_encryption_version() != STO_EV_PLAINTEXT
def is_encrypted_with_user_pw(self):
return self.get_encryption_version() == STO_EV_USER_PW
def is_encrypted_with_hw_device(self):
return self.get_encryption_version() == STO_EV_XPUB_PW
def get_encryption_version(self):
"""Return the version of encryption used for this storage.
0: plaintext / no encryption
ECIES, private key derived from a password,
1: password is provided by user
2: password is derived from an xpub; used with hw wallets
"""
return self._encryption_version
def _init_encryption_version(self):
try: try:
return base64.b64decode(self.raw)[0:4] == b'BIE1' magic = base64.b64decode(self.raw)[0:4]
if magic == b'BIE1':
return STO_EV_USER_PW
elif magic == b'BIE2':
return STO_EV_XPUB_PW
else:
return STO_EV_PLAINTEXT
except: except:
return False return STO_EV_PLAINTEXT
def file_exists(self): def file_exists(self):
return self.path and os.path.exists(self.path) return self.path and os.path.exists(self.path)
@ -120,20 +165,50 @@ class WalletStorage(PrintError):
ec_key = bitcoin.EC_KEY(secret) ec_key = bitcoin.EC_KEY(secret)
return ec_key return ec_key
def _get_encryption_magic(self):
v = self._encryption_version
if v == STO_EV_USER_PW:
return b'BIE1'
elif v == STO_EV_XPUB_PW:
return b'BIE2'
else:
raise Exception('no encryption magic for version: %s' % v)
def decrypt(self, password): def decrypt(self, password):
ec_key = self.get_key(password) ec_key = self.get_key(password)
s = zlib.decompress(ec_key.decrypt_message(self.raw)) if self.raw else None if self.raw:
enc_magic = self._get_encryption_magic()
s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic))
else:
s = None
self.pubkey = ec_key.get_public_key() self.pubkey = ec_key.get_public_key()
s = s.decode('utf8') s = s.decode('utf8')
self.load_data(s) self.load_data(s)
def set_password(self, password, encrypt): def check_password(self, password):
self.put('use_encryption', bool(password)) """Raises an InvalidPassword exception on invalid password"""
if encrypt and password: if not self.is_encrypted():
return
if self.pubkey and self.pubkey != self.get_key(password).get_public_key():
raise InvalidPassword()
def set_keystore_encryption(self, enable):
self.put('use_encryption', enable)
def set_password(self, password, enc_version=None):
"""Set a password to be used for encrypting this storage."""
if enc_version is None:
enc_version = self._encryption_version
if password and enc_version != STO_EV_PLAINTEXT:
ec_key = self.get_key(password) ec_key = self.get_key(password)
self.pubkey = ec_key.get_public_key() self.pubkey = ec_key.get_public_key()
self._encryption_version = enc_version
else: else:
self.pubkey = None self.pubkey = None
self._encryption_version = STO_EV_PLAINTEXT
# make sure next storage.write() saves changes
with self.lock:
self.modified = True
def get(self, key, default=None): def get(self, key, default=None):
with self.lock: with self.lock:
@ -175,7 +250,8 @@ class WalletStorage(PrintError):
if self.pubkey: if self.pubkey:
s = bytes(s, 'utf8') s = bytes(s, 'utf8')
c = zlib.compress(s) c = zlib.compress(s)
s = bitcoin.encrypt_message(c, self.pubkey) enc_magic = self._get_encryption_magic()
s = bitcoin.encrypt_message(c, self.pubkey, enc_magic)
s = s.decode('utf8') s = s.decode('utf8')
temp_path = "%s.tmp.%s" % (self.path, os.getpid()) temp_path = "%s.tmp.%s" % (self.path, os.getpid())
@ -263,6 +339,9 @@ class WalletStorage(PrintError):
self.write() self.write()
def convert_wallet_type(self): def convert_wallet_type(self):
if not self._is_upgrade_method_needed(0, 13):
return
wallet_type = self.get('wallet_type') wallet_type = self.get('wallet_type')
if wallet_type == 'btchip': wallet_type = 'ledger' if wallet_type == 'btchip': wallet_type = 'ledger'
if self.get('keystore') or self.get('x1/') or wallet_type=='imported': if self.get('keystore') or self.get('x1/') or wallet_type=='imported':
@ -446,6 +525,9 @@ class WalletStorage(PrintError):
self.put('seed_version', 16) self.put('seed_version', 16)
def convert_imported(self): def convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
# '/x' is the internal ID for imported accounts # '/x' is the internal ID for imported accounts
d = self.get('accounts', {}).get('/x', {}).get('imported',{}) d = self.get('accounts', {}).get('/x', {}).get('imported',{})
if not d: if not d:
@ -472,6 +554,9 @@ class WalletStorage(PrintError):
raise BaseException('no addresses or privkeys') raise BaseException('no addresses or privkeys')
def convert_account(self): def convert_account(self):
if not self._is_upgrade_method_needed(0, 13):
return
self.put('accounts', None) self.put('accounts', None)
def _is_upgrade_method_needed(self, min_version, max_version): def _is_upgrade_method_needed(self, min_version, max_version):

View File

@ -84,11 +84,13 @@ class Synchronizer(ThreadJob):
return bh2u(hashlib.sha256(status.encode('ascii')).digest()) return bh2u(hashlib.sha256(status.encode('ascii')).digest())
def on_address_status(self, response): def on_address_status(self, response):
if self.wallet.synchronizer is None:
return # we have been killed, this was just an orphan callback
params, result = self.parse_response(response) params, result = self.parse_response(response)
if not params: if not params:
return return
addr = params[0] addr = params[0]
history = self.wallet.get_address_history(addr) history = self.wallet.history.get(addr, [])
if self.get_status(history) != result: if self.get_status(history) != result:
if self.requested_histories.get(addr) is None: if self.requested_histories.get(addr) is None:
self.requested_histories[addr] = result self.requested_histories[addr] = result
@ -98,6 +100,8 @@ class Synchronizer(ThreadJob):
self.requested_addrs.remove(addr) self.requested_addrs.remove(addr)
def on_address_history(self, response): def on_address_history(self, response):
if self.wallet.synchronizer is None:
return # we have been killed, this was just an orphan callback
params, result = self.parse_response(response) params, result = self.parse_response(response)
if not params: if not params:
return return
@ -127,6 +131,8 @@ class Synchronizer(ThreadJob):
self.requested_histories.pop(addr) self.requested_histories.pop(addr)
def tx_response(self, response): def tx_response(self, response):
if self.wallet.synchronizer is None:
return # we have been killed, this was just an orphan callback
params, result = self.parse_response(response) params, result = self.parse_response(response)
if not params: if not params:
return return

View File

@ -11,8 +11,9 @@ from lib.bitcoin import (
var_int, op_push, address_to_script, regenerate_key, var_int, op_push, address_to_script, regenerate_key,
verify_message, deserialize_privkey, serialize_privkey, is_segwit_address, verify_message, deserialize_privkey, serialize_privkey, is_segwit_address,
is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub, is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub,
xpub_type, is_xprv, is_bip32_derivation, seed_type, NetworkConstants) xpub_type, is_xprv, is_bip32_derivation, seed_type)
from lib.util import bfh from lib.util import bfh
from lib import constants
try: try:
import ecdsa import ecdsa
@ -168,12 +169,12 @@ class Test_bitcoin_testnet(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
NetworkConstants.set_testnet() constants.set_testnet()
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
super().tearDownClass() super().tearDownClass()
NetworkConstants.set_mainnet() constants.set_mainnet()
def test_address_to_script(self): def test_address_to_script(self):
# bech32 native segwit # bech32 native segwit
@ -271,6 +272,7 @@ class Test_keyImport(unittest.TestCase):
priv_pub_addr = ( priv_pub_addr = (
{'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6', {'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
'exported_privkey': 'p2pkh:KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997', 'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997',
'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR', 'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR',
'minikey' : False, 'minikey' : False,
@ -278,7 +280,17 @@ class Test_keyImport(unittest.TestCase):
'compressed': True, 'compressed': True,
'addr_encoding': 'base58', 'addr_encoding': 'base58',
'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'}, 'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'},
{'priv': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',
'exported_privkey': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',
'pub': '0352d78b4b37e0f6d4e164423436f2925fa57817467178eca550a88f2821973c41',
'address': '1GXgZ5Qi6gmXTHVSpUPZLy4Ci2nbfb3ZNb',
'minikey': False,
'txin_type': 'p2pkh',
'compressed': True,
'addr_encoding': 'base58',
'scripthash': 'a9b2a76fc196c553b352186dfcca81fcf323a721cd8431328f8e9d54216818c1'},
{'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', {'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
'exported_privkey': 'p2pkh:5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f', 'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f',
'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6', 'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6',
'minikey': False, 'minikey': False,
@ -286,7 +298,17 @@ class Test_keyImport(unittest.TestCase):
'compressed': False, 'compressed': False,
'addr_encoding': 'base58', 'addr_encoding': 'base58',
'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'}, 'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'},
{'priv': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',
'exported_privkey': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',
'pub': '048f0431b0776e8210376c81280011c2b68be43194cb00bd47b7e9aa66284b713ce09556cde3fee606051a07613f3c159ef3953b8927c96ae3dae94a6ba4182e0e',
'address': '147kiRHHm9fqeMQSgqf4k35XzuWLP9fmmS',
'minikey': False,
'txin_type': 'p2pkh',
'compressed': False,
'addr_encoding': 'base58',
'scripthash': '6dd2e07ad2de9ba8eec4bbe8467eb53f8845acff0d9e6f5627391acc22ff62df'},
{'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz', {'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz',
'exported_privkey': 'p2wpkh-p2sh:Kz9XebiCXL2BZzhYJViiHDzn5iup1povWV8aqstzWU4sz1K5nVva',
'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81', 'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81',
'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7', 'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7',
'minikey': False, 'minikey': False,
@ -294,7 +316,17 @@ class Test_keyImport(unittest.TestCase):
'compressed': True, 'compressed': True,
'addr_encoding': 'base58', 'addr_encoding': 'base58',
'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'}, 'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'},
{'priv': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW',
'exported_privkey': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW',
'pub': '0229da20a15b3363b2c28e3c5093c180b56c439df0b968a970366bb1f38435361e',
'address': '3C79goMwT7zSTjXnPoCg6VFGAnUpZAkyus',
'minikey': False,
'txin_type': 'p2wpkh-p2sh',
'compressed': True,
'addr_encoding': 'base58',
'scripthash': '714bf6bfe1083e69539f40d4c7a7dca85d187471b35642e55f20d7e866494cf7'},
{'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj', {'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj',
'exported_privkey': 'p2wpkh:Kz6SuyPM5VktY5dr2d2YqdVgBA6LCWkiHqXJaC3BzxnMPSUuYzmF',
'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b', 'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b',
'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue', 'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue',
'minikey': False, 'minikey': False,
@ -302,8 +334,18 @@ class Test_keyImport(unittest.TestCase):
'compressed': True, 'compressed': True,
'addr_encoding': 'bech32', 'addr_encoding': 'bech32',
'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'}, 'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'},
{'priv': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo',
'exported_privkey': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo',
'pub': '038c57657171c1f73e34d5b3971d05867d50221ad94980f7e87cbc2344425e6a1e',
'address': 'bc1qpakeeg4d9ydyjxd8paqrw4xy9htsg532xzxn50',
'minikey': False,
'txin_type': 'p2wpkh',
'compressed': True,
'addr_encoding': 'bech32',
'scripthash': '242f02adde84ebb2a7dd778b2f3a81b3826f111da4d8960d826d7a4b816cb261'},
# from http://bitscan.com/articles/security/spotlight-on-mini-private-keys # from http://bitscan.com/articles/security/spotlight-on-mini-private-keys
{'priv': 'SzavMBLoXU6kDrqtUVmffv', {'priv': 'SzavMBLoXU6kDrqtUVmffv',
'exported_privkey': 'p2pkh:L53fCHmQhbNp1B4JipfBtfeHZH7cAibzG9oK19XfiFzxHgAkz6JK',
'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9', 'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9',
'address': '19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR', 'address': '19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR',
'minikey': True, 'minikey': True,
@ -344,6 +386,7 @@ class Test_keyImport(unittest.TestCase):
def test_is_private_key(self): def test_is_private_key(self):
for priv_details in self.priv_pub_addr: for priv_details in self.priv_pub_addr:
self.assertTrue(is_private_key(priv_details['priv'])) self.assertTrue(is_private_key(priv_details['priv']))
self.assertTrue(is_private_key(priv_details['exported_privkey']))
self.assertFalse(is_private_key(priv_details['pub'])) self.assertFalse(is_private_key(priv_details['pub']))
self.assertFalse(is_private_key(priv_details['address'])) self.assertFalse(is_private_key(priv_details['address']))
self.assertFalse(is_private_key("not a privkey")) self.assertFalse(is_private_key("not a privkey"))
@ -352,8 +395,7 @@ class Test_keyImport(unittest.TestCase):
for priv_details in self.priv_pub_addr: for priv_details in self.priv_pub_addr:
txin_type, privkey, compressed = deserialize_privkey(priv_details['priv']) txin_type, privkey, compressed = deserialize_privkey(priv_details['priv'])
priv2 = serialize_privkey(privkey, compressed, txin_type) priv2 = serialize_privkey(privkey, compressed, txin_type)
if not priv_details['minikey']: self.assertEqual(priv_details['exported_privkey'], priv2)
self.assertEqual(priv_details['priv'], priv2)
def test_address_to_scripthash(self): def test_address_to_scripthash(self):
for priv_details in self.priv_pub_addr: for priv_details in self.priv_pub_addr:

View File

@ -6,8 +6,7 @@ import tempfile
import shutil import shutil
from io import StringIO from io import StringIO
from lib.simple_config import (SimpleConfig, read_system_config, from lib.simple_config import (SimpleConfig, read_user_config)
read_user_config)
class Test_SimpleConfig(unittest.TestCase): class Test_SimpleConfig(unittest.TestCase):
@ -37,18 +36,15 @@ class Test_SimpleConfig(unittest.TestCase):
def test_simple_config_key_rename(self): def test_simple_config_key_rename(self):
"""auto_cycle was renamed auto_connect""" """auto_cycle was renamed auto_connect"""
fake_read_system = lambda : {}
fake_read_user = lambda _: {"auto_cycle": True} fake_read_user = lambda _: {"auto_cycle": True}
read_user_dir = lambda : self.user_dir read_user_dir = lambda : self.user_dir
config = SimpleConfig(options=self.options, config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user, read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir) read_user_dir_function=read_user_dir)
self.assertEqual(config.get("auto_connect"), True) self.assertEqual(config.get("auto_connect"), True)
self.assertEqual(config.get("auto_cycle"), None) self.assertEqual(config.get("auto_cycle"), None)
fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True} fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True}
config = SimpleConfig(options=self.options, config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user, read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir) read_user_dir_function=read_user_dir)
self.assertEqual(config.get("auto_connect"), False) self.assertEqual(config.get("auto_connect"), False)
@ -57,110 +53,51 @@ class Test_SimpleConfig(unittest.TestCase):
def test_simple_config_command_line_overrides_everything(self): def test_simple_config_command_line_overrides_everything(self):
"""Options passed by command line override all other configuration """Options passed by command line override all other configuration
sources""" sources"""
fake_read_system = lambda : {"electrum_path": "a"}
fake_read_user = lambda _: {"electrum_path": "b"} fake_read_user = lambda _: {"electrum_path": "b"}
read_user_dir = lambda : self.user_dir read_user_dir = lambda : self.user_dir
config = SimpleConfig(options=self.options, config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user, read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir) read_user_dir_function=read_user_dir)
self.assertEqual(self.options.get("electrum_path"), self.assertEqual(self.options.get("electrum_path"),
config.get("electrum_path")) config.get("electrum_path"))
def test_simple_config_user_config_overrides_system_config(self):
"""Options passed in user config override system config."""
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
fake_read_user = lambda _: {"electrum_path": "b"}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir)
self.assertEqual("b", config.get("electrum_path"))
def test_simple_config_system_config_ignored_if_portable(self):
"""If electrum is started with the "portable" flag, system
configuration is completely ignored."""
fake_read_system = lambda : {"some_key": "some_value"}
fake_read_user = lambda _: {}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={"portable": True},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir)
self.assertEqual(config.get("some_key"), None)
def test_simple_config_user_config_is_used_if_others_arent_specified(self): def test_simple_config_user_config_is_used_if_others_arent_specified(self):
"""If no system-wide configuration and no command-line options are """If no system-wide configuration and no command-line options are
specified, the user configuration is used instead.""" specified, the user configuration is used instead."""
fake_read_system = lambda : {}
fake_read_user = lambda _: {"electrum_path": self.electrum_dir} fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
read_user_dir = lambda : self.user_dir read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={}, config = SimpleConfig(options={},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user, read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir) read_user_dir_function=read_user_dir)
self.assertEqual(self.options.get("electrum_path"), self.assertEqual(self.options.get("electrum_path"),
config.get("electrum_path")) config.get("electrum_path"))
def test_cannot_set_options_passed_by_command_line(self): def test_cannot_set_options_passed_by_command_line(self):
fake_read_system = lambda : {}
fake_read_user = lambda _: {"electrum_path": "b"} fake_read_user = lambda _: {"electrum_path": "b"}
read_user_dir = lambda : self.user_dir read_user_dir = lambda : self.user_dir
config = SimpleConfig(options=self.options, config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user, read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir) read_user_dir_function=read_user_dir)
config.set_key("electrum_path", "c") config.set_key("electrum_path", "c")
self.assertEqual(self.options.get("electrum_path"), self.assertEqual(self.options.get("electrum_path"),
config.get("electrum_path")) config.get("electrum_path"))
def test_can_set_options_from_system_config(self):
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
fake_read_user = lambda _: {}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir)
config.set_key("electrum_path", "c")
self.assertEqual("c", config.get("electrum_path"))
def test_can_set_options_set_in_user_config(self): def test_can_set_options_set_in_user_config(self):
another_path = tempfile.mkdtemp() another_path = tempfile.mkdtemp()
fake_read_system = lambda : {}
fake_read_user = lambda _: {"electrum_path": self.electrum_dir} fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
read_user_dir = lambda : self.user_dir read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={}, config = SimpleConfig(options={},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir)
config.set_key("electrum_path", another_path)
self.assertEqual(another_path, config.get("electrum_path"))
def test_can_set_options_from_system_config_if_portable(self):
"""If the "portable" flag is set, the user can overwrite system
configuration options."""
another_path = tempfile.mkdtemp()
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
fake_read_user = lambda _: {}
read_user_dir = lambda : self.user_dir
config = SimpleConfig(options={"portable": True},
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user, read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir) read_user_dir_function=read_user_dir)
config.set_key("electrum_path", another_path) config.set_key("electrum_path", another_path)
self.assertEqual(another_path, config.get("electrum_path")) self.assertEqual(another_path, config.get("electrum_path"))
def test_user_config_is_not_written_with_read_only_config(self): def test_user_config_is_not_written_with_read_only_config(self):
"""The user config does not contain command-line options or system """The user config does not contain command-line options when saved."""
options when saved."""
fake_read_system = lambda : {"something": "b"}
fake_read_user = lambda _: {"something": "a"} fake_read_user = lambda _: {"something": "a"}
read_user_dir = lambda : self.user_dir read_user_dir = lambda : self.user_dir
self.options.update({"something": "c"}) self.options.update({"something": "c"})
config = SimpleConfig(options=self.options, config = SimpleConfig(options=self.options,
read_system_config_function=fake_read_system,
read_user_config_function=fake_read_user, read_user_config_function=fake_read_user,
read_user_dir_function=read_user_dir) read_user_dir_function=read_user_dir)
config.save_user_config() config.save_user_config()
@ -168,48 +105,10 @@ class Test_SimpleConfig(unittest.TestCase):
with open(os.path.join(self.electrum_dir, "config"), "r") as f: with open(os.path.join(self.electrum_dir, "config"), "r") as f:
contents = f.read() contents = f.read()
result = ast.literal_eval(contents) result = ast.literal_eval(contents)
result.pop('config_version', None)
self.assertEqual({"something": "a"}, result) self.assertEqual({"something": "a"}, result)
class TestSystemConfig(unittest.TestCase):
sample_conf = """
[client]
gap_limit = 5
[something_else]
everything = 42
"""
def setUp(self):
super(TestSystemConfig, self).setUp()
self.thefile = tempfile.mkstemp(suffix=".electrum.test.conf")[1]
def tearDown(self):
super(TestSystemConfig, self).tearDown()
os.remove(self.thefile)
def test_read_system_config_file_does_not_exist(self):
somefile = "/foo/I/do/not/exist/electrum.conf"
result = read_system_config(somefile)
self.assertEqual({}, result)
def test_read_system_config_file_returns_file_options(self):
with open(self.thefile, "w") as f:
f.write(self.sample_conf)
result = read_system_config(self.thefile)
self.assertEqual({"gap_limit": "5"}, result)
def test_read_system_config_file_no_sections(self):
with open(self.thefile, "w") as f:
f.write("gap_limit = 5") # The file has no sections at all
result = read_system_config(self.thefile)
self.assertEqual({}, result)
class TestUserConfig(unittest.TestCase): class TestUserConfig(unittest.TestCase):
def setUp(self): def setUp(self):

View File

@ -231,6 +231,389 @@ class TestTransaction(unittest.TestCase):
tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000') tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000')
self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid()) self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid())
def test_txid_input_p2wsh_p2sh_not_multisig(self):
tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000')
self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid())
# input: p2sh, not multisig
def test_txid_regression_issue_3899(self):
tx = transaction.Transaction('0100000004328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c010000000b0009630330472d5fae685bffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c020000000b0009630359646d5fae6858ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c030000000b000963034bd4715fae6854ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c040000000b000963036de8705fae6860ffffffff0130750000000000001976a914b5abca61d20f9062fb1fdbb880d9d93bac36675188ac00000000')
self.assertEqual('f570d5d1e965ee61bcc7005f8fefb1d3abbed9d7ddbe035e2a68fa07e5fc4a0d', tx.txid())
# these transactions are from Bitcoin Core unit tests --->
# https://github.com/bitcoin/bitcoin/blob/11376b5583a283772c82f6d32d0007cdbf5b8ef0/src/test/data/tx_valid.json
def test_txid_bitcoin_core_0001(self):
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
self.assertEqual('23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63', tx.txid())
def test_txid_bitcoin_core_0002(self):
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
self.assertEqual('fcabc409d8e685da28536e1e5ccc91264d755cd4c57ed4cae3dbaa4d3b93e8ed', tx.txid())
def test_txid_bitcoin_core_0003(self):
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
self.assertEqual('c9aa95f2c48175fdb70b34c23f1c3fc44f869b073a6f79b1343fbce30c3cb575', tx.txid())
def test_txid_bitcoin_core_0004(self):
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
self.assertEqual('da94fda32b55deb40c3ed92e135d69df7efc4ee6665e0beb07ef500f407c9fd2', tx.txid())
def test_txid_bitcoin_core_0005(self):
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
self.assertEqual('f76f897b206e4f78d60fe40f2ccb542184cfadc34354d3bb9bdc30cc2f432b86', tx.txid())
def test_txid_bitcoin_core_0006(self):
tx = transaction.Transaction('01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d00000000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac00000000')
self.assertEqual('c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73', tx.txid())
def test_txid_bitcoin_core_0007(self):
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000')
self.assertEqual('e41ffe19dff3cbedb413a2ca3fbbcd05cb7fd7397ffa65052f8928aa9c700092', tx.txid())
def test_txid_bitcoin_core_0008(self):
tx = transaction.Transaction('01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000')
self.assertEqual('f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb', tx.txid())
def test_txid_bitcoin_core_0009(self):
tx = transaction.Transaction('01000000020002000000000000000000000000000000000000000000000000000000000000000000000151ffffffff0001000000000000000000000000000000000000000000000000000000000000000000006b483045022100c9cdd08798a28af9d1baf44a6c77bcc7e279f47dc487c8c899911bc48feaffcc0220503c5c50ae3998a733263c5c0f7061b483e2b56c4c41b456e7d2f5a78a74c077032102d5c25adb51b61339d2b05315791e21bbe80ea470a49db0135720983c905aace0ffffffff010000000000000000015100000000')
self.assertEqual('b56471690c3ff4f7946174e51df68b47455a0d29344c351377d712e6d00eabe5', tx.txid())
def test_txid_bitcoin_core_0010(self):
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000')
self.assertEqual('99517e5b47533453cc7daa332180f578be68b80370ecfe84dbfff7f19d791da4', tx.txid())
def test_txid_bitcoin_core_0011(self):
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100c66c9cdf4c43609586d15424c54707156e316d88b0a1534c9e6b0d4f311406310221009c0fe51dbc9c4ab7cc25d3fdbeccf6679fe6827f08edf2b4a9f16ee3eb0e438a0123210338e8034509af564c62644c07691942e0c056752008a173c89f60ab2a88ac2ebfacffffffff010000000000000000015100000000')
self.assertEqual('ab097537b528871b9b64cb79a769ae13c3c3cd477cc9dddeebe657eabd7fdcea', tx.txid())
def test_txid_bitcoin_core_0012(self):
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010040075af0750700015100000000')
self.assertEqual('4d163e00f1966e9a1eab8f9374c3e37f4deb4857c247270e25f7d79a999d2dc9', tx.txid())
def test_txid_bitcoin_core_0013(self):
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510000000000000000015100000000')
self.assertEqual('9fe2ef9dde70e15d78894a4800b7df3bbfb1addb9a6f7d7c204492fdb6ee6cc4', tx.txid())
def test_txid_bitcoin_core_0014(self):
tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025151ffffffff010000000000000000015100000000')
self.assertEqual('99d3825137602e577aeaf6a2e3c9620fd0e605323dc5265da4a570593be791d4', tx.txid())
def test_txid_bitcoin_core_0015(self):
tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6451515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151ffffffff010000000000000000015100000000')
self.assertEqual('c0d67409923040cc766bbea12e4c9154393abef706db065ac2e07d91a9ba4f84', tx.txid())
def test_txid_bitcoin_core_0016(self):
tx = transaction.Transaction('010000000200010000000000000000000000000000000000000000000000000000000000000000000049483045022100d180fd2eb9140aeb4210c9204d3f358766eb53842b2a9473db687fa24b12a3cc022079781799cd4f038b85135bbe49ec2b57f306b2bb17101b17f71f000fcab2b6fb01ffffffff0002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000')
self.assertEqual('c610d85d3d5fdf5046be7f123db8a0890cee846ee58de8a44667cfd1ab6b8666', tx.txid())
def test_txid_bitcoin_core_0017(self):
tx = transaction.Transaction('01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df101010000000002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000')
self.assertEqual('a647a7b3328d2c698bfa1ee2dd4e5e05a6cea972e764ccb9bd29ea43817ca64f', tx.txid())
def test_txid_bitcoin_core_0018(self):
tx = transaction.Transaction('010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c59da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbea097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf950650802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e18217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a3788dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9cfdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa34b26f62db4a88ac00000000')
self.assertEqual('afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae', tx.txid())
def test_txid_bitcoin_core_0019(self):
tx = transaction.Transaction('01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe0000483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa0148304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f4014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000')
self.assertEqual('f4b05f978689c89000f729cae187dcfbe64c9819af67a4f05c0b4d59e717d64d', tx.txid())
def test_txid_bitcoin_core_0020(self):
tx = transaction.Transaction('0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000')
self.assertEqual('cc60b1f899ec0a69b7c3f25ddf32c4524096a9c5b01cbd84c6d0312a0c478984', tx.txid())
def test_txid_bitcoin_core_0021(self):
tx = transaction.Transaction('01000000012c651178faca83be0b81c8c1375c4b0ad38d53c8fe1b1c4255f5e795c25792220000000049483045022100d6044562284ac76c985018fc4a90127847708c9edb280996c507b28babdc4b2a02203d74eca3f1a4d1eea7ff77b528fde6d5dc324ec2dbfdb964ba885f643b9704cd01ffffffff010100000000000000232102c2410f8891ae918cab4ffc4bb4a3b0881be67c7a1e7faa8b5acf9ab8932ec30cac00000000')
self.assertEqual('1edc7f214659d52c731e2016d258701911bd62a0422f72f6c87a1bc8dd3f8667', tx.txid())
def test_txid_bitcoin_core_0022(self):
tx = transaction.Transaction('0100000001f725ea148d92096a79b1709611e06e94c63c4ef61cbae2d9b906388efd3ca99c000000000100ffffffff0101000000000000002321028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02ac00000000')
self.assertEqual('018adb7133fde63add9149a2161802a1bcf4bdf12c39334e880c073480eda2ff', tx.txid())
def test_txid_bitcoin_core_0023(self):
tx = transaction.Transaction('0100000001be599efaa4148474053c2fa031c7262398913f1dc1d9ec201fd44078ed004e44000000004900473044022022b29706cb2ed9ef0cb3c97b72677ca2dfd7b4160f7b4beb3ba806aa856c401502202d1e52582412eba2ed474f1f437a427640306fd3838725fab173ade7fe4eae4a01ffffffff010100000000000000232103ac4bba7e7ca3e873eea49e08132ad30c7f03640b6539e9b59903cf14fd016bbbac00000000')
self.assertEqual('1464caf48c708a6cc19a296944ded9bb7f719c9858986d2501cf35068b9ce5a2', tx.txid())
def test_txid_bitcoin_core_0024(self):
tx = transaction.Transaction('010000000112b66d5e8c7d224059e946749508efea9d66bf8d0c83630f080cf30be8bb6ae100000000490047304402206ffe3f14caf38ad5c1544428e99da76ffa5455675ec8d9780fac215ca17953520220779502985e194d84baa36b9bd40a0dbd981163fa191eb884ae83fc5bd1c86b1101ffffffff010100000000000000232103905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9fac00000000')
self.assertEqual('1fb73fbfc947d52f5d80ba23b67c06a232ad83fdd49d1c0a657602f03fbe8f7a', tx.txid())
def test_txid_bitcoin_core_0025(self):
tx = transaction.Transaction('0100000001b0ef70cc644e0d37407e387e73bfad598d852a5aa6d691d72b2913cebff4bceb000000004a00473044022068cd4851fc7f9a892ab910df7a24e616f293bcb5c5fbdfbc304a194b26b60fba022078e6da13d8cb881a22939b952c24f88b97afd06b4c47a47d7f804c9a352a6d6d0100ffffffff0101000000000000002321033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194ac00000000')
self.assertEqual('24cecfce0fa880b09c9b4a66c5134499d1b09c01cc5728cd182638bea070e6ab', tx.txid())
def test_txid_bitcoin_core_0026(self):
tx = transaction.Transaction('0100000001c188aa82f268fcf08ba18950f263654a3ea6931dabc8bf3ed1d4d42aaed74cba000000004b0000483045022100940378576e069aca261a6b26fb38344e4497ca6751bb10905c76bb689f4222b002204833806b014c26fd801727b792b1260003c55710f87c5adbd7a9cb57446dbc9801ffffffff0101000000000000002321037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1ac00000000')
self.assertEqual('9eaa819e386d6a54256c9283da50c230f3d8cd5376d75c4dcc945afdeb157dd7', tx.txid())
def test_txid_bitcoin_core_0027(self):
tx = transaction.Transaction('01000000012432b60dc72cebc1a27ce0969c0989c895bdd9e62e8234839117f8fc32d17fbc000000004a493046022100a576b52051962c25e642c0fd3d77ee6c92487048e5d90818bcf5b51abaccd7900221008204f8fb121be4ec3b24483b1f92d89b1b0548513a134e345c5442e86e8617a501ffffffff010000000000000000016a00000000')
self.assertEqual('46224764c7870f95b58f155bce1e38d4da8e99d42dbb632d0dd7c07e092ee5aa', tx.txid())
def test_txid_bitcoin_core_0028(self):
tx = transaction.Transaction('01000000014710b0e7cf9f8930de259bdc4b84aa5dfb9437b665a3e3a21ff26e0bf994e183000000004a493046022100a166121a61b4eeb19d8f922b978ff6ab58ead8a5a5552bf9be73dc9c156873ea02210092ad9bc43ee647da4f6652c320800debcf08ec20a094a0aaf085f63ecb37a17201ffffffff010000000000000000016a00000000')
self.assertEqual('8d66836045db9f2d7b3a75212c5e6325f70603ee27c8333a3bce5bf670d9582e', tx.txid())
def test_txid_bitcoin_core_0029(self):
tx = transaction.Transaction('01000000015ebaa001d8e4ec7a88703a3bcf69d98c874bca6299cca0f191512bf2a7826832000000004948304502203bf754d1c6732fbf87c5dcd81258aefd30f2060d7bd8ac4a5696f7927091dad1022100f5bcb726c4cf5ed0ed34cc13dadeedf628ae1045b7cb34421bc60b89f4cecae701ffffffff010000000000000000016a00000000')
self.assertEqual('aab7ef280abbb9cc6fbaf524d2645c3daf4fcca2b3f53370e618d9cedf65f1f8', tx.txid())
def test_txid_bitcoin_core_0030(self):
tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a900000000924830450221009c0a27f886a1d8cb87f6f595fbc3163d28f7a81ec3c4b252ee7f3ac77fd13ffa02203caa8dfa09713c8c4d7ef575c75ed97812072405d932bd11e6a1593a98b679370148304502201e3861ef39a526406bad1e20ecad06be7375ad40ddb582c9be42d26c3a0d7b240221009d0a3985e96522e59635d19cc4448547477396ce0ef17a58e7d74c3ef464292301ffffffff010000000000000000016a00000000')
self.assertEqual('6327783a064d4e350c454ad5cd90201aedf65b1fc524e73709c52f0163739190', tx.txid())
def test_txid_bitcoin_core_0031(self):
tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0100ffffffff010000000000000000016a00000000')
self.assertEqual('892464645599cc3c2d165adcc612e5f982a200dfaa3e11e9ce1d228027f46880', tx.txid())
def test_txid_bitcoin_core_0032(self):
tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510151ffffffff010000000000000000016a00000000')
self.assertEqual('578db8c6c404fec22c4a8afeaf32df0e7b767c4dda3478e0471575846419e8fc', tx.txid())
def test_txid_bitcoin_core_0033(self):
tx = transaction.Transaction('0100000001e0be9e32f1f89c3d916c4f21e55cdcd096741b895cc76ac353e6023a05f4f7cc00000000d86149304602210086e5f736a2c3622ebb62bd9d93d8e5d76508b98be922b97160edc3dcca6d8c47022100b23c312ac232a4473f19d2aeb95ab7bdf2b65518911a0d72d50e38b5dd31dc820121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac4730440220508fa761865c8abd81244a168392876ee1d94e8ed83897066b5e2df2400dad24022043f5ee7538e87e9c6aef7ef55133d3e51da7cc522830a9c4d736977a76ef755c0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000')
self.assertEqual('974f5148a0946f9985e75a240bb24c573adbbdc25d61e7b016cdbb0a5355049f', tx.txid())
def test_txid_bitcoin_core_0034(self):
tx = transaction.Transaction('01000000013c6f30f99a5161e75a2ce4bca488300ca0c6112bde67f0807fe983feeff0c91001000000e608646561646265656675ab61493046022100ce18d384221a731c993939015e3d1bcebafb16e8c0b5b5d14097ec8177ae6f28022100bcab227af90bab33c3fe0a9abfee03ba976ee25dc6ce542526e9b2e56e14b7f10121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac493046022100c3b93edcc0fd6250eb32f2dd8a0bba1754b0f6c3be8ed4100ed582f3db73eba2022100bf75b5bd2eff4d6bf2bda2e34a40fcc07d4aa3cf862ceaa77b47b81eff829f9a01ab21038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000')
self.assertEqual('b0097ec81df231893a212657bf5fe5a13b2bff8b28c0042aca6fc4159f79661b', tx.txid())
def test_txid_bitcoin_core_0035(self):
tx = transaction.Transaction('01000000016f3dbe2ca96fa217e94b1017860be49f20820dea5c91bdcb103b0049d5eb566000000000fd1d0147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac47304402203757e937ba807e4a5da8534c17f9d121176056406a6465054bdd260457515c1a02200f02eccf1bec0f3a0d65df37889143c2e88ab7acec61a7b6f5aa264139141a2b0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000')
self.assertEqual('feeba255656c80c14db595736c1c7955c8c0a497622ec96e3f2238fbdd43a7c9', tx.txid())
def test_txid_bitcoin_core_0036(self):
tx = transaction.Transaction('01000000012139c555ccb81ee5b1e87477840991ef7b386bc3ab946b6b682a04a621006b5a01000000fdb40148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f2204148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390175ac4830450220646b72c35beeec51f4d5bc1cbae01863825750d7f490864af354e6ea4f625e9c022100f04b98432df3a9641719dbced53393022e7249fb59db993af1118539830aab870148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a580039017521038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000')
self.assertEqual('a0c984fc820e57ddba97f8098fa640c8a7eb3fe2f583923da886b7660f505e1e', tx.txid())
def test_txid_bitcoin_core_0037(self):
tx = transaction.Transaction('0100000002f9cbafc519425637ba4227f8d0a0b7160b4e65168193d5af39747891de98b5b5000000006b4830450221008dd619c563e527c47d9bd53534a770b102e40faa87f61433580e04e271ef2f960220029886434e18122b53d5decd25f1f4acb2480659fea20aabd856987ba3c3907e0121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffff42e7988254800876b69f24676b3e0205b77be476512ca4d970707dd5c60598ab00000000fd260100483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a53034930460221008431bdfa72bc67f9d41fe72e94c88fb8f359ffa30b33c72c121c5a877d922e1002210089ef5fc22dd8bfc6bf9ffdb01a9862d27687d424d1fefbab9e9c7176844a187a014c9052483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c7153aeffffffff01a08601000000000017a914d8dacdadb7462ae15cd906f1878706d0da8660e68700000000')
self.assertEqual('5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f', tx.txid())
def test_txid_bitcoin_core_0038(self):
tx = transaction.Transaction('0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000')
self.assertEqual('ded7ff51d89a4e1ec48162aee5a96447214d93dfb3837946af2301a28f65dbea', tx.txid())
def test_txid_bitcoin_core_0039(self):
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000')
self.assertEqual('3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e', tx.txid())
def test_txid_bitcoin_core_0040(self):
tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d')
self.assertEqual('abd62b4627d8d9b2d95fcfd8c87e37d2790637ce47d28018e3aece63c1d62649', tx.txid())
def test_txid_bitcoin_core_0041(self):
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d')
self.assertEqual('58b6de8413603b7f556270bf48caedcf17772e7105f5419f6a80be0df0b470da', tx.txid())
def test_txid_bitcoin_core_0042(self):
tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff')
self.assertEqual('5f99c0abf511294d76cbe144d86b77238a03e086974bc7a8ea0bdb2c681a0324', tx.txid())
def test_txid_bitcoin_core_0043(self):
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000')
self.assertEqual('25d35877eaba19497710666473c50d5527d38503e3521107a3fc532b74cd7453', tx.txid())
def test_txid_bitcoin_core_0044(self):
tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff')
self.assertEqual('1b9aef851895b93c62c29fbd6ca4d45803f4007eff266e2f96ff11e9b6ef197b', tx.txid())
def test_txid_bitcoin_core_0045(self):
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000')
self.assertEqual('3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e', tx.txid())
def test_txid_bitcoin_core_0046(self):
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000')
self.assertEqual('f53761038a728b1f17272539380d96e93f999218f8dcb04a8469b523445cd0fd', tx.txid())
def test_txid_bitcoin_core_0047(self):
tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000')
self.assertEqual('d193f0f32fceaf07bb25c897c8f99ca6f69a52f6274ca64efc2a2e180cb97fc1', tx.txid())
def test_txid_bitcoin_core_0048(self):
tx = transaction.Transaction('010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000')
self.assertEqual('50a1e0e6a134a564efa078e3bd088e7e8777c2c0aec10a752fd8706470103b89', tx.txid())
def test_txid_bitcoin_core_0049(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000')
self.assertEqual('e2207d1aaf6b74e5d98c2fa326d2dc803b56b30a3f90ce779fa5edb762f38755', tx.txid())
def test_txid_bitcoin_core_0050(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000')
self.assertEqual('f335864f7c12ec7946d2c123deb91eb978574b647af125a414262380c7fbd55c', tx.txid())
def test_txid_bitcoin_core_0051(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000')
self.assertEqual('d1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2', tx.txid())
def test_txid_bitcoin_core_0052(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000')
self.assertEqual('3a13e1b6371c545147173cc4055f0ed73686a9f73f092352fb4b39ca27d360e6', tx.txid())
def test_txid_bitcoin_core_0053(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000')
self.assertEqual('bffda23e40766d292b0510a1b556453c558980c70c94ab158d8286b3413e220d', tx.txid())
def test_txid_bitcoin_core_0054(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000')
self.assertEqual('01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd', tx.txid())
def test_txid_bitcoin_core_0055(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000')
self.assertEqual('f6d2359c5de2d904e10517d23e7c8210cca71076071bbf46de9fbd5f6233dbf1', tx.txid())
def test_txid_bitcoin_core_0056(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000')
self.assertEqual('19c2b7377229dae7aa3e50142a32fd37cef7171a01682f536e9ffa80c186f6c9', tx.txid())
def test_txid_bitcoin_core_0057(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000')
self.assertEqual('c9dda3a24cc8a5acb153d1085ecd2fecf6f87083122f8cdecc515b1148d4c40d', tx.txid())
def test_txid_bitcoin_core_0058(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000')
self.assertEqual('d1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2', tx.txid())
def test_txid_bitcoin_core_0059(self):
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000')
self.assertEqual('01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd', tx.txid())
def test_txid_bitcoin_core_0060(self):
tx = transaction.Transaction('02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2010000000100000000000000000000000000')
self.assertEqual('4b5e0aae1251a9dc66b4d5f483f1879bf518ea5e1765abc5a9f2084b43ed1ea7', tx.txid())
def test_txid_bitcoin_core_0061(self):
tx = transaction.Transaction('0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000')
self.assertEqual('5f16eb3ca4581e2dfb46a28140a4ee15f85e4e1c032947da8b93549b53c105f5', tx.txid())
def test_txid_bitcoin_core_0062(self):
tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000')
self.assertEqual('b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004', tx.txid())
def test_txid_bitcoin_core_0063(self):
tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000')
self.assertEqual('b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004', tx.txid())
def test_txid_bitcoin_core_0064(self):
tx = transaction.Transaction('01000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000')
self.assertEqual('fee125c6cd142083fabd0187b1dd1f94c66c89ec6e6ef6da1374881c0c19aece', tx.txid())
def test_txid_bitcoin_core_0065(self):
tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000023220020ff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000')
self.assertEqual('5f32557914351fee5f89ddee6c8983d476491d29e601d854e3927299e50450da', tx.txid())
def test_txid_bitcoin_core_0066(self):
tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff05540b0000000000000151d0070000000000000151840300000000000001513c0f00000000000001512c010000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71000000000000')
self.assertEqual('07dfa2da3d67c8a2b9f7bd31862161f7b497829d5da90a88ba0f1a905e7a43f7', tx.txid())
def test_txid_bitcoin_core_0067(self):
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
def test_txid_bitcoin_core_0068(self):
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff0484030000000000000151d0070000000000000151540b0000000000000151c800000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('f92bb6e4f3ff89172f23ef647f74c13951b665848009abb5862cdf7a0412415a', tx.txid())
def test_txid_bitcoin_core_0069(self):
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
def test_txid_bitcoin_core_0070(self):
tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff04b60300000000000001519e070000000000000151860b00000000000001009600000000000000015100000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('e657e25fc9f2b33842681613402759222a58cf7dd504d6cdc0b69a0b8c2e7dcb', tx.txid())
def test_txid_bitcoin_core_0071(self):
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
def test_txid_bitcoin_core_0072(self):
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('4ede5e22992d43d42ccdf6553fb46e448aa1065ba36423f979605c1e5ab496b8', tx.txid())
def test_txid_bitcoin_core_0073(self):
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
def test_txid_bitcoin_core_0074(self):
tx = transaction.Transaction('01000000000103000100000000000000000000000000000000000000000000000000000000000000000000000200000000010000000000000000000000000000000000000000000000000000000000000100000000ffffffff000100000000000000000000000000000000000000000000000000000000000002000000000200000003e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('cfe9f4b19f52b8366860aec0d2b5815e329299b2e9890d477edd7f1182be7ac8', tx.txid())
def test_txid_bitcoin_core_0075(self):
tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('aee8f4865ca40fa77ff2040c0d7de683bea048b103d42ca406dc07dd29d539cb', tx.txid())
def test_txid_bitcoin_core_0076(self):
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
def test_txid_bitcoin_core_0077(self):
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
def test_txid_bitcoin_core_0078(self):
tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd08020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000')
self.assertEqual('d93ab9e12d7c29d2adc13d5cdf619d53eec1f36eb6612f55af52be7ba0448e97', tx.txid())
def test_txid_bitcoin_core_0079(self):
tx = transaction.Transaction('0100000000010c00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff0001000000000000000000000000000000000000000000000000000000000000020000006a473044022026c2e65b33fcd03b2a3b0f25030f0244bd23cc45ae4dec0f48ae62255b1998a00220463aa3982b718d593a6b9e0044513fd67a5009c2fdccc59992cffc2b167889f4012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000030000006a4730440220008bd8382911218dcb4c9f2e75bf5c5c3635f2f2df49b36994fde85b0be21a1a02205a539ef10fb4c778b522c1be852352ea06c67ab74200977c722b0bc68972575a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000040000006b483045022100d9436c32ff065127d71e1a20e319e4fe0a103ba0272743dbd8580be4659ab5d302203fd62571ee1fe790b182d078ecfd092a509eac112bea558d122974ef9cc012c7012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000050000006a47304402200e2c149b114ec546015c13b2b464bbcb0cdc5872e6775787527af6cbc4830b6c02207e9396c6979fb15a9a2b96ca08a633866eaf20dc0ff3c03e512c1d5a1654f148012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000060000006b483045022100b20e70d897dc15420bccb5e0d3e208d27bdd676af109abbd3f88dbdb7721e6d6022005836e663173fbdfe069f54cde3c2decd3d0ea84378092a5d9d85ec8642e8a41012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff00010000000000000000000000000000000000000000000000000000000000000700000000ffffffff00010000000000000000000000000000000000000000000000000000000000000800000000ffffffff00010000000000000000000000000000000000000000000000000000000000000900000000ffffffff00010000000000000000000000000000000000000000000000000000000000000a00000000ffffffff00010000000000000000000000000000000000000000000000000000000000000b0000006a47304402206639c6e05e3b9d2675a7f3876286bdf7584fe2bbd15e0ce52dd4e02c0092cdc60220757d60b0a61fc95ada79d23746744c72bac1545a75ff6c2c7cdb6ae04e7e9592012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0ce8030000000000000151e9030000000000000151ea030000000000000151eb030000000000000151ec030000000000000151ed030000000000000151ee030000000000000151ef030000000000000151f0030000000000000151f1030000000000000151f2030000000000000151f30300000000000001510248304502210082219a54f61bf126bfc3fa068c6e33831222d1d7138c6faa9d33ca87fd4202d6022063f9902519624254d7c2c8ea7ba2d66ae975e4e229ae38043973ec707d5d4a83012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022017fb58502475848c1b09f162cb1688d0920ff7f142bed0ef904da2ccc88b168f02201798afa61850c65e77889cbcd648a5703b487895517c88f85cdd18b021ee246a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000000247304402202830b7926e488da75782c81a54cd281720890d1af064629ebf2e31bf9f5435f30220089afaa8b455bbeb7d9b9c3fe1ed37d07685ade8455c76472cda424d93e4074a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022026326fcdae9207b596c2b05921dbac11d81040c4d40378513670f19d9f4af893022034ecd7a282c0163b89aaa62c22ec202cef4736c58cd251649bad0d8139bcbf55012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71024730440220214978daeb2f38cd426ee6e2f44131a33d6b191af1c216247f1dd7d74c16d84a02205fdc05529b0bc0c430b4d5987264d9d075351c4f4484c16e91662e90a72aab24012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402204a6e9f199dc9672cf2ff8094aaa784363be1eb62b679f7ff2df361124f1dca3302205eeb11f70fab5355c9c8ad1a0700ea355d315e334822fa182227e9815308ee8f012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
self.assertEqual('b83579db5246aa34255642768167132a0c3d2932b186cd8fb9f5490460a0bf91', tx.txid())
def test_txid_bitcoin_core_0080(self):
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000')
self.assertEqual('2b1e44fff489d09091e5e20f9a01bbc0e8d80f0662e629fd10709cdb4922a874', tx.txid())
def test_txid_bitcoin_core_0081(self):
tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff01d00700000000000001510003483045022100e078de4e96a0e05dcdc0a414124dd8475782b5f3f0ed3f607919e9a5eeeb22bf02201de309b3a3109adb3de8074b3610d4cf454c49b61247a2779a0bcbf31c889333032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc711976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac00000000')
self.assertEqual('60ebb1dd0b598e20dd0dd462ef6723dd49f8f803b6a2492926012360119cfdd7', tx.txid())
def test_txid_bitcoin_core_0082(self):
tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff02e8030000000000000151e90300000000000001510247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000')
self.assertEqual('ed0c7f4163e275f3f77064f471eac861d01fdf55d03aa6858ebd3781f70bf003', tx.txid())
def test_txid_bitcoin_core_0083(self):
tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff02e9030000000000000151e80300000000000001510248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000')
self.assertEqual('f531ddf5ce141e1c8a7fdfc85cc634e5ff686f446a5cf7483e9dbe076b844862', tx.txid())
def test_txid_bitcoin_core_0084(self):
tx = transaction.Transaction('01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000')
self.assertEqual('98229b70948f1c17851a541f1fe532bf02c408267fecf6d7e174c359ae870654', tx.txid())
def test_txid_bitcoin_core_0085(self):
tx = transaction.Transaction('01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000')
self.assertEqual('570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab', tx.txid())
def test_txid_bitcoin_core_0086(self):
tx = transaction.Transaction('01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000')
self.assertEqual('e0b8142f587aaa322ca32abce469e90eda187f3851043cc4f2a0fff8c13fc84e', tx.txid())
def test_txid_bitcoin_core_0087(self):
tx = transaction.Transaction('0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000')
self.assertEqual('b9ecf72df06b8f98f8b63748d1aded5ffc1a1186f8a302e63cf94f6250e29f4d', tx.txid())
def test_txid_bitcoin_core_0088(self):
tx = transaction.Transaction('0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000')
self.assertEqual('27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac', tx.txid())
def test_txid_bitcoin_core_0089(self):
tx = transaction.Transaction('010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000')
self.assertEqual('22d020638e3b7e1f2f9a63124ac76f5e333c74387862e3675f64b25e960d3641', tx.txid())
def test_txid_bitcoin_core_0090(self):
tx = transaction.Transaction('0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9781d66b61fb5a7ef00ac5ad5bc6ffc78be7b44a566e3c87870e1079368df4c4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000')
self.assertEqual('2862bc0c69d2af55da7284d1b16a7cddc03971b77e5a97939cca7631add83bf5', tx.txid())
def test_txid_bitcoin_core_0091(self):
tx = transaction.Transaction('01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601522102cd74a2809ffeeed0092bc124fd79836706e41f048db3f6ae9df8708cefb83a1c2102e615999372426e46fd107b76eaf007156a507584aa2cc21de9eee3bdbd26d36c4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000')
self.assertEqual('1aebf0c98f01381765a8c33d688f8903e4d01120589ac92b78f1185dc1f4119c', tx.txid())
def test_txid_bitcoin_core_0092(self):
tx = transaction.Transaction('010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960101022102966f109c54e85d3aee8321301136cedeb9fc710fdef58a9de8a73942f8e567c021034ffc99dd9a79dd3cb31e2ab3e0b09e0e67db41ac068c625cd1f491576016c84e9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000')
self.assertEqual('45d17fb7db86162b2b6ca29fa4e163acf0ef0b54110e49b819bda1f948d423a3', tx.txid())
# txns from Bitcoin Core ends <---
class NetworkMock(object): class NetworkMock(object):

View File

@ -128,7 +128,7 @@ class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
long_user_id, short_id = trustedcoin.get_user_id( long_user_id, short_id = trustedcoin.get_user_id(
{'x1/': {'xpub': xpub1}, {'x1/': {'xpub': xpub1},
'x2/': {'xpub': xpub2}}) 'x2/': {'xpub': xpub2}})
xpub3 = trustedcoin.make_xpub(trustedcoin.signing_xpub, long_user_id) xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(), long_user_id)
ks3 = keystore.from_xpub(xpub3) ks3 = keystore.from_xpub(xpub3)
self._check_xpub_keystore_sanity(ks3) self._check_xpub_keystore_sanity(ks3)
self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore)) self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))

View File

@ -32,6 +32,8 @@ from .util import print_error, profiler
from . import bitcoin from . import bitcoin
from .bitcoin import * from .bitcoin import *
import struct import struct
import traceback
import sys
# #
# Workalike python implementation of Bitcoin's CDataStream class. # Workalike python implementation of Bitcoin's CDataStream class.
@ -45,6 +47,14 @@ class SerializationError(Exception):
""" Thrown when there's a problem deserializing or serializing """ """ Thrown when there's a problem deserializing or serializing """
class UnknownTxinType(Exception):
pass
class NotRecognizedRedeemScript(Exception):
pass
class BCDataStream(object): class BCDataStream(object):
def __init__(self): def __init__(self):
self.input = None self.input = None
@ -295,17 +305,30 @@ def parse_scriptSig(d, _bytes):
decoded = [ x for x in script_GetOp(_bytes) ] decoded = [ x for x in script_GetOp(_bytes) ]
except Exception as e: except Exception as e:
# coinbase transactions raise an exception # coinbase transactions raise an exception
print_error("cannot find address in input script", bh2u(_bytes)) print_error("parse_scriptSig: cannot find address in input script (coinbase?)",
bh2u(_bytes))
return return
match = [ opcodes.OP_PUSHDATA4 ] match = [ opcodes.OP_PUSHDATA4 ]
if match_decoded(decoded, match): if match_decoded(decoded, match):
item = decoded[0][1] item = decoded[0][1]
if item[0] == 0: if item[0] == 0:
# segwit embedded into p2sh
# witness version 0
d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item))
d['type'] = 'p2wpkh-p2sh' if len(item) == 22 else 'p2wsh-p2sh' if len(item) == 22:
d['type'] = 'p2wpkh-p2sh'
elif len(item) == 34:
d['type'] = 'p2wsh-p2sh'
else:
print_error("unrecognized txin type", bh2u(item))
elif opcodes.OP_1 <= item[0] <= opcodes.OP_16:
# segwit embedded into p2sh
# witness version 1-16
pass
else: else:
# payto_pubkey # assert item[0] == 0x30
# pay-to-pubkey
d['type'] = 'p2pk' d['type'] = 'p2pk'
d['address'] = "(pubkey)" d['address'] = "(pubkey)"
d['signatures'] = [bh2u(item)] d['signatures'] = [bh2u(item)]
@ -314,9 +337,9 @@ def parse_scriptSig(d, _bytes):
d['pubkeys'] = ["(pubkey)"] d['pubkeys'] = ["(pubkey)"]
return return
# non-generated TxIn transactions push a signature # p2pkh TxIn transactions push a signature
# (seventy-something bytes) and then their public key # (71-73 bytes) and then their public key
# (65 bytes) onto the stack: # (33 or 65 bytes) onto the stack:
match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ] match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ]
if match_decoded(decoded, match): if match_decoded(decoded, match):
sig = bh2u(decoded[0][1]) sig = bh2u(decoded[0][1])
@ -325,7 +348,8 @@ def parse_scriptSig(d, _bytes):
signatures = parse_sig([sig]) signatures = parse_sig([sig])
pubkey, address = xpubkey_to_address(x_pubkey) pubkey, address = xpubkey_to_address(x_pubkey)
except: except:
print_error("cannot find address in input script", bh2u(_bytes)) print_error("parse_scriptSig: cannot find address in input script (p2pkh?)",
bh2u(_bytes))
return return
d['type'] = 'p2pkh' d['type'] = 'p2pkh'
d['signatures'] = signatures d['signatures'] = signatures
@ -337,31 +361,42 @@ def parse_scriptSig(d, _bytes):
# p2sh transaction, m of n # p2sh transaction, m of n
match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1) match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1)
if not match_decoded(decoded, match): if match_decoded(decoded, match):
print_error("cannot find address in input script", bh2u(_bytes)) x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
try:
m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1])
except NotRecognizedRedeemScript:
print_error("parse_scriptSig: cannot find address in input script (p2sh?)",
bh2u(_bytes))
# we could still guess:
# d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1]))
return
# write result in d
d['type'] = 'p2sh'
d['num_sig'] = m
d['signatures'] = parse_sig(x_sig)
d['x_pubkeys'] = x_pubkeys
d['pubkeys'] = pubkeys
d['redeemScript'] = redeemScript
d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript)))
return return
x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1]) print_error("parse_scriptSig: cannot find address in input script (unknown)",
# write result in d bh2u(_bytes))
d['type'] = 'p2sh'
d['num_sig'] = m
d['signatures'] = parse_sig(x_sig)
d['x_pubkeys'] = x_pubkeys
d['pubkeys'] = pubkeys
d['redeemScript'] = redeemScript
d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript)))
def parse_redeemScript(s): def parse_redeemScript(s):
dec2 = [ x for x in script_GetOp(s) ] dec2 = [ x for x in script_GetOp(s) ]
m = dec2[0][0] - opcodes.OP_1 + 1 try:
n = dec2[-2][0] - opcodes.OP_1 + 1 m = dec2[0][0] - opcodes.OP_1 + 1
n = dec2[-2][0] - opcodes.OP_1 + 1
except IndexError:
raise NotRecognizedRedeemScript()
op_m = opcodes.OP_1 + m - 1 op_m = opcodes.OP_1 + m - 1
op_n = opcodes.OP_1 + n - 1 op_n = opcodes.OP_1 + n - 1
match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ]
if not match_decoded(dec2, match_multisig): if not match_decoded(dec2, match_multisig):
print_error("cannot find address in input script", bh2u(s)) raise NotRecognizedRedeemScript()
return
x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
redeemScript = multisig_script(pubkeys, m) redeemScript = multisig_script(pubkeys, m)
@ -416,7 +451,11 @@ def parse_input(vds):
d['num_sig'] = 0 d['num_sig'] = 0
if scriptSig: if scriptSig:
d['scriptSig'] = bh2u(scriptSig) d['scriptSig'] = bh2u(scriptSig)
parse_scriptSig(d, scriptSig) try:
parse_scriptSig(d, scriptSig)
except BaseException:
traceback.print_exc(file=sys.stderr)
print_error('failed to parse scriptSig', bh2u(scriptSig))
else: else:
d['scriptSig'] = '' d['scriptSig'] = ''
@ -430,21 +469,55 @@ def parse_witness(vds, txin):
if n == 0xffffffff: if n == 0xffffffff:
txin['value'] = vds.read_uint64() txin['value'] = vds.read_uint64()
n = vds.read_compact_size() n = vds.read_compact_size()
# now 'n' is the number of items in the witness
w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n)) w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n))
if txin['type'] == 'coinbase':
pass add_w = lambda x: var_int(len(x) // 2) + x
elif n > 2: txin['witness'] = var_int(n) + ''.join(add_w(i) for i in w)
txin['signatures'] = parse_sig(w[1:-1])
m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) # FIXME: witness version > 0 will probably fail here.
txin['num_sig'] = m # For native segwit, we would need the scriptPubKey of the parent txn
txin['x_pubkeys'] = x_pubkeys # to determine witness program version, and properly parse the witness.
txin['pubkeys'] = pubkeys # In case of p2sh-segwit, we can tell based on the scriptSig in this txn.
txin['witnessScript'] = witnessScript # The code below assumes witness version 0.
else: # p2sh-segwit should work in that case; for native segwit we need to tell
txin['num_sig'] = 1 # between p2wpkh and p2wsh; we do this based on number of witness items,
txin['x_pubkeys'] = [w[1]] # hence (FIXME) p2wsh with n==2 (maybe n==1 ?) will probably fail.
txin['pubkeys'] = [safe_parse_pubkey(w[1])] # If v==0 and n==2, we need parent scriptPubKey to distinguish between p2wpkh and p2wsh.
txin['signatures'] = parse_sig([w[0]]) try:
if txin['type'] == 'coinbase':
pass
elif txin['type'] == 'p2wsh-p2sh' or n > 2:
try:
m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1]))
except NotRecognizedRedeemScript:
raise UnknownTxinType()
txin['signatures'] = parse_sig(w[1:-1])
txin['num_sig'] = m
txin['x_pubkeys'] = x_pubkeys
txin['pubkeys'] = pubkeys
txin['witnessScript'] = witnessScript
if not txin.get('scriptSig'): # native segwit script
txin['type'] = 'p2wsh'
txin['address'] = bitcoin.script_to_p2wsh(txin['witnessScript'])
elif txin['type'] == 'p2wpkh-p2sh' or n == 2:
txin['num_sig'] = 1
txin['x_pubkeys'] = [w[1]]
txin['pubkeys'] = [safe_parse_pubkey(w[1])]
txin['signatures'] = parse_sig([w[0]])
if not txin.get('scriptSig'): # native segwit script
txin['type'] = 'p2wpkh'
txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0]))
else:
raise UnknownTxinType()
except UnknownTxinType:
txin['type'] = 'unknown'
# FIXME: GUI might show 'unknown' address (e.g. for a non-multisig p2wsh)
except BaseException:
txin['type'] = 'unknown'
traceback.print_exc(file=sys.stderr)
print_error('failed to parse witness', txin.get('witness'))
def parse_output(vds, i): def parse_output(vds, i):
d = {} d = {}
@ -463,9 +536,18 @@ def deserialize(raw):
start = vds.read_cursor start = vds.read_cursor
d['version'] = vds.read_int32() d['version'] = vds.read_int32()
n_vin = vds.read_compact_size() n_vin = vds.read_compact_size()
is_segwit = (n_vin == 0)
if is_segwit:
marker = vds.read_bytes(1)
assert marker == b'\x01'
n_vin = vds.read_compact_size()
d['inputs'] = [parse_input(vds) for i in range(n_vin)] d['inputs'] = [parse_input(vds) for i in range(n_vin)]
n_vout = vds.read_compact_size() n_vout = vds.read_compact_size()
d['outputs'] = [parse_output(vds, i) for i in range(n_vout)] d['outputs'] = [parse_output(vds, i) for i in range(n_vout)]
if is_segwit:
for i in range(n_vin):
txin = d['inputs'][i]
parse_witness(vds, txin)
d['lockTime'] = vds.read_uint32() d['lockTime'] = vds.read_uint32()
return d return d
@ -657,7 +739,9 @@ class Transaction:
witness_script = multisig_script(pubkeys, txin['num_sig']) witness_script = multisig_script(pubkeys, txin['num_sig'])
witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script) witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script)
else: else:
raise BaseException('wrong txin type') witness = txin.get('witness', None)
if not witness:
raise BaseException('wrong txin type:', txin['type'])
if self.is_txin_complete(txin) or estimate_size: if self.is_txin_complete(txin) or estimate_size:
value_field = '' value_field = ''
else: else:
@ -666,7 +750,8 @@ class Transaction:
@classmethod @classmethod
def is_segwit_input(cls, txin): def is_segwit_input(cls, txin):
return cls.is_segwit_inputtype(txin['type']) has_nonzero_witness = txin.get('witness', '00') != '00'
return cls.is_segwit_inputtype(txin['type']) or has_nonzero_witness
@classmethod @classmethod
def is_segwit_inputtype(cls, txin_type): def is_segwit_inputtype(cls, txin_type):
@ -733,6 +818,14 @@ class Transaction:
def serialize_outpoint(self, txin): def serialize_outpoint(self, txin):
return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4) return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4)
@classmethod
def get_outpoint_from_txin(cls, txin):
if txin['type'] == 'coinbase':
return None
prevout_hash = txin['prevout_hash']
prevout_n = txin['prevout_n']
return prevout_hash + ':%d' % prevout_n
@classmethod @classmethod
def serialize_input(self, txin, script): def serialize_input(self, txin, script):
# Prev hash and index # Prev hash and index

View File

@ -29,7 +29,6 @@ import traceback
import urllib import urllib
import threading import threading
import hmac import hmac
import requests
from .i18n import _ from .i18n import _
@ -41,11 +40,7 @@ def inv_dict(d):
return {v: k for k, v in d.items()} return {v: k for k, v in d.items()}
is_bundle = getattr(sys, 'frozen', False)
is_macOS = sys.platform == 'darwin'
base_units = {'ZCL':8, 'mZCL':5, 'uZCL':2} base_units = {'ZCL':8, 'mZCL':5, 'uZCL':2}
fee_levels = [_('Within 25 blocks'), _('Within 10 blocks'), _('Within 5 blocks'), _('Within 2 blocks'), _('In the next block')]
def normalize_version(v): def normalize_version(v):
return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
@ -62,17 +57,80 @@ class InvalidPassword(Exception):
def __str__(self): def __str__(self):
return _("Incorrect password") return _("Incorrect password")
class FileImportFailed(Exception):
def __init__(self, message=''):
self.message = str(message)
def __str__(self):
return _("Failed to import from file.") + "\n" + self.message
class FileExportFailed(Exception):
def __init__(self, message=''):
self.message = str(message)
def __str__(self):
return _("Failed to export to file.") + "\n" + self.message
class TimeoutException(Exception):
def __init__(self, message=''):
self.message = str(message)
def __str__(self):
if not self.message:
return _("Operation timed out.")
return self.message
# Throw this exception to unwind the stack like when an error occurs. # Throw this exception to unwind the stack like when an error occurs.
# However unlike other exceptions the user won't be informed. # However unlike other exceptions the user won't be informed.
class UserCancelled(Exception): class UserCancelled(Exception):
'''An exception that is suppressed from the user''' '''An exception that is suppressed from the user'''
pass pass
class Satoshis(object):
def __new__(cls, value):
self = super(Satoshis, cls).__new__(cls)
self.value = value
return self
def __repr__(self):
return 'Satoshis(%d)'%self.value
def __str__(self):
return format_satoshis(self.value) + " BTC"
class Fiat(object):
def __new__(cls, value, ccy):
self = super(Fiat, cls).__new__(cls)
self.ccy = ccy
self.value = value
return self
def __repr__(self):
return 'Fiat(%s)'% self.__str__()
def __str__(self):
if self.value.is_nan():
return _('No Data')
else:
return "{:.2f}".format(self.value) + ' ' + self.ccy
class MyEncoder(json.JSONEncoder): class MyEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
from .transaction import Transaction from .transaction import Transaction
if isinstance(obj, Transaction): if isinstance(obj, Transaction):
return obj.as_dict() return obj.as_dict()
if isinstance(obj, Satoshis):
return str(obj)
if isinstance(obj, Fiat):
return str(obj)
if isinstance(obj, Decimal):
return str(obj)
if isinstance(obj, datetime):
return obj.isoformat(' ')[:-3]
return super(MyEncoder, self).default(obj) return super(MyEncoder, self).default(obj)
class PrintError(object): class PrintError(object):
@ -81,8 +139,12 @@ class PrintError(object):
return self.__class__.__name__ return self.__class__.__name__
def print_error(self, *msg): def print_error(self, *msg):
# only prints with --verbose flag
print_error("[%s]" % self.diagnostic_name(), *msg) print_error("[%s]" % self.diagnostic_name(), *msg)
def print_stderr(self, *msg):
print_stderr("[%s]" % self.diagnostic_name(), *msg)
def print_msg(self, *msg): def print_msg(self, *msg):
print_msg("[%s]" % self.diagnostic_name(), *msg) print_msg("[%s]" % self.diagnostic_name(), *msg)
@ -347,7 +409,7 @@ def format_satoshis_plain(x, decimal_point = 8):
def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespaces=False): def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespaces=False):
from locale import localeconv from locale import localeconv
if x is None: if x is None:
return 'Unknown' return 'unknown'
x = int(x) # Some callers pass Decimal x = int(x) # Some callers pass Decimal
scale_factor = pow (10, decimal_point) scale_factor = pow (10, decimal_point)
integer_part = "{:n}".format(int(abs(x) / scale_factor)) integer_part = "{:n}".format(int(abs(x) / scale_factor))
@ -367,10 +429,9 @@ def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespa
return result return result
def timestamp_to_datetime(timestamp): def timestamp_to_datetime(timestamp):
try: if timestamp is None:
return datetime.fromtimestamp(timestamp)
except:
return None return None
return datetime.fromtimestamp(timestamp)
def format_time(timestamp): def format_time(timestamp):
date = timestamp_to_datetime(timestamp) date = timestamp_to_datetime(timestamp)
@ -453,8 +514,8 @@ testnet_block_explorers = {
} }
def block_explorer_info(): def block_explorer_info():
from . import bitcoin from . import constants
return testnet_block_explorers if bitcoin.NetworkConstants.TESTNET else mainnet_block_explorers return testnet_block_explorers if constants.net.TESTNET else mainnet_block_explorers
def block_explorer(config): def block_explorer(config):
return config.get('block_explorer', 'zclassic-ce.io') return config.get('block_explorer', 'zclassic-ce.io')
@ -470,7 +531,7 @@ def block_explorer_URL(config, kind, item):
if not kind_str: if not kind_str:
return return
url_parts = [be_tuple[0], kind_str, item] url_parts = [be_tuple[0], kind_str, item]
return "/".join(url_parts) return ''.join(url_parts)
# URL decode # URL decode
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) #_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
@ -693,8 +754,56 @@ class QueuePipe:
self.send(request) self.send(request)
def get_cert_path():
if is_bundle and is_macOS:
# set in ./electrum def setup_thread_excepthook():
return requests.utils.DEFAULT_CA_BUNDLE_PATH """
return requests.certs.where() Workaround for `sys.excepthook` thread bug from:
http://bugs.python.org/issue1230540
Call once from the main thread before creating any threads.
"""
init_original = threading.Thread.__init__
def init(self, *args, **kwargs):
init_original(self, *args, **kwargs)
run_original = self.run
def run_with_except_hook(*args2, **kwargs2):
try:
run_original(*args2, **kwargs2)
except Exception:
sys.excepthook(*sys.exc_info())
self.run = run_with_except_hook
threading.Thread.__init__ = init
def versiontuple(v):
return tuple(map(int, (v.split("."))))
def import_meta(path, validater, load_meta):
try:
with open(path, 'r') as f:
d = validater(json.loads(f.read()))
load_meta(d)
#backwards compatibility for JSONDecodeError
except ValueError:
traceback.print_exc(file=sys.stderr)
raise FileImportFailed(_("Invalid JSON code."))
except BaseException as e:
traceback.print_exc(file=sys.stdout)
raise FileImportFailed(e)
def export_meta(meta, fileName):
try:
with open(fileName, 'w+') as f:
json.dump(meta, f, indent=4, sort_keys=True)
except (IOError, os.error) as e:
traceback.print_exc(file=sys.stderr)
raise FileExportFailed(e)

View File

@ -36,15 +36,22 @@ class SPV(ThreadJob):
self.merkle_roots = {} self.merkle_roots = {}
def run(self): def run(self):
interface = self.network.interface
if not interface:
return
blockchain = interface.blockchain
if not blockchain:
return
lh = self.network.get_local_height() lh = self.network.get_local_height()
unverified = self.wallet.get_unverified_txs() unverified = self.wallet.get_unverified_txs()
for tx_hash, tx_height in unverified.items(): for tx_hash, tx_height in unverified.items():
# do not request merkle branch before headers are available # do not request merkle branch before headers are available
if (tx_height > 0) and (tx_height <= lh): if (tx_height > 0) and (tx_height <= lh):
header = self.network.blockchain().read_header(tx_height) header = blockchain.read_header(tx_height)
if header is None and self.network.interface: if header is None:
index = tx_height // NetworkConstants.CHUNK_SIZE index = tx_height // constants.net.CHUNK_SIZE
self.network.request_chunk(self.network.interface, index) if index < len(blockchain.checkpoints):
self.network.request_chunk(interface, index)
else: else:
if tx_hash not in self.merkle_roots: if tx_hash not in self.merkle_roots:
request = ('blockchain.transaction.get_merkle', request = ('blockchain.transaction.get_merkle',
@ -58,6 +65,8 @@ class SPV(ThreadJob):
self.undo_verifications() self.undo_verifications()
def verify_merkle(self, r): def verify_merkle(self, r):
if self.wallet.verifier is None:
return # we have been killed, this was just an orphan callback
if r.get('error'): if r.get('error'):
self.print_error('received an error:', r) self.print_error('received an error:', r)
return return
@ -70,17 +79,26 @@ class SPV(ThreadJob):
pos = merkle.get('pos') pos = merkle.get('pos')
merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos) merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos)
header = self.network.blockchain().read_header(tx_height) header = self.network.blockchain().read_header(tx_height)
if not header or header.get('merkle_root') != merkle_root: # FIXME: if verification fails below,
# FIXME: we should make a fresh connection to a server to # we should make a fresh connection to a server to
# recover from this, as this TX will now never verify # recover from this, as this TX will now never verify
self.print_error("merkle verification failed for", tx_hash) if not header:
self.print_error(
"merkle verification failed for {} (missing header {})"
.format(tx_hash, tx_height))
return
if header.get('merkle_root') != merkle_root:
self.print_error(
"merkle verification failed for {} (merkle root mismatch {} != {})"
.format(tx_hash, header.get('merkle_root'), merkle_root))
return return
# we passed all the tests # we passed all the tests
self.merkle_roots[tx_hash] = merkle_root self.merkle_roots[tx_hash] = merkle_root
self.print_error("verified %s" % tx_hash) self.print_error("verified %s" % tx_hash)
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos)) self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
def hash_merkle_root(self, merkle_s, target_hash, pos): @classmethod
def hash_merkle_root(cls, merkle_s, target_hash, pos):
h = hash_decode(target_hash) h = hash_decode(target_hash)
for i in range(len(merkle_s)): for i in range(len(merkle_s)):
item = merkle_s[i] item = merkle_s[i]

View File

@ -1,7 +1,5 @@
# version of the client package ELECTRUM_VERSION = '1.0.7' # '3.1' = version of the client package
ELECTRUM_VERSION = '1.0.6' PROTOCOL_VERSION = '1.1' # protocol version requested
# protocol version requested
PROTOCOL_VERSION = '1.1'
# The hash of the mnemonic seed must begin with this # The hash of the mnemonic seed must begin with this
SEED_PREFIX = '01' # Standard wallet SEED_PREFIX = '01' # Standard wallet

File diff suppressed because it is too large Load Diff

View File

@ -84,7 +84,8 @@ class WsClientThread(util.DaemonThread):
l = self.subscriptions.get(addr, []) l = self.subscriptions.get(addr, [])
l.append((ws, amount)) l.append((ws, amount))
self.subscriptions[addr] = l self.subscriptions[addr] = l
self.network.send([('blockchain.address.subscribe', [addr])], self.response_queue.put) h = self.network.addr_to_scripthash(addr)
self.network.send([('blockchain.scripthash.subscribe', [h])], self.response_queue.put)
def run(self): def run(self):
@ -100,10 +101,13 @@ class WsClientThread(util.DaemonThread):
result = r.get('result') result = r.get('result')
if result is None: if result is None:
continue continue
if method == 'blockchain.address.subscribe': if method == 'blockchain.scripthash.subscribe':
self.network.send([('blockchain.address.get_balance', params)], self.response_queue.put) self.network.send([('blockchain.scripthash.get_balance', params)], self.response_queue.put)
elif method == 'blockchain.address.get_balance': elif method == 'blockchain.scripthash.get_balance':
addr = params[0] h = params[0]
addr = self.network.h2addr.get(h, None)
if addr is None:
util.print_error("can't find address for scripthash: %s" % h)
l = self.subscriptions.get(addr, []) l = self.subscriptions.get(addr, [])
for ws, amount in l: for ws, amount in l:
if not ws.closed: if not ws.closed:

View File

@ -23,7 +23,7 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
from . import util from . import util
from .util import profiler, bh2u, get_cert_path from .util import profiler, bh2u
import ecdsa import ecdsa
import hashlib import hashlib

View File

@ -194,7 +194,7 @@ class Plugin(BasePlugin):
return return
wallet = window.wallet wallet = window.wallet
if wallet.has_password(): if wallet.has_keystore_encryption():
password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.') password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.')
if not password: if not password:
return return

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