diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..ef05ebc2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,2 @@ + diff --git a/.travis.yml b/.travis.yml index 48355183b..9b129e33d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - 3.5 - 3.6 install: - - pip install -r requirements_travis.txt + - pip install -r contrib/requirements/requirements-travis.txt cache: - pip script: @@ -12,3 +12,17 @@ script: after_success: - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi - 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 diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 6413c9e22..0ed9b70f2 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,34 +1,55 @@ + # Release 3.1 - (to be released) - * Mempory pool based fee estimates. If this option is activated, - users can set transaction fees that target a desired depth in the - memory pool. This feature might be controversial, because miners - could conspire and fill the memory pool with expensive transactions - that never get mined. However, our current time-based fee estimates - results in sticky fees, which cause inexperienced users to overpay, - while more advanced users visit (and trust) websites that display - memorypool data, and set their fee accordingly. - * Local transactions: Transactions that have not been broadcasted can - be saved in the wallet file, and their outputs can be used in - subsequent transactions. Transactions that disapear from the memory - pool stay in the wallet, and can be rebroadcasted. This feature can - be combined with cold storage, to create several transactions - before broadcasting. - * The initial headers download was replaced with hardcoded - checkpoints, one per retargeting period. Past headers are - downloaded when needed. - * The two coin selection policies have been merged, and the policy - choice was removed from preferences. Previously, the 'privacy' - policy has been unusable because it was was not prioritizing - confirmed coins. + * Memory-pool based transaction fees. Users can set dynamic fees that + target a desired depth in the memory pool. This feature is + optional, and ETA-based estimates (from Bitcoin Core) remain the + default. Note that miners could exploit this feature, if they + conspired and filled the memory pool with expensive transactions + that never get mined. However, since the Electrum client already + trusts an Electrum server with fee estimates, activating this + feature does not introduce any new vulnerability; the client uses a + hard threshold to detect unusually high fees. In practice, + ETA-based estimates have resulted in sticky fees, and caused many + users to overpay for transactions. Advanced users tend to visit + (and trust) websites that display memory-pool data in order to set + their fees. + * Local transactions: Transactions can be saved in the wallet without + being broadcast. The inputs of local transactions are considered as + spent, and their change outputs can be re-used in subsequent + transactions. This can be combined with cold storage, in order to + create several transactions before broadcasting them. Outgoing + transactions that have been removed from the memory pool are also + saved in the wallet, and can be broadcast again. + * Checkpoints: The initial download of a headers file was replaced + with hardcoded checkpoints. The wallet uses one checkpoint per + 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 computed from transaction size. - * RBF is enabled by default. This might cause some issues with - merchants that use wallets that do not display RBF transactions - until they are confirmed. + * The wallet history can be filtered by time interval. + * Replace-by-fee is enabled by default. Note that this might cause + some issues with wallets that do not display RBF transactions until + they are confirmed. * Watching-only wallets and hardware wallets can be encrypted. * Semi-automated crash reporting * The SSL checkbox option was removed from the GUI. + * 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 : diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx index d8af8c9d6..e5a656049 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/build-osx/make_osx @@ -16,11 +16,16 @@ cd $build_dir/../.. export PYTHONHASHSEED=22 VERSION=`git describe --tags` + +# Paramterize PYTHON_VERSION=3.6.4 +BUILDDIR=/tmp/electrum-build +PACKAGE=Electrum +GIT_REPO=https://github.com/spesmilo/electrum 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 pyenv update else @@ -31,12 +36,10 @@ pyenv global $PYTHON_VERSION || \ fail "Unable to use Python $PYTHON_VERSION" -if ! which pyinstaller > /dev/null; then - info "Installing pyinstaller" - python3 -m pip install pyinstaller -I --user || fail "Could not install pyinstaller" -fi +info "Installing pyinstaller" +python3 -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952 -I --user || fail "Could not install pyinstaller" -info "Using these versions for building Electrum:" +info "Using these versions for building $PACKAGE:" sw_vers python3 --version echo -n "Pyinstaller " @@ -45,31 +48,37 @@ pyinstaller --version rm -rf ./dist -rm -rf /tmp/electrum-build > /dev/null 2>&1 -mkdir /tmp/electrum-build +rm -rf $BUILDDIR > /dev/null 2>&1 +mkdir $BUILDDIR info "Downloading icons and locale..." 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 -cp -R /tmp/electrum-build/electrum-locale/locale/ ./lib/locale/ -cp /tmp/electrum-build/electrum-icons/icons_rc.py ./gui/qt/ +cp -R $BUILDDIR/electrum-locale/locale/ ./lib/locale/ +cp $BUILDDIR/electrum-icons/icons_rc.py ./gui/qt/ + + +info "Downloading libusb..." +curl https://homebrew.bintray.com/bottles/libusb-1.0.21.el_capitan.bottle.tar.gz | \ +tar xz --directory $BUILDDIR +cp $BUILDDIR/libusb/1.0.21/lib/libusb-1.0.dylib contrib/build-osx info "Installing requirements..." python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \ -python3 -m pip install pyqt5 --user || \ +python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user || \ fail "Could not install requirements" info "Installing hardware wallet requirements..." python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --user || \ fail "Could not install hardware wallet requirements" -info "Building Electrum..." -python3 setup.py install --user > /dev/null || fail "Could not build Electrum" +info "Building $PACKAGE..." +python3 setup.py install --user > /dev/null || fail "Could not build $PACKAGE" info "Building binary" pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary" info "Creating .DMG" -hdiutil create -fs HFS+ -volname "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" diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec index cfce7172f..caef2519f 100644 --- a/contrib/build-osx/osx.spec +++ b/contrib/build-osx/osx.spec @@ -1,10 +1,15 @@ # -*- 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 os +PACKAGE='Electrum' +PYPKG='electrum' +MAIN_SCRIPT='electrum' +ICONS_FILE='electrum.icns' + for i, x in enumerate(sys.argv): if x == '--name': VERSION = sys.argv[i+1] @@ -22,21 +27,27 @@ hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') datas = [ - (electrum+'lib/currencies.json', 'electrum'), - (electrum+'lib/servers.json', 'electrum'), - (electrum+'lib/checkpoints.json', 'electrum'), - (electrum+'lib/servers_testnet.json', 'electrum'), - (electrum+'lib/checkpoints_testnet.json', 'electrum'), - (electrum+'lib/wordlist/english.txt', 'electrum/wordlist'), - (electrum+'lib/locale', 'electrum/locale'), - (electrum+'plugins', 'electrum_plugins'), + (electrum+'lib/currencies.json', PYPKG), + (electrum+'lib/servers.json', PYPKG), + (electrum+'lib/checkpoints.json', PYPKG), + (electrum+'lib/servers_testnet.json', PYPKG), + (electrum+'lib/checkpoints_testnet.json', PYPKG), + (electrum+'lib/wordlist/english.txt', PYPKG + '/wordlist'), + (electrum+'lib/locale', PYPKG + '/locale'), + (electrum+'plugins', PYPKG + '_plugins'), ] datas += collect_data_files('trezorlib') datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') +# Add libusb so Trezor will work +binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")] + +# Workaround for "Retro Look": +binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]] + # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports -a = Analysis([electrum+'electrum', +a = Analysis([electrum+MAIN_SCRIPT, electrum+'gui/qt/main_window.py', electrum+'gui/text.py', electrum+'lib/util.py', @@ -52,13 +63,14 @@ a = Analysis([electrum+'electrum', electrum+'plugins/keepkey/qt.py', electrum+'plugins/ledger/qt.py', ], + binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[]) # http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal for d in a.datas: - if 'pyconfig' in d[0]: + if 'pyconfig' in d[0]: a.datas.remove(d) break @@ -68,19 +80,19 @@ exe = EXE(pyz, a.scripts, a.binaries, a.datas, - name='Electrum', + name=PACKAGE, debug=False, strip=False, upx=True, - icon=electrum+'electrum.icns', + icon=electrum+ICONS_FILE, console=False) app = BUNDLE(exe, version = VERSION, - name='Electrum.app', - icon=electrum+'electrum.icns', + name=PACKAGE + '.app', + icon=electrum+ICONS_FILE, bundle_identifier=None, info_plist = { 'NSHighResolutionCapable':'True' } -) \ No newline at end of file +) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index a8f743588..f0c346a4e 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/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 $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 pushd $WINEPREFIX/drive_c/electrum diff --git a/contrib/build-wine/build.sh b/contrib/build-wine/build.sh index a4e39adf1..8bf650626 100755 --- a/contrib/build-wine/build.sh +++ b/contrib/build-wine/build.sh @@ -13,8 +13,7 @@ echo "Clearing $here/build and $here/dist..." rm "$here"/build/* -rf rm "$here"/dist/* -rf -$here/prepare-wine.sh && \ -$here/prepare-pyinstaller.sh || exit 1 +$here/prepare-wine.sh || exit 1 echo "Resetting modification time in C:\Python..." # (Because of some bugs in pyinstaller) diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index 5a6be0c54..f6888a03d 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -1,6 +1,6 @@ # -*- 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 for i, x in enumerate(sys.argv): @@ -19,6 +19,12 @@ hiddenimports += collect_submodules('trezorlib') hiddenimports += collect_submodules('btchip') 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 = [ (home+'lib/currencies.json', 'electrum'), (home+'lib/servers.json', 'electrum'), @@ -52,6 +58,7 @@ a = Analysis([home+'electrum', home+'plugins/ledger/qt.py', #home+'packages/requests/utils.py' ], + binaries=binaries, datas=datas, #pathex=[home+'lib', home+'gui', home+'plugins'], hiddenimports=hiddenimports, diff --git a/contrib/build-wine/prepare-pyinstaller.sh b/contrib/build-wine/prepare-pyinstaller.sh deleted file mode 100755 index cf8a326cd..000000000 --- a/contrib/build-wine/prepare-pyinstaller.sh +++ /dev/null @@ -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 diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index fc6080ea4..158e72d30 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -3,8 +3,13 @@ # 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_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e + ZBAR_URL=https://sourceforge.net/projects/zbarw/files/zbarw-20121031-setup.exe/download 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 ## These settings probably don't need change @@ -75,28 +80,21 @@ done $PYTHON -m pip install pip --upgrade # Install pywin32-ctypes (needed by pyinstaller) -$PYTHON -m pip install pywin32-ctypes +$PYTHON -m pip install pywin32-ctypes==0.1.2 -# Install PyQt -$PYTHON -m pip install PyQt5 +# install PySocks +$PYTHON -m pip install win_inet_pton==1.0.1 -## Install pyinstaller -#$PYTHON -m pip install pyinstaller==3.3 +$PYTHON -m pip install -r ../../deterministic-build/requirements-binaries.txt +# Install PyInstaller +$PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip # Install ZBar wget -q -O zbar.exe "$ZBAR_URL" verify_hash zbar.exe $ZBAR_SHA256 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) $PYTHON -m pip install setuptools --upgrade @@ -106,6 +104,11 @@ wget -q -O nsis.exe "$NSIS_URL" verify_hash nsis.exe $NSIS_SHA256 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 #wget -O upx.zip "https://downloads.sourceforge.net/project/upx/upx/3.08/upx308w.zip" #unzip -o upx.zip @@ -114,5 +117,4 @@ wine nsis.exe /S # add dlls needed for pyinstaller: 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" diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt new file mode 100644 index 000000000..381b4378f --- /dev/null +++ b/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 diff --git a/contrib/freeze_packages.sh b/contrib/freeze_packages.sh index 2d6b03755..3471e528b 100755 --- a/contrib/freeze_packages.sh +++ b/contrib/freeze_packages.sh @@ -6,34 +6,17 @@ contrib=$(dirname "$0") 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 -rm "$venv_dir" -rf -virtualenv -p $(which python3) $venv_dir + source $venv_dir/bin/activate -source $venv_dir/bin/activate + echo "Installing $i dependencies" -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 -virtualenv -p $(which python3) $venv_dir - -source $venv_dir/bin/activate - -echo "Installing hw wallet 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" diff --git a/contrib/requirements/requirements-binaries.txt b/contrib/requirements/requirements-binaries.txt new file mode 100644 index 000000000..68181dd31 --- /dev/null +++ b/contrib/requirements/requirements-binaries.txt @@ -0,0 +1,3 @@ +PyQt5 +pycryptodomex +websocket-client \ No newline at end of file diff --git a/requirements-hw.txt b/contrib/requirements/requirements-hw.txt similarity index 100% rename from requirements-hw.txt rename to contrib/requirements/requirements-hw.txt diff --git a/requirements_travis.txt b/contrib/requirements/requirements-travis.txt similarity index 100% rename from requirements_travis.txt rename to contrib/requirements/requirements-travis.txt diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt new file mode 100644 index 000000000..227ec1cd9 --- /dev/null +++ b/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 diff --git a/electrum b/electrum index 25495f79e..0e109e8ef 100755 --- a/electrum +++ b/electrum @@ -91,7 +91,7 @@ if is_local or is_android: from electrum import bitcoin, util from electrum import SimpleConfig, Network 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 set_verbosity, InvalidPassword from electrum.commands import get_parser, known_commands, Commands, config_variables @@ -194,8 +194,9 @@ def init_daemon(config_options): sys.exit(0) if storage.is_encrypted(): if storage.is_encrypted_with_hw_device(): - raise NotImplementedError("CLI functionality of encrypted hw wallets") - if config.get('password'): + plugins = init_plugins(config, 'cmdline') + password = get_password_for_hw_device_encrypted_storage(plugins) + elif config.get('password'): password = config.get('password') else: password = prompt_password('Password:', False) @@ -222,7 +223,7 @@ def init_cmdline(config_options, server): if cmdname in ['payto', 'paytomany'] and config.get('broadcast'): cmd.requires_network = True - # instanciate wallet for command-line + # instantiate wallet for command-line storage = WalletStorage(config.get_wallet_path()) 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)\ or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())): if storage.is_encrypted_with_hw_device(): - raise NotImplementedError("CLI functionality of encrypted hw wallets") - if config.get('password'): + # this case is handled later in the control flow + password = None + elif config.get('password'): password = config.get('password') else: password = prompt_password('Password:', False) @@ -260,7 +262,42 @@ def init_cmdline(config_options, server): 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') cmd = known_commands[cmdname] password = config_options.get('password') @@ -268,7 +305,8 @@ def run_offline_command(config, config_options): storage = WalletStorage(config.get_wallet_path()) if storage.is_encrypted(): 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) wallet = Wallet(storage) else: @@ -437,8 +475,8 @@ if __name__ == '__main__': print_msg("Daemon not running; try 'electrum daemon start'") sys.exit(1) else: - init_plugins(config, 'cmdline') - result = run_offline_command(config, config_options) + plugins = init_plugins(config, 'cmdline') + result = run_offline_command(config, config_options, plugins) # print result if isinstance(result, str): print_msg(result) diff --git a/electrum-env b/electrum-env index 42220edab..c05b2d1ab 100755 --- a/electrum-env +++ b/electrum-env @@ -9,6 +9,8 @@ # python-qt and its dependencies will still need to be installed with # your package manager. +PYTHON_VER="$(python3 -c 'import sys; print(sys.version[:3])')" + if [ -e ./env/bin/activate ]; then source ./env/bin/activate else @@ -17,7 +19,7 @@ else python3 setup.py install fi -export PYTHONPATH="/usr/local/lib/python3.5/site-packages:$PYTHONPATH" +export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH" ./electrum "$@" diff --git a/gui/kivy/Readme.md b/gui/kivy/Readme.md index 2c8a55f2f..faf8e5672 100644 --- a/gui/kivy/Readme.md +++ b/gui/kivy/Readme.md @@ -22,7 +22,7 @@ git merge agilewalker/master ``` ## 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 cd /opt @@ -31,6 +31,9 @@ cd buildozer sudo python3 setup.py install ``` +2.2 Download the [Crystax NDK](https://www.crystax.net/en/download) manually. +Extract into `/opt/crystax-ndk-10.3.2` + ## 3. Update the Android SDK build tools 3.1 Start the Android SDK manager: @@ -40,7 +43,7 @@ sudo python3 setup.py install 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 Install "Android Support Library Repository" from the SDK manager. diff --git a/gui/kivy/i18n.py b/gui/kivy/i18n.py index e0be39082..733249d3e 100644 --- a/gui/kivy/i18n.py +++ b/gui/kivy/i18n.py @@ -1,21 +1,22 @@ import gettext + class _(str): observers = set() lang = None - def __new__(cls, s, *args, **kwargs): + def __new__(cls, s): if _.lang is None: _.switch_lang('en') - t = _.translate(s, *args, **kwargs) + t = _.translate(s) o = super(_, cls).__new__(cls, t) o.source_text = s return o @staticmethod def translate(s, *args, **kwargs): - return _.lang(s).format(args, kwargs) + return _.lang(s) @staticmethod def bind(label): diff --git a/gui/kivy/uix/dialogs/bump_fee_dialog.py b/gui/kivy/uix/dialogs/bump_fee_dialog.py index a5c74cee7..e27c9e543 100644 --- a/gui/kivy/uix/dialogs/bump_fee_dialog.py +++ b/gui/kivy/uix/dialogs/bump_fee_dialog.py @@ -3,7 +3,6 @@ from kivy.factory import Factory from kivy.properties import ObjectProperty from kivy.lang import Builder -from electrum.util import fee_levels from electrum_gui.kivy.i18n import _ Builder.load_string(''' @@ -29,7 +28,11 @@ Builder.load_string(''' text: _('New Fee') value: '' Label: - id: tooltip + id: tooltip1 + text: '' + size_hint_y: None + Label: + id: tooltip2 text: '' size_hint_y: None Slider: @@ -72,39 +75,39 @@ class BumpFeeDialog(Factory.Popup): self.tx_size = size self.callback = callback self.config = app.electrum_config - self.fee_step = self.config.max_fee_rate() / 10 - self.dynfees = self.config.get('dynamic_fees', True) and self.app.network + self.mempool = self.config.use_mempool_fees() + 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.update_slider() self.update_text() def update_text(self): - value = int(self.ids.slider.value) - self.ids.new_fee.value = self.app.format_amount_and_units(self.get_fee()) - if self.dynfees: - value = int(self.ids.slider.value) - self.ids.tooltip.text = fee_levels[value] + fee = self.get_fee() + self.ids.new_fee.value = self.app.format_amount_and_units(fee) + pos = int(self.ids.slider.value) + fee_rate = self.get_fee_rate() + text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, fee_rate) + self.ids.tooltip1.text = text + self.ids.tooltip2.text = tooltip def update_slider(self): slider = self.ids.slider + maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool) + slider.range = (0, maxp) + slider.step = 1 + slider.value = pos + + def get_fee_rate(self): + pos = int(self.ids.slider.value) if self.dynfees: - slider.range = (0, 4) - slider.step = 1 - slider.value = 3 + fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(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) + fee_rate = self.config.static_fee(pos) + return fee_rate def get_fee(self): - value = int(self.ids.slider.value) - if self.dynfees: - if self.config.has_fee_estimates(): - dynfee = self.config.dynfee(value) - return int(dynfee * self.tx_size // 1000) - else: - return int(value*self.fee_step * self.tx_size // 1000) + fee_rate = self.get_fee_rate() + return int(fee_rate * self.tx_size // 1000) def on_ok(self): new_fee = self.get_fee() diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py index 25e9926c4..cf29f36b8 100644 --- a/gui/kivy/uix/dialogs/fee_dialog.py +++ b/gui/kivy/uix/dialogs/fee_dialog.py @@ -3,7 +3,6 @@ from kivy.factory import Factory from kivy.properties import ObjectProperty from kivy.lang import Builder -from electrum.util import fee_levels from electrum_gui.kivy.i18n import _ Builder.load_string(''' @@ -78,8 +77,8 @@ class FeeDialog(Factory.Popup): self.config = config self.fee_rate = self.config.fee_per_kb() self.callback = callback - self.mempool = self.config.get('mempool_fees', False) - self.dynfees = self.config.get('dynamic_fees', True) + self.mempool = self.config.use_mempool_fees() + self.dynfees = self.config.is_dynfee() self.ids.mempool.active = self.mempool self.ids.dynfees.active = self.dynfees self.update_slider() diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py index e73f33650..dad215e87 100644 --- a/gui/kivy/uix/dialogs/settings.py +++ b/gui/kivy/uix/dialogs/settings.py @@ -8,7 +8,6 @@ from electrum.i18n import languages from electrum_gui.kivy.i18n import _ from electrum.plugins import run_hook from electrum import coinchooser -from electrum.util import fee_levels from .choice_dialog import ChoiceDialog diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index ed1471d52..0133f789e 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -522,7 +522,12 @@ class AddressScreen(CScreen): def update(self): self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)] 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 container = self.screen.ids.search_container container.clear_widgets() diff --git a/gui/kivy/uix/ui_screens/address.kv b/gui/kivy/uix/ui_screens/address.kv index 3d594c9c3..d0247a342 100644 --- a/gui/kivy/uix/ui_screens/address.kv +++ b/gui/kivy/uix/ui_screens/address.kv @@ -50,7 +50,7 @@ AddressScreen: name: 'address' message: '' pr_status: 'Pending' - show_change: False + show_change: 0 show_used: 0 on_message: self.parent.update() @@ -70,9 +70,9 @@ AddressScreen: spacing: '5dp' AddressButton: id: search - text: _('Change') if root.show_change else _('Receiving') + text: {0:_('Receiving'), 1:_('Change'), 2:_('All')}[root.show_change] 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()) AddressFilter: opacity: 1 diff --git a/gui/qt/__init__.py b/gui/qt/__init__.py index 16d07948f..0879208f9 100644 --- a/gui/qt/__init__.py +++ b/gui/qt/__init__.py @@ -25,6 +25,7 @@ import signal import sys +import traceback try: @@ -94,6 +95,8 @@ class ElectrumGui: QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) + if hasattr(QGuiApplication, 'setDesktopFileName'): + QGuiApplication.setDesktopFileName('electrum.desktop') self.config = config self.daemon = daemon self.plugins = plugins @@ -190,8 +193,10 @@ class ElectrumGui: else: try: wallet = self.daemon.load_wallet(path, None) - except BaseException as e: - d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e)) + except BaseException as e: + traceback.print_exc(file=sys.stdout) + d = QMessageBox(QMessageBox.Warning, _('Error'), + _('Cannot load wallet:') + '\n' + str(e)) d.exec_() return if not wallet: @@ -208,7 +213,14 @@ class ElectrumGui: return wallet.start_threads(self.daemon.network) self.daemon.add_wallet(wallet) - w = self.create_window_for_wallet(wallet) + try: + w = self.create_window_for_wallet(wallet) + except BaseException as e: + traceback.print_exc(file=sys.stdout) + d = QMessageBox(QMessageBox.Warning, _('Error'), + _('Cannot create window for wallet:') + '\n' + str(e)) + d.exec_() + return if uri: w.pay_to_URI(uri) w.bring_to_top() @@ -241,8 +253,7 @@ class ElectrumGui: return except GoBack: return - except: - import traceback + except BaseException as e: traceback.print_exc(file=sys.stdout) return self.timer.start() diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py index 7e8dda1ed..27c9efb59 100644 --- a/gui/qt/contact_list.py +++ b/gui/qt/contact_list.py @@ -32,7 +32,7 @@ from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import ( QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem) -from .util import MyTreeWidget +from .util import MyTreeWidget, import_meta_gui, export_meta_gui class ContactList(MyTreeWidget): @@ -53,12 +53,10 @@ class ContactList(MyTreeWidget): self.parent.set_contact(item.text(0), item.text(1)) def import_contacts(self): - wallet_folder = self.parent.get_wallet_folder() - filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder) - if not filename: - return - self.parent.contacts.import_file(filename) - self.on_update() + import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update) + + def export_contacts(self): + export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file) def create_menu(self, position): menu = QMenu() @@ -66,6 +64,7 @@ class ContactList(MyTreeWidget): if not selected: menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog()) menu.addAction(_("Import file"), lambda: self.import_contacts()) + menu.addAction(_("Export file"), lambda: self.export_contacts()) else: names = [item.text(0) for item in selected] keys = [item.text(1) for item in selected] diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py index a603af5ef..091da1864 100644 --- a/gui/qt/exception_window.py +++ b/gui/qt/exception_window.py @@ -34,7 +34,7 @@ from PyQt5.QtWidgets import * from electrum.i18n import _ import sys -from electrum import ELECTRUM_VERSION +from electrum import ELECTRUM_VERSION, bitcoin issue_template = """
@@ -105,12 +105,24 @@ class Exception_Window(QWidget): self.show() 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.update(self.get_additional_info()) report = json.dumps(report) - response = requests.post(report_server, data=report) - QMessageBox.about(self, "Crash report", response.text) - self.close() + try: + response = requests.post(report_server, data=report, timeout=20) + except BaseException as e: + traceback.print_exc(file=sys.stderr) + self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' + + str(e) + '\n' + + _("Please report this issue manually.")) + return + else: + QMessageBox.about(self, "Crash report", response.text) + self.close() def on_close(self): Exception_Window._active_window = None diff --git a/gui/qt/fee_slider.py b/gui/qt/fee_slider.py index 209b0de76..04911d878 100644 --- a/gui/qt/fee_slider.py +++ b/gui/qt/fee_slider.py @@ -21,7 +21,7 @@ class FeeSlider(QSlider): def moved(self, pos): with self.lock: 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: fee_rate = self.config.static_fee(pos) tooltip = self.get_tooltip(pos, fee_rate) @@ -30,7 +30,7 @@ class FeeSlider(QSlider): self.callback(self.dyn, 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) if self.dyn: return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate @@ -40,7 +40,7 @@ class FeeSlider(QSlider): def update(self): with self.lock: 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) self.setRange(0, maxp) self.setValue(pos) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 63a0b4b9b..b2029d370 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -24,13 +24,18 @@ # SOFTWARE. import webbrowser +import datetime -from electrum.wallet import UnrelatedTransactionException, TX_HEIGHT_LOCAL +from electrum.wallet import AddTransactionException, TX_HEIGHT_LOCAL from .util import * from electrum.i18n import _ from electrum.util import block_explorer_URL 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 TX_ICONS = [ @@ -56,41 +61,181 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): AcceptFileDragDrop.__init__(self, ".txn") self.refresh_headers() self.setColumnHidden(1, True) + self.start_timestamp = None + self.end_timestamp = None + self.years = [] def refresh_headers(self): - headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')] + headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')] fx = self.parent.fx 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) def get_domain(self): '''Replaced in address_dialog.py''' 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 def on_update(self): 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() current_tx = item.data(0, Qt.UserRole) if item else None self.clear() - fx = self.parent.fx if fx: fx.history_used_spot = False - for h_item in h: - tx_hash, height, conf, timestamp, value, balance = h_item + for tx_item in self.transactions: + 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) has_invoice = self.wallet.invoices.paid.get(tx_hash) icon = QIcon(":icons/" + TX_ICONS[status]) v_str = self.parent.format_amount(value, True, 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] - 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) - for amount in [value, balance]: - text = fx.historical_value_str(amount, date) - entry.append(text) + fiat_value = tx_item['fiat_value'].value + value_str = fx.format_fiat(fiat_value) + 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.setIcon(0, icon) item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) @@ -104,12 +249,27 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): if value and value < 0: item.setForeground(3, 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: item.setData(0, Qt.UserRole, tx_hash) self.insertTopLevelItem(0, item) if current_tx == tx_hash: 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): if self.permit_edit(item, column): super(HistoryList, self).on_doubleclick(item, column) @@ -151,25 +311,19 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): else: column_title = self.headerItem().text(column) column_data = item.text(column) - tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) height, conf, timestamp = self.wallet.get_tx_height(tx_hash) tx = self.wallet.transactions.get(tx_hash) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_unconfirmed = height <= 0 pr_key = self.wallet.invoices.paid.get(tx_hash) - menu = QMenu() - if height == TX_HEIGHT_LOCAL: menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) - menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) - if column in self.editable_columns: - menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column)) - + for c in self.editable_columns: + menu.addAction(_("Edit {}").format(self.headerItem().text(c)), lambda: self.editItem(item, c)) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) - if is_unconfirmed and tx: rbf = is_mine and not tx.is_final() if rbf: @@ -187,13 +341,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): def remove_local_tx(self, delete_tx): to_delete = {delete_tx} to_delete |= self.wallet.get_depending_transactions(delete_tx) - question = _("Are you sure you want to remove this transaction?") if len(to_delete) > 1: question = _( "Are you sure you want to remove this transaction and {} child transactions?".format(len(to_delete) - 1) ) - answer = QMessageBox.question(self.parent, _("Please confirm"), question, QMessageBox.Yes, QMessageBox.No) if answer == QMessageBox.No: return @@ -204,13 +356,54 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): self.parent.need_update.set() def onFileAdded(self, fn): - with open(fn) as f: - tx = self.parent.tx_from_text(f.read()) - try: - self.wallet.add_transaction(tx.txid(), tx) - except UnrelatedTransactionException as e: - self.parent.show_error(e) + try: + with open(fn) as f: + tx = self.parent.tx_from_text(f.read()) + self.parent.save_transaction_into_wallet(tx) + except IOError as e: + self.parent.show_error(e) + + def export_history_dialog(self): + d = WindowModalDialog(self, _('Export History')) + d.setMinimumSize(400, 200) + vbox = QVBoxLayout(d) + defaultname = os.path.expanduser('~/electrum-history.csv') + select_msg = _('Select file to export your wallet transactions to') + hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg) + vbox.addLayout(hbox) + vbox.addStretch(1) + hbox = Buttons(CancelButton(d), OkButton(d, _('Export'))) + vbox.addLayout(hbox) + #run_hook('export_history_dialog', self, hbox) + self.update() + if not d.exec_(): + return + filename = filename_e.text() + if not filename: + return + try: + self.do_export_history(self.wallet, filename, csv_button.isChecked()) + except (IOError, os.error) as reason: + export_error_label = _("Electrum was unable to produce a transaction export.") + self.parent.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history")) + return + self.parent.show_message(_("Your wallet history has been successfully exported.")) + + def do_export_history(self, wallet, fileName, is_csv): + history = self.transactions + lines = [] + for item in history: + if is_csv: + lines.append([item['txid'], item.get('label', ''), item['confirmations'], item['value'], item['date']]) + else: + lines.append(item) + with open(fileName, "w+") as f: + if is_csv: + import csv + transaction = csv.writer(f, lineterminator='\n') + transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"]) + for line in lines: + transaction.writerow(line) else: - self.wallet.save_transactions(write=True) - # need to update at least: history_list, utxo_list, address_list - self.parent.need_update.set() + from electrum.util import json_encode + f.write(json_encode(history)) diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py index 19cfea60a..586dd71c5 100644 --- a/gui/qt/invoice_list.py +++ b/gui/qt/invoice_list.py @@ -23,10 +23,11 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from .util import * from electrum.i18n import _ from electrum.util import format_time +from .util import * + class InvoiceList(MyTreeWidget): 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)) def import_invoices(self): - wallet_folder = self.parent.get_wallet_folder() - filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder) - if not filename: - return - self.parent.invoices.import_file(filename) - self.on_update() + import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update) + + def export_invoices(self): + export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file) def create_menu(self, position): menu = QMenu() diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index e4f5085a7..480d67fa5 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -39,33 +39,26 @@ import PyQt5.QtCore as QtCore from .exception_window import Exception_Hook from PyQt5.QtWidgets import * -from electrum.util import bh2u, bfh - from electrum import keystore, simple_config from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS, NetworkConstants from electrum.plugins import run_hook from electrum.i18n import _ from electrum.util import (format_time, format_satoshis, PrintError, format_satoshis_plain, NotEnoughFunds, - UserCancelled, NoDynamicFeeEstimates) + UserCancelled, NoDynamicFeeEstimates, profiler, + export_meta, import_meta, bh2u, bfh) from electrum import Transaction from electrum import util, bitcoin, commands, coinchooser from electrum import paymentrequest -from electrum.wallet import Multisig_Wallet -try: - from electrum.plot import plot_history -except: - plot_history = None +from electrum.wallet import Multisig_Wallet, AddTransactionException from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit from .qrcodewidget import QRCodeWidget, QRDialog from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit from .transaction_dialog import show_transaction from .fee_slider import FeeSlider - from .util import * -from electrum.util import profiler class StatusBarButton(QPushButton): def __init__(self, icon, tooltip, func): @@ -488,11 +481,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): contacts_menu = wallet_menu.addMenu(_("Contacts")) contacts_menu.addAction(_("&New"), self.new_contact_dialog) 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.addAction(_("Import"), lambda: self.invoice_list.import_invoices()) - hist_menu = wallet_menu.addMenu(_("&History")) - hist_menu.addAction("Plot", self.plot_history_dialog).setEnabled(plot_history is not None) - hist_menu.addAction("Export", self.export_history_dialog) + invoices_menu.addAction(_("Export"), lambda: self.invoice_list.export_invoices()) wallet_menu.addSeparator() 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 self.history_list = l = HistoryList(self) l.searchable_list = l - return l + return self.create_list_tab(l, l.get_list_header()) def show_address(self, addr): from . import address_dialog @@ -1081,7 +1073,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def fee_cb(dyn, pos, fee_rate): if dyn: - if self.config.get('mempool_fees'): + if self.config.use_mempool_fees(): self.config.set_key('depth_level', pos, False) else: self.config.set_key('fee_level', pos, False) @@ -1136,7 +1128,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def feerounding_onclick(): text = (self.feerounding_text + '\n\n' + _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + - _('At most 100 satoshis might be lost due to this rounding.') + '\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.')) QMessageBox.information(self, 'Fee rounding', text) @@ -1518,7 +1511,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): x_fee_address, x_fee_amount = x_fee 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: 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.addCopyButton(self.app) 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))) d.setLayout(vbox) d.exec_() @@ -2133,7 +2124,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): task = partial(self.wallet.sign_message, address, message, password) def show_signed_message(sig): - signature.setText(base64.b64encode(sig).decode('ascii')) + try: + signature.setText(base64.b64encode(sig).decode('ascii')) + except RuntimeError: + # (signature) wrapped C/C++ object has been deleted + pass + self.wallet.thread.add(task, on_success=show_signed_message) def do_verify(self, address, message, signature): @@ -2197,7 +2193,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): return cyphertext = encrypted_e.toPlainText() 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): message = message_e.toPlainText() @@ -2296,25 +2300,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): return self.tx_from_text(file_content) def do_process_from_text(self): - from electrum.transaction import SerializationError text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction")) if not text: return - try: - tx = self.tx_from_text(text) - if tx: - self.show_transaction(tx) - except SerializationError as e: - self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) + tx = self.tx_from_text(text) + if tx: + self.show_transaction(tx) def do_process_from_file(self): - from electrum.transaction import SerializationError - try: - tx = self.read_tx_from_file() - if tx: - self.show_transaction(tx) - except SerializationError as e: - self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) + tx = self.read_tx_from_file() + if tx: + self.show_transaction(tx) def do_process_from_txid(self): 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.')) d = WindowModalDialog(self, _('Private keys')) - d.setMinimumSize(850, 300) + d.setMinimumSize(980, 300) vbox = QVBoxLayout(d) 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)) def do_import_labels(self): - labelsFile = self.getOpenFileName(_("Open labels file"), "*.json") - if not labelsFile: return - try: - 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 import_labels(path): + def _validate(data): + return data # TODO - 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 import_labels_assign(data): + for key, value in data.items(): + self.wallet.set_label(key, value) + import_meta(path, _validate, import_labels_assign) - 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.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history")) - return - self.show_message(_("Your wallet history has been successfully exported.")) - - def plot_history_dialog(self): - if plot_history is None: - return - wallet = self.wallet - history = wallet.get_history() - if len(history) > 0: - plt = plot_history(self.wallet, history) - plt.show() - - def do_export_history(self, wallet, fileName, is_csv): - history = wallet.get_history() - lines = [] - for item in history: - tx_hash, height, confirmations, timestamp, value, balance = item - if height>0: - if timestamp is not None: - time_string = format_time(timestamp) - else: - time_string = _("unverified") - else: - time_string = _("unconfirmed") - - if value is not None: - value_string = format_satoshis(value, True) - else: - value_string = '--' - - if tx_hash: - label = wallet.get_label(tx_hash) - else: - label = "" - - if is_csv: - lines.append([tx_hash, label, confirmations, value_string, time_string]) - else: - lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string}) + def on_import(): + self.need_update.set() + import_meta_gui(self, _('labels'), import_labels, on_import) - with open(fileName, "w+") as f: - if is_csv: - transaction = csv.writer(f, lineterminator='\n') - transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"]) - for line in lines: - transaction.writerow(line) - else: - import json - f.write(json.dumps(lines, indent = 4)) + def do_export_labels(self): + def export_labels(filename): + export_meta(self.wallet.labels, filename) + export_meta_gui(self, _('labels'), export_labels) def sweep_key_dialog(self): 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_combo = QComboBox() 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): self.config.set_key('mempool_fees', x==1) self.fee_slider.update() @@ -2893,6 +2810,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): unconf_cb.stateChanged.connect(on_unconf) 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 hist_checkbox = QCheckBox() fiat_address_checkbox = QCheckBox() @@ -3192,3 +3121,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if is_final: new_tx.set_rbf(False) 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 + + diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 5097af28a..d918f02aa 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -25,6 +25,7 @@ import copy import datetime import json +import traceback from PyQt5.QtCore import * from PyQt5.QtGui import * @@ -33,18 +34,27 @@ from PyQt5.QtWidgets import * from electrum.bitcoin import base_encode from electrum.i18n import _ from electrum.plugins import run_hook +from electrum import simple_config from electrum.util import bfh -from electrum.wallet import UnrelatedTransactionException +from electrum.wallet import AddTransactionException +from electrum.transaction import SerializationError from .util import * dialogs = [] # Otherwise python randomly garbage collects the dialogs... + def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False): - d = TxDialog(tx, parent, desc, prompt_if_unsaved) - dialogs.append(d) - d.show() + try: + 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) + d.show() + class TxDialog(QDialog, MessageBoxMixin): @@ -58,7 +68,10 @@ class TxDialog(QDialog, MessageBoxMixin): # e.g. the FX plugin. If this happens during or after a long # sign operation the signatures are lost. self.tx = copy.deepcopy(tx) - self.tx.deserialize() + try: + self.tx.deserialize() + except BaseException as e: + raise SerializationError(e) self.main_window = parent self.wallet = parent.wallet self.prompt_if_unsaved = prompt_if_unsaved @@ -179,17 +192,9 @@ class TxDialog(QDialog, MessageBoxMixin): self.main_window.sign_tx(self.tx, sign_done) def save(self): - if not self.wallet.add_transaction(self.tx.txid(), 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.show_message(_("Transaction saved successfully")) - self.saved = True + if self.main_window.save_transaction_into_wallet(self.tx): + self.save_button.setDisabled(True) + self.saved = True def export(self): @@ -236,9 +241,13 @@ class TxDialog(QDialog, MessageBoxMixin): else: amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit 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: - 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.fee_label.setText(fee_str) self.size_label.setText(size_str) diff --git a/gui/qt/util.py b/gui/qt/util.py index 5dbda84a6..369c05e87 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -6,11 +6,15 @@ import queue from collections import namedtuple from functools import partial -from electrum.i18n import _ from PyQt5.QtGui import * from PyQt5.QtCore 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': MONOSPACE_FONT = 'Lucida Console' elif platform.system() == 'Darwin': @@ -21,8 +25,6 @@ else: dialogs = [] -from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED - pr_icons = { PR_UNPAID:":icons/unpaid.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): from .qrtextedit import ScanQRTextEdit dialog = WindowModalDialog(parent, title) - dialog.setMinimumWidth(500) + dialog.setMinimumWidth(600) l = QVBoxLayout() dialog.setLayout(l) l.addWidget(QLabel(label)) @@ -389,7 +391,9 @@ class MyTreeWidget(QTreeWidget): self.editor = None self.pending_update = False 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.setItemDelegate(ElectrumItemDelegate(self)) self.itemDoubleClicked.connect(self.on_doubleclick) @@ -406,11 +410,15 @@ class MyTreeWidget(QTreeWidget): def editItem(self, item, column): if column in self.editable_columns: - self.editing_itemcol = (item, column, item.text(column)) - # Calling setFlags causes on_changed events for some reason - item.setFlags(item.flags() | Qt.ItemIsEditable) - QTreeWidget.editItem(self, item, column) - item.setFlags(item.flags() & ~Qt.ItemIsEditable) + try: + self.editing_itemcol = (item, column, item.text(column)) + # Calling setFlags causes on_changed events for some reason + item.setFlags(item.flags() | Qt.ItemIsEditable) + QTreeWidget.editItem(self, item, column) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + except RuntimeError: + # (item) wrapped C/C++ object has been deleted + pass def keyPressEvent(self, event): if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None: @@ -673,6 +681,35 @@ class AcceptFileDragDrop: 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__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 843397559..2926cb732 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -47,28 +47,6 @@ def read_json(filename, default): return r - - -# Version numbers for BIP32 extended keys -# standard: xprv, xpub -# segwit in p2sh: yprv, ypub -# native segwit: zprv, zpub -XPRV_HEADERS = { - 'standard': 0x0488ade4, - 'p2wpkh-p2sh': 0x049d7878, - 'p2wsh-p2sh': 0x295b005, - 'p2wpkh': 0x4b2430c, - 'p2wsh': 0x2aa7a99 -} -XPUB_HEADERS = { - 'standard': 0x0488b21e, - 'p2wpkh-p2sh': 0x049d7cb2, - 'p2wsh-p2sh': 0x295b43f, - 'p2wpkh': 0x4b24746, - 'p2wsh': 0x2aa7ed3 -} - - class NetworkConstants: @classmethod @@ -83,6 +61,21 @@ class NetworkConstants: cls.DEFAULT_SERVERS = read_json('servers.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 def set_testnet(cls): cls.TESTNET = True @@ -95,15 +88,26 @@ class NetworkConstants: cls.DEFAULT_SERVERS = read_json('servers_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() ################################## transactions -FEE_STEP = 10000 -MAX_FEE_RATE = 300000 - - COINBASE_MATURITY = 100 COIN = 100000000 @@ -508,9 +512,8 @@ def DecodeBase58Check(psz): return key - -# extended key export format for segwit - +# backwards compat +# extended WIF for segwit (used in 3.0.x; but still used internally) SCRIPT_TYPES = { 'p2pkh':0, 'p2wpkh':1, @@ -521,25 +524,42 @@ SCRIPT_TYPES = { } -def serialize_privkey(secret, compressed, txin_type): - prefix = bytes([(SCRIPT_TYPES[txin_type]+NetworkConstants.WIF_PREFIX)&255]) +def serialize_privkey(secret, compressed, txin_type, internal_use=False): + if internal_use: + prefix = bytes([(SCRIPT_TYPES[txin_type] + NetworkConstants.WIF_PREFIX) & 255]) + else: + prefix = bytes([NetworkConstants.WIF_PREFIX]) suffix = b'\01' if compressed else b'' 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): - # whether the pubkey is compressed should be visible from the keystore - vch = DecodeBase58Check(key) if is_minikey(key): 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] - assert len(vch) in [33, 34] - compressed = len(vch) == 34 - return txin_type, vch[1:33], compressed else: - raise BaseException("cannot deserialize", key) + assert vch[0] == NetworkConstants.WIF_PREFIX + + assert len(vch) in [33, 34] + compressed = len(vch) == 34 + return txin_type, vch[1:33], compressed + def regenerate_key(pk): assert len(pk) == 32 @@ -893,11 +913,11 @@ def _CKD_pub(cK, c, s): def xprv_header(xtype): - return bfh("%08x" % XPRV_HEADERS[xtype]) + return bfh("%08x" % NetworkConstants.XPRV_HEADERS[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): @@ -919,7 +939,7 @@ def deserialize_xkey(xkey, prv): child_number = xkey[9:13] c = xkey[13:13+32] 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(): raise BaseException('Invalid xpub format', hex(header)) xtype = list(headers.keys())[list(headers.values()).index(header)] diff --git a/lib/blockchain.py b/lib/blockchain.py index 8a69276f3..d592e584b 100644 --- a/lib/blockchain.py +++ b/lib/blockchain.py @@ -181,7 +181,8 @@ class Blockchain(util.PrintError): if d < 0: chunk = chunk[-d:] d = 0 - self.write(chunk, d, index > len(self.checkpoints)) + truncate = index >= len(self.checkpoints) + self.write(chunk, d, truncate) self.swap_with_parent() def swap_with_parent(self): @@ -338,7 +339,7 @@ class Blockchain(util.PrintError): self.save_chunk(idx, data) return True except BaseException as e: - self.print_error('verify_chunk failed', str(e)) + self.print_error('verify_chunk %d failed'%idx, str(e)) return False def get_checkpoints(self): diff --git a/lib/coinchooser.py b/lib/coinchooser.py index 472e3aa38..c4ca7a151 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -25,7 +25,7 @@ from collections import defaultdict, namedtuple 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 .util import NotEnoughFunds, PrintError @@ -87,6 +87,8 @@ def strip_unneeded(bkts, sufficient_funds): class CoinChooserBase(PrintError): + enable_output_value_rounding = False + def keys(self, coins): raise NotImplementedError @@ -135,7 +137,13 @@ class CoinChooserBase(PrintError): zeroes = [trailing_zeroes(i) for i in output_amounts] min_zeroes = min(zeroes) max_zeroes = max(zeroes) - zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1) + + if n > 1: + zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1) + else: + # if there is only one change output, this will ensure that we aim + # to have one that is exactly as precise as the most precise output + zeroes = [min_zeroes] # Calculate change; randomize it a bit if using more than 1 output remaining = change_amount @@ -150,8 +158,10 @@ class CoinChooserBase(PrintError): n -= 1 # Last change output. Round down to maximum precision but lose - # no more than 100 satoshis to fees (2dp) - N = pow(10, min(2, zeroes[0])) + # no more than 10**max_dp_to_round_for_privacy + # e.g. a max of 2 decimal places means losing 100 satoshis to fees + max_dp_to_round_for_privacy = 2 if self.enable_output_value_rounding else 0 + N = pow(10, min(max_dp_to_round_for_privacy, zeroes[0])) amount = (remaining // N) * N amounts.append(amount) @@ -230,6 +240,13 @@ class CoinChooserBase(PrintError): tx.add_inputs([coin for b in buckets for coin in b.coins]) 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 output_weight = 4 * Transaction.estimated_output_size(change_addrs[0]) fee = lambda count: fee_estimator_w(tx_weight + count * output_weight) @@ -370,4 +387,6 @@ def get_name(config): def get_coin_chooser(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 diff --git a/lib/commands.py b/lib/commands.py index c0bfb48ed..23c35b896 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -138,6 +138,8 @@ class Commands: @command('wp') def password(self, password=None, new_password=None): """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() self.wallet.update_password(password, new_password, b) self.wallet.storage.write() @@ -440,46 +442,20 @@ class Commands: return tx.as_dict() @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.""" - balance = 0 - out = [] - for item in self.wallet.get_history(): - tx_hash, height, conf, timestamp, value, balance = item - if timestamp: - date = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] - else: - date = "----" - label = self.wallet.get_label(tx_hash) - tx = self.wallet.transactions.get(tx_hash) - tx.deserialize() - input_addresses = [] - 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 + kwargs = {'show_addresses': show_addresses} + if year: + import time + start_date = datetime.datetime(year, 1, 1) + end_date = datetime.datetime(year+1, 1, 1) + kwargs['from_timestamp'] = time.mktime(start_date.timetuple()) + kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) + if show_fiat: + from .exchange_rate import FxThread + fx = FxThread(self.config, None) + kwargs['fx'] = fx + return self.wallet.get_full_history(**kwargs) @command('w') def setlabel(self, key, label): @@ -736,6 +712,9 @@ command_options = { 'pending': (None, "Show only pending requests."), 'expired': (None, "Show only expired 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, 'nbits': int, 'imax': int, + 'year': int, 'entropy': int, 'tx': tx_from_str, 'pubkeys': json_loads, diff --git a/lib/contacts.py b/lib/contacts.py index 3b5a3255d..0015a8610 100644 --- a/lib/contacts.py +++ b/lib/contacts.py @@ -23,9 +23,12 @@ import re import dns import json +import traceback +import sys from . import bitcoin from . import dnssec +from .util import export_meta, import_meta class Contacts(dict): @@ -48,14 +51,15 @@ class Contacts(dict): self.storage.put('contacts', dict(self)) def import_file(self, path): - try: - with open(path, 'r') as f: - d = self._validate(json.loads(f.read())) - except: - return - self.update(d) + import_meta(path, self._validate, self.load_meta) + + def load_meta(self, data): + self.update(data) self.save() + def export_file(self, filename): + export_meta(self, filename) + def __setitem__(self, key, value): dict.__setitem__(self, key, value) self.save() @@ -113,13 +117,13 @@ class Contacts(dict): return None def _validate(self, data): - for k,v in list(data.items()): + for k, v in list(data.items()): if k == 'contacts': return self._validate(v) if not bitcoin.is_address(k): data.pop(k) else: - _type,_ = v + _type, _ = v if _type != 'address': data.pop(k) return data diff --git a/lib/currencies.json b/lib/currencies.json index 81680d104..a4e85f1f6 100644 --- a/lib/currencies.json +++ b/lib/currencies.json @@ -1,631 +1,798 @@ { - "BTCChina": [ - "CNY" - ], "BitPay": [ - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTC", - "BTN", - "BWP", - "BZD", - "CAD", - "CDF", - "CHF", - "CLF", - "CLP", - "CNY", - "COP", - "CRC", - "CUP", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ETB", - "EUR", - "FJD", - "FKP", - "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", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SDG", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "STD", - "SVC", - "SYP", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XAG", - "XAU", - "XCD", - "XOF", - "XPF", - "YER", - "ZAR", - "ZMW", + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BCH", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTC", + "BTN", + "BWP", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNY", + "COP", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ETB", + "EUR", + "FJD", + "FKP", + "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", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMW", "ZWL" - ], + ], "BitStamp": [ "USD" - ], + ], "BitcoinAverage": [ - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTN", - "BWP", - "BYN", - "BZD", - "CAD", - "CDF", - "CHF", - "CLF", - "CLP", - "CNH", - "CNY", - "COP", - "CRC", - "CUC", - "CUP", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ERN", - "ETB", - "ETH", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GGP", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "IMP", - "INR", - "IQD", - "IRR", - "ISK", - "JEP", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KPW", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LTC", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SDG", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "SSP", - "STD", - "SVC", - "SYP", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XAG", - "XAU", - "XCD", - "XDR", - "XOF", - "XPD", - "XPF", - "XPT", - "XRP", - "YER", - "ZAR", - "ZEC", - "ZMW", + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNH", + "CNY", + "COP", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GGP", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "IMP", + "INR", + "IQD", + "IRR", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "YER", + "ZAR", + "ZMW", "ZWL" - ], + ], "Bitmarket": [ "PLN" - ], + ], "Bitso": [ "MXN" - ], + ], "Bitvalor": [ "BRL" - ], + ], "BlockchainInfo": [ - "AUD", - "BRL", - "CAD", - "CHF", - "CLP", - "CNY", - "DKK", - "EUR", - "GBP", - "HKD", - "INR", - "ISK", - "JPY", - "KRW", - "NZD", - "PLN", - "RUB", - "SEK", - "SGD", - "THB", - "TWD", + "AUD", + "BRL", + "CAD", + "CHF", + "CLP", + "CNY", + "DKK", + "EUR", + "GBP", + "HKD", + "INR", + "ISK", + "JPY", + "KRW", + "NZD", + "PLN", + "RUB", + "SEK", + "SGD", + "THB", + "TWD", "USD" - ], + ], + "CoinDesk": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTC", + "BTN", + "BWP", + "BYR", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNY", + "COP", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EEK", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LTL", + "LVL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MTL", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBT", + "XCD", + "XDR", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMK", + "ZMW", + "ZWL" + ], "Coinbase": [ - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTN", - "BWP", - "BYN", - "BYR", - "BZD", - "CAD", - "CDF", - "CHF", - "CLF", - "CLP", - "CNY", - "COP", - "CRC", - "CUC", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EEK", - "EGP", - "ERN", - "ETB", - "ETH", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GGP", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "IMP", - "INR", - "IQD", - "ISK", - "JEP", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LTC", - "LTL", - "LVL", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MTL", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "SSP", - "STD", - "SVC", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XAG", - "XAU", - "XCD", - "XDR", - "XOF", - "XPD", - "XPF", - "XPT", - "YER", - "ZAR", - "ZMK", - "ZMW", + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BCH", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BYR", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNH", + "CNY", + "COP", + "CRC", + "CUC", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EEK", + "EGP", + "ERN", + "ETB", + "ETH", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GGP", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "IMP", + "INR", + "IQD", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LTC", + "LTL", + "LVL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MTL", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "YER", + "ZAR", + "ZMK", + "ZMW", "ZWL" - ], - "Coinsecure": [ - "INR" - ], + ], "Foxbit": [ "BRL" - ], + ], "Kraken": [ - "CAD", - "EUR", - "GBP", - "JPY", + "CAD", + "EUR", + "GBP", + "JPY", "USD" - ], + ], "LocalBitcoins": [ - "AED", - "ARS", - "AUD", - "BDT", - "BRL", - "BYN", - "CAD", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CZK", - "DKK", - "DOP", - "EGP", - "EUR", - "GBP", - "GHS", - "HKD", - "HRK", - "HUF", - "IDR", - "INR", - "IRR", - "ISK", - "JPY", - "KES", - "KZT", - "MAD", - "MMK", - "MXN", - "MYR", - "NGN", - "NOK", - "NZD", - "OMR", - "PAB", - "PEN", - "PHP", - "PKR", - "PLN", - "QAR", - "RON", - "RSD", - "RUB", - "SAR", - "SEK", - "SGD", - "THB", - "TRY", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "VEF", - "VND", - "XAF", - "ZAR" - ], + "AED", + "ARS", + "AUD", + "BAM", + "BDT", + "BHD", + "BOB", + "BRL", + "BYN", + "CAD", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CZK", + "DKK", + "DOP", + "EGP", + "ETH", + "EUR", + "GBP", + "GHS", + "HKD", + "HRK", + "HUF", + "IDR", + "ILS", + "INR", + "IRR", + "JOD", + "JPY", + "KES", + "KRW", + "KZT", + "LKR", + "MAD", + "MXN", + "MYR", + "NGN", + "NOK", + "NZD", + "PAB", + "PEN", + "PHP", + "PKR", + "PLN", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SEK", + "SGD", + "THB", + "TRY", + "TTD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "VEF", + "VND", + "XAR", + "ZAR", + "ZMW" + ], "MercadoBitcoin": [ "BRL" - ], + ], "NegocieCoins": [ "BRL" - ], - "Winkdex": [ - "USD" - ], + ], "WEX": [ "EUR", "RUB", diff --git a/lib/daemon.py b/lib/daemon.py index 38baeb158..bebcf4046 100644 --- a/lib/daemon.py +++ b/lib/daemon.py @@ -25,6 +25,8 @@ import ast import os import time +import traceback +import sys # from jsonrpc import JSONRPCResponseManager import jsonrpclib @@ -121,13 +123,12 @@ class Daemon(DaemonThread): self.config = config if config.get('offline'): self.network = None - self.fx = None else: self.network = Network(config) 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.gui = None self.wallets = {} # Setup JSONRPC server @@ -301,4 +302,8 @@ class Daemon(DaemonThread): gui_name = 'qt' gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui']) self.gui = gui.ElectrumGui(config, self, plugins) - self.gui.main() + try: + self.gui.main() + except BaseException as e: + traceback.print_exc(file=sys.stdout) + # app will exit now diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 23de311ec..1c8c8a959 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -2,6 +2,8 @@ from datetime import datetime import inspect import requests import sys +import os +import json from threading import Thread import time import csv @@ -33,7 +35,7 @@ class ExchangeBase(PrintError): def get_json(self, site, get_string): # APIs must have https 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() def get_csv(self, site, get_string): @@ -59,28 +61,54 @@ class ExchangeBase(PrintError): t.setDaemon(True) 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: 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.on_history() except BaseException as e: self.print_error("failed fx history:", e) - - def get_historical_rates(self, ccy): - result = self.history.get(ccy) - if not result and ccy in self.history_ccys(): - t = Thread(target=self.get_historical_rates_safe, args=(ccy,)) + 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, cache_dir): + if ccy not in self.history_ccys(): + return + 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.start() - return result def history_ccys(self): return [] 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): rates = self.get_rates('') @@ -99,7 +127,7 @@ class BitcoinAverage(ExchangeBase): 'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD', 'ZAR'] - def historical_rates(self, ccy): + def request_history(self, ccy): history = self.get_csv('apiv2.bitcoinaverage.com', "/indices/global/history/BTC%s?period=alltime&format=csv" % ccy) return dict([(h['DateTime'][:10], h['Average']) @@ -127,7 +155,7 @@ class BitcoinVenezuela(ExchangeBase): def history_ccys(self): return ['ARS', 'EUR', 'USD', 'VEF'] - def historical_rates(self, ccy): + def request_history(self, ccy): return self.get_json('api.bitcoinvenezuela.com', "/historical/index.php?coin=BTC")[ccy +'_BTC'] @@ -199,23 +227,24 @@ class Coinbase(ExchangeBase): class CoinDesk(ExchangeBase): - def get_rates(self, ccy): + def get_currencies(self): dicts = self.get_json('api.coindesk.com', '/v1/bpi/supported-currencies.json') + return [d['currency'] for d in dicts] + + def get_rates(self, ccy): json = self.get_json('api.coindesk.com', '/v1/bpi/currentprice/%s.json' % ccy) - ccys = [d['currency'] for d in dicts] - result = dict.fromkeys(ccys) - result[ccy] = Decimal(json['bpi'][ccy]['rate_float']) + result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])} return result def history_starts(self): - return { 'USD': '2012-11-30' } + return { 'USD': '2012-11-30', 'EUR': '2013-09-01' } def history_ccys(self): return self.history_starts().keys() - def historical_rates(self, ccy): + def request_history(self, ccy): start = self.history_starts()[ccy] end = datetime.today().strftime('%Y-%m-%d') # Note ?currency and ?index don't work as documented. Sigh. @@ -313,7 +342,7 @@ class Winkdex(ExchangeBase): def history_ccys(self): return ['USD'] - def historical_rates(self, ccy): + def request_history(self, ccy): json = self.get_json('winkdex.com', "/api/v0/series?start_time=1342915200") history = json['series'][0]['results'] @@ -346,7 +375,9 @@ def get_exchanges_and_currencies(): exchange = klass(None, None) try: d[name] = exchange.get_currencies() + print(name, "ok") except: + print(name, "error") continue with open(path, 'w') as f: f.write(json.dumps(d, indent=4, sort_keys=True)) @@ -377,7 +408,10 @@ class FxThread(ThreadJob): self.history_used_spot = False self.ccy_combo = None self.hist_checkbox = None + self.cache_dir = os.path.join(config.path, 'cache') self.set_exchange(self.config_exchange()) + if not os.path.exists(self.cache_dir): + os.mkdir(self.cache_dir) def get_currencies(self, h): d = get_exchanges_by_ccy(h) @@ -400,7 +434,7 @@ class FxThread(ThreadJob): # This runs from the plugins thread which catches exceptions if self.is_enabled(): 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(): self.timeout = time.time() + 150 self.exchange.update(self.ccy) @@ -448,45 +482,59 @@ class FxThread(ThreadJob): # A new exchange means new fx quotes, initially empty. Force # a quote refresh self.timeout = 0 + self.exchange.read_historical_rates(self.ccy, self.cache_dir) def on_quotes(self): - self.network.trigger_callback('on_quotes') + if self.network: + self.network.trigger_callback('on_quotes') def on_history(self): - self.network.trigger_callback('on_history') + if self.network: + self.network.trigger_callback('on_history') def exchange_rate(self): '''Returns None, or the exchange rate as a Decimal''' rate = self.exchange.quotes.get(self.ccy) - if rate: - return Decimal(rate) + if rate is None: + return Decimal('NaN') + return Decimal(rate) def format_amount_and_units(self, btc_balance): 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): 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) + 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): - if satoshis is None: # Can happen with incomplete history - return _("Unknown") - if rate: - value = Decimal(satoshis) / COIN * Decimal(rate) - return "%s" % (self.ccy_amount_str(value, True)) - return _("No data") + return self.format_fiat(self.fiat_value(satoshis, rate)) + + def format_fiat(self, value): + if value.is_nan(): + return _("No data") + return "%s" % (self.ccy_amount_str(value, True)) def history_rate(self, d_t): rate = self.exchange.historical_rate(self.ccy, d_t) # Frequently there is no rate for today, until tomorrow :) # Use spot quotes in that case - if rate is None and (datetime.today().date() - d_t.date()).days <= 2: - rate = self.exchange.quotes.get(self.ccy) + if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2: + rate = self.exchange.quotes.get(self.ccy, 'NaN') self.history_used_spot = True - return rate + return Decimal(rate) def historical_value_str(self, satoshis, d_t): - rate = self.history_rate(d_t) - return self.value_str(satoshis, rate) + return self.format_fiat(self.historical_value(satoshis, d_t)) + + 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) diff --git a/lib/keystore.py b/lib/keystore.py index e3579958a..011602ec4 100644 --- a/lib/keystore.py +++ b/lib/keystore.py @@ -139,7 +139,10 @@ class Imported_KeyStore(Software_KeyStore): def import_privkey(self, sec, password): txin_type, privkey, compressed = deserialize_privkey(sec) 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 def delete_imported_key(self, key): diff --git a/lib/network.py b/lib/network.py index bf7d4eb41..92edda958 100644 --- a/lib/network.py +++ b/lib/network.py @@ -549,7 +549,7 @@ class Network(util.DaemonThread): self.donation_address = result elif method == 'mempool.get_fee_histogram': if error is None: - self.print_error(result) + self.print_error('fee_histogram', result) self.config.mempool_fees = result self.notify('fee_histogram') elif method == 'blockchain.estimatefee': @@ -777,25 +777,29 @@ class Network(util.DaemonThread): error = response.get('error') result = response.get('result') params = response.get('params') + blockchain = interface.blockchain if result is None or params is None or error is not None: interface.print_error(error or 'bad response') return index = params[0] # Ignore unsolicited chunks if index not in self.requested_chunks: + interface.print_error("received chunk %d (unsolicited)" % index) return + else: + interface.print_error("received chunk %d" % index) self.requested_chunks.remove(index) - connect = interface.blockchain.connect_chunk(index, result) + connect = blockchain.connect_chunk(index, result) if not connect: self.connection_down(interface.server) return # 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) else: interface.mode = 'default' - interface.print_error('catch up done', interface.blockchain.height()) - interface.blockchain.catch_up = None + interface.print_error('catch up done', blockchain.height()) + blockchain.catch_up = None self.notify('updated') def request_header(self, interface, height): @@ -987,7 +991,7 @@ class Network(util.DaemonThread): if not height: return if height < self.max_checkpoint(): - self.connection_down(interface) + self.connection_down(interface.server) return interface.tip_header = header interface.tip = height diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py index 8c9c6009c..471186705 100644 --- a/lib/paymentrequest.py +++ b/lib/paymentrequest.py @@ -40,6 +40,7 @@ except ImportError: from . import bitcoin from . import util from .util import print_error, bh2u, bfh +from .util import export_meta, import_meta from . import transaction from . import x509 from . import rsakey @@ -467,24 +468,29 @@ class InvoiceStore(object): continue def import_file(self, path): - try: - with open(path, 'r') as f: - d = json.loads(f.read()) - self.load(d) - except: - traceback.print_exc(file=sys.stderr) - return + def validate(data): + return data # TODO + import_meta(path, validate, self.on_import) + + def on_import(self, data): + self.load(data) self.save() - def save(self): - l = {} + def export_file(self, filename): + export_meta(self.dump(), filename) + + def dump(self): + d = {} for k, pr in self.invoices.items(): - l[k] = { + d[k] = { 'hex': bh2u(pr.raw), 'requestor': pr.requestor, 'txid': pr.tx } - self.storage.put('invoices', l) + return d + + def save(self): + self.storage.put('invoices', self.dump()) def get_status(self, key): pr = self.get(key) diff --git a/lib/plot.py b/lib/plot.py index 06f8edd75..82a83fe65 100644 --- a/lib/plot.py +++ b/lib/plot.py @@ -14,17 +14,16 @@ from matplotlib.patches import Ellipse from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker -def plot_history(wallet, history): +def plot_history(history): hist_in = defaultdict(int) hist_out = defaultdict(int) for item in history: - tx_hash, height, confirmations, timestamp, value, balance = item - if not confirmations: + if not item['confirmations']: continue - if timestamp is None: + if item['timestamp'] is None: continue - value = value*1./COIN - date = datetime.datetime.fromtimestamp(timestamp) + value = item['value'].value/COIN + date = item['date'] datenum = int(md.date2num(datetime.date(date.year, date.month, 1))) if value > 0: hist_in[datenum] += value diff --git a/lib/simple_config.py b/lib/simple_config.py index b7a41ddcc..bf57d5b53 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -5,14 +5,22 @@ import os import stat from copy import deepcopy + from .util import (user_dir, print_error, PrintError, NoDynamicFeeEstimates, format_satoshis) - -from .bitcoin import MAX_FEE_RATE +from .i18n import _ FEE_ETA_TARGETS = [25, 10, 5, 2] FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000] +# satoshi per kbyte +FEERATE_MAX_DYNAMIC = 1500000 +FEERATE_WARNING_HIGH_FEE = 600000 +FEERATE_FALLBACK_STATIC_FEE = 150000 +FEERATE_DEFAULT_RELAY = 1000 +FEERATE_STATIC_VALUES = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000] + + config = None @@ -39,7 +47,6 @@ class SimpleConfig(PrintError): 2. User configuration (in the user's config directory) 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, read_user_dir_function=None): @@ -261,13 +268,19 @@ class SimpleConfig(PrintError): path = wallet.storage.path self.set_key('gui_last_wallet', path) - def max_fee_rate(self): - f = self.get('max_fee_rate', MAX_FEE_RATE) - if f==0: - f = MAX_FEE_RATE - return f - + def impose_hard_limits_on_fee(func): + def get_fee_within_limits(self, *args, **kwargs): + fee = func(self, *args, **kwargs) + if fee is None: + 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): + """Returns fee in sat/kbyte.""" if i < 4: j = FEE_ETA_TARGETS[i] fee = self.fee_estimates.get(j) @@ -276,8 +289,6 @@ class SimpleConfig(PrintError): fee = self.fee_estimates.get(2) if fee is not None: fee += fee/2 - if fee is not None: - fee = min(5*MAX_FEE_RATE, fee) return fee def fee_to_depth(self, target_fee): @@ -290,7 +301,9 @@ class SimpleConfig(PrintError): return 0 return depth + @impose_hard_limits_on_fee def depth_to_fee(self, i): + """Returns fee in sat/kbyte.""" target = self.depth_target(i) depth = 0 for fee, s in self.mempool_fees: @@ -305,6 +318,8 @@ class SimpleConfig(PrintError): return FEE_DEPTH_TARGETS[i] def eta_target(self, i): + if i == len(FEE_ETA_TARGETS): + return 1 return FEE_ETA_TARGETS[i] def fee_to_eta(self, fee_per_kb): @@ -320,17 +335,26 @@ class SimpleConfig(PrintError): return "%.1f MB from tip"%(depth/1000000) 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): 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() fee_rate = self.fee_per_kb() target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate) return target def get_fee_text(self, pos, dyn, mempool, fee_rate): + """Returns (text, tooltip) where + text is what we target: static fee / num blocks to confirm in / mempool depth + tooltip is the corresponding estimate (e.g. num blocks for a static fee) + """ rate_str = (format_satoshis(fee_rate/1000, False, 0, 0, False) + ' sat/byte') if fee_rate is not None else 'unknown' if dyn: if mempool: @@ -342,18 +366,14 @@ class SimpleConfig(PrintError): tooltip = rate_str else: text = rate_str - if mempool: - if self.has_fee_mempool(): - depth = self.fee_to_depth(fee_rate) - tooltip = self.depth_tooltip(depth) - else: - tooltip = '' + if mempool and self.has_fee_mempool(): + depth = self.fee_to_depth(fee_rate) + tooltip = self.depth_tooltip(depth) + elif not mempool and self.has_fee_etas(): + eta = self.fee_to_eta(fee_rate) + tooltip = self.eta_tooltip(eta) else: - if self.has_fee_etas(): - eta = self.fee_to_eta(fee_rate) - tooltip = self.eta_tooltip(eta) - else: - tooltip = '' + tooltip = '' return text, tooltip def get_depth_level(self): @@ -361,7 +381,7 @@ class SimpleConfig(PrintError): return min(maxp, self.get('depth_level', 2)) 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)) def get_fee_slider(self, dyn, mempool): @@ -372,7 +392,7 @@ class SimpleConfig(PrintError): fee_rate = self.depth_to_fee(pos) else: 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) else: fee_rate = self.fee_per_kb() @@ -380,12 +400,11 @@ class SimpleConfig(PrintError): maxp = 9 return maxp, pos, fee_rate - def static_fee(self, i): - return self.fee_rates[i] + return FEERATE_STATIC_VALUES[i] 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__) def has_fee_etas(self): @@ -394,11 +413,17 @@ class SimpleConfig(PrintError): def has_fee_mempool(self): return bool(self.mempool_fees) + def has_dynamic_fees_ready(self): + if self.use_mempool_fees(): + return self.has_fee_mempool() + else: + return self.has_fee_etas() + def is_dynfee(self): - return self.get('dynamic_fees', True) + return bool(self.get('dynamic_fees', True)) def use_mempool_fees(self): - return self.get('mempool_fees', False) + return bool(self.get('mempool_fees', False)) def fee_per_kb(self): """Returns sat/kvB fee to pay for a txn. @@ -410,7 +435,7 @@ class SimpleConfig(PrintError): else: fee_rate = self.eta_to_fee(self.get_fee_level()) 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 def fee_per_byte(self): @@ -428,7 +453,12 @@ class SimpleConfig(PrintError): @classmethod 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): self.fee_estimates[key] = value diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py index dbae5005d..03f1d00f0 100644 --- a/lib/tests/test_bitcoin.py +++ b/lib/tests/test_bitcoin.py @@ -271,6 +271,7 @@ class Test_keyImport(unittest.TestCase): priv_pub_addr = ( {'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6', + 'exported_privkey': 'p2pkh:KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6', 'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997', 'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR', 'minikey' : False, @@ -278,7 +279,17 @@ class Test_keyImport(unittest.TestCase): 'compressed': True, 'addr_encoding': 'base58', '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', + 'exported_privkey': 'p2pkh:5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', 'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f', 'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6', 'minikey': False, @@ -286,7 +297,17 @@ class Test_keyImport(unittest.TestCase): 'compressed': False, 'addr_encoding': 'base58', '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', + 'exported_privkey': 'p2wpkh-p2sh:Kz9XebiCXL2BZzhYJViiHDzn5iup1povWV8aqstzWU4sz1K5nVva', 'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81', 'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7', 'minikey': False, @@ -294,7 +315,17 @@ class Test_keyImport(unittest.TestCase): 'compressed': True, 'addr_encoding': 'base58', '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', + 'exported_privkey': 'p2wpkh:Kz6SuyPM5VktY5dr2d2YqdVgBA6LCWkiHqXJaC3BzxnMPSUuYzmF', 'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b', 'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue', 'minikey': False, @@ -302,8 +333,18 @@ class Test_keyImport(unittest.TestCase): 'compressed': True, 'addr_encoding': 'bech32', '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 {'priv': 'SzavMBLoXU6kDrqtUVmffv', + 'exported_privkey': 'p2pkh:L53fCHmQhbNp1B4JipfBtfeHZH7cAibzG9oK19XfiFzxHgAkz6JK', 'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9', 'address': '19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR', 'minikey': True, @@ -344,6 +385,7 @@ class Test_keyImport(unittest.TestCase): def test_is_private_key(self): for priv_details in self.priv_pub_addr: 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['address'])) 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: txin_type, privkey, compressed = deserialize_privkey(priv_details['priv']) priv2 = serialize_privkey(privkey, compressed, txin_type) - if not priv_details['minikey']: - self.assertEqual(priv_details['priv'], priv2) + self.assertEqual(priv_details['exported_privkey'], priv2) def test_address_to_scripthash(self): for priv_details in self.priv_pub_addr: diff --git a/lib/tests/test_transaction.py b/lib/tests/test_transaction.py index 609006cdd..cc800454c 100644 --- a/lib/tests/test_transaction.py +++ b/lib/tests/test_transaction.py @@ -235,6 +235,385 @@ class TestTransaction(unittest.TestCase): tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000') self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid()) + # input: p2sh, not multisig + def test_txid_regression_issue_3899(self): + tx = transaction.Transaction('0100000004328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c010000000b0009630330472d5fae685bffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c020000000b0009630359646d5fae6858ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c030000000b000963034bd4715fae6854ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c040000000b000963036de8705fae6860ffffffff0130750000000000001976a914b5abca61d20f9062fb1fdbb880d9d93bac36675188ac00000000') + self.assertEqual('f570d5d1e965ee61bcc7005f8fefb1d3abbed9d7ddbe035e2a68fa07e5fc4a0d', tx.txid()) + + +# these transactions are from Bitcoin Core unit tests ---> +# https://github.com/bitcoin/bitcoin/blob/11376b5583a283772c82f6d32d0007cdbf5b8ef0/src/test/data/tx_valid.json + + def test_txid_bitcoin_core_0001(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63', tx.txid()) + + def test_txid_bitcoin_core_0002(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('fcabc409d8e685da28536e1e5ccc91264d755cd4c57ed4cae3dbaa4d3b93e8ed', tx.txid()) + + def test_txid_bitcoin_core_0003(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('c9aa95f2c48175fdb70b34c23f1c3fc44f869b073a6f79b1343fbce30c3cb575', tx.txid()) + + def test_txid_bitcoin_core_0004(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('da94fda32b55deb40c3ed92e135d69df7efc4ee6665e0beb07ef500f407c9fd2', tx.txid()) + + def test_txid_bitcoin_core_0005(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('f76f897b206e4f78d60fe40f2ccb542184cfadc34354d3bb9bdc30cc2f432b86', tx.txid()) + + def test_txid_bitcoin_core_0006(self): + tx = transaction.Transaction('01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d00000000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac00000000') + self.assertEqual('c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73', tx.txid()) + + def test_txid_bitcoin_core_0007(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000') + self.assertEqual('e41ffe19dff3cbedb413a2ca3fbbcd05cb7fd7397ffa65052f8928aa9c700092', tx.txid()) + + def test_txid_bitcoin_core_0008(self): + tx = transaction.Transaction('01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000') + self.assertEqual('f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb', tx.txid()) + + def test_txid_bitcoin_core_0009(self): + tx = transaction.Transaction('01000000020002000000000000000000000000000000000000000000000000000000000000000000000151ffffffff0001000000000000000000000000000000000000000000000000000000000000000000006b483045022100c9cdd08798a28af9d1baf44a6c77bcc7e279f47dc487c8c899911bc48feaffcc0220503c5c50ae3998a733263c5c0f7061b483e2b56c4c41b456e7d2f5a78a74c077032102d5c25adb51b61339d2b05315791e21bbe80ea470a49db0135720983c905aace0ffffffff010000000000000000015100000000') + self.assertEqual('b56471690c3ff4f7946174e51df68b47455a0d29344c351377d712e6d00eabe5', tx.txid()) + + def test_txid_bitcoin_core_0010(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000') + self.assertEqual('99517e5b47533453cc7daa332180f578be68b80370ecfe84dbfff7f19d791da4', tx.txid()) + + def test_txid_bitcoin_core_0011(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100c66c9cdf4c43609586d15424c54707156e316d88b0a1534c9e6b0d4f311406310221009c0fe51dbc9c4ab7cc25d3fdbeccf6679fe6827f08edf2b4a9f16ee3eb0e438a0123210338e8034509af564c62644c07691942e0c056752008a173c89f60ab2a88ac2ebfacffffffff010000000000000000015100000000') + self.assertEqual('ab097537b528871b9b64cb79a769ae13c3c3cd477cc9dddeebe657eabd7fdcea', tx.txid()) + + def test_txid_bitcoin_core_0012(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010040075af0750700015100000000') + self.assertEqual('4d163e00f1966e9a1eab8f9374c3e37f4deb4857c247270e25f7d79a999d2dc9', tx.txid()) + + def test_txid_bitcoin_core_0013(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510000000000000000015100000000') + self.assertEqual('9fe2ef9dde70e15d78894a4800b7df3bbfb1addb9a6f7d7c204492fdb6ee6cc4', tx.txid()) + + def test_txid_bitcoin_core_0014(self): + tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025151ffffffff010000000000000000015100000000') + self.assertEqual('99d3825137602e577aeaf6a2e3c9620fd0e605323dc5265da4a570593be791d4', tx.txid()) + + def test_txid_bitcoin_core_0015(self): + tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6451515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151ffffffff010000000000000000015100000000') + self.assertEqual('c0d67409923040cc766bbea12e4c9154393abef706db065ac2e07d91a9ba4f84', tx.txid()) + + def test_txid_bitcoin_core_0016(self): + tx = transaction.Transaction('010000000200010000000000000000000000000000000000000000000000000000000000000000000049483045022100d180fd2eb9140aeb4210c9204d3f358766eb53842b2a9473db687fa24b12a3cc022079781799cd4f038b85135bbe49ec2b57f306b2bb17101b17f71f000fcab2b6fb01ffffffff0002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000') + self.assertEqual('c610d85d3d5fdf5046be7f123db8a0890cee846ee58de8a44667cfd1ab6b8666', tx.txid()) + + def test_txid_bitcoin_core_0017(self): + tx = transaction.Transaction('01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df101010000000002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000') + self.assertEqual('a647a7b3328d2c698bfa1ee2dd4e5e05a6cea972e764ccb9bd29ea43817ca64f', tx.txid()) + + def test_txid_bitcoin_core_0018(self): + tx = transaction.Transaction('010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c59da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbea097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf950650802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e18217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a3788dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9cfdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa34b26f62db4a88ac00000000') + self.assertEqual('afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae', tx.txid()) + + def test_txid_bitcoin_core_0019(self): + tx = transaction.Transaction('01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe0000483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa0148304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f4014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000') + self.assertEqual('f4b05f978689c89000f729cae187dcfbe64c9819af67a4f05c0b4d59e717d64d', tx.txid()) + + def test_txid_bitcoin_core_0020(self): + tx = transaction.Transaction('0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000') + self.assertEqual('cc60b1f899ec0a69b7c3f25ddf32c4524096a9c5b01cbd84c6d0312a0c478984', tx.txid()) + + def test_txid_bitcoin_core_0021(self): + tx = transaction.Transaction('01000000012c651178faca83be0b81c8c1375c4b0ad38d53c8fe1b1c4255f5e795c25792220000000049483045022100d6044562284ac76c985018fc4a90127847708c9edb280996c507b28babdc4b2a02203d74eca3f1a4d1eea7ff77b528fde6d5dc324ec2dbfdb964ba885f643b9704cd01ffffffff010100000000000000232102c2410f8891ae918cab4ffc4bb4a3b0881be67c7a1e7faa8b5acf9ab8932ec30cac00000000') + self.assertEqual('1edc7f214659d52c731e2016d258701911bd62a0422f72f6c87a1bc8dd3f8667', tx.txid()) + + def test_txid_bitcoin_core_0022(self): + tx = transaction.Transaction('0100000001f725ea148d92096a79b1709611e06e94c63c4ef61cbae2d9b906388efd3ca99c000000000100ffffffff0101000000000000002321028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02ac00000000') + self.assertEqual('018adb7133fde63add9149a2161802a1bcf4bdf12c39334e880c073480eda2ff', tx.txid()) + + def test_txid_bitcoin_core_0023(self): + tx = transaction.Transaction('0100000001be599efaa4148474053c2fa031c7262398913f1dc1d9ec201fd44078ed004e44000000004900473044022022b29706cb2ed9ef0cb3c97b72677ca2dfd7b4160f7b4beb3ba806aa856c401502202d1e52582412eba2ed474f1f437a427640306fd3838725fab173ade7fe4eae4a01ffffffff010100000000000000232103ac4bba7e7ca3e873eea49e08132ad30c7f03640b6539e9b59903cf14fd016bbbac00000000') + self.assertEqual('1464caf48c708a6cc19a296944ded9bb7f719c9858986d2501cf35068b9ce5a2', tx.txid()) + + def test_txid_bitcoin_core_0024(self): + tx = transaction.Transaction('010000000112b66d5e8c7d224059e946749508efea9d66bf8d0c83630f080cf30be8bb6ae100000000490047304402206ffe3f14caf38ad5c1544428e99da76ffa5455675ec8d9780fac215ca17953520220779502985e194d84baa36b9bd40a0dbd981163fa191eb884ae83fc5bd1c86b1101ffffffff010100000000000000232103905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9fac00000000') + self.assertEqual('1fb73fbfc947d52f5d80ba23b67c06a232ad83fdd49d1c0a657602f03fbe8f7a', tx.txid()) + + def test_txid_bitcoin_core_0025(self): + tx = transaction.Transaction('0100000001b0ef70cc644e0d37407e387e73bfad598d852a5aa6d691d72b2913cebff4bceb000000004a00473044022068cd4851fc7f9a892ab910df7a24e616f293bcb5c5fbdfbc304a194b26b60fba022078e6da13d8cb881a22939b952c24f88b97afd06b4c47a47d7f804c9a352a6d6d0100ffffffff0101000000000000002321033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194ac00000000') + self.assertEqual('24cecfce0fa880b09c9b4a66c5134499d1b09c01cc5728cd182638bea070e6ab', tx.txid()) + + def test_txid_bitcoin_core_0026(self): + tx = transaction.Transaction('0100000001c188aa82f268fcf08ba18950f263654a3ea6931dabc8bf3ed1d4d42aaed74cba000000004b0000483045022100940378576e069aca261a6b26fb38344e4497ca6751bb10905c76bb689f4222b002204833806b014c26fd801727b792b1260003c55710f87c5adbd7a9cb57446dbc9801ffffffff0101000000000000002321037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1ac00000000') + self.assertEqual('9eaa819e386d6a54256c9283da50c230f3d8cd5376d75c4dcc945afdeb157dd7', tx.txid()) + + def test_txid_bitcoin_core_0027(self): + tx = transaction.Transaction('01000000012432b60dc72cebc1a27ce0969c0989c895bdd9e62e8234839117f8fc32d17fbc000000004a493046022100a576b52051962c25e642c0fd3d77ee6c92487048e5d90818bcf5b51abaccd7900221008204f8fb121be4ec3b24483b1f92d89b1b0548513a134e345c5442e86e8617a501ffffffff010000000000000000016a00000000') + self.assertEqual('46224764c7870f95b58f155bce1e38d4da8e99d42dbb632d0dd7c07e092ee5aa', tx.txid()) + + def test_txid_bitcoin_core_0028(self): + tx = transaction.Transaction('01000000014710b0e7cf9f8930de259bdc4b84aa5dfb9437b665a3e3a21ff26e0bf994e183000000004a493046022100a166121a61b4eeb19d8f922b978ff6ab58ead8a5a5552bf9be73dc9c156873ea02210092ad9bc43ee647da4f6652c320800debcf08ec20a094a0aaf085f63ecb37a17201ffffffff010000000000000000016a00000000') + self.assertEqual('8d66836045db9f2d7b3a75212c5e6325f70603ee27c8333a3bce5bf670d9582e', tx.txid()) + + def test_txid_bitcoin_core_0029(self): + tx = transaction.Transaction('01000000015ebaa001d8e4ec7a88703a3bcf69d98c874bca6299cca0f191512bf2a7826832000000004948304502203bf754d1c6732fbf87c5dcd81258aefd30f2060d7bd8ac4a5696f7927091dad1022100f5bcb726c4cf5ed0ed34cc13dadeedf628ae1045b7cb34421bc60b89f4cecae701ffffffff010000000000000000016a00000000') + self.assertEqual('aab7ef280abbb9cc6fbaf524d2645c3daf4fcca2b3f53370e618d9cedf65f1f8', tx.txid()) + + def test_txid_bitcoin_core_0030(self): + tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a900000000924830450221009c0a27f886a1d8cb87f6f595fbc3163d28f7a81ec3c4b252ee7f3ac77fd13ffa02203caa8dfa09713c8c4d7ef575c75ed97812072405d932bd11e6a1593a98b679370148304502201e3861ef39a526406bad1e20ecad06be7375ad40ddb582c9be42d26c3a0d7b240221009d0a3985e96522e59635d19cc4448547477396ce0ef17a58e7d74c3ef464292301ffffffff010000000000000000016a00000000') + self.assertEqual('6327783a064d4e350c454ad5cd90201aedf65b1fc524e73709c52f0163739190', tx.txid()) + + def test_txid_bitcoin_core_0031(self): + tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0100ffffffff010000000000000000016a00000000') + self.assertEqual('892464645599cc3c2d165adcc612e5f982a200dfaa3e11e9ce1d228027f46880', tx.txid()) + + def test_txid_bitcoin_core_0032(self): + tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510151ffffffff010000000000000000016a00000000') + self.assertEqual('578db8c6c404fec22c4a8afeaf32df0e7b767c4dda3478e0471575846419e8fc', tx.txid()) + + def test_txid_bitcoin_core_0033(self): + tx = transaction.Transaction('0100000001e0be9e32f1f89c3d916c4f21e55cdcd096741b895cc76ac353e6023a05f4f7cc00000000d86149304602210086e5f736a2c3622ebb62bd9d93d8e5d76508b98be922b97160edc3dcca6d8c47022100b23c312ac232a4473f19d2aeb95ab7bdf2b65518911a0d72d50e38b5dd31dc820121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac4730440220508fa761865c8abd81244a168392876ee1d94e8ed83897066b5e2df2400dad24022043f5ee7538e87e9c6aef7ef55133d3e51da7cc522830a9c4d736977a76ef755c0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000') + self.assertEqual('974f5148a0946f9985e75a240bb24c573adbbdc25d61e7b016cdbb0a5355049f', tx.txid()) + + def test_txid_bitcoin_core_0034(self): + tx = transaction.Transaction('01000000013c6f30f99a5161e75a2ce4bca488300ca0c6112bde67f0807fe983feeff0c91001000000e608646561646265656675ab61493046022100ce18d384221a731c993939015e3d1bcebafb16e8c0b5b5d14097ec8177ae6f28022100bcab227af90bab33c3fe0a9abfee03ba976ee25dc6ce542526e9b2e56e14b7f10121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac493046022100c3b93edcc0fd6250eb32f2dd8a0bba1754b0f6c3be8ed4100ed582f3db73eba2022100bf75b5bd2eff4d6bf2bda2e34a40fcc07d4aa3cf862ceaa77b47b81eff829f9a01ab21038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000') + self.assertEqual('b0097ec81df231893a212657bf5fe5a13b2bff8b28c0042aca6fc4159f79661b', tx.txid()) + + def test_txid_bitcoin_core_0035(self): + tx = transaction.Transaction('01000000016f3dbe2ca96fa217e94b1017860be49f20820dea5c91bdcb103b0049d5eb566000000000fd1d0147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac47304402203757e937ba807e4a5da8534c17f9d121176056406a6465054bdd260457515c1a02200f02eccf1bec0f3a0d65df37889143c2e88ab7acec61a7b6f5aa264139141a2b0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000') + self.assertEqual('feeba255656c80c14db595736c1c7955c8c0a497622ec96e3f2238fbdd43a7c9', tx.txid()) + + def test_txid_bitcoin_core_0036(self): + tx = transaction.Transaction('01000000012139c555ccb81ee5b1e87477840991ef7b386bc3ab946b6b682a04a621006b5a01000000fdb40148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f2204148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390175ac4830450220646b72c35beeec51f4d5bc1cbae01863825750d7f490864af354e6ea4f625e9c022100f04b98432df3a9641719dbced53393022e7249fb59db993af1118539830aab870148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a580039017521038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000') + self.assertEqual('a0c984fc820e57ddba97f8098fa640c8a7eb3fe2f583923da886b7660f505e1e', tx.txid()) + + def test_txid_bitcoin_core_0037(self): + tx = transaction.Transaction('0100000002f9cbafc519425637ba4227f8d0a0b7160b4e65168193d5af39747891de98b5b5000000006b4830450221008dd619c563e527c47d9bd53534a770b102e40faa87f61433580e04e271ef2f960220029886434e18122b53d5decd25f1f4acb2480659fea20aabd856987ba3c3907e0121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffff42e7988254800876b69f24676b3e0205b77be476512ca4d970707dd5c60598ab00000000fd260100483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a53034930460221008431bdfa72bc67f9d41fe72e94c88fb8f359ffa30b33c72c121c5a877d922e1002210089ef5fc22dd8bfc6bf9ffdb01a9862d27687d424d1fefbab9e9c7176844a187a014c9052483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c7153aeffffffff01a08601000000000017a914d8dacdadb7462ae15cd906f1878706d0da8660e68700000000') + self.assertEqual('5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f', tx.txid()) + + def test_txid_bitcoin_core_0038(self): + tx = transaction.Transaction('0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000') + self.assertEqual('ded7ff51d89a4e1ec48162aee5a96447214d93dfb3837946af2301a28f65dbea', tx.txid()) + + def test_txid_bitcoin_core_0039(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000') + self.assertEqual('3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e', tx.txid()) + + def test_txid_bitcoin_core_0040(self): + tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d') + self.assertEqual('abd62b4627d8d9b2d95fcfd8c87e37d2790637ce47d28018e3aece63c1d62649', tx.txid()) + + def test_txid_bitcoin_core_0041(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d') + self.assertEqual('58b6de8413603b7f556270bf48caedcf17772e7105f5419f6a80be0df0b470da', tx.txid()) + + def test_txid_bitcoin_core_0042(self): + tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff') + self.assertEqual('5f99c0abf511294d76cbe144d86b77238a03e086974bc7a8ea0bdb2c681a0324', tx.txid()) + + def test_txid_bitcoin_core_0043(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000') + self.assertEqual('25d35877eaba19497710666473c50d5527d38503e3521107a3fc532b74cd7453', tx.txid()) + + def test_txid_bitcoin_core_0044(self): + tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff') + self.assertEqual('1b9aef851895b93c62c29fbd6ca4d45803f4007eff266e2f96ff11e9b6ef197b', tx.txid()) + + def test_txid_bitcoin_core_0045(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000') + self.assertEqual('3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e', tx.txid()) + + def test_txid_bitcoin_core_0046(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000') + self.assertEqual('f53761038a728b1f17272539380d96e93f999218f8dcb04a8469b523445cd0fd', tx.txid()) + + def test_txid_bitcoin_core_0047(self): + tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000') + self.assertEqual('d193f0f32fceaf07bb25c897c8f99ca6f69a52f6274ca64efc2a2e180cb97fc1', tx.txid()) + + def test_txid_bitcoin_core_0048(self): + tx = transaction.Transaction('010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000') + self.assertEqual('50a1e0e6a134a564efa078e3bd088e7e8777c2c0aec10a752fd8706470103b89', tx.txid()) + + def test_txid_bitcoin_core_0049(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000') + self.assertEqual('e2207d1aaf6b74e5d98c2fa326d2dc803b56b30a3f90ce779fa5edb762f38755', tx.txid()) + + def test_txid_bitcoin_core_0050(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000') + self.assertEqual('f335864f7c12ec7946d2c123deb91eb978574b647af125a414262380c7fbd55c', tx.txid()) + + def test_txid_bitcoin_core_0051(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000') + self.assertEqual('d1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2', tx.txid()) + + def test_txid_bitcoin_core_0052(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000') + self.assertEqual('3a13e1b6371c545147173cc4055f0ed73686a9f73f092352fb4b39ca27d360e6', tx.txid()) + + def test_txid_bitcoin_core_0053(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000') + self.assertEqual('bffda23e40766d292b0510a1b556453c558980c70c94ab158d8286b3413e220d', tx.txid()) + + def test_txid_bitcoin_core_0054(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000') + self.assertEqual('01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd', tx.txid()) + + def test_txid_bitcoin_core_0055(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000') + self.assertEqual('f6d2359c5de2d904e10517d23e7c8210cca71076071bbf46de9fbd5f6233dbf1', tx.txid()) + + def test_txid_bitcoin_core_0056(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000') + self.assertEqual('19c2b7377229dae7aa3e50142a32fd37cef7171a01682f536e9ffa80c186f6c9', tx.txid()) + + def test_txid_bitcoin_core_0057(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000') + self.assertEqual('c9dda3a24cc8a5acb153d1085ecd2fecf6f87083122f8cdecc515b1148d4c40d', tx.txid()) + + def test_txid_bitcoin_core_0058(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000') + self.assertEqual('d1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2', tx.txid()) + + def test_txid_bitcoin_core_0059(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000') + self.assertEqual('01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd', tx.txid()) + + def test_txid_bitcoin_core_0060(self): + tx = transaction.Transaction('02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2010000000100000000000000000000000000') + self.assertEqual('4b5e0aae1251a9dc66b4d5f483f1879bf518ea5e1765abc5a9f2084b43ed1ea7', tx.txid()) + + def test_txid_bitcoin_core_0061(self): + tx = transaction.Transaction('0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000') + self.assertEqual('5f16eb3ca4581e2dfb46a28140a4ee15f85e4e1c032947da8b93549b53c105f5', tx.txid()) + + def test_txid_bitcoin_core_0062(self): + tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000') + self.assertEqual('b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004', tx.txid()) + + def test_txid_bitcoin_core_0063(self): + tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000') + self.assertEqual('b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004', tx.txid()) + + def test_txid_bitcoin_core_0064(self): + tx = transaction.Transaction('01000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000') + self.assertEqual('fee125c6cd142083fabd0187b1dd1f94c66c89ec6e6ef6da1374881c0c19aece', tx.txid()) + + def test_txid_bitcoin_core_0065(self): + tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000023220020ff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000') + self.assertEqual('5f32557914351fee5f89ddee6c8983d476491d29e601d854e3927299e50450da', tx.txid()) + + def test_txid_bitcoin_core_0066(self): + tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff05540b0000000000000151d0070000000000000151840300000000000001513c0f00000000000001512c010000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71000000000000') + self.assertEqual('07dfa2da3d67c8a2b9f7bd31862161f7b497829d5da90a88ba0f1a905e7a43f7', tx.txid()) + + def test_txid_bitcoin_core_0067(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0068(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff0484030000000000000151d0070000000000000151540b0000000000000151c800000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('f92bb6e4f3ff89172f23ef647f74c13951b665848009abb5862cdf7a0412415a', tx.txid()) + + def test_txid_bitcoin_core_0069(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0070(self): + tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff04b60300000000000001519e070000000000000151860b00000000000001009600000000000000015100000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('e657e25fc9f2b33842681613402759222a58cf7dd504d6cdc0b69a0b8c2e7dcb', tx.txid()) + + def test_txid_bitcoin_core_0071(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0072(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('4ede5e22992d43d42ccdf6553fb46e448aa1065ba36423f979605c1e5ab496b8', tx.txid()) + + def test_txid_bitcoin_core_0073(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0074(self): + tx = transaction.Transaction('01000000000103000100000000000000000000000000000000000000000000000000000000000000000000000200000000010000000000000000000000000000000000000000000000000000000000000100000000ffffffff000100000000000000000000000000000000000000000000000000000000000002000000000200000003e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('cfe9f4b19f52b8366860aec0d2b5815e329299b2e9890d477edd7f1182be7ac8', tx.txid()) + + def test_txid_bitcoin_core_0075(self): + tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('aee8f4865ca40fa77ff2040c0d7de683bea048b103d42ca406dc07dd29d539cb', tx.txid()) + + def test_txid_bitcoin_core_0076(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0077(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0078(self): + tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd08020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000') + self.assertEqual('d93ab9e12d7c29d2adc13d5cdf619d53eec1f36eb6612f55af52be7ba0448e97', tx.txid()) + + def test_txid_bitcoin_core_0079(self): + tx = transaction.Transaction('0100000000010c00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff0001000000000000000000000000000000000000000000000000000000000000020000006a473044022026c2e65b33fcd03b2a3b0f25030f0244bd23cc45ae4dec0f48ae62255b1998a00220463aa3982b718d593a6b9e0044513fd67a5009c2fdccc59992cffc2b167889f4012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000030000006a4730440220008bd8382911218dcb4c9f2e75bf5c5c3635f2f2df49b36994fde85b0be21a1a02205a539ef10fb4c778b522c1be852352ea06c67ab74200977c722b0bc68972575a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000040000006b483045022100d9436c32ff065127d71e1a20e319e4fe0a103ba0272743dbd8580be4659ab5d302203fd62571ee1fe790b182d078ecfd092a509eac112bea558d122974ef9cc012c7012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000050000006a47304402200e2c149b114ec546015c13b2b464bbcb0cdc5872e6775787527af6cbc4830b6c02207e9396c6979fb15a9a2b96ca08a633866eaf20dc0ff3c03e512c1d5a1654f148012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000060000006b483045022100b20e70d897dc15420bccb5e0d3e208d27bdd676af109abbd3f88dbdb7721e6d6022005836e663173fbdfe069f54cde3c2decd3d0ea84378092a5d9d85ec8642e8a41012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff00010000000000000000000000000000000000000000000000000000000000000700000000ffffffff00010000000000000000000000000000000000000000000000000000000000000800000000ffffffff00010000000000000000000000000000000000000000000000000000000000000900000000ffffffff00010000000000000000000000000000000000000000000000000000000000000a00000000ffffffff00010000000000000000000000000000000000000000000000000000000000000b0000006a47304402206639c6e05e3b9d2675a7f3876286bdf7584fe2bbd15e0ce52dd4e02c0092cdc60220757d60b0a61fc95ada79d23746744c72bac1545a75ff6c2c7cdb6ae04e7e9592012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0ce8030000000000000151e9030000000000000151ea030000000000000151eb030000000000000151ec030000000000000151ed030000000000000151ee030000000000000151ef030000000000000151f0030000000000000151f1030000000000000151f2030000000000000151f30300000000000001510248304502210082219a54f61bf126bfc3fa068c6e33831222d1d7138c6faa9d33ca87fd4202d6022063f9902519624254d7c2c8ea7ba2d66ae975e4e229ae38043973ec707d5d4a83012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022017fb58502475848c1b09f162cb1688d0920ff7f142bed0ef904da2ccc88b168f02201798afa61850c65e77889cbcd648a5703b487895517c88f85cdd18b021ee246a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000000247304402202830b7926e488da75782c81a54cd281720890d1af064629ebf2e31bf9f5435f30220089afaa8b455bbeb7d9b9c3fe1ed37d07685ade8455c76472cda424d93e4074a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022026326fcdae9207b596c2b05921dbac11d81040c4d40378513670f19d9f4af893022034ecd7a282c0163b89aaa62c22ec202cef4736c58cd251649bad0d8139bcbf55012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71024730440220214978daeb2f38cd426ee6e2f44131a33d6b191af1c216247f1dd7d74c16d84a02205fdc05529b0bc0c430b4d5987264d9d075351c4f4484c16e91662e90a72aab24012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402204a6e9f199dc9672cf2ff8094aaa784363be1eb62b679f7ff2df361124f1dca3302205eeb11f70fab5355c9c8ad1a0700ea355d315e334822fa182227e9815308ee8f012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('b83579db5246aa34255642768167132a0c3d2932b186cd8fb9f5490460a0bf91', tx.txid()) + + def test_txid_bitcoin_core_0080(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000') + self.assertEqual('2b1e44fff489d09091e5e20f9a01bbc0e8d80f0662e629fd10709cdb4922a874', tx.txid()) + + def test_txid_bitcoin_core_0081(self): + tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff01d00700000000000001510003483045022100e078de4e96a0e05dcdc0a414124dd8475782b5f3f0ed3f607919e9a5eeeb22bf02201de309b3a3109adb3de8074b3610d4cf454c49b61247a2779a0bcbf31c889333032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc711976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac00000000') + self.assertEqual('60ebb1dd0b598e20dd0dd462ef6723dd49f8f803b6a2492926012360119cfdd7', tx.txid()) + + def test_txid_bitcoin_core_0082(self): + tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff02e8030000000000000151e90300000000000001510247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000') + self.assertEqual('ed0c7f4163e275f3f77064f471eac861d01fdf55d03aa6858ebd3781f70bf003', tx.txid()) + + def test_txid_bitcoin_core_0083(self): + tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff02e9030000000000000151e80300000000000001510248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000') + self.assertEqual('f531ddf5ce141e1c8a7fdfc85cc634e5ff686f446a5cf7483e9dbe076b844862', tx.txid()) + + def test_txid_bitcoin_core_0084(self): + tx = transaction.Transaction('01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000') + self.assertEqual('98229b70948f1c17851a541f1fe532bf02c408267fecf6d7e174c359ae870654', tx.txid()) + + def test_txid_bitcoin_core_0085(self): + tx = transaction.Transaction('01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000') + self.assertEqual('570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab', tx.txid()) + + def test_txid_bitcoin_core_0086(self): + tx = transaction.Transaction('01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000') + self.assertEqual('e0b8142f587aaa322ca32abce469e90eda187f3851043cc4f2a0fff8c13fc84e', tx.txid()) + + def test_txid_bitcoin_core_0087(self): + tx = transaction.Transaction('0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000') + self.assertEqual('b9ecf72df06b8f98f8b63748d1aded5ffc1a1186f8a302e63cf94f6250e29f4d', tx.txid()) + + def test_txid_bitcoin_core_0088(self): + tx = transaction.Transaction('0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000') + self.assertEqual('27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac', tx.txid()) + + def test_txid_bitcoin_core_0089(self): + tx = transaction.Transaction('010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000') + self.assertEqual('22d020638e3b7e1f2f9a63124ac76f5e333c74387862e3675f64b25e960d3641', tx.txid()) + + def test_txid_bitcoin_core_0090(self): + tx = transaction.Transaction('0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9781d66b61fb5a7ef00ac5ad5bc6ffc78be7b44a566e3c87870e1079368df4c4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000') + self.assertEqual('2862bc0c69d2af55da7284d1b16a7cddc03971b77e5a97939cca7631add83bf5', tx.txid()) + + def test_txid_bitcoin_core_0091(self): + tx = transaction.Transaction('01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601522102cd74a2809ffeeed0092bc124fd79836706e41f048db3f6ae9df8708cefb83a1c2102e615999372426e46fd107b76eaf007156a507584aa2cc21de9eee3bdbd26d36c4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000') + self.assertEqual('1aebf0c98f01381765a8c33d688f8903e4d01120589ac92b78f1185dc1f4119c', tx.txid()) + + def test_txid_bitcoin_core_0092(self): + tx = transaction.Transaction('010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960101022102966f109c54e85d3aee8321301136cedeb9fc710fdef58a9de8a73942f8e567c021034ffc99dd9a79dd3cb31e2ab3e0b09e0e67db41ac068c625cd1f491576016c84e9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000') + self.assertEqual('45d17fb7db86162b2b6ca29fa4e163acf0ef0b54110e49b819bda1f948d423a3', tx.txid()) + +# txns from Bitcoin Core ends <--- + class NetworkMock(object): diff --git a/lib/transaction.py b/lib/transaction.py index b23cf9cf2..4ee57d4e1 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -32,6 +32,8 @@ from .util import print_error, profiler from . import bitcoin from .bitcoin import * import struct +import traceback +import sys # # 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) ] except Exception as e: # 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 match = [ opcodes.OP_PUSHDATA4 ] @@ -334,9 +337,9 @@ def parse_scriptSig(d, _bytes): d['pubkeys'] = ["(pubkey)"] return - # non-generated TxIn transactions push a signature - # (seventy-something bytes) and then their public key - # (65 bytes) onto the stack: + # p2pkh TxIn transactions push a signature + # (71-73 bytes) and then their public key + # (33 or 65 bytes) onto the stack: match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ] if match_decoded(decoded, match): sig = bh2u(decoded[0][1]) @@ -345,7 +348,8 @@ def parse_scriptSig(d, _bytes): signatures = parse_sig([sig]) pubkey, address = xpubkey_to_address(x_pubkey) 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 d['type'] = 'p2pkh' d['signatures'] = signatures @@ -357,30 +361,41 @@ def parse_scriptSig(d, _bytes): # p2sh transaction, m of n match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1) - if not match_decoded(decoded, match): - print_error("cannot find address in input script", bh2u(_bytes)) + if match_decoded(decoded, match): + x_sig = [bh2u(x[1]) for x in decoded[1:-1]] + try: + m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1]) + except NotRecognizedRedeemScript: + print_error("parse_scriptSig: cannot find address in input script (p2sh?)", + bh2u(_bytes)) + # we could still guess: + # d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1])) + return + # write result in d + d['type'] = 'p2sh' + d['num_sig'] = m + d['signatures'] = parse_sig(x_sig) + d['x_pubkeys'] = x_pubkeys + d['pubkeys'] = pubkeys + d['redeemScript'] = redeemScript + d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript))) return - x_sig = [bh2u(x[1]) for x in decoded[1:-1]] - m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1]) - # write result in d - d['type'] = 'p2sh' - d['num_sig'] = m - d['signatures'] = parse_sig(x_sig) - d['x_pubkeys'] = x_pubkeys - d['pubkeys'] = pubkeys - d['redeemScript'] = redeemScript - d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript))) + + print_error("parse_scriptSig: cannot find address in input script (unknown)", + bh2u(_bytes)) def parse_redeemScript(s): dec2 = [ x for x in script_GetOp(s) ] - m = dec2[0][0] - opcodes.OP_1 + 1 - n = dec2[-2][0] - opcodes.OP_1 + 1 + try: + m = dec2[0][0] - opcodes.OP_1 + 1 + n = dec2[-2][0] - opcodes.OP_1 + 1 + except IndexError: + raise NotRecognizedRedeemScript() op_m = opcodes.OP_1 + m - 1 op_n = opcodes.OP_1 + n - 1 match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] if not match_decoded(dec2, match_multisig): - print_error("cannot find address in input script", bh2u(s)) raise NotRecognizedRedeemScript() x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] @@ -436,7 +451,11 @@ def parse_input(vds): d['num_sig'] = 0 if scriptSig: d['scriptSig'] = bh2u(scriptSig) - parse_scriptSig(d, scriptSig) + try: + parse_scriptSig(d, scriptSig) + except BaseException: + traceback.print_exc(file=sys.stderr) + print_error('failed to parse scriptSig', bh2u(scriptSig)) else: d['scriptSig'] = '' @@ -465,25 +484,40 @@ def parse_witness(vds, txin): # 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. # If v==0 and n==2, we need parent scriptPubKey to distinguish between p2wpkh and p2wsh. - if txin['type'] == 'coinbase': - pass - elif txin['type'] == 'p2wsh-p2sh' or n > 2: - try: - m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) - except NotRecognizedRedeemScript: + try: + if txin['type'] == 'coinbase': + pass + elif txin['type'] == 'p2wsh-p2sh' or n > 2: + try: + m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) + except NotRecognizedRedeemScript: + raise UnknownTxinType() + txin['signatures'] = parse_sig(w[1:-1]) + txin['num_sig'] = m + txin['x_pubkeys'] = x_pubkeys + txin['pubkeys'] = pubkeys + txin['witnessScript'] = witnessScript + if not txin.get('scriptSig'): # native segwit script + txin['type'] = 'p2wsh' + txin['address'] = bitcoin.script_to_p2wsh(txin['witnessScript']) + elif txin['type'] == 'p2wpkh-p2sh' or n == 2: + txin['num_sig'] = 1 + txin['x_pubkeys'] = [w[1]] + txin['pubkeys'] = [safe_parse_pubkey(w[1])] + txin['signatures'] = parse_sig([w[0]]) + if not txin.get('scriptSig'): # native segwit script + txin['type'] = 'p2wpkh' + txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0])) + else: raise UnknownTxinType() - txin['signatures'] = parse_sig(w[1:-1]) - txin['num_sig'] = m - txin['x_pubkeys'] = x_pubkeys - txin['pubkeys'] = pubkeys - txin['witnessScript'] = witnessScript - elif txin['type'] == 'p2wpkh-p2sh' or n == 2: - txin['num_sig'] = 1 - txin['x_pubkeys'] = [w[1]] - txin['pubkeys'] = [safe_parse_pubkey(w[1])] - txin['signatures'] = parse_sig([w[0]]) - else: - raise UnknownTxinType() + except UnknownTxinType: + txin['type'] = 'unknown' + # FIXME: GUI might show 'unknown' address (e.g. for a non-multisig p2wsh) + except BaseException: + txin['type'] = 'unknown' + traceback.print_exc(file=sys.stderr) + print_error('failed to parse witness', txin.get('witness')) + def parse_output(vds, i): d = {} @@ -513,20 +547,7 @@ def deserialize(raw): if is_segwit: for i in range(n_vin): txin = d['inputs'][i] - try: - 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']) + parse_witness(vds, txin) d['lockTime'] = vds.read_uint32() return d diff --git a/lib/util.py b/lib/util.py index b8e8ac8e5..9a99c9d4e 100644 --- a/lib/util.py +++ b/lib/util.py @@ -41,7 +41,6 @@ def inv_dict(d): 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): return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] @@ -58,17 +57,70 @@ class InvalidPassword(Exception): def __str__(self): 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. # However unlike other exceptions the user won't be informed. class UserCancelled(Exception): '''An exception that is suppressed from the user''' 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): def default(self, obj): from .transaction import Transaction if isinstance(obj, Transaction): 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) class PrintError(object): @@ -367,10 +419,7 @@ def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespa return result def timestamp_to_datetime(timestamp): - try: - return datetime.fromtimestamp(timestamp) - except: - return None + return datetime.fromtimestamp(timestamp) def format_time(timestamp): date = timestamp_to_datetime(timestamp) @@ -734,4 +783,31 @@ def setup_thread_excepthook(): self.run = run_with_except_hook - threading.Thread.__init__ = init \ No newline at end of file + 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) diff --git a/lib/verifier.py b/lib/verifier.py index 20e83fd2e..c2d0f1250 100644 --- a/lib/verifier.py +++ b/lib/verifier.py @@ -36,15 +36,22 @@ class SPV(ThreadJob): self.merkle_roots = {} def run(self): + interface = self.network.interface + if not interface: + return + blockchain = interface.blockchain + if not blockchain: + return lh = self.network.get_local_height() unverified = self.wallet.get_unverified_txs() for tx_hash, tx_height in unverified.items(): # do not request merkle branch before headers are available if (tx_height > 0) and (tx_height <= lh): - header = self.network.blockchain().read_header(tx_height) - if header is None and self.network.interface: + header = blockchain.read_header(tx_height) + if header is None: index = tx_height // 2016 - self.network.request_chunk(self.network.interface, index) + if index < len(blockchain.checkpoints): + self.network.request_chunk(interface, index) else: if tx_hash not in self.merkle_roots: request = ('blockchain.transaction.get_merkle', @@ -70,10 +77,18 @@ class SPV(ThreadJob): pos = merkle.get('pos') merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos) header = self.network.blockchain().read_header(tx_height) - if not header or header.get('merkle_root') != merkle_root: - # FIXME: we should make a fresh connection to a server to - # recover from this, as this TX will now never verify - self.print_error("merkle verification failed for", tx_hash) + # FIXME: if verification fails below, + # we should make a fresh connection to a server to + # recover from this, as this TX will now never verify + 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 # we passed all the tests self.merkle_roots[tx_hash] = merkle_root diff --git a/lib/wallet.py b/lib/wallet.py index 6a682debf..5d3916510 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -38,6 +38,7 @@ import traceback from functools import partial from collections import defaultdict from numbers import Number +from decimal import Decimal import sys @@ -77,9 +78,9 @@ TX_HEIGHT_UNCONFIRMED = 0 def relayfee(network): - RELAY_FEE = 1000 + from .simple_config import FEERATE_DEFAULT_RELAY 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) def dust_threshold(network): @@ -156,9 +157,18 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100): return tx -class UnrelatedTransactionException(Exception): - def __init__(self): - self.args = ("Transaction is unrelated to this wallet ", ) +class AddTransactionException(Exception): + pass + + +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): @@ -184,6 +194,7 @@ class Abstract_Wallet(PrintError): self.labels = storage.get('labels', {}) self.frozen_addresses = set(storage.get('frozen_addresses',[])) self.history = storage.get('addr_history',{}) # address -> list(txid, height) + self.fiat_value = storage.get('fiat_value', {}) self.load_keystore() self.load_addresses() @@ -206,7 +217,7 @@ class Abstract_Wallet(PrintError): self.up_to_date = False # 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.check_history() @@ -269,17 +280,17 @@ class Abstract_Wallet(PrintError): self.pruned_txo = {} self.spent_outpoints = {} self.history = {} + self.verified_tx = {} + self.transactions = {} self.save_transactions() @profiler def build_spent_outpoints(self): self.spent_outpoints = {} - for txid, tx in self.transactions.items(): - for txi in tx.inputs(): - ser = Transaction.get_outpoint_from_txin(txi) - if ser is None: - continue - self.spent_outpoints[ser] = txid + for txid, items in self.txi.items(): + for addr, l in items.items(): + for ser, v in l: + self.spent_outpoints[ser] = txid @profiler def check_history(self): @@ -320,6 +331,9 @@ class Abstract_Wallet(PrintError): def synchronize(self): pass + def is_deterministic(self): + return self.keystore.is_deterministic() + def set_up_to_date(self, up_to_date): with self.lock: self.up_to_date = up_to_date @@ -341,13 +355,37 @@ class Abstract_Wallet(PrintError): if old_text: self.labels.pop(name) changed = True - if changed: run_hook('set_label', self, name, text) self.storage.put('labels', self.labels) - 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): return address in self.get_addresses() @@ -359,23 +397,21 @@ class Abstract_Wallet(PrintError): def get_address_index(self, address): raise NotImplementedError() + def get_redeem_script(self, address): + return None + def export_private_key(self, address, password): - """ extended WIF format """ if self.is_watching_only(): return [] index = self.get_address_index(address) pk, compressed = self.keystore.get_private_key(index, password) - if self.txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: - pubkeys = self.get_public_keys(address) - redeem_script = self.pubkeys_to_redeem_script(pubkeys) - else: - redeem_script = None - return bitcoin.serialize_privkey(pk, compressed, self.txin_type), redeem_script - + txin_type = self.get_txin_type(address) + redeem_script = self.get_redeem_script(address) + serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type) + return serialized_privkey, redeem_script def get_public_keys(self, address): - sequence = self.get_address_index(address) - return self.get_pubkeys(*sequence) + return [self.get_public_key(address)] def add_unverified_tx(self, tx_hash, tx_height): if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \ @@ -467,6 +503,17 @@ class Abstract_Wallet(PrintError): delta += v 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): """ effect of tx on wallet """ addresses = self.get_addresses() @@ -671,14 +718,21 @@ class Abstract_Wallet(PrintError): def get_address_history(self, addr): 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: 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] h.append((tx_hash, tx_height)) 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, {}) for addr, l in dd.items(): for n, v, is_cb in l: @@ -686,6 +740,16 @@ class Abstract_Wallet(PrintError): self.print_error("found pay-to-pubkey address:", 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): """Returns a set of transaction hashes from the wallet history that are 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: continue # this outpoint (ser) has already been spent, by spending_tx - if spending_tx_hash not in self.transactions: - # can't find this txn: delete and ignore it - self.spent_outpoints.pop(ser) - continue + assert spending_tx_hash in self.transactions conflicting_txns |= {spending_tx_hash} txid = tx.txid() if txid in conflicting_txns: @@ -716,9 +777,24 @@ class Abstract_Wallet(PrintError): return conflicting_txns def add_transaction(self, tx_hash, tx): - is_coinbase = tx.inputs()[0]['type'] == 'coinbase' - related = False - 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: + # 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' + tx_height = self.get_tx_height(tx_hash)[0] + 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. # In case of a conflict, # 1. confirmed > mempool > local @@ -728,7 +804,6 @@ class Abstract_Wallet(PrintError): # or drop this txn conflicting_txns = self.get_conflicting_transactions(tx) if conflicting_txns: - tx_height = self.get_tx_height(tx_hash)[0] existing_mempool_txn = any( self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) for tx_hash2 in conflicting_txns) @@ -748,44 +823,34 @@ class Abstract_Wallet(PrintError): to_remove |= self.get_depending_transactions(conflicting_tx_hash) for tx_hash2 in to_remove: self.remove_transaction(tx_hash2) - # add inputs self.txi[tx_hash] = d = {} for txi in tx.inputs(): - addr = txi.get('address') + addr = self.get_txin_address(txi) if txi['type'] != 'coinbase': prevout_hash = txi['prevout_hash'] prevout_n = txi['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 if addr and self.is_mine(addr): - related = True dd = self.txo.get(prevout_hash, {}) for n, v, is_cb in dd.get(addr, []): if n == prevout_n: if d.get(addr) is None: d[addr] = [] d[addr].append((ser, v)) + # we only track is_mine spends + self.spent_outpoints[ser] = tx_hash break else: self.pruned_txo[ser] = tx_hash - # add outputs self.txo[tx_hash] = d = {} for n, txo in enumerate(tx.outputs()): + v = txo[2] ser = tx_hash + ':%d'%n - _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 + addr = self.get_txout_address(txo) if addr and self.is_mine(addr): - related = True if d.get(addr) is None: d[addr] = [] d[addr].append((n, v, is_coinbase)) @@ -797,30 +862,19 @@ class Abstract_Wallet(PrintError): if dd.get(addr) is None: dd[addr] = [] dd[addr].append((ser, v)) - - if not related: - raise UnrelatedTransactionException() - # save self.transactions[tx_hash] = tx return True def remove_transaction(self, tx_hash): def undo_spend(outpoint_to_txid_map): - if tx: - # if we have the tx, this should often be faster - for txi in tx.inputs(): - ser = Transaction.get_outpoint_from_txin(txi) + for addr, l in self.txi[tx_hash].items(): + for ser, v in l: 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: self.print_error("removing tx from history", tx_hash) - #tx = self.transactions.pop(tx_hash) - tx = self.transactions.get(tx_hash, None) + self.transactions.pop(tx_hash, None) undo_spend(self.pruned_txo) undo_spend(self.spent_outpoints) @@ -850,13 +904,17 @@ class Abstract_Wallet(PrintError): def receive_history_callback(self, addr, hist, tx_fees): with self.lock: - old_hist = self.history.get(addr, []) + old_hist = self.get_address_history(addr) for tx_hash, height in old_hist: if (tx_hash, height) not in hist: # make tx local self.unverified_tx.pop(tx_hash, None) self.verified_tx.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 for tx_hash, tx_height in hist: @@ -914,6 +972,105 @@ class Abstract_Wallet(PrintError): 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): label = self.labels.get(tx_hash, '') if label is '': @@ -989,8 +1146,9 @@ class Abstract_Wallet(PrintError): if fixed_fee is None and config.fee_per_kb() is None: raise NoDynamicFeeEstimates() - for item in inputs: - self.add_input_info(item) + if not is_sweep: + for item in inputs: + self.add_input_info(item) # change address if change_addr: @@ -1006,7 +1164,8 @@ class Abstract_Wallet(PrintError): if not change_addrs: change_addrs = [random.choice(addrs)] else: - change_addrs = [inputs[0]['address']] + # coin_chooser will set change address + change_addrs = [] # Fee estimator if fixed_fee is None: @@ -1534,6 +1693,63 @@ class Abstract_Wallet(PrintError): children |= self.get_depending_transactions(other_hash) 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): # wallet with a single keystore @@ -1705,12 +1921,10 @@ class Imported_Wallet(Simple_Wallet): self.add_address(addr) return addr - def export_private_key(self, address, password): + def get_redeem_script(self, address): d = self.addresses[address] - pubkey = d['pubkey'] redeem_script = d['redeem_script'] - sec = pw_decode(self.keystore.keypairs[pubkey], password) - return sec, redeem_script + return redeem_script def get_txin_type(self, address): return self.addresses[address].get('type', 'address') @@ -1748,9 +1962,6 @@ class Deterministic_Wallet(Abstract_Wallet): def has_seed(self): return self.keystore.has_seed() - def is_deterministic(self): - return self.keystore.is_deterministic() - def get_receiving_addresses(self): return self.receiving_addresses @@ -1812,15 +2023,16 @@ class Deterministic_Wallet(Abstract_Wallet): def create_new_address(self, for_change=False): assert type(for_change) is bool - addr_list = self.change_addresses if for_change else self.receiving_addresses - n = len(addr_list) - x = self.derive_pubkeys(for_change, n) - address = self.pubkeys_to_address(x) - addr_list.append(address) - self._addr_to_addr_index[address] = (for_change, n) - self.save_addresses() - self.add_address(address) - return address + with self.lock: + addr_list = self.change_addresses if for_change else self.receiving_addresses + n = len(addr_list) + x = self.derive_pubkeys(for_change, n) + address = self.pubkeys_to_address(x) + addr_list.append(address) + self._addr_to_addr_index[address] = (for_change, n) + self.save_addresses() + self.add_address(address) + return address def synchronize_sequence(self, for_change): limit = self.gap_limit_for_change if for_change else self.gap_limit @@ -1836,16 +2048,8 @@ class Deterministic_Wallet(Abstract_Wallet): def synchronize(self): with self.lock: - if self.is_deterministic(): - self.synchronize_sequence(False) - 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) + self.synchronize_sequence(False) + self.synchronize_sequence(True) def is_beyond_limit(self, 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): 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): derivation = self.get_address_index(address) x_pubkey = self.keystore.get_xpubkey(*derivation) @@ -1938,6 +2139,10 @@ class Multisig_Wallet(Deterministic_Wallet): def get_pubkeys(self, 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): redeem_script = self.pubkeys_to_redeem_script(pubkeys) 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): 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): return [k.derive_pubkey(c, i) for k in self.get_keystores()] diff --git a/plugins/digitalbitbox/cmdline.py b/plugins/digitalbitbox/cmdline.py index 7902c98a9..82192cfda 100644 --- a/plugins/digitalbitbox/cmdline.py +++ b/plugins/digitalbitbox/cmdline.py @@ -9,3 +9,6 @@ class Plugin(DigitalBitboxPlugin): if not isinstance(keystore, self.keystore_class): return keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/plugins/digitalbitbox/digitalbitbox.py b/plugins/digitalbitbox/digitalbitbox.py index c57e395dd..2f63fda4d 100644 --- a/plugins/digitalbitbox/digitalbitbox.py +++ b/plugins/digitalbitbox/digitalbitbox.py @@ -661,7 +661,8 @@ class DigitalBitboxPlugin(HW_PluginBase): def create_client(self, device, handler): if device.interface_number == 0 or device.usage_page == 0xffff: - self.handler = handler + if handler: + self.handler = handler client = self.get_dbb_device(device) if client is not None: client = DigitalBitbox_Client(self, client) diff --git a/plugins/hw_wallet/qt.py b/plugins/hw_wallet/qt.py index a9f7291c4..d2a3bb5f3 100644 --- a/plugins/hw_wallet/qt.py +++ b/plugins/hw_wallet/qt.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- mode: python -*- # # Electrum - lightweight Bitcoin client @@ -184,10 +184,12 @@ class QtPluginBase(object): if not isinstance(keystore, self.keystore_class): continue if not self.libraries_available: - window.show_error( - _("Cannot find python library for") + " '%s'.\n" % self.name \ - + _("Make sure you install it with python3") - ) + if hasattr(self, 'libraries_available_message'): + message = self.libraries_available_message + '\n' + else: + message = _("Cannot find python library for") + " '%s'.\n" % self.name + message += _("Make sure you install it with python3") + window.show_error(message) return tooltip = self.device + '\n' + (keystore.label or 'unnamed') cb = partial(self.show_settings_dialog, window, keystore) diff --git a/plugins/keepkey/cmdline.py b/plugins/keepkey/cmdline.py index cd30bc0cc..4262b7019 100644 --- a/plugins/keepkey/cmdline.py +++ b/plugins/keepkey/cmdline.py @@ -9,3 +9,6 @@ class Plugin(KeepKeyPlugin): if not isinstance(keystore, self.keystore_class): return keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/plugins/ledger/cmdline.py b/plugins/ledger/cmdline.py index b0b252ac8..5d8c9f46d 100644 --- a/plugins/ledger/cmdline.py +++ b/plugins/ledger/cmdline.py @@ -9,3 +9,6 @@ class Plugin(LedgerPlugin): if not isinstance(keystore, self.keystore_class): return keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index cf56ee973..3d34bc9da 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -10,7 +10,7 @@ from electrum.plugins import BasePlugin from electrum.keystore import Hardware_KeyStore from electrum.transaction import Transaction 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: import hid @@ -57,9 +57,6 @@ class Ledger_Client(): def i4b(self, x): return pack('>I', x) - def versiontuple(self, v): - return tuple(map(int, (v.split(".")))) - def test_pin_unlocked(func): """Function decorator to test the Ledger for being unlocked, and if not, raise a human-readable exception. @@ -140,9 +137,9 @@ class Ledger_Client(): try: firmwareInfo = self.dongleObject.getFirmwareVersion() firmware = firmwareInfo['version'] - self.multiOutputSupported = self.versiontuple(firmware) >= self.versiontuple(MULTI_OUTPUT_SUPPORT) - self.nativeSegwitSupported = self.versiontuple(firmware) >= self.versiontuple(SEGWIT_SUPPORT) - self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and self.versiontuple(firmware) >= self.versiontuple(SEGWIT_SUPPORT_SPECIAL)) + self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT) + self.nativeSegwitSupported = versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT) + self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL)) if not checkFirmware(firmwareInfo): self.dongleObject.dongle.close() @@ -519,7 +516,8 @@ class LedgerPlugin(HW_PluginBase): return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) def create_client(self, device, handler): - self.handler = handler + if handler: + self.handler = handler client = self.get_btchip_device(device) if client is not None: diff --git a/plugins/trezor/cmdline.py b/plugins/trezor/cmdline.py index 9149eeee4..630578acc 100644 --- a/plugins/trezor/cmdline.py +++ b/plugins/trezor/cmdline.py @@ -9,3 +9,6 @@ class Plugin(TrezorPlugin): if not isinstance(keystore, self.keystore_class): return keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py index df3c96f88..f80346c71 100644 --- a/plugins/trezor/trezor.py +++ b/plugins/trezor/trezor.py @@ -2,7 +2,7 @@ import threading 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, TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants) from electrum.i18n import _ @@ -86,6 +86,7 @@ class TrezorPlugin(HW_PluginBase): libraries_URL = 'https://github.com/trezor/python-trezor' minimum_firmware = (1, 5, 2) keystore_class = TrezorKeyStore + minimum_library = (0, 9, 0) MAX_LABEL_LEN = 32 @@ -96,6 +97,19 @@ class TrezorPlugin(HW_PluginBase): try: # Minimal test if python-trezor is installed 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 except ImportError: self.libraries_available = False diff --git a/pubkeys/bauerj.asc b/pubkeys/bauerj.asc new file mode 100644 index 000000000..b50bed1b1 --- /dev/null +++ b/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----- diff --git a/setup.py b/setup.py index 3375061b1..63581a614 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,10 @@ import platform import imp 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() 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): 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']: parser = argparse.ArgumentParser() @@ -38,17 +41,7 @@ if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']: setup( name="Electrum", version=version.ELECTRUM_VERSION, - install_requires=[ - 'pyaes>=0.1a1', - 'ecdsa>=0.9', - 'pbkdf2', - 'requests', - 'qrcode', - 'protobuf', - 'dnspython', - 'jsonrpclib-pelix', - 'PySocks>=1.6.6', - ], + install_requires=requirements, extras_require={ 'hardware': requirements_hw, },