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,
},