Browse Source

Merge branch 'master' into zbar_windows

3.1
ThomasV 7 years ago
committed by GitHub
parent
commit
050f9b7d3a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/ISSUE_TEMPLATE.md
  2. 16
      .travis.yml
  3. 69
      RELEASE-NOTES
  4. 37
      contrib/build-osx/make_osx
  5. 40
      contrib/build-osx/osx.spec
  6. 6
      contrib/build-wine/build-electrum-git.sh
  7. 3
      contrib/build-wine/build.sh
  8. 9
      contrib/build-wine/deterministic.spec
  9. 24
      contrib/build-wine/prepare-pyinstaller.sh
  10. 30
      contrib/build-wine/prepare-wine.sh
  11. 5
      contrib/deterministic-build/requirements-binaries.txt
  12. 27
      contrib/freeze_packages.sh
  13. 3
      contrib/requirements/requirements-binaries.txt
  14. 0
      contrib/requirements/requirements-hw.txt
  15. 0
      contrib/requirements/requirements-travis.txt
  16. 9
      contrib/requirements/requirements.txt
  17. 58
      electrum
  18. 4
      electrum-env
  19. 7
      gui/kivy/Readme.md
  20. 7
      gui/kivy/i18n.py
  21. 49
      gui/kivy/uix/dialogs/bump_fee_dialog.py
  22. 5
      gui/kivy/uix/dialogs/fee_dialog.py
  23. 1
      gui/kivy/uix/dialogs/settings.py
  24. 7
      gui/kivy/uix/screens.py
  25. 6
      gui/kivy/uix/ui_screens/address.kv
  26. 17
      gui/qt/__init__.py
  27. 13
      gui/qt/contact_list.py
  28. 16
      gui/qt/exception_window.py
  29. 6
      gui/qt/fee_slider.py
  30. 247
      gui/qt/history_list.py
  31. 13
      gui/qt/invoice_list.py
  32. 193
      gui/qt/main_window.py
  33. 31
      gui/qt/transaction_dialog.py
  34. 47
      gui/qt/util.py
  35. 98
      lib/bitcoin.py
  36. 5
      lib/blockchain.py
  37. 27
      lib/coinchooser.py
  38. 58
      lib/commands.py
  39. 16
      lib/contacts.py
  40. 205
      lib/currencies.json
  41. 9
      lib/daemon.py
  42. 114
      lib/exchange_rate.py
  43. 5
      lib/keystore.py
  44. 16
      lib/network.py
  45. 28
      lib/paymentrequest.py
  46. 11
      lib/plot.py
  47. 84
      lib/simple_config.py
  48. 45
      lib/tests/test_bitcoin.py
  49. 379
      lib/tests/test_transaction.py
  50. 65
      lib/transaction.py
  51. 84
      lib/util.py
  52. 27
      lib/verifier.py
  53. 372
      lib/wallet.py
  54. 3
      plugins/digitalbitbox/cmdline.py
  55. 1
      plugins/digitalbitbox/digitalbitbox.py
  56. 12
      plugins/hw_wallet/qt.py
  57. 3
      plugins/keepkey/cmdline.py
  58. 3
      plugins/ledger/cmdline.py
  59. 12
      plugins/ledger/ledger.py
  60. 3
      plugins/trezor/cmdline.py
  61. 16
      plugins/trezor/trezor.py
  62. 166
      pubkeys/bauerj.asc
  63. 19
      setup.py

2
.github/ISSUE_TEMPLATE.md

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

16
.travis.yml

@ -4,7 +4,7 @@ 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
script: script:
@ -12,3 +12,17 @@ script:
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
script: ./contrib/build-wine/build.sh
after_success: true

69
RELEASE-NOTES

@ -1,34 +1,55 @@
# Release 3.1 - (to be released) # Release 3.1 - (to be released)
* Mempory pool based fee estimates. If this option is activated, * Memory-pool based transaction fees. Users can set dynamic fees that
users can set transaction fees that target a desired depth in the target a desired depth in the memory pool. This feature is
memory pool. This feature might be controversial, because miners optional, and ETA-based estimates (from Bitcoin Core) remain the
could conspire and fill the memory pool with expensive transactions default. Note that miners could exploit this feature, if they
that never get mined. However, our current time-based fee estimates conspired and filled the memory pool with expensive transactions
results in sticky fees, which cause inexperienced users to overpay, that never get mined. However, since the Electrum client already
while more advanced users visit (and trust) websites that display trusts an Electrum server with fee estimates, activating this
memorypool data, and set their fee accordingly. feature does not introduce any new vulnerability; the client uses a
* Local transactions: Transactions that have not been broadcasted can hard threshold to detect unusually high fees. In practice,
be saved in the wallet file, and their outputs can be used in ETA-based estimates have resulted in sticky fees, and caused many
subsequent transactions. Transactions that disapear from the memory users to overpay for transactions. Advanced users tend to visit
pool stay in the wallet, and can be rebroadcasted. This feature can (and trust) websites that display memory-pool data in order to set
be combined with cold storage, to create several transactions their fees.
before broadcasting. * Local transactions: Transactions can be saved in the wallet without
* The initial headers download was replaced with hardcoded being broadcast. The inputs of local transactions are considered as
checkpoints, one per retargeting period. Past headers are spent, and their change outputs can be re-used in subsequent
downloaded when needed. transactions. This can be combined with cold storage, in order to
* The two coin selection policies have been merged, and the policy create several transactions before broadcasting them. Outgoing
choice was removed from preferences. Previously, the 'privacy' transactions that have been removed from the memory pool are also
policy has been unusable because it was was not prioritizing saved in the wallet, and can be broadcast again.
confirmed coins. * Checkpoints: The initial download of a headers file was replaced
with hardcoded checkpoints. The wallet uses one checkpoint per
retargetting period. The headers for a retargetting 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 * The 'Send' tab of the Qt GUI displays how transaction fees are
computed from transaction size. computed from transaction size.
* RBF is enabled by default. This might cause some issues with * The wallet history can be filtered by time interval.
merchants that use wallets that do not display RBF transactions * Replace-by-fee is enabled by default. Note that this might cause
until they are confirmed. some issues with wallets that do not display RBF transactions until
they are confirmed.
* Watching-only wallets and hardware wallets can be encrypted. * Watching-only wallets and hardware wallets can be encrypted.
* Semi-automated crash reporting * Semi-automated crash reporting
* The SSL checkbox option was removed from the GUI. * The SSL checkbox option was removed from the GUI.
* 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
value 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 set of addresses in the wallet.
# Release 3.0.6 : # Release 3.0.6 :

37
contrib/build-osx/make_osx

@ -16,11 +16,16 @@ cd $build_dir/../..
export PYTHONHASHSEED=22 export PYTHONHASHSEED=22
VERSION=`git describe --tags` VERSION=`git describe --tags`
# Paramterize
PYTHON_VERSION=3.6.4 PYTHON_VERSION=3.6.4
BUILDDIR=/tmp/electrum-build
PACKAGE=Electrum
GIT_REPO=https://github.com/spesmilo/electrum
info "Installing Python $PYTHON_VERSION" info "Installing Python $PYTHON_VERSION"
export PATH="~/.pyenv/bin:~/.pyenv/shims:$PATH:~/Library/Python/3.6/bin" export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH"
if [ -d "~/.pyenv" ]; then if [ -d "~/.pyenv" ]; then
pyenv update pyenv update
else else
@ -31,12 +36,10 @@ pyenv global $PYTHON_VERSION || \
fail "Unable to use Python $PYTHON_VERSION" fail "Unable to use Python $PYTHON_VERSION"
if ! which pyinstaller > /dev/null; then
info "Installing pyinstaller" info "Installing pyinstaller"
python3 -m pip install pyinstaller -I --user || fail "Could not install pyinstaller" python3 -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952 -I --user || fail "Could not install pyinstaller"
fi
info "Using these versions for building Electrum:" info "Using these versions for building $PACKAGE:"
sw_vers sw_vers
python3 --version python3 --version
echo -n "Pyinstaller " echo -n "Pyinstaller "
@ -45,31 +48,37 @@ pyinstaller --version
rm -rf ./dist rm -rf ./dist
rm -rf /tmp/electrum-build > /dev/null 2>&1 rm -rf $BUILDDIR > /dev/null 2>&1
mkdir /tmp/electrum-build mkdir $BUILDDIR
info "Downloading icons and locale..." info "Downloading icons and locale..."
for repo in icons locale; do for repo in icons locale; do
git clone https://github.com/spesmilo/electrum-$repo /tmp/electrum-build/electrum-$repo git clone $GIT_REPO-$repo $BUILDDIR/electrum-$repo
done done
cp -R /tmp/electrum-build/electrum-locale/locale/ ./lib/locale/ cp -R $BUILDDIR/electrum-locale/locale/ ./lib/locale/
cp /tmp/electrum-build/electrum-icons/icons_rc.py ./gui/qt/ 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..." info "Installing requirements..."
python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \ python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \
python3 -m pip install pyqt5 --user || \ python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user || \
fail "Could not install requirements" fail "Could not install requirements"
info "Installing hardware wallet requirements..." info "Installing hardware wallet requirements..."
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --user || \ python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --user || \
fail "Could not install hardware wallet requirements" fail "Could not install hardware wallet requirements"
info "Building Electrum..." info "Building $PACKAGE..."
python3 setup.py install --user > /dev/null || fail "Could not build Electrum" python3 setup.py install --user > /dev/null || fail "Could not build $PACKAGE"
info "Building binary" info "Building binary"
pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary" pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary"
info "Creating .DMG" info "Creating .DMG"
hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG" hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG"

40
contrib/build-osx/osx.spec

@ -1,10 +1,15 @@
# -*- 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 import os
PACKAGE='Electrum'
PYPKG='electrum'
MAIN_SCRIPT='electrum'
ICONS_FILE='electrum.icns'
for i, x in enumerate(sys.argv): for i, x in enumerate(sys.argv):
if x == '--name': if x == '--name':
VERSION = sys.argv[i+1] VERSION = sys.argv[i+1]
@ -22,21 +27,27 @@ hiddenimports += collect_submodules('btchip')
hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('keepkeylib')
datas = [ datas = [
(electrum+'lib/currencies.json', 'electrum'), (electrum+'lib/currencies.json', PYPKG),
(electrum+'lib/servers.json', 'electrum'), (electrum+'lib/servers.json', PYPKG),
(electrum+'lib/checkpoints.json', 'electrum'), (electrum+'lib/checkpoints.json', PYPKG),
(electrum+'lib/servers_testnet.json', 'electrum'), (electrum+'lib/servers_testnet.json', PYPKG),
(electrum+'lib/checkpoints_testnet.json', 'electrum'), (electrum+'lib/checkpoints_testnet.json', PYPKG),
(electrum+'lib/wordlist/english.txt', 'electrum/wordlist'), (electrum+'lib/wordlist/english.txt', PYPKG + '/wordlist'),
(electrum+'lib/locale', 'electrum/locale'), (electrum+'lib/locale', PYPKG + '/locale'),
(electrum+'plugins', 'electrum_plugins'), (electrum+'plugins', PYPKG + '_plugins'),
] ]
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')
# 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 # 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+'electrum', a = Analysis([electrum+MAIN_SCRIPT,
electrum+'gui/qt/main_window.py', electrum+'gui/qt/main_window.py',
electrum+'gui/text.py', electrum+'gui/text.py',
electrum+'lib/util.py', electrum+'lib/util.py',
@ -52,6 +63,7 @@ a = Analysis([electrum+'electrum',
electrum+'plugins/keepkey/qt.py', electrum+'plugins/keepkey/qt.py',
electrum+'plugins/ledger/qt.py', electrum+'plugins/ledger/qt.py',
], ],
binaries=binaries,
datas=datas, datas=datas,
hiddenimports=hiddenimports, hiddenimports=hiddenimports,
hookspath=[]) hookspath=[])
@ -68,17 +80,17 @@ exe = EXE(pyz,
a.scripts, a.scripts,
a.binaries, a.binaries,
a.datas, a.datas,
name='Electrum', name=PACKAGE,
debug=False, debug=False,
strip=False, strip=False,
upx=True, upx=True,
icon=electrum+'electrum.icns', icon=electrum+ICONS_FILE,
console=False) console=False)
app = BUNDLE(exe, app = BUNDLE(exe,
version = VERSION, version = VERSION,
name='Electrum.app', name=PACKAGE + '.app',
icon=electrum+'electrum.icns', icon=electrum+ICONS_FILE,
bundle_identifier=None, bundle_identifier=None,
info_plist = { info_plist = {
'NSHighResolutionCapable':'True' 'NSHighResolutionCapable':'True'

6
contrib/build-wine/build-electrum-git.sh

@ -56,6 +56,12 @@ cp electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/
# Install frozen dependencies # Install frozen dependencies
$PYTHON -m pip install -r ../../deterministic-build/requirements.txt $PYTHON -m pip install -r ../../deterministic-build/requirements.txt
# Workaround until they upload binary wheels themselves:
wget 'https://ci.appveyor.com/api/buildjobs/bwr3yfghdemoryy8/artifacts/dist%2Fpyblake2-1.1.0-cp35-cp35m-win32.whl' -O pyblake2-1.1.0-cp35-cp35m-win32.whl
$PYTHON -m pip install ./pyblake2-1.1.0-cp35-cp35m-win32.whl
$PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt $PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt
pushd $WINEPREFIX/drive_c/electrum pushd $WINEPREFIX/drive_c/electrum

3
contrib/build-wine/build.sh

@ -13,8 +13,7 @@ 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 || 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)

9
contrib/build-wine/deterministic.spec

@ -1,6 +1,6 @@
# -*- 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
for i, x in enumerate(sys.argv): for i, x in enumerate(sys.argv):
@ -19,6 +19,12 @@ hiddenimports += collect_submodules('trezorlib')
hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('btchip')
hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('keepkeylib')
# 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'),
(home+'lib/servers.json', 'electrum'), (home+'lib/servers.json', 'electrum'),
@ -52,6 +58,7 @@ a = Analysis([home+'electrum',
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,

24
contrib/build-wine/prepare-pyinstaller.sh

@ -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

30
contrib/build-wine/prepare-wine.sh

@ -3,8 +3,13 @@
# 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_URL=https://prdownloads.sourceforge.net/nsis/nsis-3.02.1-setup.exe?download
NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e
ZBAR_URL=https://sourceforge.net/projects/zbarw/files/zbarw-20121031-setup.exe/download ZBAR_URL=https://sourceforge.net/projects/zbarw/files/zbarw-20121031-setup.exe/download
ZBAR_SHA256=177e32b272fa76528a3af486b74e9cb356707be1c5ace4ed3fcee9723e2c2c02 ZBAR_SHA256=177e32b272fa76528a3af486b74e9cb356707be1c5ace4ed3fcee9723e2c2c02
LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.21/libusb-1.0.21.7z?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
@ -75,28 +80,21 @@ done
$PYTHON -m pip install pip --upgrade $PYTHON -m pip install pip --upgrade
# Install pywin32-ctypes (needed by pyinstaller) # Install pywin32-ctypes (needed by pyinstaller)
$PYTHON -m pip install pywin32-ctypes $PYTHON -m pip install pywin32-ctypes==0.1.2
# Install PyQt # install PySocks
$PYTHON -m pip install PyQt5 $PYTHON -m pip install win_inet_pton==1.0.1
## Install pyinstaller $PYTHON -m pip install -r ../../deterministic-build/requirements-binaries.txt
#$PYTHON -m pip install pyinstaller==3.3
# Install PyInstaller
$PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip
# Install ZBar # Install ZBar
wget -q -O zbar.exe "$ZBAR_URL" wget -q -O zbar.exe "$ZBAR_URL"
verify_hash zbar.exe $ZBAR_SHA256 verify_hash zbar.exe $ZBAR_SHA256
wine zbar.exe /S wine zbar.exe /S
# install Cryptodome
$PYTHON -m pip install pycryptodomex
# install PySocks
$PYTHON -m pip install win_inet_pton
# install websocket (python2)
$PYTHON -m pip install websocket-client
# 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
@ -106,6 +104,11 @@ wget -q -O nsis.exe "$NSIS_URL"
verify_hash nsis.exe $NSIS_SHA256 verify_hash nsis.exe $NSIS_SHA256
wine nsis.exe /S wine nsis.exe /S
wget -q -O libusb.7z "$LIBUSB_URL"
verify_hash libusb.7z "$LIBUSB_SHA256"
7z x -olibusb libusb.7z
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"
#unzip -o upx.zip #unzip -o upx.zip
@ -114,5 +117,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. Please run prepare-pyinstaller.sh" echo "Wine is configured. Please run prepare-pyinstaller.sh"

5
contrib/deterministic-build/requirements-binaries.txt

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

27
contrib/freeze_packages.sh

@ -6,34 +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; }
# standard Electrum dependencies for i in '' '-hw' '-binaries'; do
rm "$venv_dir" -rf
virtualenv -p $(which python3) $venv_dir
source $venv_dir/bin/activate
echo "Installing main dependencies"
pushd $contrib/..
python setup.py install
popd
pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements.txt
# hw wallet library dependencies
rm "$venv_dir" -rf rm "$venv_dir" -rf
virtualenv -p $(which python3) $venv_dir virtualenv -p $(which python3) $venv_dir
source $venv_dir/bin/activate source $venv_dir/bin/activate
echo "Installing hw wallet dependencies" echo "Installing $i dependencies"
python -m pip install -r $contrib/../requirements-hw.txt --upgrade
pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements-hw.txt python -m pip install -r $contrib/requirements/requirements${i}.txt --upgrade
pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements${i}.txt
done
echo "Done. Updated requirements" echo "Done. Updated requirements"

3
contrib/requirements/requirements-binaries.txt

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

0
requirements-hw.txt → contrib/requirements/requirements-hw.txt

0
requirements_travis.txt → contrib/requirements/requirements-travis.txt

9
contrib/requirements/requirements.txt

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

58
electrum

@ -91,7 +91,7 @@ if is_local or is_android:
from electrum import bitcoin, util from electrum import bitcoin, util
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
@ -194,8 +194,9 @@ def init_daemon(config_options):
sys.exit(0) sys.exit(0)
if storage.is_encrypted(): if storage.is_encrypted():
if storage.is_encrypted_with_hw_device(): if storage.is_encrypted_with_hw_device():
raise NotImplementedError("CLI functionality of encrypted hw wallets") plugins = init_plugins(config, 'cmdline')
if config.get('password'): 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 +223,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():
@ -240,8 +241,9 @@ def init_cmdline(config_options, server):
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 storage.is_encrypted_with_hw_device(): if storage.is_encrypted_with_hw_device():
raise NotImplementedError("CLI functionality of encrypted hw wallets") # this case is handled later in the control flow
if config.get('password'): 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)
@ -260,7 +262,42 @@ 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')
@ -268,7 +305,8 @@ def run_offline_command(config, config_options):
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(): if storage.is_encrypted_with_hw_device():
raise NotImplementedError("CLI functionality of encrypted hw wallets") 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:
@ -437,8 +475,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)

4
electrum-env

@ -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 "$@"

7
gui/kivy/Readme.md

@ -22,7 +22,7 @@ git merge agilewalker/master
``` ```
## 2. Install buildozer ## 2. Install buildozer
Buildozer is a frontend to p4a. Luckily we don't need to patch it: 2.1 Buildozer is a frontend to p4a. Luckily we don't need to patch it:
```sh ```sh
cd /opt cd /opt
@ -31,6 +31,9 @@ cd buildozer
sudo python3 setup.py install 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 ## 3. Update the Android SDK build tools
3.1 Start the Android SDK manager: 3.1 Start the Android SDK manager:
@ -40,7 +43,7 @@ sudo python3 setup.py install
3.3 Close the SDK manager. 3.3 Close the SDK manager.
3.3 Reopen the SDK manager, scroll to the bottom and install the latest build tools (probably v27) 3.4 Reopen the SDK manager, scroll to the bottom and install the latest build tools (probably v27)
## 4. Install the Support Library Repository ## 4. Install the Support Library Repository
Install "Android Support Library Repository" from the SDK manager. Install "Android Support Library Repository" from the SDK manager.

7
gui/kivy/i18n.py

@ -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):

49
gui/kivy/uix/dialogs/bump_fee_dialog.py

@ -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
if self.dynfees: maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool)
slider.range = (0, 4) slider.range = (0, maxp)
slider.step = 1 slider.step = 1
slider.value = 3 slider.value = pos
else:
slider.range = (1, 10)
slider.step = 1
rate = self.init_fee*1000//self.tx_size
slider.value = min( rate * 2 // self.fee_step, 10)
def get_fee(self): def get_fee_rate(self):
value = int(self.ids.slider.value) pos = int(self.ids.slider.value)
if self.dynfees: if self.dynfees:
if self.config.has_fee_estimates(): fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(pos)
dynfee = self.config.dynfee(value)
return int(dynfee * self.tx_size // 1000)
else: else:
return int(value*self.fee_step * self.tx_size // 1000) fee_rate = self.config.static_fee(pos)
return fee_rate
def get_fee(self):
fee_rate = self.get_fee_rate()
return int(fee_rate * self.tx_size // 1000)
def on_ok(self): def on_ok(self):
new_fee = self.get_fee() new_fee = self.get_fee()

5
gui/kivy/uix/dialogs/fee_dialog.py

@ -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('''
@ -78,8 +77,8 @@ 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.mempool = self.config.get('mempool_fees', False) self.mempool = self.config.use_mempool_fees()
self.dynfees = self.config.get('dynamic_fees', True) self.dynfees = self.config.is_dynfee()
self.ids.mempool.active = self.mempool self.ids.mempool.active = self.mempool
self.ids.dynfees.active = self.dynfees self.ids.dynfees.active = self.dynfees
self.update_slider() self.update_slider()

1
gui/kivy/uix/dialogs/settings.py

@ -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

7
gui/kivy/uix/screens.py

@ -522,7 +522,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()

6
gui/kivy/uix/ui_screens/address.kv

@ -50,7 +50,7 @@ AddressScreen:
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 +70,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

17
gui/qt/__init__.py

@ -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
@ -191,7 +194,9 @@ class ElectrumGui:
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)
try:
w = self.create_window_for_wallet(wallet) 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()

13
gui/qt/contact_list.py

@ -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,6 +64,7 @@ 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]

16
gui/qt/exception_window.py

@ -34,7 +34,7 @@ from PyQt5.QtWidgets import *
from electrum.i18n import _ from electrum.i18n import _
import sys import sys
from electrum import ELECTRUM_VERSION from electrum import ELECTRUM_VERSION, bitcoin
issue_template = """<h2>Traceback</h2> issue_template = """<h2>Traceback</h2>
<pre> <pre>
@ -105,10 +105,22 @@ class Exception_Window(QWidget):
self.show() self.show()
def send_report(self): def send_report(self):
if bitcoin.NetworkConstants.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 = self.get_traceback_info()
report.update(self.get_additional_info()) report.update(self.get_additional_info())
report = json.dumps(report) report = json.dumps(report)
response = requests.post(report_server, data=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) QMessageBox.about(self, "Crash report", response.text)
self.close() self.close()

6
gui/qt/fee_slider.py

@ -21,7 +21,7 @@ class FeeSlider(QSlider):
def moved(self, pos): def moved(self, pos):
with self.lock: with self.lock:
if self.dyn: if self.dyn:
fee_rate = self.config.depth_to_fee(pos) if self.config.get('mempool_fees') else self.config.eta_to_fee(pos) fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos)
else: else:
fee_rate = self.config.static_fee(pos) fee_rate = self.config.static_fee(pos)
tooltip = self.get_tooltip(pos, fee_rate) tooltip = self.get_tooltip(pos, fee_rate)
@ -30,7 +30,7 @@ class FeeSlider(QSlider):
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):
mempool = self.config.get('mempool_fees') mempool = self.config.use_mempool_fees()
target, estimate = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate) target, estimate = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate)
if self.dyn: if self.dyn:
return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate
@ -40,7 +40,7 @@ class FeeSlider(QSlider):
def update(self): def update(self):
with self.lock: with self.lock:
self.dyn = self.config.is_dynfee() self.dyn = self.config.is_dynfee()
mempool = self.config.get('mempool_fees') mempool = self.config.use_mempool_fees()
maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool) maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool)
self.setRange(0, maxp) self.setRange(0, maxp)
self.setValue(pos) self.setValue(pos)

247
gui/qt/history_list.py

@ -24,13 +24,18 @@
# SOFTWARE. # SOFTWARE.
import webbrowser import webbrowser
import datetime
from electrum.wallet import UnrelatedTransactionException, TX_HEIGHT_LOCAL 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
from electrum.util import timestamp_to_datetime, profiler from electrum.util import timestamp_to_datetime, profiler
try:
from electrum.plot import plot_history
except:
plot_history = None
# note: this list needs to be kept in sync with another in kivy # note: this list needs to be kept in sync with another in kivy
TX_ICONS = [ TX_ICONS = [
@ -56,41 +61,181 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
AcceptFileDragDrop.__init__(self, ".txn") AcceptFileDragDrop.__init__(self, ".txn")
self.refresh_headers() self.refresh_headers()
self.setColumnHidden(1, True) self.setColumnHidden(1, True)
self.start_timestamp = None
self.end_timestamp = None
self.years = []
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')])
headers.extend(['%s '%fx.ccy + _('Acquisition price')])
headers.extend(['%s '%fx.ccy + _('Capital Gains')])
self.editable_columns |= {6}
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)
if s == _('All'):
self.start_timestamp = None
self.end_timestamp = None
elif s == _('Custom'):
start_date = self.select_date()
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.update()
def get_list_header(self):
self.period_combo = QComboBox()
self.period_combo.addItems([_('All'), _('Custom')])
self.period_combo.activated.connect(self.on_combo)
self.summary_button = QPushButton(_('Summary'))
self.summary_button.pressed.connect(self.show_summary)
self.export_button = QPushButton(_('Export'))
self.export_button.pressed.connect(self.export_history_dialog)
self.plot_button = QPushButton(_('Plot'))
self.plot_button.pressed.connect(self.plot_history_dialog)
return self.period_combo, self.summary_button, self.export_button, self.plot_button
def select_date(self):
h = self.summary
d = WindowModalDialog(self, _("Custom dates"))
d.setMinimumSize(600, 150)
d.b = True
d.start_date = None
d.end_date = None
vbox = QVBoxLayout()
grid = QGridLayout()
start_edit = QPushButton()
def on_start():
start_edit.setText('')
d.b = True
d.start_date = None
start_edit.pressed.connect(on_start)
def on_end():
end_edit.setText('')
d.b = False
d.end_date = None
end_edit = QPushButton()
end_edit.pressed.connect(on_end)
grid.addWidget(QLabel(_("Start date")), 0, 0)
grid.addWidget(start_edit, 0, 1)
grid.addWidget(QLabel(_("End date")), 1, 0)
grid.addWidget(end_edit, 1, 1)
def on_date(date):
ts = time.mktime(date.toPyDate().timetuple())
if d.b:
d.start_date = ts
start_edit.setText(date.toString())
else:
d.end_date = ts
end_edit.setText(date.toString())
cal = QCalendarWidget()
cal.setGridVisible(True)
cal.clicked[QDate].connect(on_date)
vbox.addLayout(grid)
vbox.addWidget(cal)
vbox.addLayout(Buttons(OkButton(d), CancelButton(d)))
d.setLayout(vbox)
if d.exec_():
self.start_timestamp = d.start_date
self.end_timestamp = d.end_date
self.update()
def show_summary(self):
h = self.summary
format_amount = lambda x: self.parent.format_amount(x) + ' '+ self.parent.base_unit()
d = WindowModalDialog(self, _("Summary"))
d.setMinimumSize(600, 150)
vbox = QVBoxLayout()
grid = QGridLayout()
start_date = h.get('start_date')
end_date = h.get('end_date')
if start_date is None and end_date is None:
return
grid.addWidget(QLabel(_("Start")), 0, 0)
grid.addWidget(QLabel(start_date.isoformat(' ')), 0, 1)
grid.addWidget(QLabel(_("End")), 1, 0)
grid.addWidget(QLabel(end_date.isoformat(' ')), 1, 1)
grid.addWidget(QLabel(_("Initial balance")), 2, 0)
grid.addWidget(QLabel(format_amount(h['start_balance'].value)), 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'].value)), 4, 1)
grid.addWidget(QLabel(str(h.get('end_fiat_balance'))), 4, 2)
grid.addWidget(QLabel(_("Income")), 6, 0)
grid.addWidget(QLabel(str(h.get('fiat_income'))), 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:
return
if len(self.transactions) > 0:
plt = plot_history(self.transactions)
plt.show()
@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.start_timestamp is None and self.end_timestamp is None:
start_date = self.summary.get('start_date')
end_date = self.summary.get('end_date')
if start_date and end_date:
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
if value is not None and fx and fx.show_history():
date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp)
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)
# 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 = QTreeWidgetItem(entry) item = QTreeWidgetItem(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 ""))
@ -104,12 +249,27 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
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)
@ -151,25 +311,19 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
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: if height == TX_HEIGHT_LOCAL:
menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
menu.addAction(_("Copy {}").format(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: for c in self.editable_columns:
menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column)) 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()
if rbf: if rbf:
@ -187,13 +341,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
def remove_local_tx(self, delete_tx): def remove_local_tx(self, delete_tx):
to_delete = {delete_tx} to_delete = {delete_tx}
to_delete |= self.wallet.get_depending_transactions(delete_tx) to_delete |= self.wallet.get_depending_transactions(delete_tx)
question = _("Are you sure you want to remove this transaction?") question = _("Are you sure you want to remove this transaction?")
if len(to_delete) > 1: if len(to_delete) > 1:
question = _( question = _(
"Are you sure you want to remove this transaction and {} child transactions?".format(len(to_delete) - 1) "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) answer = QMessageBox.question(self.parent, _("Please confirm"), question, QMessageBox.Yes, QMessageBox.No)
if answer == QMessageBox.No: if answer == QMessageBox.No:
return return
@ -204,13 +356,54 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
self.parent.need_update.set() self.parent.need_update.set()
def onFileAdded(self, fn): def onFileAdded(self, fn):
try:
with open(fn) as f: with open(fn) as f:
tx = self.parent.tx_from_text(f.read()) tx = self.parent.tx_from_text(f.read())
try: self.parent.save_transaction_into_wallet(tx)
self.wallet.add_transaction(tx.txid(), tx) except IOError as e:
except UnrelatedTransactionException as e:
self.parent.show_error(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: else:
self.wallet.save_transactions(write=True) lines.append(item)
# need to update at least: history_list, utxo_list, address_list with open(fileName, "w+") as f:
self.parent.need_update.set() 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))

13
gui/qt/invoice_list.py

@ -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, Requestor, Description, Amount filter_columns = [0, 1, 2, 3] # Date, Requestor, 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()

193
gui/qt/main_window.py

@ -39,33 +39,26 @@ import PyQt5.QtCore as QtCore
from .exception_window import Exception_Hook 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, NetworkConstants
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 *
from electrum.util import profiler
class StatusBarButton(QPushButton): class StatusBarButton(QPushButton):
def __init__(self, icon, tooltip, func): def __init__(self, icon, tooltip, func):
@ -488,11 +481,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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"))
@ -755,7 +747,7 @@ 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 return self.create_list_tab(l, l.get_list_header())
def show_address(self, addr): def show_address(self, addr):
from . import address_dialog from . import address_dialog
@ -1081,7 +1073,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def fee_cb(dyn, pos, fee_rate): def fee_cb(dyn, pos, fee_rate):
if dyn: if dyn:
if self.config.get('mempool_fees'): if self.config.use_mempool_fees():
self.config.set_key('depth_level', pos, False) self.config.set_key('depth_level', pos, False)
else: else:
self.config.set_key('fee_level', pos, False) self.config.set_key('fee_level', pos, False)
@ -1136,7 +1128,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def feerounding_onclick(): def feerounding_onclick():
text = (self.feerounding_text + '\n\n' + text = (self.feerounding_text + '\n\n' +
_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + _('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.') + '\n' + _('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.')) _('Also, dust is not kept as change, but added to the fee.'))
QMessageBox.information(self, 'Fee rounding', text) QMessageBox.information(self, 'Fee rounding', text)
@ -1518,7 +1511,7 @@ 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."))
@ -2100,8 +2093,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_()
@ -2133,7 +2124,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):
try:
signature.setText(base64.b64encode(sig).decode('ascii')) 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):
@ -2197,7 +2193,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
return 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()
@ -2296,25 +2300,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
try:
tx = self.read_tx_from_file() tx = self.read_tx_from_file()
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_txid(self): def do_process_from_txid(self):
from electrum import transaction from electrum import transaction
@ -2340,7 +2336,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."),
@ -2433,102 +2429,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()
for key, value in json.loads(data).items():
self.wallet.set_label(key, value)
self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile))
except (IOError, os.error) as reason:
self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason))
self.address_list.update()
self.history_list.update()
def do_export_labels(self):
labels = self.wallet.labels
try:
fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.json', "*.json")
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): def import_labels_assign(data):
d = WindowModalDialog(self, _('Export History')) for key, value in data.items():
d.setMinimumSize(400, 200) self.wallet.set_label(key, value)
vbox = QVBoxLayout(d) import_meta(path, _validate, import_labels_assign)
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.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: def on_import():
lines.append([tx_hash, label, confirmations, value_string, time_string]) self.need_update.set()
else: import_meta_gui(self, _('labels'), import_labels, on_import)
lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
with open(fileName, "w+") as f: def do_export_labels(self):
if is_csv: def export_labels(filename):
transaction = csv.writer(f, lineterminator='\n') export_meta(self.wallet.labels, filename)
transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"]) export_meta_gui(self, _('labels'), export_labels)
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'))
@ -2687,7 +2604,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
fee_type_label = HelpLabel(_('Fee estimation') + ':', msg) fee_type_label = HelpLabel(_('Fee estimation') + ':', msg)
fee_type_combo = QComboBox() fee_type_combo = QComboBox()
fee_type_combo.addItems([_('Time based'), _('Mempool based')]) fee_type_combo.addItems([_('Time based'), _('Mempool based')])
fee_type_combo.setCurrentIndex(1 if self.config.get('mempool_fees') else 0) fee_type_combo.setCurrentIndex(1 if self.config.use_mempool_fees() else 0)
def on_fee_type(x): def on_fee_type(x):
self.config.set_key('mempool_fees', x==1) self.config.set_key('mempool_fees', x==1)
self.fee_slider.update() self.fee_slider.update()
@ -2893,6 +2810,18 @@ 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 satoshis 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()
fiat_address_checkbox = QCheckBox() fiat_address_checkbox = QCheckBox()
@ -3192,3 +3121,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.show_message(_("Transaction saved successfully"))
return True

31
gui/qt/transaction_dialog.py

@ -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,19 +34,28 @@ 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 UnrelatedTransactionException 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):
try:
d = TxDialog(tx, parent, desc, prompt_if_unsaved) d = TxDialog(tx, parent, desc, prompt_if_unsaved)
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) dialogs.append(d)
d.show() d.show()
class TxDialog(QDialog, MessageBoxMixin): class TxDialog(QDialog, MessageBoxMixin):
def __init__(self, tx, parent, desc, prompt_if_unsaved): def __init__(self, tx, parent, desc, prompt_if_unsaved):
@ -58,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)
try:
self.tx.deserialize() 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
@ -179,16 +192,8 @@ 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 not self.wallet.add_transaction(self.tx.txid(), self.tx): if self.main_window.save_transaction_into_wallet(self.tx):
self.show_error(_("Transaction could not be saved. It conflicts with current history."))
return
self.wallet.save_transactions(write=True)
# need to update at least: history_list, utxo_list, address_list
self.main_window.need_update.set()
self.save_button.setDisabled(True) self.save_button.setDisabled(True)
self.show_message(_("Transaction saved successfully"))
self.saved = True self.saved = True
@ -238,7 +243,11 @@ class TxDialog(QDialog, MessageBoxMixin):
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)

47
gui/qt/util.py

@ -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",
@ -254,7 +256,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,7 +391,9 @@ 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)
@ -406,11 +410,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:
try:
self.editing_itemcol = (item, column, item.text(column)) self.editing_itemcol = (item, column, item.text(column))
# Calling setFlags causes on_changed events for some reason # Calling setFlags causes on_changed events for some reason
item.setFlags(item.flags() | Qt.ItemIsEditable) item.setFlags(item.flags() | Qt.ItemIsEditable)
QTreeWidget.editItem(self, item, column) QTreeWidget.editItem(self, item, column)
item.setFlags(item.flags() & ~Qt.ItemIsEditable) 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:
@ -673,6 +681,35 @@ class AcceptFileDragDrop:
raise NotImplementedError() 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)))
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"))

98
lib/bitcoin.py

@ -47,28 +47,6 @@ def read_json(filename, default):
return r 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: class NetworkConstants:
@classmethod @classmethod
@ -83,6 +61,21 @@ class NetworkConstants:
cls.DEFAULT_SERVERS = read_json('servers.json', {}) cls.DEFAULT_SERVERS = read_json('servers.json', {})
cls.CHECKPOINTS = read_json('checkpoints.json', []) cls.CHECKPOINTS = read_json('checkpoints.json', [])
cls.XPRV_HEADERS = {
'standard': 0x0488ade4, # xprv
'p2wpkh-p2sh': 0x049d7878, # yprv
'p2wsh-p2sh': 0x0295b005, # Yprv
'p2wpkh': 0x04b2430c, # zprv
'p2wsh': 0x02aa7a99, # Zprv
}
cls.XPUB_HEADERS = {
'standard': 0x0488b21e, # xpub
'p2wpkh-p2sh': 0x049d7cb2, # ypub
'p2wsh-p2sh': 0x0295b43f, # Ypub
'p2wpkh': 0x04b24746, # zpub
'p2wsh': 0x02aa7ed3, # Zpub
}
@classmethod @classmethod
def set_testnet(cls): def set_testnet(cls):
cls.TESTNET = True cls.TESTNET = True
@ -95,15 +88,26 @@ class NetworkConstants:
cls.DEFAULT_SERVERS = read_json('servers_testnet.json', {}) cls.DEFAULT_SERVERS = read_json('servers_testnet.json', {})
cls.CHECKPOINTS = read_json('checkpoints_testnet.json', []) cls.CHECKPOINTS = read_json('checkpoints_testnet.json', [])
cls.XPRV_HEADERS = {
'standard': 0x04358394, # tprv
'p2wpkh-p2sh': 0x044a4e28, # uprv
'p2wsh-p2sh': 0x024285b5, # Uprv
'p2wpkh': 0x045f18bc, # vprv
'p2wsh': 0x02575048, # Vprv
}
cls.XPUB_HEADERS = {
'standard': 0x043587cf, # tpub
'p2wpkh-p2sh': 0x044a5262, # upub
'p2wsh-p2sh': 0x024285ef, # Upub
'p2wpkh': 0x045f1cf6, # vpub
'p2wsh': 0x02575483, # Vpub
}
NetworkConstants.set_mainnet() NetworkConstants.set_mainnet()
################################## transactions ################################## transactions
FEE_STEP = 10000
MAX_FEE_RATE = 300000
COINBASE_MATURITY = 100 COINBASE_MATURITY = 100
COIN = 100000000 COIN = 100000000
@ -508,9 +512,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,
@ -521,25 +524,42 @@ SCRIPT_TYPES = {
} }
def serialize_privkey(secret, compressed, txin_type): def serialize_privkey(secret, compressed, txin_type, internal_use=False):
if internal_use:
prefix = bytes([(SCRIPT_TYPES[txin_type] + NetworkConstants.WIF_PREFIX) & 255]) prefix = bytes([(SCRIPT_TYPES[txin_type] + NetworkConstants.WIF_PREFIX) & 255])
else:
prefix = bytes([NetworkConstants.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 = None
if ':' in key:
txin_type, key = key.split(sep=':', maxsplit=1)
assert txin_type in SCRIPT_TYPES
vch = DecodeBase58Check(key)
if not vch:
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] - NetworkConstants.WIF_PREFIX] txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - NetworkConstants.WIF_PREFIX]
else:
assert vch[0] == NetworkConstants.WIF_PREFIX
assert len(vch) in [33, 34] assert len(vch) in [33, 34]
compressed = len(vch) == 34 compressed = len(vch) == 34
return txin_type, vch[1:33], compressed return txin_type, vch[1:33], compressed
else:
raise BaseException("cannot deserialize", key)
def regenerate_key(pk): def regenerate_key(pk):
assert len(pk) == 32 assert len(pk) == 32
@ -893,11 +913,11 @@ def _CKD_pub(cK, c, s):
def xprv_header(xtype): def xprv_header(xtype):
return bfh("%08x" % XPRV_HEADERS[xtype]) return bfh("%08x" % NetworkConstants.XPRV_HEADERS[xtype])
def xpub_header(xtype): def xpub_header(xtype):
return bfh("%08x" % XPUB_HEADERS[xtype]) return bfh("%08x" % NetworkConstants.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, child_number=b'\x00'*4):
@ -919,7 +939,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 = NetworkConstants.XPRV_HEADERS if prv else NetworkConstants.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)]

5
lib/blockchain.py

@ -181,7 +181,8 @@ class Blockchain(util.PrintError):
if d < 0: if d < 0:
chunk = chunk[-d:] chunk = chunk[-d:]
d = 0 d = 0
self.write(chunk, d, index > len(self.checkpoints)) 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):
@ -338,7 +339,7 @@ class Blockchain(util.PrintError):
self.save_chunk(idx, data) self.save_chunk(idx, data)
return True return True
except BaseException as e: except BaseException as e:
self.print_error('verify_chunk failed', str(e)) self.print_error('verify_chunk %d failed'%idx, str(e))
return False return False
def get_checkpoints(self): def get_checkpoints(self):

27
lib/coinchooser.py

@ -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)
if n > 1:
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 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)
@ -370,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

58
lib/commands.py

@ -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()
@ -440,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):
@ -736,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"),
} }
@ -746,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,

16
lib/contacts.py

@ -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()

205
lib/currencies.json

@ -1,7 +1,4 @@
{ {
"BTCChina": [
"CNY"
],
"BitPay": [ "BitPay": [
"AED", "AED",
"AFN", "AFN",
@ -15,6 +12,7 @@
"AZN", "AZN",
"BAM", "BAM",
"BBD", "BBD",
"BCH",
"BDT", "BDT",
"BGN", "BGN",
"BHD", "BHD",
@ -211,7 +209,6 @@
"EGP", "EGP",
"ERN", "ERN",
"ETB", "ETB",
"ETH",
"EUR", "EUR",
"FJD", "FJD",
"FKP", "FKP",
@ -254,7 +251,6 @@
"LKR", "LKR",
"LRD", "LRD",
"LSL", "LSL",
"LTC",
"LYD", "LYD",
"MAD", "MAD",
"MDL", "MDL",
@ -331,10 +327,8 @@
"XPD", "XPD",
"XPF", "XPF",
"XPT", "XPT",
"XRP",
"YER", "YER",
"ZAR", "ZAR",
"ZEC",
"ZMW", "ZMW",
"ZWL" "ZWL"
], ],
@ -371,6 +365,175 @@
"TWD", "TWD",
"USD" "USD"
], ],
"CoinDesk": [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTC",
"BTN",
"BWP",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EEK",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JEP",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LVL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MTL",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"STD",
"SVC",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XBT",
"XCD",
"XDR",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZMW",
"ZWL"
],
"Coinbase": [ "Coinbase": [
"AED", "AED",
"AFN", "AFN",
@ -384,6 +547,7 @@
"AZN", "AZN",
"BAM", "BAM",
"BBD", "BBD",
"BCH",
"BDT", "BDT",
"BGN", "BGN",
"BHD", "BHD",
@ -403,6 +567,7 @@
"CHF", "CHF",
"CLF", "CLF",
"CLP", "CLP",
"CNH",
"CNY", "CNY",
"COP", "COP",
"CRC", "CRC",
@ -542,9 +707,6 @@
"ZMW", "ZMW",
"ZWL" "ZWL"
], ],
"Coinsecure": [
"INR"
],
"Foxbit": [ "Foxbit": [
"BRL" "BRL"
], ],
@ -559,7 +721,10 @@
"AED", "AED",
"ARS", "ARS",
"AUD", "AUD",
"BAM",
"BDT", "BDT",
"BHD",
"BOB",
"BRL", "BRL",
"BYN", "BYN",
"CAD", "CAD",
@ -572,6 +737,7 @@
"DKK", "DKK",
"DOP", "DOP",
"EGP", "EGP",
"ETH",
"EUR", "EUR",
"GBP", "GBP",
"GHS", "GHS",
@ -579,20 +745,21 @@
"HRK", "HRK",
"HUF", "HUF",
"IDR", "IDR",
"ILS",
"INR", "INR",
"IRR", "IRR",
"ISK", "JOD",
"JPY", "JPY",
"KES", "KES",
"KRW",
"KZT", "KZT",
"LKR",
"MAD", "MAD",
"MMK",
"MXN", "MXN",
"MYR", "MYR",
"NGN", "NGN",
"NOK", "NOK",
"NZD", "NZD",
"OMR",
"PAB", "PAB",
"PEN", "PEN",
"PHP", "PHP",
@ -602,20 +769,23 @@
"RON", "RON",
"RSD", "RSD",
"RUB", "RUB",
"RWF",
"SAR", "SAR",
"SEK", "SEK",
"SGD", "SGD",
"THB", "THB",
"TRY", "TRY",
"TWD", "TTD",
"TZS", "TZS",
"UAH", "UAH",
"UGX", "UGX",
"USD", "USD",
"UYU",
"VEF", "VEF",
"VND", "VND",
"XAF", "XAR",
"ZAR" "ZAR",
"ZMW"
], ],
"MercadoBitcoin": [ "MercadoBitcoin": [
"BRL" "BRL"
@ -623,9 +793,6 @@
"NegocieCoins": [ "NegocieCoins": [
"BRL" "BRL"
], ],
"Winkdex": [
"USD"
],
"WEX": [ "WEX": [
"EUR", "EUR",
"RUB", "RUB",

9
lib/daemon.py

@ -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
@ -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)
try:
self.gui.main() self.gui.main()
except BaseException as e:
traceback.print_exc(file=sys.stdout)
# app will exit now

114
lib/exchange_rate.py

@ -2,6 +2,8 @@ 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
@ -33,7 +35,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):
@ -59,28 +61,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('')
@ -99,7 +127,7 @@ class BitcoinAverage(ExchangeBase):
'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD', 'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD',
'ZAR'] 'ZAR']
def historical_rates(self, ccy): def request_history(self, ccy):
history = self.get_csv('apiv2.bitcoinaverage.com', history = self.get_csv('apiv2.bitcoinaverage.com',
"/indices/global/history/BTC%s?period=alltime&format=csv" % ccy) "/indices/global/history/BTC%s?period=alltime&format=csv" % ccy)
return dict([(h['DateTime'][:10], h['Average']) return dict([(h['DateTime'][:10], h['Average'])
@ -127,7 +155,7 @@ class BitcoinVenezuela(ExchangeBase):
def history_ccys(self): def history_ccys(self):
return ['ARS', 'EUR', 'USD', 'VEF'] return ['ARS', 'EUR', 'USD', 'VEF']
def historical_rates(self, ccy): def request_history(self, ccy):
return self.get_json('api.bitcoinvenezuela.com', return self.get_json('api.bitcoinvenezuela.com',
"/historical/index.php?coin=BTC")[ccy +'_BTC'] "/historical/index.php?coin=BTC")[ccy +'_BTC']
@ -199,23 +227,24 @@ class Coinbase(ExchangeBase):
class CoinDesk(ExchangeBase): class CoinDesk(ExchangeBase):
def get_rates(self, ccy): def get_currencies(self):
dicts = self.get_json('api.coindesk.com', dicts = self.get_json('api.coindesk.com',
'/v1/bpi/supported-currencies.json') '/v1/bpi/supported-currencies.json')
return [d['currency'] for d in dicts]
def get_rates(self, ccy):
json = self.get_json('api.coindesk.com', json = self.get_json('api.coindesk.com',
'/v1/bpi/currentprice/%s.json' % ccy) '/v1/bpi/currentprice/%s.json' % ccy)
ccys = [d['currency'] for d in dicts] result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])}
result = dict.fromkeys(ccys)
result[ccy] = Decimal(json['bpi'][ccy]['rate_float'])
return result return result
def history_starts(self): def history_starts(self):
return { 'USD': '2012-11-30' } return { 'USD': '2012-11-30', 'EUR': '2013-09-01' }
def history_ccys(self): def history_ccys(self):
return self.history_starts().keys() return self.history_starts().keys()
def historical_rates(self, ccy): def request_history(self, ccy):
start = self.history_starts()[ccy] start = self.history_starts()[ccy]
end = datetime.today().strftime('%Y-%m-%d') end = datetime.today().strftime('%Y-%m-%d')
# Note ?currency and ?index don't work as documented. Sigh. # Note ?currency and ?index don't work as documented. Sigh.
@ -313,7 +342,7 @@ class Winkdex(ExchangeBase):
def history_ccys(self): def history_ccys(self):
return ['USD'] return ['USD']
def historical_rates(self, ccy): def request_history(self, ccy):
json = self.get_json('winkdex.com', json = self.get_json('winkdex.com',
"/api/v0/series?start_time=1342915200") "/api/v0/series?start_time=1342915200")
history = json['series'][0]['results'] history = json['series'][0]['results']
@ -346,7 +375,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))
@ -377,7 +408,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)
@ -400,7 +434,7 @@ class FxThread(ThreadJob):
# 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)
@ -448,45 +482,59 @@ 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):
if self.network:
self.network.trigger_callback('on_quotes') self.network.trigger_callback('on_quotes')
def on_history(self): def on_history(self):
if self.network:
self.network.trigger_callback('on_history') 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('NaN')
return Decimal(rate) return Decimal(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 FX 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):
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)

5
lib/keystore.py

@ -139,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):

16
lib/network.py

@ -549,7 +549,7 @@ class Network(util.DaemonThread):
self.donation_address = result self.donation_address = result
elif method == 'mempool.get_fee_histogram': elif method == 'mempool.get_fee_histogram':
if error is None: if error is None:
self.print_error(result) self.print_error('fee_histogram', result)
self.config.mempool_fees = result self.config.mempool_fees = result
self.notify('fee_histogram') self.notify('fee_histogram')
elif method == 'blockchain.estimatefee': elif method == 'blockchain.estimatefee':
@ -777,25 +777,29 @@ 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):
@ -987,7 +991,7 @@ class Network(util.DaemonThread):
if not height: if not height:
return return
if height < self.max_checkpoint(): if height < self.max_checkpoint():
self.connection_down(interface) self.connection_down(interface.server)
return return
interface.tip_header = header interface.tip_header = header
interface.tip = height interface.tip = height

28
lib/paymentrequest.py

@ -40,6 +40,7 @@ except ImportError:
from . import bitcoin from . import bitcoin
from . import util from . import util
from .util import print_error, bh2u, bfh 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
@ -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)

11
lib/plot.py

@ -14,17 +14,16 @@ 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): def plot_history(history):
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

84
lib/simple_config.py

@ -5,14 +5,22 @@ import os
import stat import stat
from copy import deepcopy from copy import deepcopy
from .util import (user_dir, print_error, PrintError, from .util import (user_dir, print_error, PrintError,
NoDynamicFeeEstimates, format_satoshis) NoDynamicFeeEstimates, format_satoshis)
from .i18n import _
from .bitcoin import MAX_FEE_RATE
FEE_ETA_TARGETS = [25, 10, 5, 2] FEE_ETA_TARGETS = [25, 10, 5, 2]
FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000] 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]
config = None config = None
@ -39,7 +47,6 @@ class SimpleConfig(PrintError):
2. User configuration (in the user's config directory) 2. User configuration (in the user's config directory)
They are taken in order (1. overrides config options set in 2.) They are taken in order (1. overrides config options set in 2.)
""" """
fee_rates = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
def __init__(self, options=None, read_user_config_function=None, def __init__(self, options=None, read_user_config_function=None,
read_user_dir_function=None): read_user_dir_function=None):
@ -261,13 +268,19 @@ 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 max_fee_rate(self): def impose_hard_limits_on_fee(func):
f = self.get('max_fee_rate', MAX_FEE_RATE) def get_fee_within_limits(self, *args, **kwargs):
if f==0: fee = func(self, *args, **kwargs)
f = MAX_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
@impose_hard_limits_on_fee
def eta_to_fee(self, i): def eta_to_fee(self, i):
"""Returns fee in sat/kbyte."""
if i < 4: if i < 4:
j = FEE_ETA_TARGETS[i] j = FEE_ETA_TARGETS[i]
fee = self.fee_estimates.get(j) fee = self.fee_estimates.get(j)
@ -276,8 +289,6 @@ class SimpleConfig(PrintError):
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 fee_to_depth(self, target_fee): def fee_to_depth(self, target_fee):
@ -290,7 +301,9 @@ class SimpleConfig(PrintError):
return 0 return 0
return depth return depth
@impose_hard_limits_on_fee
def depth_to_fee(self, i): def depth_to_fee(self, i):
"""Returns fee in sat/kbyte."""
target = self.depth_target(i) target = self.depth_target(i)
depth = 0 depth = 0
for fee, s in self.mempool_fees: for fee, s in self.mempool_fees:
@ -305,6 +318,8 @@ class SimpleConfig(PrintError):
return FEE_DEPTH_TARGETS[i] return FEE_DEPTH_TARGETS[i]
def eta_target(self, i): def eta_target(self, i):
if i == len(FEE_ETA_TARGETS):
return 1
return FEE_ETA_TARGETS[i] return FEE_ETA_TARGETS[i]
def fee_to_eta(self, fee_per_kb): def fee_to_eta(self, fee_per_kb):
@ -320,17 +335,26 @@ class SimpleConfig(PrintError):
return "%.1f MB from tip"%(depth/1000000) return "%.1f MB from tip"%(depth/1000000)
def eta_tooltip(self, x): def eta_tooltip(self, x):
return 'Low fee' if x < 0 else 'Within %d blocks'%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): def get_fee_status(self):
dyn = self.is_dynfee() dyn = self.is_dynfee()
mempool = self.get('mempool_fees') mempool = self.use_mempool_fees()
pos = self.get_depth_level() if mempool else self.get_fee_level() pos = self.get_depth_level() if mempool else self.get_fee_level()
fee_rate = self.fee_per_kb() fee_rate = self.fee_per_kb()
target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate) target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate)
return target return target
def get_fee_text(self, pos, dyn, mempool, fee_rate): 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' rate_str = (format_satoshis(fee_rate/1000, False, 0, 0, False) + ' sat/byte') if fee_rate is not None else 'unknown'
if dyn: if dyn:
if mempool: if mempool:
@ -342,14 +366,10 @@ class SimpleConfig(PrintError):
tooltip = rate_str tooltip = rate_str
else: else:
text = rate_str text = rate_str
if mempool: if mempool and self.has_fee_mempool():
if self.has_fee_mempool():
depth = self.fee_to_depth(fee_rate) depth = self.fee_to_depth(fee_rate)
tooltip = self.depth_tooltip(depth) tooltip = self.depth_tooltip(depth)
else: elif not mempool and self.has_fee_etas():
tooltip = ''
else:
if self.has_fee_etas():
eta = self.fee_to_eta(fee_rate) eta = self.fee_to_eta(fee_rate)
tooltip = self.eta_tooltip(eta) tooltip = self.eta_tooltip(eta)
else: else:
@ -361,7 +381,7 @@ class SimpleConfig(PrintError):
return min(maxp, self.get('depth_level', 2)) return min(maxp, self.get('depth_level', 2))
def get_fee_level(self): def get_fee_level(self):
maxp = len(FEE_ETA_TARGETS) - 1 maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
return min(maxp, self.get('fee_level', 2)) return min(maxp, self.get('fee_level', 2))
def get_fee_slider(self, dyn, mempool): def get_fee_slider(self, dyn, mempool):
@ -372,7 +392,7 @@ class SimpleConfig(PrintError):
fee_rate = self.depth_to_fee(pos) fee_rate = self.depth_to_fee(pos)
else: else:
pos = self.get_fee_level() pos = self.get_fee_level()
maxp = len(FEE_ETA_TARGETS) - 1 maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
fee_rate = self.eta_to_fee(pos) fee_rate = self.eta_to_fee(pos)
else: else:
fee_rate = self.fee_per_kb() fee_rate = self.fee_per_kb()
@ -380,12 +400,11 @@ class SimpleConfig(PrintError):
maxp = 9 maxp = 9
return maxp, pos, fee_rate 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_etas(self): def has_fee_etas(self):
@ -394,11 +413,17 @@ class SimpleConfig(PrintError):
def has_fee_mempool(self): def has_fee_mempool(self):
return bool(self.mempool_fees) 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()
def is_dynfee(self): def is_dynfee(self):
return self.get('dynamic_fees', True) return bool(self.get('dynamic_fees', True))
def use_mempool_fees(self): def use_mempool_fees(self):
return self.get('mempool_fees', False) return bool(self.get('mempool_fees', False))
def fee_per_kb(self): def fee_per_kb(self):
"""Returns sat/kvB fee to pay for a txn. """Returns sat/kvB fee to pay for a txn.
@ -410,7 +435,7 @@ class SimpleConfig(PrintError):
else: else:
fee_rate = self.eta_to_fee(self.get_fee_level()) 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): def fee_per_byte(self):
@ -428,7 +453,12 @@ class SimpleConfig(PrintError):
@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

45
lib/tests/test_bitcoin.py

@ -271,6 +271,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 +279,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 +297,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 +315,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 +333,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 +385,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 +394,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:

379
lib/tests/test_transaction.py

@ -235,6 +235,385 @@ class TestTransaction(unittest.TestCase):
tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000') tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000')
self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid()) 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):

65
lib/transaction.py

@ -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.
@ -303,7 +305,8 @@ 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 ]
@ -334,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])
@ -345,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
@ -357,11 +361,16 @@ 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))
return
x_sig = [bh2u(x[1]) for x in decoded[1:-1]] x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
try:
m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1]) 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 # write result in d
d['type'] = 'p2sh' d['type'] = 'p2sh'
d['num_sig'] = m d['num_sig'] = m
@ -370,17 +379,23 @@ def parse_scriptSig(d, _bytes):
d['pubkeys'] = pubkeys d['pubkeys'] = pubkeys
d['redeemScript'] = redeemScript d['redeemScript'] = redeemScript
d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript))) d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript)))
return
print_error("parse_scriptSig: cannot find address in input script (unknown)",
bh2u(_bytes))
def parse_redeemScript(s): def parse_redeemScript(s):
dec2 = [ x for x in script_GetOp(s) ] dec2 = [ x for x in script_GetOp(s) ]
try:
m = dec2[0][0] - opcodes.OP_1 + 1 m = dec2[0][0] - opcodes.OP_1 + 1
n = dec2[-2][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() raise NotRecognizedRedeemScript()
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]
@ -436,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)
try:
parse_scriptSig(d, scriptSig) 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'] = ''
@ -465,6 +484,7 @@ def parse_witness(vds, txin):
# between p2wpkh and p2wsh; we do this based on number of witness items, # between p2wpkh and p2wsh; we do this based on number of witness items,
# hence (FIXME) p2wsh with n==2 (maybe n==1 ?) will probably fail. # hence (FIXME) p2wsh with n==2 (maybe n==1 ?) will probably fail.
# If v==0 and n==2, we need parent scriptPubKey to distinguish between p2wpkh and p2wsh. # If v==0 and n==2, we need parent scriptPubKey to distinguish between p2wpkh and p2wsh.
try:
if txin['type'] == 'coinbase': if txin['type'] == 'coinbase':
pass pass
elif txin['type'] == 'p2wsh-p2sh' or n > 2: elif txin['type'] == 'p2wsh-p2sh' or n > 2:
@ -477,13 +497,27 @@ def parse_witness(vds, txin):
txin['x_pubkeys'] = x_pubkeys txin['x_pubkeys'] = x_pubkeys
txin['pubkeys'] = pubkeys txin['pubkeys'] = pubkeys
txin['witnessScript'] = witnessScript 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: elif txin['type'] == 'p2wpkh-p2sh' or n == 2:
txin['num_sig'] = 1 txin['num_sig'] = 1
txin['x_pubkeys'] = [w[1]] txin['x_pubkeys'] = [w[1]]
txin['pubkeys'] = [safe_parse_pubkey(w[1])] txin['pubkeys'] = [safe_parse_pubkey(w[1])]
txin['signatures'] = parse_sig([w[0]]) 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: else:
raise UnknownTxinType() 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 = {}
@ -513,20 +547,7 @@ def deserialize(raw):
if is_segwit: if is_segwit:
for i in range(n_vin): for i in range(n_vin):
txin = d['inputs'][i] txin = d['inputs'][i]
try:
parse_witness(vds, txin) parse_witness(vds, txin)
except UnknownTxinType:
txin['type'] = 'unknown'
# FIXME: GUI might show 'unknown' address (e.g. for a non-multisig p2wsh)
continue
# segwit-native script
if not txin.get('scriptSig'):
if txin['num_sig'] == 1:
txin['type'] = 'p2wpkh'
txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0]))
else:
txin['type'] = 'p2wsh'
txin['address'] = bitcoin.script_to_p2wsh(txin['witnessScript'])
d['lockTime'] = vds.read_uint32() d['lockTime'] = vds.read_uint32()
return d return d

84
lib/util.py

@ -41,7 +41,6 @@ def inv_dict(d):
base_units = {'BTC':8, 'mBTC':5, 'uBTC':2} base_units = {'BTC':8, 'mBTC':5, 'uBTC':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(".")]
@ -58,17 +57,70 @@ 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
# 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 None:
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):
@ -367,10 +419,7 @@ 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:
return datetime.fromtimestamp(timestamp) return datetime.fromtimestamp(timestamp)
except:
return None
def format_time(timestamp): def format_time(timestamp):
date = timestamp_to_datetime(timestamp) date = timestamp_to_datetime(timestamp)
@ -735,3 +784,30 @@ def setup_thread_excepthook():
self.run = run_with_except_hook self.run = run_with_except_hook
threading.Thread.__init__ = init 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)

27
lib/verifier.py

@ -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 // 2016 index = tx_height // 2016
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',
@ -70,10 +77,18 @@ 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

372
lib/wallet.py

@ -38,6 +38,7 @@ import traceback
from functools import partial from functools import partial
from collections import defaultdict from collections import defaultdict
from numbers import Number from numbers import Number
from decimal import Decimal
import sys import sys
@ -77,9 +78,9 @@ TX_HEIGHT_UNCONFIRMED = 0
def relayfee(network): def relayfee(network):
RELAY_FEE = 1000 from .simple_config import FEERATE_DEFAULT_RELAY
MAX_RELAY_FEE = 50000 MAX_RELAY_FEE = 50000
f = network.relay_fee if network and network.relay_fee else RELAY_FEE f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY
return min(f, MAX_RELAY_FEE) return min(f, MAX_RELAY_FEE)
def dust_threshold(network): def dust_threshold(network):
@ -156,9 +157,18 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100):
return tx return tx
class UnrelatedTransactionException(Exception): class AddTransactionException(Exception):
def __init__(self): pass
self.args = ("Transaction is unrelated to this wallet ", )
class UnrelatedTransactionException(AddTransactionException):
def __str__(self):
return _("Transaction is unrelated to this wallet.")
class NotIsMineTransactionException(AddTransactionException):
def __str__(self):
return _("Only transactions with inputs owned by the wallet can be added.")
class Abstract_Wallet(PrintError): class Abstract_Wallet(PrintError):
@ -184,6 +194,7 @@ class Abstract_Wallet(PrintError):
self.labels = storage.get('labels', {}) self.labels = storage.get('labels', {})
self.frozen_addresses = set(storage.get('frozen_addresses',[])) self.frozen_addresses = set(storage.get('frozen_addresses',[]))
self.history = storage.get('addr_history',{}) # address -> list(txid, height) self.history = storage.get('addr_history',{}) # address -> list(txid, height)
self.fiat_value = storage.get('fiat_value', {})
self.load_keystore() self.load_keystore()
self.load_addresses() self.load_addresses()
@ -206,7 +217,7 @@ class Abstract_Wallet(PrintError):
self.up_to_date = False self.up_to_date = False
# locks: if you need to take multiple ones, acquire them in the order they are defined here! # locks: if you need to take multiple ones, acquire them in the order they are defined here!
self.lock = threading.Lock() self.lock = threading.RLock()
self.transaction_lock = threading.RLock() self.transaction_lock = threading.RLock()
self.check_history() self.check_history()
@ -269,16 +280,16 @@ class Abstract_Wallet(PrintError):
self.pruned_txo = {} self.pruned_txo = {}
self.spent_outpoints = {} self.spent_outpoints = {}
self.history = {} self.history = {}
self.verified_tx = {}
self.transactions = {}
self.save_transactions() self.save_transactions()
@profiler @profiler
def build_spent_outpoints(self): def build_spent_outpoints(self):
self.spent_outpoints = {} self.spent_outpoints = {}
for txid, tx in self.transactions.items(): for txid, items in self.txi.items():
for txi in tx.inputs(): for addr, l in items.items():
ser = Transaction.get_outpoint_from_txin(txi) for ser, v in l:
if ser is None:
continue
self.spent_outpoints[ser] = txid self.spent_outpoints[ser] = txid
@profiler @profiler
@ -320,6 +331,9 @@ class Abstract_Wallet(PrintError):
def synchronize(self): def synchronize(self):
pass pass
def is_deterministic(self):
return self.keystore.is_deterministic()
def set_up_to_date(self, up_to_date): def set_up_to_date(self, up_to_date):
with self.lock: with self.lock:
self.up_to_date = up_to_date self.up_to_date = up_to_date
@ -341,13 +355,37 @@ class Abstract_Wallet(PrintError):
if old_text: if old_text:
self.labels.pop(name) self.labels.pop(name)
changed = True changed = True
if changed: if changed:
run_hook('set_label', self, name, text) run_hook('set_label', self, name, text)
self.storage.put('labels', self.labels) self.storage.put('labels', self.labels)
return changed return changed
def set_fiat_value(self, txid, ccy, text):
if txid not in self.transactions:
return
if not text:
d = self.fiat_value.get(ccy, {})
if d and txid in d:
d.pop(txid)
else:
return
else:
try:
Decimal(text)
except:
return
if ccy not in self.fiat_value:
self.fiat_value[ccy] = {}
self.fiat_value[ccy][txid] = text
self.storage.put('fiat_value', self.fiat_value)
def get_fiat_value(self, txid, ccy):
fiat_value = self.fiat_value.get(ccy, {}).get(txid)
try:
return Decimal(fiat_value)
except:
return
def is_mine(self, address): def is_mine(self, address):
return address in self.get_addresses() return address in self.get_addresses()
@ -359,23 +397,21 @@ class Abstract_Wallet(PrintError):
def get_address_index(self, address): def get_address_index(self, address):
raise NotImplementedError() raise NotImplementedError()
def get_redeem_script(self, address):
return None
def export_private_key(self, address, password): def export_private_key(self, address, password):
""" extended WIF format """
if self.is_watching_only(): if self.is_watching_only():
return [] return []
index = self.get_address_index(address) index = self.get_address_index(address)
pk, compressed = self.keystore.get_private_key(index, password) pk, compressed = self.keystore.get_private_key(index, password)
if self.txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: txin_type = self.get_txin_type(address)
pubkeys = self.get_public_keys(address) redeem_script = self.get_redeem_script(address)
redeem_script = self.pubkeys_to_redeem_script(pubkeys) serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
else: return serialized_privkey, redeem_script
redeem_script = None
return bitcoin.serialize_privkey(pk, compressed, self.txin_type), redeem_script
def get_public_keys(self, address): def get_public_keys(self, address):
sequence = self.get_address_index(address) return [self.get_public_key(address)]
return self.get_pubkeys(*sequence)
def add_unverified_tx(self, tx_hash, tx_height): def add_unverified_tx(self, tx_hash, tx_height):
if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \ if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \
@ -467,6 +503,17 @@ class Abstract_Wallet(PrintError):
delta += v delta += v
return delta return delta
def get_tx_value(self, txid):
" effect of tx on the entire domain"
delta = 0
for addr, d in self.txi.get(txid, {}).items():
for n, v in d:
delta -= v
for addr, d in self.txo.get(txid, {}).items():
for n, v, cb in d:
delta += v
return delta
def get_wallet_delta(self, tx): def get_wallet_delta(self, tx):
""" effect of tx on wallet """ """ effect of tx on wallet """
addresses = self.get_addresses() addresses = self.get_addresses()
@ -671,14 +718,21 @@ class Abstract_Wallet(PrintError):
def get_address_history(self, addr): def get_address_history(self, addr):
h = [] h = []
with self.transaction_lock: # we need self.transaction_lock but get_tx_height will take self.lock
# so we need to take that too here, to enforce order of locks
with self.lock, self.transaction_lock:
for tx_hash in self.transactions: for tx_hash in self.transactions:
if addr in self.txi.get(tx_hash, []) or addr in self.txo.get(tx_hash, []): if addr in self.txi.get(tx_hash, []) or addr in self.txo.get(tx_hash, []):
tx_height = self.get_tx_height(tx_hash)[0] tx_height = self.get_tx_height(tx_hash)[0]
h.append((tx_hash, tx_height)) h.append((tx_hash, tx_height))
return h return h
def find_pay_to_pubkey_address(self, prevout_hash, prevout_n): def get_txin_address(self, txi):
addr = txi.get('address')
if addr != "(pubkey)":
return addr
prevout_hash = txi.get('prevout_hash')
prevout_n = txi.get('prevout_n')
dd = self.txo.get(prevout_hash, {}) dd = self.txo.get(prevout_hash, {})
for addr, l in dd.items(): for addr, l in dd.items():
for n, v, is_cb in l: for n, v, is_cb in l:
@ -686,6 +740,16 @@ class Abstract_Wallet(PrintError):
self.print_error("found pay-to-pubkey address:", addr) self.print_error("found pay-to-pubkey address:", addr)
return addr return addr
def get_txout_address(self, txo):
_type, x, v = txo
if _type == TYPE_ADDRESS:
addr = x
elif _type == TYPE_PUBKEY:
addr = bitcoin.public_key_to_p2pkh(bfh(x))
else:
addr = None
return addr
def get_conflicting_transactions(self, tx): def get_conflicting_transactions(self, tx):
"""Returns a set of transaction hashes from the wallet history that are """Returns a set of transaction hashes from the wallet history that are
directly conflicting with tx, i.e. they have common outpoints being directly conflicting with tx, i.e. they have common outpoints being
@ -702,10 +766,7 @@ class Abstract_Wallet(PrintError):
if spending_tx_hash is None: if spending_tx_hash is None:
continue continue
# this outpoint (ser) has already been spent, by spending_tx # this outpoint (ser) has already been spent, by spending_tx
if spending_tx_hash not in self.transactions: assert spending_tx_hash in self.transactions
# can't find this txn: delete and ignore it
self.spent_outpoints.pop(ser)
continue
conflicting_txns |= {spending_tx_hash} conflicting_txns |= {spending_tx_hash}
txid = tx.txid() txid = tx.txid()
if txid in conflicting_txns: if txid in conflicting_txns:
@ -716,9 +777,24 @@ class Abstract_Wallet(PrintError):
return conflicting_txns return conflicting_txns
def add_transaction(self, tx_hash, tx): def add_transaction(self, tx_hash, tx):
# we need self.transaction_lock but get_tx_height will take self.lock
# so we need to take that too here, to enforce order of locks
with self.lock, self.transaction_lock:
# NOTE: returning if tx in self.transactions might seem like a good idea
# BUT we track is_mine inputs in a txn, and during subsequent calls
# of add_transaction tx, we might learn of more-and-more inputs of
# being is_mine, as we roll the gap_limit forward
is_coinbase = tx.inputs()[0]['type'] == 'coinbase' is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
related = False tx_height = self.get_tx_height(tx_hash)[0]
with self.transaction_lock: is_mine = any([self.is_mine(txin['address']) for txin in tx.inputs()])
# do not save if tx is local and not mine
if tx_height == TX_HEIGHT_LOCAL and not is_mine:
# FIXME the test here should be for "not all is_mine"; cannot detect conflict in some cases
raise NotIsMineTransactionException()
# raise exception if unrelated to wallet
is_for_me = any([self.is_mine(self.get_txout_address(txo)) for txo in tx.outputs()])
if not is_mine and not is_for_me:
raise UnrelatedTransactionException()
# Find all conflicting transactions. # Find all conflicting transactions.
# In case of a conflict, # In case of a conflict,
# 1. confirmed > mempool > local # 1. confirmed > mempool > local
@ -728,7 +804,6 @@ class Abstract_Wallet(PrintError):
# or drop this txn # or drop this txn
conflicting_txns = self.get_conflicting_transactions(tx) conflicting_txns = self.get_conflicting_transactions(tx)
if conflicting_txns: if conflicting_txns:
tx_height = self.get_tx_height(tx_hash)[0]
existing_mempool_txn = any( existing_mempool_txn = any(
self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
for tx_hash2 in conflicting_txns) for tx_hash2 in conflicting_txns)
@ -748,44 +823,34 @@ class Abstract_Wallet(PrintError):
to_remove |= self.get_depending_transactions(conflicting_tx_hash) to_remove |= self.get_depending_transactions(conflicting_tx_hash)
for tx_hash2 in to_remove: for tx_hash2 in to_remove:
self.remove_transaction(tx_hash2) self.remove_transaction(tx_hash2)
# add inputs # add inputs
self.txi[tx_hash] = d = {} self.txi[tx_hash] = d = {}
for txi in tx.inputs(): for txi in tx.inputs():
addr = txi.get('address') addr = self.get_txin_address(txi)
if txi['type'] != 'coinbase': if txi['type'] != 'coinbase':
prevout_hash = txi['prevout_hash'] prevout_hash = txi['prevout_hash']
prevout_n = txi['prevout_n'] prevout_n = txi['prevout_n']
ser = prevout_hash + ':%d'%prevout_n ser = prevout_hash + ':%d'%prevout_n
self.spent_outpoints[ser] = tx_hash
if addr == "(pubkey)":
addr = self.find_pay_to_pubkey_address(prevout_hash, prevout_n)
# find value from prev output # find value from prev output
if addr and self.is_mine(addr): if addr and self.is_mine(addr):
related = True
dd = self.txo.get(prevout_hash, {}) dd = self.txo.get(prevout_hash, {})
for n, v, is_cb in dd.get(addr, []): for n, v, is_cb in dd.get(addr, []):
if n == prevout_n: if n == prevout_n:
if d.get(addr) is None: if d.get(addr) is None:
d[addr] = [] d[addr] = []
d[addr].append((ser, v)) d[addr].append((ser, v))
# we only track is_mine spends
self.spent_outpoints[ser] = tx_hash
break break
else: else:
self.pruned_txo[ser] = tx_hash self.pruned_txo[ser] = tx_hash
# add outputs # add outputs
self.txo[tx_hash] = d = {} self.txo[tx_hash] = d = {}
for n, txo in enumerate(tx.outputs()): for n, txo in enumerate(tx.outputs()):
v = txo[2]
ser = tx_hash + ':%d'%n ser = tx_hash + ':%d'%n
_type, x, v = txo addr = self.get_txout_address(txo)
if _type == TYPE_ADDRESS:
addr = x
elif _type == TYPE_PUBKEY:
addr = bitcoin.public_key_to_p2pkh(bfh(x))
else:
addr = None
if addr and self.is_mine(addr): if addr and self.is_mine(addr):
related = True
if d.get(addr) is None: if d.get(addr) is None:
d[addr] = [] d[addr] = []
d[addr].append((n, v, is_coinbase)) d[addr].append((n, v, is_coinbase))
@ -797,30 +862,19 @@ class Abstract_Wallet(PrintError):
if dd.get(addr) is None: if dd.get(addr) is None:
dd[addr] = [] dd[addr] = []
dd[addr].append((ser, v)) dd[addr].append((ser, v))
if not related:
raise UnrelatedTransactionException()
# save # save
self.transactions[tx_hash] = tx self.transactions[tx_hash] = tx
return True return True
def remove_transaction(self, tx_hash): def remove_transaction(self, tx_hash):
def undo_spend(outpoint_to_txid_map): def undo_spend(outpoint_to_txid_map):
if tx: for addr, l in self.txi[tx_hash].items():
# if we have the tx, this should often be faster for ser, v in l:
for txi in tx.inputs():
ser = Transaction.get_outpoint_from_txin(txi)
outpoint_to_txid_map.pop(ser, None) outpoint_to_txid_map.pop(ser, None)
else:
for ser, hh in list(outpoint_to_txid_map.items()):
if hh == tx_hash:
outpoint_to_txid_map.pop(ser)
with self.transaction_lock: with self.transaction_lock:
self.print_error("removing tx from history", tx_hash) self.print_error("removing tx from history", tx_hash)
#tx = self.transactions.pop(tx_hash) self.transactions.pop(tx_hash, None)
tx = self.transactions.get(tx_hash, None)
undo_spend(self.pruned_txo) undo_spend(self.pruned_txo)
undo_spend(self.spent_outpoints) undo_spend(self.spent_outpoints)
@ -850,13 +904,17 @@ class Abstract_Wallet(PrintError):
def receive_history_callback(self, addr, hist, tx_fees): def receive_history_callback(self, addr, hist, tx_fees):
with self.lock: with self.lock:
old_hist = self.history.get(addr, []) old_hist = self.get_address_history(addr)
for tx_hash, height in old_hist: for tx_hash, height in old_hist:
if (tx_hash, height) not in hist: if (tx_hash, height) not in hist:
# make tx local # make tx local
self.unverified_tx.pop(tx_hash, None) self.unverified_tx.pop(tx_hash, None)
self.verified_tx.pop(tx_hash, None) self.verified_tx.pop(tx_hash, None)
self.verifier.merkle_roots.pop(tx_hash, None) self.verifier.merkle_roots.pop(tx_hash, None)
# but remove completely if not is_mine
if self.txi[tx_hash] == {}:
# FIXME the test here should be for "not all is_mine"; cannot detect conflict in some cases
self.remove_transaction(tx_hash)
self.history[addr] = hist self.history[addr] = hist
for tx_hash, tx_height in hist: for tx_hash, tx_height in hist:
@ -914,6 +972,105 @@ class Abstract_Wallet(PrintError):
return h2 return h2
def balance_at_timestamp(self, domain, target_timestamp):
h = self.get_history(domain)
for tx_hash, height, conf, timestamp, value, balance in h:
if timestamp > target_timestamp:
return balance - value
# return last balance
return balance
@profiler
def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False):
from .util import timestamp_to_datetime, Satoshis, Fiat
out = []
capital_gains = 0
fiat_income = 0
h = self.get_history(domain)
for tx_hash, height, conf, timestamp, value, balance in h:
if from_timestamp and timestamp < from_timestamp:
continue
if to_timestamp and timestamp >= to_timestamp:
continue
item = {
'txid':tx_hash,
'height':height,
'confirmations':conf,
'timestamp':timestamp,
'value': Satoshis(value),
'balance': Satoshis(balance)
}
item['date'] = timestamp_to_datetime(timestamp) if timestamp is not None else None
item['label'] = self.get_label(tx_hash)
if show_addresses:
tx = self.transactions.get(tx_hash)
tx.deserialize()
input_addresses = []
output_addresses = []
for x in tx.inputs():
if x['type'] == 'coinbase': continue
addr = self.get_txin_address(x)
if addr is None:
continue
input_addresses.append(addr)
for addr, v in tx.get_outputs():
output_addresses.append(addr)
item['input_addresses'] = input_addresses
item['output_addresses'] = output_addresses
if fx is not None:
date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp)
fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
if fiat_value is None:
fiat_value = fx.historical_value(value, date)
fiat_default = True
else:
fiat_default = False
item['fiat_value'] = Fiat(fiat_value, fx.ccy)
item['fiat_default'] = fiat_default
if value is not None and value < 0:
ap, lp = self.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy)
cg = lp - ap
item['acquisition_price'] = Fiat(ap, fx.ccy)
item['capital_gain'] = Fiat(cg, fx.ccy)
capital_gains += cg
else:
if fiat_value is not None:
fiat_income += fiat_value
out.append(item)
# add summary
if out:
b, v = out[0]['balance'].value, out[0]['value'].value
start_balance = None if b is None or v is None else b - v
end_balance = out[-1]['balance'].value
if from_timestamp is not None and to_timestamp is not None:
start_date = timestamp_to_datetime(from_timestamp)
end_date = timestamp_to_datetime(to_timestamp)
else:
start_date = out[0]['date']
end_date = out[-1]['date']
summary = {
'start_date': start_date,
'end_date': end_date,
'start_balance': Satoshis(start_balance),
'end_balance': Satoshis(end_balance)
}
if fx:
unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy)
summary['capital_gains'] = Fiat(capital_gains, fx.ccy)
summary['fiat_income'] = Fiat(fiat_income, fx.ccy)
summary['unrealized_gains'] = Fiat(unrealized, fx.ccy)
if start_date:
summary['start_fiat_balance'] = Fiat(fx.historical_value(start_balance, start_date), fx.ccy)
if end_date:
summary['end_fiat_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy)
else:
summary = {}
return {
'transactions': out,
'summary': summary
}
def get_label(self, tx_hash): def get_label(self, tx_hash):
label = self.labels.get(tx_hash, '') label = self.labels.get(tx_hash, '')
if label is '': if label is '':
@ -989,6 +1146,7 @@ class Abstract_Wallet(PrintError):
if fixed_fee is None and config.fee_per_kb() is None: if fixed_fee is None and config.fee_per_kb() is None:
raise NoDynamicFeeEstimates() raise NoDynamicFeeEstimates()
if not is_sweep:
for item in inputs: for item in inputs:
self.add_input_info(item) self.add_input_info(item)
@ -1006,7 +1164,8 @@ class Abstract_Wallet(PrintError):
if not change_addrs: if not change_addrs:
change_addrs = [random.choice(addrs)] change_addrs = [random.choice(addrs)]
else: else:
change_addrs = [inputs[0]['address']] # coin_chooser will set change address
change_addrs = []
# Fee estimator # Fee estimator
if fixed_fee is None: if fixed_fee is None:
@ -1534,6 +1693,63 @@ class Abstract_Wallet(PrintError):
children |= self.get_depending_transactions(other_hash) children |= self.get_depending_transactions(other_hash)
return children return children
def txin_value(self, txin):
txid = txin['prevout_hash']
prev_n = txin['prevout_n']
for address, d in self.txo[txid].items():
for n, v, cb in d:
if n == prev_n:
return v
raise BaseException('unknown txin value')
def price_at_timestamp(self, txid, price_func):
height, conf, timestamp = self.get_tx_height(txid)
return price_func(timestamp if timestamp else time.time())
def unrealized_gains(self, domain, price_func, ccy):
coins = self.get_utxos(domain)
now = time.time()
p = price_func(now)
ap = sum(self.coin_price(coin['prevout_hash'], price_func, ccy, self.txin_value(coin)) for coin in coins)
lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN)
return lp - ap
def capital_gain(self, txid, price_func, ccy):
"""
Difference between the fiat price of coins leaving the wallet because of transaction txid,
and the price of these coins when they entered the wallet.
price_func: function that returns the fiat price given a timestamp
"""
out_value = - self.get_tx_value(txid)/Decimal(COIN)
fiat_value = self.get_fiat_value(txid, ccy)
liquidation_price = - fiat_value if fiat_value else out_value * self.price_at_timestamp(txid, price_func)
acquisition_price = out_value * self.average_price(txid, price_func, ccy)
return acquisition_price, liquidation_price
def average_price(self, txid, price_func, ccy):
""" Average acquisition price of the inputs of a transaction """
input_value = 0
total_price = 0
for addr, d in self.txi.get(txid, {}).items():
for ser, v in d:
input_value += v
total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v)
return total_price / (input_value/Decimal(COIN))
def coin_price(self, txid, price_func, ccy, txin_value):
"""
Acquisition price of a coin.
This assumes that either all inputs are mine, or no input is mine.
"""
if self.txi.get(txid, {}) != {}:
return self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
else:
fiat_value = self.get_fiat_value(txid, ccy)
if fiat_value is not None:
return fiat_value
else:
p = self.price_at_timestamp(txid, price_func)
return p * txin_value/Decimal(COIN)
class Simple_Wallet(Abstract_Wallet): class Simple_Wallet(Abstract_Wallet):
# wallet with a single keystore # wallet with a single keystore
@ -1705,12 +1921,10 @@ class Imported_Wallet(Simple_Wallet):
self.add_address(addr) self.add_address(addr)
return addr return addr
def export_private_key(self, address, password): def get_redeem_script(self, address):
d = self.addresses[address] d = self.addresses[address]
pubkey = d['pubkey']
redeem_script = d['redeem_script'] redeem_script = d['redeem_script']
sec = pw_decode(self.keystore.keypairs[pubkey], password) return redeem_script
return sec, redeem_script
def get_txin_type(self, address): def get_txin_type(self, address):
return self.addresses[address].get('type', 'address') return self.addresses[address].get('type', 'address')
@ -1748,9 +1962,6 @@ class Deterministic_Wallet(Abstract_Wallet):
def has_seed(self): def has_seed(self):
return self.keystore.has_seed() return self.keystore.has_seed()
def is_deterministic(self):
return self.keystore.is_deterministic()
def get_receiving_addresses(self): def get_receiving_addresses(self):
return self.receiving_addresses return self.receiving_addresses
@ -1812,6 +2023,7 @@ class Deterministic_Wallet(Abstract_Wallet):
def create_new_address(self, for_change=False): def create_new_address(self, for_change=False):
assert type(for_change) is bool assert type(for_change) is bool
with self.lock:
addr_list = self.change_addresses if for_change else self.receiving_addresses addr_list = self.change_addresses if for_change else self.receiving_addresses
n = len(addr_list) n = len(addr_list)
x = self.derive_pubkeys(for_change, n) x = self.derive_pubkeys(for_change, n)
@ -1836,16 +2048,8 @@ class Deterministic_Wallet(Abstract_Wallet):
def synchronize(self): def synchronize(self):
with self.lock: with self.lock:
if self.is_deterministic():
self.synchronize_sequence(False) self.synchronize_sequence(False)
self.synchronize_sequence(True) self.synchronize_sequence(True)
else:
if len(self.receiving_addresses) != len(self.keystore.keypairs):
pubkeys = self.keystore.keypairs.keys()
self.receiving_addresses = [self.pubkeys_to_address(i) for i in pubkeys]
self.save_addresses()
for addr in self.receiving_addresses:
self.add_address(addr)
def is_beyond_limit(self, address): def is_beyond_limit(self, address):
is_change, i = self.get_address_index(address) is_change, i = self.get_address_index(address)
@ -1898,9 +2102,6 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
def get_pubkey(self, c, i): def get_pubkey(self, c, i):
return self.derive_pubkeys(c, i) return self.derive_pubkeys(c, i)
def get_public_keys(self, address):
return [self.get_public_key(address)]
def add_input_sig_info(self, txin, address): def add_input_sig_info(self, txin, address):
derivation = self.get_address_index(address) derivation = self.get_address_index(address)
x_pubkey = self.keystore.get_xpubkey(*derivation) x_pubkey = self.keystore.get_xpubkey(*derivation)
@ -1938,6 +2139,10 @@ class Multisig_Wallet(Deterministic_Wallet):
def get_pubkeys(self, c, i): def get_pubkeys(self, c, i):
return self.derive_pubkeys(c, i) return self.derive_pubkeys(c, i)
def get_public_keys(self, address):
sequence = self.get_address_index(address)
return self.get_pubkeys(*sequence)
def pubkeys_to_address(self, pubkeys): def pubkeys_to_address(self, pubkeys):
redeem_script = self.pubkeys_to_redeem_script(pubkeys) redeem_script = self.pubkeys_to_redeem_script(pubkeys)
return bitcoin.redeem_script_to_address(self.txin_type, redeem_script) return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
@ -1945,6 +2150,11 @@ class Multisig_Wallet(Deterministic_Wallet):
def pubkeys_to_redeem_script(self, pubkeys): def pubkeys_to_redeem_script(self, pubkeys):
return transaction.multisig_script(sorted(pubkeys), self.m) return transaction.multisig_script(sorted(pubkeys), self.m)
def get_redeem_script(self, address):
pubkeys = self.get_public_keys(address)
redeem_script = self.pubkeys_to_redeem_script(pubkeys)
return redeem_script
def derive_pubkeys(self, c, i): def derive_pubkeys(self, c, i):
return [k.derive_pubkey(c, i) for k in self.get_keystores()] return [k.derive_pubkey(c, i) for k in self.get_keystores()]

3
plugins/digitalbitbox/cmdline.py

@ -9,3 +9,6 @@ class Plugin(DigitalBitboxPlugin):
if not isinstance(keystore, self.keystore_class): if not isinstance(keystore, self.keystore_class):
return return
keystore.handler = self.handler keystore.handler = self.handler
def create_handler(self, window):
return self.handler

1
plugins/digitalbitbox/digitalbitbox.py

@ -661,6 +661,7 @@ class DigitalBitboxPlugin(HW_PluginBase):
def create_client(self, device, handler): def create_client(self, device, handler):
if device.interface_number == 0 or device.usage_page == 0xffff: if device.interface_number == 0 or device.usage_page == 0xffff:
if handler:
self.handler = handler self.handler = handler
client = self.get_dbb_device(device) client = self.get_dbb_device(device)
if client is not None: if client is not None:

12
plugins/hw_wallet/qt.py

@ -1,4 +1,4 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- mode: python -*- # -*- mode: python -*-
# #
# Electrum - lightweight Bitcoin client # Electrum - lightweight Bitcoin client
@ -184,10 +184,12 @@ class QtPluginBase(object):
if not isinstance(keystore, self.keystore_class): if not isinstance(keystore, self.keystore_class):
continue continue
if not self.libraries_available: if not self.libraries_available:
window.show_error( if hasattr(self, 'libraries_available_message'):
_("Cannot find python library for") + " '%s'.\n" % self.name \ message = self.libraries_available_message + '\n'
+ _("Make sure you install it with python3") else:
) message = _("Cannot find python library for") + " '%s'.\n" % self.name
message += _("Make sure you install it with python3")
window.show_error(message)
return return
tooltip = self.device + '\n' + (keystore.label or 'unnamed') tooltip = self.device + '\n' + (keystore.label or 'unnamed')
cb = partial(self.show_settings_dialog, window, keystore) cb = partial(self.show_settings_dialog, window, keystore)

3
plugins/keepkey/cmdline.py

@ -9,3 +9,6 @@ class Plugin(KeepKeyPlugin):
if not isinstance(keystore, self.keystore_class): if not isinstance(keystore, self.keystore_class):
return return
keystore.handler = self.handler keystore.handler = self.handler
def create_handler(self, window):
return self.handler

3
plugins/ledger/cmdline.py

@ -9,3 +9,6 @@ class Plugin(LedgerPlugin):
if not isinstance(keystore, self.keystore_class): if not isinstance(keystore, self.keystore_class):
return return
keystore.handler = self.handler keystore.handler = self.handler
def create_handler(self, window):
return self.handler

12
plugins/ledger/ledger.py

@ -10,7 +10,7 @@ from electrum.plugins import BasePlugin
from electrum.keystore import Hardware_KeyStore from electrum.keystore import Hardware_KeyStore
from electrum.transaction import Transaction from electrum.transaction import Transaction
from ..hw_wallet import HW_PluginBase from ..hw_wallet import HW_PluginBase
from electrum.util import print_error, is_verbose, bfh, bh2u from electrum.util import print_error, is_verbose, bfh, bh2u, versiontuple
try: try:
import hid import hid
@ -57,9 +57,6 @@ class Ledger_Client():
def i4b(self, x): def i4b(self, x):
return pack('>I', x) return pack('>I', x)
def versiontuple(self, v):
return tuple(map(int, (v.split("."))))
def test_pin_unlocked(func): def test_pin_unlocked(func):
"""Function decorator to test the Ledger for being unlocked, and if not, """Function decorator to test the Ledger for being unlocked, and if not,
raise a human-readable exception. raise a human-readable exception.
@ -140,9 +137,9 @@ class Ledger_Client():
try: try:
firmwareInfo = self.dongleObject.getFirmwareVersion() firmwareInfo = self.dongleObject.getFirmwareVersion()
firmware = firmwareInfo['version'] firmware = firmwareInfo['version']
self.multiOutputSupported = self.versiontuple(firmware) >= self.versiontuple(MULTI_OUTPUT_SUPPORT) self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT)
self.nativeSegwitSupported = self.versiontuple(firmware) >= self.versiontuple(SEGWIT_SUPPORT) self.nativeSegwitSupported = versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT)
self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and self.versiontuple(firmware) >= self.versiontuple(SEGWIT_SUPPORT_SPECIAL)) self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL))
if not checkFirmware(firmwareInfo): if not checkFirmware(firmwareInfo):
self.dongleObject.dongle.close() self.dongleObject.dongle.close()
@ -519,6 +516,7 @@ class LedgerPlugin(HW_PluginBase):
return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG)
def create_client(self, device, handler): def create_client(self, device, handler):
if handler:
self.handler = handler self.handler = handler
client = self.get_btchip_device(device) client = self.get_btchip_device(device)

3
plugins/trezor/cmdline.py

@ -9,3 +9,6 @@ class Plugin(TrezorPlugin):
if not isinstance(keystore, self.keystore_class): if not isinstance(keystore, self.keystore_class):
return return
keystore.handler = self.handler keystore.handler = self.handler
def create_handler(self, window):
return self.handler

16
plugins/trezor/trezor.py

@ -2,7 +2,7 @@ import threading
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from electrum.util import bfh, bh2u from electrum.util import bfh, bh2u, versiontuple
from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey,
TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants) TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants)
from electrum.i18n import _ from electrum.i18n import _
@ -86,6 +86,7 @@ class TrezorPlugin(HW_PluginBase):
libraries_URL = 'https://github.com/trezor/python-trezor' libraries_URL = 'https://github.com/trezor/python-trezor'
minimum_firmware = (1, 5, 2) minimum_firmware = (1, 5, 2)
keystore_class = TrezorKeyStore keystore_class = TrezorKeyStore
minimum_library = (0, 9, 0)
MAX_LABEL_LEN = 32 MAX_LABEL_LEN = 32
@ -96,6 +97,19 @@ class TrezorPlugin(HW_PluginBase):
try: try:
# Minimal test if python-trezor is installed # Minimal test if python-trezor is installed
import trezorlib import trezorlib
try:
library_version = trezorlib.__version__
except AttributeError:
# python-trezor only introduced __version__ in 0.9.0
library_version = 'unknown'
if library_version == 'unknown' or \
versiontuple(library_version) < self.minimum_library:
self.libraries_available_message = (
_("Library version for '{}' is too old.").format(name)
+ '\nInstalled: {}, Needed: {}'
.format(library_version, self.minimum_library))
self.print_stderr(self.libraries_available_message)
raise ImportError()
self.libraries_available = True self.libraries_available = True
except ImportError: except ImportError:
self.libraries_available = False self.libraries_available = False

166
pubkeys/bauerj.asc

@ -0,0 +1,166 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
402TZWn+BlhGAFxa+Wzl46MVavI=
=bDjo
-----END PGP PUBLIC KEY BLOCK-----

19
setup.py

@ -9,7 +9,10 @@ import platform
import imp import imp
import argparse import argparse
with open('requirements-hw.txt') as f: with open('contrib/requirements/requirements.txt') as f:
requirements = f.read().splitlines()
with open('contrib/requirements/requirements-hw.txt') as f:
requirements_hw = f.read().splitlines() requirements_hw = f.read().splitlines()
version = imp.load_source('version', 'lib/version.py') version = imp.load_source('version', 'lib/version.py')
@ -17,7 +20,7 @@ version = imp.load_source('version', 'lib/version.py')
if sys.version_info[:3] < (3, 4, 0): if sys.version_info[:3] < (3, 4, 0):
sys.exit("Error: Electrum requires Python version >= 3.4.0...") sys.exit("Error: Electrum requires Python version >= 3.4.0...")
data_files = ['requirements-hw.txt'] data_files = ['contrib/requirements/' + r for r in ['requirements.txt', 'requirements-hw.txt']]
if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']: if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -38,17 +41,7 @@ if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']:
setup( setup(
name="Electrum", name="Electrum",
version=version.ELECTRUM_VERSION, version=version.ELECTRUM_VERSION,
install_requires=[ install_requires=requirements,
'pyaes>=0.1a1',
'ecdsa>=0.9',
'pbkdf2',
'requests',
'qrcode',
'protobuf',
'dnspython',
'jsonrpclib-pelix',
'PySocks>=1.6.6',
],
extras_require={ extras_require={
'hardware': requirements_hw, 'hardware': requirements_hw,
}, },

Loading…
Cancel
Save