Merge branch 'master' into master
This commit is contained in:
commit
4694a65592
|
@ -0,0 +1,2 @@
|
||||||
|
<!-- Note: This website is for bug reports, not general questions.
|
||||||
|
Do not post issues about non-bitcoin versions of Electrum. -->
|
21
.travis.yml
21
.travis.yml
|
@ -4,11 +4,28 @@ python:
|
||||||
- 3.5
|
- 3.5
|
||||||
- 3.6
|
- 3.6
|
||||||
install:
|
install:
|
||||||
- pip install -r requirements_travis.txt
|
- pip install -r contrib/requirements/requirements-travis.txt
|
||||||
cache:
|
cache:
|
||||||
- pip
|
- pip: true
|
||||||
|
- directories:
|
||||||
|
- /tmp/electrum-build
|
||||||
script:
|
script:
|
||||||
- tox
|
- tox
|
||||||
after_success:
|
after_success:
|
||||||
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi
|
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi
|
||||||
- coveralls
|
- coveralls
|
||||||
|
jobs:
|
||||||
|
include:
|
||||||
|
- stage: windows build
|
||||||
|
sudo: true
|
||||||
|
python: 3.5
|
||||||
|
install:
|
||||||
|
- sudo dpkg --add-architecture i386
|
||||||
|
- wget -nc https://dl.winehq.org/wine-builds/Release.key
|
||||||
|
- sudo apt-key add Release.key
|
||||||
|
- sudo apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/
|
||||||
|
- sudo apt-get update -qq
|
||||||
|
- sudo apt-get install -qq winehq-stable dirmngr gnupg2 p7zip-full
|
||||||
|
before_script: ls -lah /tmp/electrum-build
|
||||||
|
script: ./contrib/build-wine/build.sh
|
||||||
|
after_success: true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
FROM ubuntu:18.04
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
ENV VERSION 1.0.6
|
ENV VERSION 1.0.7
|
||||||
|
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
|
|
16
README.rst
16
README.rst
|
@ -101,30 +101,18 @@ Run the docker image::
|
||||||
./run-docker.sh
|
./run-docker.sh
|
||||||
|
|
||||||
|
|
||||||
Building Releases
|
See `contrib/build-osx/`.
|
||||||
=================
|
|
||||||
|
|
||||||
|
|
||||||
MacOS
|
|
||||||
------
|
|
||||||
|
|
||||||
Simply - ::
|
|
||||||
|
|
||||||
./setup-mac.sh
|
|
||||||
|
|
||||||
sudo ./create-dmg.sh
|
|
||||||
|
|
||||||
Windows
|
Windows
|
||||||
-------
|
-------
|
||||||
|
|
||||||
See `contrib/build-wine/README` file.
|
See `contrib/build-wine/`.
|
||||||
|
|
||||||
|
|
||||||
Android
|
Android
|
||||||
-------
|
-------
|
||||||
|
|
||||||
See `gui/kivy/Readme.txt` file.
|
See `gui/kivy/Readme.txt` file.
|
||||||
UPSTREAM PATCH: https://github.com/spesmilo/electrum/blob/master/gui/kivy/Readme.md
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,80 @@
|
||||||
|
|
||||||
|
# Release 3.1 - (March 5, 2018)
|
||||||
|
|
||||||
|
* Memory-pool based fee estimation. Dynamic fees can target a desired
|
||||||
|
depth in the memory pool. This feature is optional, and ETA-based
|
||||||
|
estimates from Bitcoin Core are still available. Note that miners
|
||||||
|
could exploit this feature, if they conspired and filled the memory
|
||||||
|
pool with expensive transactions that never get mined. However,
|
||||||
|
since the Electrum client already trusts an Electrum server with
|
||||||
|
fee estimates, activating this feature does not introduce any new
|
||||||
|
vulnerability. In addition, the client uses a hard threshold to
|
||||||
|
protect itself from servers sending excessive fee estimates. In
|
||||||
|
practice, ETA-based estimates have resulted in sticky fees, and
|
||||||
|
caused many users to overpay for transactions. Advanced users tend
|
||||||
|
to visit (and trust) websites that display memory-pool data in
|
||||||
|
order to set their fees.
|
||||||
|
* Capital gains: For each outgoing transaction, the difference
|
||||||
|
between the acquisition and liquidation prices of outgoing coins is
|
||||||
|
displayed in the wallet history. By default, historical exchange
|
||||||
|
rates are used to compute acquisition and liquidation prices. These
|
||||||
|
values can also be entered manually, in order to match the actual
|
||||||
|
price realized by the user. The order of liquidation of coins is
|
||||||
|
the natural order defined by the blockchain; this results in
|
||||||
|
capital gain values that are invariant to changes in the set of
|
||||||
|
addresses that are in the wallet. Any other ordering strategy (such
|
||||||
|
as FIFO, LIFO) would result in capital gain values that depend on
|
||||||
|
the presence of other addresses in the wallet.
|
||||||
|
* Local transactions: Transactions can be saved in the wallet without
|
||||||
|
being broadcast. The inputs of local transactions are considered as
|
||||||
|
spent, and their change outputs can be re-used in subsequent
|
||||||
|
transactions. This can be combined with cold storage, in order to
|
||||||
|
create several transactions before broadcasting them. Outgoing
|
||||||
|
transactions that have been removed from the memory pool are also
|
||||||
|
saved in the wallet, and can be broadcast again.
|
||||||
|
* Checkpoints: The initial download of a headers file was replaced
|
||||||
|
with hardcoded checkpoints. The wallet uses one checkpoint per
|
||||||
|
retargeting period. The headers for a retargeting period are
|
||||||
|
downloaded only if transactions need to be verified in this period.
|
||||||
|
* The 'privacy' and 'priority' coin selection policies have been
|
||||||
|
merged into one. Previously, the 'privacy' policy has been unusable
|
||||||
|
because it was was not prioritizing confirmed coins. The new policy
|
||||||
|
is similar to 'privacy', except that it de-prioritizes addresses
|
||||||
|
that have unconfirmed coins.
|
||||||
|
* The 'Send' tab of the Qt GUI displays how transaction fees are
|
||||||
|
computed from transaction size.
|
||||||
|
* The wallet history can be filtered by time interval.
|
||||||
|
* Replace-by-fee is enabled by default. Note that this might cause
|
||||||
|
some issues with wallets that do not display RBF transactions until
|
||||||
|
they are confirmed.
|
||||||
|
* Watching-only wallets and hardware wallets can be encrypted.
|
||||||
|
* Semi-automated crash reporting
|
||||||
|
* The SSL checkbox option was removed from the GUI.
|
||||||
|
* The Trezor T hardware wallet is now supported.
|
||||||
|
* BIP84: native segwit p2wpkh scripts for bip39 seeds and hardware
|
||||||
|
wallets can now be created when specifying a BIP84 derivation
|
||||||
|
path. This is usable with Trezor and Ledger.
|
||||||
|
* Windows: the binaries now include ZBar, and QR code scanning should work.
|
||||||
|
* The Wallet Import Format (WIF) for private keys that was extended in 3.0
|
||||||
|
is changed. Keys in the previous format can be imported, compatibility
|
||||||
|
is maintained. Newly exported keys will be serialized as
|
||||||
|
"script_type:original_wif_format_key".
|
||||||
|
* BIP32 master keys for testnet once again have different version bytes than
|
||||||
|
on mainnet. For the mainnet prefixes {x,y,Y,z,Z}|{pub,prv}, the
|
||||||
|
corresponding testnet prefixes are {t,u,U,v,V}|{pub,prv}.
|
||||||
|
More details and exact version bytes are specified at:
|
||||||
|
https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst
|
||||||
|
Note that due to this change, testnet wallet files created with previous
|
||||||
|
versions of Electrum must be considered broken, and they need to be
|
||||||
|
recreated from seed words.
|
||||||
|
* A new version of the Electrum protocol is required by the client
|
||||||
|
(version 1.2). Servers using older versions of the protocol will
|
||||||
|
not be displayed in the GUI.
|
||||||
|
|
||||||
|
|
||||||
|
# Release 3.0.6 :
|
||||||
|
* Fix transaction parsing bug #3788
|
||||||
|
|
||||||
# Release 3.0.5 : (Security update)
|
# Release 3.0.5 : (Security update)
|
||||||
|
|
||||||
This is a follow-up to the 3.0.4 release, which did not completely fix
|
This is a follow-up to the 3.0.4 release, which did not completely fix
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
Building Mac OS binaries
|
||||||
|
========================
|
||||||
|
|
||||||
|
This guide explains how to build Electrum binaries for macOS systems.
|
||||||
|
We build our binaries on El Capitan (10.11.6) as building it on High Sierra
|
||||||
|
makes the binaries incompatible with older versions.
|
||||||
|
|
||||||
|
This assumes that the Xcode command line tools (and thus git) are already installed.
|
||||||
|
|
||||||
|
|
||||||
|
## 1. Run the script
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
./make_osx
|
||||||
|
|
||||||
|
## 2. Done
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/bin/bash
|
||||||
|
RED='\033[0;31m'
|
||||||
|
BLUE='\033[0,34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
function info {
|
||||||
|
printf "\r💬 ${BLUE}INFO:${NC} ${1}\n"
|
||||||
|
}
|
||||||
|
function fail {
|
||||||
|
printf "\r🗯 ${RED}ERROR:${NC} ${1}\n"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
build_dir=$(dirname "$0")
|
||||||
|
test -n "$build_dir" -a -d "$build_dir" || exit
|
||||||
|
cd $build_dir/../..
|
||||||
|
|
||||||
|
export PYTHONHASHSEED=22
|
||||||
|
VERSION=`git describe --tags`
|
||||||
|
|
||||||
|
# Paramterize
|
||||||
|
PYTHON_VERSION=3.6.4
|
||||||
|
BUILDDIR=/tmp/electrum-build
|
||||||
|
PACKAGE=Electrum-ZCL
|
||||||
|
GIT_REPO=https://github.com/z-classic/electrum-zcl
|
||||||
|
|
||||||
|
|
||||||
|
info "Installing Python $PYTHON_VERSION"
|
||||||
|
export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH"
|
||||||
|
if [ -d "~/.pyenv" ]; then
|
||||||
|
pyenv update
|
||||||
|
else
|
||||||
|
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install -s $PYTHON_VERSION && \
|
||||||
|
pyenv global $PYTHON_VERSION || \
|
||||||
|
fail "Unable to use Python $PYTHON_VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
info "Installing pyinstaller"
|
||||||
|
python3 -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952 -I --user || fail "Could not install pyinstaller"
|
||||||
|
|
||||||
|
info "Using these versions for building $PACKAGE:"
|
||||||
|
sw_vers
|
||||||
|
python3 --version
|
||||||
|
echo -n "Pyinstaller "
|
||||||
|
pyinstaller --version
|
||||||
|
|
||||||
|
rm -rf ./dist
|
||||||
|
|
||||||
|
|
||||||
|
rm -rf $BUILDDIR > /dev/null 2>&1
|
||||||
|
mkdir $BUILDDIR
|
||||||
|
|
||||||
|
info "Downloading icons and locale..."
|
||||||
|
for repo in icons locale; do
|
||||||
|
git clone $GIT_REPO-$repo $BUILDDIR/electrum-$repo
|
||||||
|
done
|
||||||
|
|
||||||
|
cp -R $BUILDDIR/electrum-locale/locale/ ./lib/locale/
|
||||||
|
cp $BUILDDIR/electrum-icons/icons_rc.py ./gui/qt/
|
||||||
|
|
||||||
|
|
||||||
|
info "Downloading libusb..."
|
||||||
|
curl https://homebrew.bintray.com/bottles/libusb-1.0.21.el_capitan.bottle.tar.gz | \
|
||||||
|
tar xz --directory $BUILDDIR
|
||||||
|
cp $BUILDDIR/libusb/1.0.21/lib/libusb-1.0.dylib contrib/build-osx
|
||||||
|
|
||||||
|
info "Installing requirements..."
|
||||||
|
python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \
|
||||||
|
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user || \
|
||||||
|
fail "Could not install requirements"
|
||||||
|
|
||||||
|
info "Installing hardware wallet requirements..."
|
||||||
|
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --user || \
|
||||||
|
fail "Could not install hardware wallet requirements"
|
||||||
|
|
||||||
|
info "Building $PACKAGE..."
|
||||||
|
python3 setup.py install --user > /dev/null || fail "Could not build $PACKAGE"
|
||||||
|
|
||||||
|
info "Building binary"
|
||||||
|
pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary"
|
||||||
|
|
||||||
|
info "Creating .DMG"
|
||||||
|
hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/Electrum-ZCL-$VERSION.dmg || fail "Could not create .DMG"
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- mode: python -*-
|
||||||
|
|
||||||
|
from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
PACKAGE='Electrum-ZCL'
|
||||||
|
PYPKG='electrum'
|
||||||
|
MAIN_SCRIPT='electrum'
|
||||||
|
ICONS_FILE='electrum.icns'
|
||||||
|
|
||||||
|
for i, x in enumerate(sys.argv):
|
||||||
|
if x == '--name':
|
||||||
|
VERSION = sys.argv[i+1]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise BaseException('no version')
|
||||||
|
|
||||||
|
electrum = os.path.abspath(".") + "/"
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
# see https://github.com/pyinstaller/pyinstaller/issues/2005
|
||||||
|
hiddenimports = []
|
||||||
|
hiddenimports += collect_submodules('trezorlib')
|
||||||
|
hiddenimports += collect_submodules('btchip')
|
||||||
|
hiddenimports += collect_submodules('keepkeylib')
|
||||||
|
hiddenimports += collect_submodules('websocket')
|
||||||
|
|
||||||
|
datas = [
|
||||||
|
(electrum+'lib/currencies.json', PYPKG),
|
||||||
|
(electrum+'lib/servers.json', PYPKG),
|
||||||
|
(electrum+'lib/checkpoints.json', PYPKG),
|
||||||
|
(electrum+'lib/servers_testnet.json', PYPKG),
|
||||||
|
(electrum+'lib/checkpoints_testnet.json', PYPKG),
|
||||||
|
(electrum+'lib/wordlist/english.txt', PYPKG + '/wordlist'),
|
||||||
|
(electrum+'lib/locale', PYPKG + '/locale'),
|
||||||
|
(electrum+'plugins', PYPKG + '_plugins'),
|
||||||
|
]
|
||||||
|
datas += collect_data_files('trezorlib')
|
||||||
|
datas += collect_data_files('btchip')
|
||||||
|
datas += collect_data_files('keepkeylib')
|
||||||
|
|
||||||
|
# Add libusb so Trezor will work
|
||||||
|
binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
|
||||||
|
|
||||||
|
# Workaround for "Retro Look":
|
||||||
|
binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
|
||||||
|
|
||||||
|
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
|
||||||
|
a = Analysis([electrum+MAIN_SCRIPT,
|
||||||
|
electrum+'gui/qt/main_window.py',
|
||||||
|
electrum+'gui/text.py',
|
||||||
|
electrum+'lib/util.py',
|
||||||
|
electrum+'lib/wallet.py',
|
||||||
|
electrum+'lib/simple_config.py',
|
||||||
|
electrum+'lib/bitcoin.py',
|
||||||
|
electrum+'lib/dnssec.py',
|
||||||
|
electrum+'lib/commands.py',
|
||||||
|
electrum+'plugins/cosigner_pool/qt.py',
|
||||||
|
electrum+'plugins/email_requests/qt.py',
|
||||||
|
electrum+'plugins/trezor/client.py',
|
||||||
|
electrum+'plugins/trezor/qt.py',
|
||||||
|
electrum+'plugins/keepkey/qt.py',
|
||||||
|
electrum+'plugins/ledger/qt.py',
|
||||||
|
],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[])
|
||||||
|
|
||||||
|
# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal
|
||||||
|
for d in a.datas:
|
||||||
|
if 'pyconfig' in d[0]:
|
||||||
|
a.datas.remove(d)
|
||||||
|
break
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
|
exe = EXE(pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
name=PACKAGE,
|
||||||
|
debug=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
icon=electrum+ICONS_FILE,
|
||||||
|
console=False)
|
||||||
|
|
||||||
|
app = BUNDLE(exe,
|
||||||
|
version = VERSION,
|
||||||
|
name=PACKAGE + '.app',
|
||||||
|
icon=electrum+ICONS_FILE,
|
||||||
|
bundle_identifier=None,
|
||||||
|
info_plist = {
|
||||||
|
'NSHighResolutionCapable':'True'
|
||||||
|
}
|
||||||
|
)
|
|
@ -2,7 +2,7 @@ Windows Binary Builds
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine.
|
These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine.
|
||||||
Produced binaries are deterministic so you should be able to generate binaries that match the official releases.
|
Produced binaries are deterministic, so you should be able to generate binaries that match the official releases.
|
||||||
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
@ -12,6 +12,7 @@ Usage:
|
||||||
|
|
||||||
- dirmngr
|
- dirmngr
|
||||||
- gpg
|
- gpg
|
||||||
|
- 7Zip
|
||||||
- Wine (>= v2)
|
- Wine (>= v2)
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,8 +20,7 @@ For example:
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$ sudo apt-get install wine-development dirmngr gnupg2
|
$ sudo apt-get install wine-development dirmngr gnupg2 p7zip-full
|
||||||
$ sudo ln -sf /usr/bin/wine-development /usr/local/bin/wine
|
|
||||||
$ wine --version
|
$ wine --version
|
||||||
wine-2.0 (Debian 2.0-3+b2)
|
wine-2.0 (Debian 2.0-3+b2)
|
||||||
```
|
```
|
||||||
|
|
|
@ -16,6 +16,7 @@ PYTHON="wine $PYHOME/python.exe -OO -B"
|
||||||
cd `dirname $0`
|
cd `dirname $0`
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
mkdir -p tmp
|
||||||
cd tmp
|
cd tmp
|
||||||
|
|
||||||
for repo in electrum electrum-locale electrum-icons; do
|
for repo in electrum electrum-locale electrum-icons; do
|
||||||
|
@ -55,7 +56,9 @@ cp -r electrum-locale/locale $WINEPREFIX/drive_c/electrum/lib/
|
||||||
cp electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/
|
cp electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/
|
||||||
|
|
||||||
# Install frozen dependencies
|
# Install frozen dependencies
|
||||||
$PYTHON -m pip install -r ../../requirements.txt
|
$PYTHON -m pip install -r ../../deterministic-build/requirements.txt
|
||||||
|
|
||||||
|
$PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt
|
||||||
|
|
||||||
pushd $WINEPREFIX/drive_c/electrum
|
pushd $WINEPREFIX/drive_c/electrum
|
||||||
$PYTHON setup.py install
|
$PYTHON setup.py install
|
||||||
|
|
|
@ -7,14 +7,13 @@ if [ ! -z "$1" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
here=$(dirname "$0")
|
here=$(dirname "$0")
|
||||||
|
test -n "$here" -a -d "$here" || exit
|
||||||
|
|
||||||
echo "Clearing $here/build and $here/dist..."
|
echo "Clearing $here/build and $here/dist..."
|
||||||
rm $here/build/* -rf
|
rm "$here"/build/* -rf
|
||||||
rm $here/dist/* -rf
|
rm "$here"/dist/* -rf
|
||||||
|
|
||||||
$here/prepare-wine.sh && \
|
$here/prepare-wine.sh || exit 1
|
||||||
$here/prepare-pyinstaller.sh && \
|
|
||||||
$here/prepare-hw.sh || exit 1
|
|
||||||
|
|
||||||
echo "Resetting modification time in C:\Python..."
|
echo "Resetting modification time in C:\Python..."
|
||||||
# (Because of some bugs in pyinstaller)
|
# (Because of some bugs in pyinstaller)
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
|
|
||||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
for i, x in enumerate(sys.argv):
|
for i, x in enumerate(sys.argv):
|
||||||
if x == '--name':
|
if x == '--name':
|
||||||
cmdline_name = sys.argv[i+1]
|
cmdline_name = sys.argv[i+1]
|
||||||
|
@ -11,13 +10,21 @@ for i, x in enumerate(sys.argv):
|
||||||
else:
|
else:
|
||||||
raise BaseException('no name')
|
raise BaseException('no name')
|
||||||
|
|
||||||
home = os.getcwd()+'\\'
|
|
||||||
|
home = 'C:\\electrum-zcl\\'
|
||||||
|
|
||||||
# see https://github.com/pyinstaller/pyinstaller/issues/2005
|
# see https://github.com/pyinstaller/pyinstaller/issues/2005
|
||||||
hiddenimports = []
|
hiddenimports = []
|
||||||
# hiddenimports += collect_submodules('trezorlib')
|
hiddenimports += collect_submodules('trezorlib')
|
||||||
# hiddenimports += collect_submodules('btchip')
|
hiddenimports += collect_submodules('btchip')
|
||||||
# hiddenimports += collect_submodules('keepkeylib')
|
hiddenimports += collect_submodules('keepkeylib')
|
||||||
|
hiddenimports += collect_submodules('websocket')
|
||||||
|
|
||||||
|
# Add libusb binary
|
||||||
|
binaries = [("c:/python3.5.4/libusb-1.0.dll", ".")]
|
||||||
|
|
||||||
|
# Workaround for "Retro Look":
|
||||||
|
binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]]
|
||||||
|
|
||||||
datas = [
|
datas = [
|
||||||
(home+'lib/currencies.json', 'electrum'),
|
(home+'lib/currencies.json', 'electrum'),
|
||||||
|
@ -26,12 +33,13 @@ datas = [
|
||||||
(home+'lib/servers_testnet.json', 'electrum'),
|
(home+'lib/servers_testnet.json', 'electrum'),
|
||||||
(home+'lib/checkpoints_testnet.json', 'electrum'),
|
(home+'lib/checkpoints_testnet.json', 'electrum'),
|
||||||
(home+'lib/wordlist/english.txt', 'electrum/wordlist'),
|
(home+'lib/wordlist/english.txt', 'electrum/wordlist'),
|
||||||
# (home+'lib/locale', 'electrum/locale'),
|
(home+'lib/locale', 'electrum-zcl/locale'),
|
||||||
(home+'plugins', 'electrum_plugins'),
|
(home+'plugins', 'electrum_plugins'),
|
||||||
|
('C:\\Program Files (x86)\\ZBar\\bin\\', '.')
|
||||||
]
|
]
|
||||||
# datas += collect_data_files('trezorlib')
|
datas += collect_data_files('trezorlib')
|
||||||
# datas += collect_data_files('btchip')
|
datas += collect_data_files('btchip')
|
||||||
# datas += collect_data_files('keepkeylib')
|
datas += collect_data_files('keepkeylib')
|
||||||
|
|
||||||
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
|
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
|
||||||
a = Analysis([home+'electrum-zcl',
|
a = Analysis([home+'electrum-zcl',
|
||||||
|
@ -45,12 +53,13 @@ a = Analysis([home+'electrum-zcl',
|
||||||
home+'lib/commands.py',
|
home+'lib/commands.py',
|
||||||
home+'plugins/cosigner_pool/qt.py',
|
home+'plugins/cosigner_pool/qt.py',
|
||||||
home+'plugins/email_requests/qt.py',
|
home+'plugins/email_requests/qt.py',
|
||||||
#home+'plugins/trezor/client.py',
|
home+'plugins/trezor/client.py',
|
||||||
#home+'plugins/trezor/qt.py',
|
home+'plugins/trezor/qt.py',
|
||||||
#home+'plugins/keepkey/qt.py',
|
home+'plugins/keepkey/qt.py',
|
||||||
#home+'plugins/ledger/qt.py',
|
home+'plugins/ledger/qt.py',
|
||||||
#home+'packages/requests/utils.py'
|
#home+'packages/requests/utils.py'
|
||||||
],
|
],
|
||||||
|
binaries=binaries,
|
||||||
datas=datas,
|
datas=datas,
|
||||||
#pathex=[home+'lib', home+'gui', home+'plugins'],
|
#pathex=[home+'lib', home+'gui', home+'plugins'],
|
||||||
hiddenimports=hiddenimports,
|
hiddenimports=hiddenimports,
|
||||||
|
@ -85,40 +94,40 @@ exe_standalone = EXE(
|
||||||
console=False)
|
console=False)
|
||||||
# console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used
|
# console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used
|
||||||
|
|
||||||
# exe_portable = EXE(
|
exe_portable = EXE(
|
||||||
# pyz,
|
pyz,
|
||||||
# a.scripts,
|
a.scripts,
|
||||||
# a.binaries,
|
a.binaries,
|
||||||
# a.datas,
|
a.datas + [ ('is_portable', 'README.md', 'DATA' ) ],
|
||||||
# name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + "-portable.exe"),
|
name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + "-portable.exe"),
|
||||||
# debug=False,
|
debug=False,
|
||||||
# strip=None,
|
strip=None,
|
||||||
# upx=False,
|
upx=False,
|
||||||
# icon=home+'icons/electrum.ico',
|
icon=home+'icons/electrum.ico',
|
||||||
# console=False)
|
console=False)
|
||||||
|
|
||||||
# #####
|
#####
|
||||||
# # exe and separate files that NSIS uses to build installer "setup" exe
|
# exe and separate files that NSIS uses to build installer "setup" exe
|
||||||
|
|
||||||
# exe_dependent = EXE(
|
exe_dependent = EXE(
|
||||||
# pyz,
|
pyz,
|
||||||
# a.scripts,
|
a.scripts,
|
||||||
# exclude_binaries=True,
|
exclude_binaries=True,
|
||||||
# name=os.path.join('build\\pyi.win32\\electrum', cmdline_name),
|
name=os.path.join('build\\pyi.win32\\electrum', cmdline_name),
|
||||||
# debug=False,
|
debug=False,
|
||||||
# strip=None,
|
strip=None,
|
||||||
# upx=False,
|
upx=False,
|
||||||
# icon=home+'icons/electrum.ico',
|
icon=home+'icons/electrum.ico',
|
||||||
# console=False)
|
console=False)
|
||||||
|
|
||||||
# coll = COLLECT(
|
coll = COLLECT(
|
||||||
# exe_dependent,
|
exe_dependent,
|
||||||
# a.binaries,
|
a.binaries,
|
||||||
# a.zipfiles,
|
a.zipfiles,
|
||||||
# a.datas,
|
a.datas,
|
||||||
# strip=None,
|
strip=None,
|
||||||
# upx=True,
|
upx=True,
|
||||||
# debug=False,
|
debug=False,
|
||||||
# icon=home+'icons/electrum.ico',
|
icon=home+'icons/electrum.ico',
|
||||||
# console=False,
|
console=False,
|
||||||
# name=os.path.join('dist', 'electrum'))
|
name=os.path.join('dist', 'electrum'))
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
TREZOR_GIT_URL=https://github.com/trezor/python-trezor.git
|
|
||||||
KEEPKEY_GIT_URL=https://github.com/keepkey/python-keepkey.git
|
|
||||||
BTCHIP_GIT_URL=https://github.com/LedgerHQ/btchip-python.git
|
|
||||||
|
|
||||||
BRANCH=master
|
|
||||||
|
|
||||||
PYTHON_VERSION=3.5.4
|
|
||||||
|
|
||||||
# These settings probably don't need any change
|
|
||||||
export WINEPREFIX=/opt/wine64
|
|
||||||
|
|
||||||
PYHOME=c:/python$PYTHON_VERSION
|
|
||||||
PYTHON="wine $PYHOME/python.exe -OO -B"
|
|
||||||
|
|
||||||
# Let's begin!
|
|
||||||
cd `dirname $0`
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd tmp
|
|
||||||
|
|
||||||
$PYTHON -m pip install setuptools --upgrade
|
|
||||||
$PYTHON -m pip install cython --upgrade
|
|
||||||
$PYTHON -m pip install trezor==0.7.16 --upgrade
|
|
||||||
$PYTHON -m pip install keepkey==4.0.0 --upgrade
|
|
||||||
$PYTHON -m pip install btchip-python==0.1.23 --upgrade
|
|
||||||
|
|
|
@ -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
|
|
|
@ -1,8 +1,18 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Please update these carefully, some versions won't work under Wine
|
# Please update these carefully, some versions won't work under Wine
|
||||||
NSIS_URL=https://prdownloads.sourceforge.net/nsis/nsis-3.02.1-setup.exe?download
|
NSIS_FILENAME=nsis-3.02.1-setup.exe
|
||||||
|
NSIS_URL=https://prdownloads.sourceforge.net/nsis/$NSIS_FILENAME?download
|
||||||
NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e
|
NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e
|
||||||
|
|
||||||
|
ZBAR_FILENAME=zbarw-20121031-setup.exe
|
||||||
|
ZBAR_URL=https://sourceforge.net/projects/zbarw/files/$ZBAR_FILENAME/download
|
||||||
|
ZBAR_SHA256=177e32b272fa76528a3af486b74e9cb356707be1c5ace4ed3fcee9723e2c2c02
|
||||||
|
|
||||||
|
LIBUSB_FILENAME=libusb-1.0.21.7z
|
||||||
|
LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.21/$LIBUSB_FILENAME?download
|
||||||
|
LIBUSB_SHA256=acdde63a40b1477898aee6153f9d91d1a2e8a5d93f832ca8ab876498f3a6d2b8
|
||||||
|
|
||||||
PYTHON_VERSION=3.5.4
|
PYTHON_VERSION=3.5.4
|
||||||
|
|
||||||
## These settings probably don't need change
|
## These settings probably don't need change
|
||||||
|
@ -21,23 +31,31 @@ verify_signature() {
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
echo "$out" >&2
|
echo "$out" >&2
|
||||||
exit 0
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_hash() {
|
verify_hash() {
|
||||||
local file=$1 expected_hash=$2 out=
|
local file=$1 expected_hash=$2
|
||||||
actual_hash=$(sha256sum $file | awk '{print $1}')
|
actual_hash=$(sha256sum $file | awk '{print $1}')
|
||||||
if [ "$actual_hash" == "$expected_hash" ]; then
|
if [ "$actual_hash" == "$expected_hash" ]; then
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
echo "$file $actual_hash (unexpected hash)" >&2
|
echo "$file $actual_hash (unexpected hash)" >&2
|
||||||
exit 0
|
rm "$file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download_if_not_exist() {
|
||||||
|
local file_name=$1 url=$2
|
||||||
|
if [ ! -e $file_name ] ; then
|
||||||
|
wget -O $PWD/$file_name "$url"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Let's begin!
|
# Let's begin!
|
||||||
cd `dirname $0`
|
here=$(dirname $(readlink -e $0))
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Clean up Wine environment
|
# Clean up Wine environment
|
||||||
|
@ -47,22 +65,21 @@ echo "done"
|
||||||
|
|
||||||
wine 'wineboot'
|
wine 'wineboot'
|
||||||
|
|
||||||
echo "Cleaning tmp"
|
mkdir -p /tmp/electrum-build
|
||||||
rm -rf tmp
|
|
||||||
mkdir -p tmp
|
|
||||||
echo "done"
|
|
||||||
|
|
||||||
cd tmp
|
cd /tmp/electrum-build
|
||||||
|
|
||||||
# Install Python
|
# Install Python
|
||||||
# note: you might need "sudo apt-get install dirmngr" for the following
|
# note: you might need "sudo apt-get install dirmngr" for the following
|
||||||
# keys from https://www.python.org/downloads/#pubkeys
|
# keys from https://www.python.org/downloads/#pubkeys
|
||||||
KEYRING_PYTHON_DEV=keyring-electrum-build-python-dev.gpg
|
KEYLIST_PYTHON_DEV="531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5"
|
||||||
gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --recv-keys 531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5
|
KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg"
|
||||||
|
KEYSERVER_PYTHON_DEV="hkp://keys.gnupg.net"
|
||||||
|
gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --keyserver $KEYSERVER_PYTHON_DEV --recv-keys $KEYLIST_PYTHON_DEV
|
||||||
for msifile in core dev exe lib pip tools; do
|
for msifile in core dev exe lib pip tools; do
|
||||||
echo "Installing $msifile..."
|
echo "Installing $msifile..."
|
||||||
wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi"
|
wget -nc "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi"
|
||||||
wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc"
|
wget -nc "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc"
|
||||||
verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV
|
verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV
|
||||||
wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION
|
wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION
|
||||||
done
|
done
|
||||||
|
@ -70,36 +87,35 @@ done
|
||||||
# upgrade pip
|
# upgrade pip
|
||||||
$PYTHON -m pip install pip --upgrade
|
$PYTHON -m pip install pip --upgrade
|
||||||
|
|
||||||
# Install PyWin32
|
# Install pywin32-ctypes (needed by pyinstaller)
|
||||||
$PYTHON -m pip install pypiwin32
|
$PYTHON -m pip install pywin32-ctypes==0.1.2
|
||||||
|
|
||||||
# Install PyQt
|
|
||||||
$PYTHON -m pip install PyQt5
|
|
||||||
|
|
||||||
## Install pyinstaller
|
|
||||||
#$PYTHON -m pip install pyinstaller==3.3
|
|
||||||
|
|
||||||
|
|
||||||
# Install ZBar
|
|
||||||
#wget -q -O zbar.exe "https://sourceforge.net/projects/zbar/files/zbar/0.10/zbar-0.10-setup.exe/download"
|
|
||||||
#wine zbar.exe
|
|
||||||
|
|
||||||
# install Cryptodome
|
|
||||||
$PYTHON -m pip install pycryptodomex
|
|
||||||
|
|
||||||
# install PySocks
|
# install PySocks
|
||||||
$PYTHON -m pip install win_inet_pton
|
$PYTHON -m pip install win_inet_pton==1.0.1
|
||||||
|
|
||||||
# install websocket (python2)
|
$PYTHON -m pip install -r $here/../deterministic-build/requirements-binaries.txt
|
||||||
$PYTHON -m pip install websocket-client
|
|
||||||
|
# Install PyInstaller
|
||||||
|
$PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip
|
||||||
|
|
||||||
|
# Install ZBar
|
||||||
|
download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL"
|
||||||
|
verify_hash $ZBAR_FILENAME "$ZBAR_SHA256"
|
||||||
|
wine "$PWD/$ZBAR_FILENAME" /S
|
||||||
|
|
||||||
# Upgrade setuptools (so Electrum can be installed later)
|
# Upgrade setuptools (so Electrum can be installed later)
|
||||||
$PYTHON -m pip install setuptools --upgrade
|
$PYTHON -m pip install setuptools --upgrade
|
||||||
|
|
||||||
# Install NSIS installer
|
# Install NSIS installer
|
||||||
wget -q -O nsis.exe "$NSIS_URL"
|
download_if_not_exist $NSIS_FILENAME "$NSIS_URL"
|
||||||
verify_hash nsis.exe $NSIS_SHA256
|
verify_hash $NSIS_FILENAME "$NSIS_SHA256"
|
||||||
wine nsis.exe /S
|
wine "$PWD/$NSIS_FILENAME" /S
|
||||||
|
|
||||||
|
download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL"
|
||||||
|
verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256"
|
||||||
|
7z x -olibusb $LIBUSB_FILENAME -aos
|
||||||
|
|
||||||
|
cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/python$PYTHON_VERSION/
|
||||||
|
|
||||||
# Install UPX
|
# Install UPX
|
||||||
#wget -O upx.zip "https://downloads.sourceforge.net/project/upx/upx/3.08/upx308w.zip"
|
#wget -O upx.zip "https://downloads.sourceforge.net/project/upx/upx/3.08/upx308w.zip"
|
||||||
|
@ -109,5 +125,4 @@ wine nsis.exe /S
|
||||||
# add dlls needed for pyinstaller:
|
# add dlls needed for pyinstaller:
|
||||||
cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/
|
cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/
|
||||||
|
|
||||||
|
echo "Wine is configured."
|
||||||
echo "Wine is configured. Please run prepare-pyinstaller.sh"
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
pycryptodomex==3.4.12
|
||||||
|
PyQt5==5.10
|
||||||
|
sip==4.19.7
|
||||||
|
six==1.11.0
|
||||||
|
websocket-client==0.46.0
|
|
@ -0,0 +1,18 @@
|
||||||
|
btchip-python==0.1.24
|
||||||
|
certifi==2018.1.18
|
||||||
|
chardet==3.0.4
|
||||||
|
click==6.7
|
||||||
|
Cython==0.27.3
|
||||||
|
ecdsa==0.13
|
||||||
|
hidapi==0.7.99.post21
|
||||||
|
idna==2.6
|
||||||
|
keepkey==4.0.2
|
||||||
|
libusb1==1.6.4
|
||||||
|
mnemonic==0.18
|
||||||
|
pbkdf2==1.3
|
||||||
|
protobuf==3.5.1
|
||||||
|
pyblake2==1.1.0
|
||||||
|
requests==2.18.4
|
||||||
|
six==1.11.0
|
||||||
|
trezor==0.9.0
|
||||||
|
urllib3==1.22
|
|
@ -1,13 +1,13 @@
|
||||||
certifi==2017.11.5
|
certifi==2018.1.18
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
dnspython==1.15.0
|
dnspython==1.15.0
|
||||||
ecdsa==0.13
|
ecdsa==0.13
|
||||||
idna==2.6
|
idna==2.6
|
||||||
jsonrpclib-pelix==0.3.1
|
jsonrpclib-pelix==0.3.1
|
||||||
pbkdf2==1.3
|
pbkdf2==1.3
|
||||||
protobuf==3.5.0.post1
|
protobuf==3.5.1
|
||||||
pyaes==1.6.1
|
pyaes==1.6.1
|
||||||
PySocks==1.6.7
|
PySocks==1.6.8
|
||||||
qrcode==5.3
|
qrcode==5.3
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
six==1.11.0
|
six==1.11.0
|
|
@ -6,17 +6,17 @@ contrib=$(dirname "$0")
|
||||||
|
|
||||||
which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; }
|
which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; }
|
||||||
|
|
||||||
rm $venv_dir -rf
|
for i in '' '-hw' '-binaries'; do
|
||||||
virtualenv $venv_dir
|
rm "$venv_dir" -rf
|
||||||
|
virtualenv -p $(which python3) $venv_dir
|
||||||
|
|
||||||
source $venv_dir/bin/activate
|
source $venv_dir/bin/activate
|
||||||
|
|
||||||
echo "Installing dependencies"
|
echo "Installing $i dependencies"
|
||||||
|
|
||||||
pushd $contrib/..
|
python -m pip install -r $contrib/requirements/requirements${i}.txt --upgrade
|
||||||
python setup.py install
|
|
||||||
popd
|
|
||||||
|
|
||||||
pip freeze | sed '/^Electrum/ d' > $contrib/requirements.txt
|
pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements${i}.txt
|
||||||
|
done
|
||||||
|
|
||||||
echo "Updated requirements"
|
echo "Done. Updated requirements"
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
contrib=$(dirname "$0")
|
contrib=$(dirname "$0")
|
||||||
|
test -n "$contrib" -a -d "$contrib" || exit
|
||||||
|
|
||||||
whereis pip3
|
whereis pip3
|
||||||
if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi
|
if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi
|
||||||
|
|
||||||
rm $contrib/../packages/ -r
|
rm "$contrib"/../packages/ -r
|
||||||
|
|
||||||
#Install pure python modules in electrum directory
|
#Install pure python modules in electrum directory
|
||||||
pip3 install -r $contrib/requirements.txt -t $contrib/../packages
|
pip3 install -r $contrib/deterministic-build/requirements.txt -t $contrib/../packages
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
PyQt5
|
||||||
|
pycryptodomex
|
||||||
|
websocket-client
|
|
@ -0,0 +1,4 @@
|
||||||
|
Cython>=0.27
|
||||||
|
trezor>=0.9.0
|
||||||
|
keepkey
|
||||||
|
btchip-python
|
|
@ -0,0 +1,9 @@
|
||||||
|
pyaes>=0.1a1
|
||||||
|
ecdsa>=0.9
|
||||||
|
pbkdf2
|
||||||
|
requests
|
||||||
|
qrcode
|
||||||
|
protobuf
|
||||||
|
dnspython
|
||||||
|
jsonrpclib-pelix
|
||||||
|
PySocks>=1.6.6
|
|
@ -9,6 +9,8 @@
|
||||||
# python-qt and its dependencies will still need to be installed with
|
# python-qt and its dependencies will still need to be installed with
|
||||||
# your package manager.
|
# your package manager.
|
||||||
|
|
||||||
|
PYTHON_VER="$(python3 -c 'import sys; print(sys.version[:3])')"
|
||||||
|
|
||||||
if [ -e ./env/bin/activate ]; then
|
if [ -e ./env/bin/activate ]; then
|
||||||
source ./env/bin/activate
|
source ./env/bin/activate
|
||||||
else
|
else
|
||||||
|
@ -17,7 +19,7 @@ else
|
||||||
python3 setup.py install
|
python3 setup.py install
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export PYTHONPATH="/usr/local/lib/python3.5/site-packages:$PYTHONPATH"
|
export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH"
|
||||||
|
|
||||||
./electrum "$@"
|
./electrum "$@"
|
||||||
|
|
||||||
|
|
75
electrum-zcl
75
electrum-zcl
|
@ -90,10 +90,12 @@ if is_local or is_android or is_macOS:
|
||||||
imp.load_module('electrum_plugins', *imp.find_module('plugins'))
|
imp.load_module('electrum_plugins', *imp.find_module('plugins'))
|
||||||
|
|
||||||
|
|
||||||
from electrum import bitcoin
|
|
||||||
|
from electrum import bitcoin, util
|
||||||
|
from electrum import constants
|
||||||
from electrum import SimpleConfig, Network
|
from electrum import SimpleConfig, Network
|
||||||
from electrum.wallet import Wallet, Imported_Wallet
|
from electrum.wallet import Wallet, Imported_Wallet
|
||||||
from electrum.storage import WalletStorage
|
from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption
|
||||||
from electrum.util import print_msg, print_stderr, json_encode, json_decode
|
from electrum.util import print_msg, print_stderr, json_encode, json_decode
|
||||||
from electrum.util import set_verbosity, InvalidPassword
|
from electrum.util import set_verbosity, InvalidPassword
|
||||||
from electrum.commands import get_parser, known_commands, Commands, config_variables
|
from electrum.commands import get_parser, known_commands, Commands, config_variables
|
||||||
|
@ -163,6 +165,8 @@ def run_non_RPC(config):
|
||||||
print_msg("Recovering wallet...")
|
print_msg("Recovering wallet...")
|
||||||
wallet.synchronize()
|
wallet.synchronize()
|
||||||
wallet.wait_until_synchronized()
|
wallet.wait_until_synchronized()
|
||||||
|
wallet.stop_threads()
|
||||||
|
# note: we don't wait for SPV
|
||||||
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
|
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
|
||||||
else:
|
else:
|
||||||
msg = "This wallet was restored offline. It may contain more addresses than displayed."
|
msg = "This wallet was restored offline. It may contain more addresses than displayed."
|
||||||
|
@ -195,7 +199,10 @@ def init_daemon(config_options):
|
||||||
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
|
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if storage.is_encrypted():
|
if storage.is_encrypted():
|
||||||
if config.get('password'):
|
if storage.is_encrypted_with_hw_device():
|
||||||
|
plugins = init_plugins(config, 'cmdline')
|
||||||
|
password = get_password_for_hw_device_encrypted_storage(plugins)
|
||||||
|
elif config.get('password'):
|
||||||
password = config.get('password')
|
password = config.get('password')
|
||||||
else:
|
else:
|
||||||
password = prompt_password('Password:', False)
|
password = prompt_password('Password:', False)
|
||||||
|
@ -222,7 +229,7 @@ def init_cmdline(config_options, server):
|
||||||
if cmdname in ['payto', 'paytomany'] and config.get('broadcast'):
|
if cmdname in ['payto', 'paytomany'] and config.get('broadcast'):
|
||||||
cmd.requires_network = True
|
cmd.requires_network = True
|
||||||
|
|
||||||
# instanciate wallet for command-line
|
# instantiate wallet for command-line
|
||||||
storage = WalletStorage(config.get_wallet_path())
|
storage = WalletStorage(config.get_wallet_path())
|
||||||
|
|
||||||
if cmd.requires_wallet and not storage.file_exists():
|
if cmd.requires_wallet and not storage.file_exists():
|
||||||
|
@ -239,7 +246,10 @@ def init_cmdline(config_options, server):
|
||||||
# commands needing password
|
# commands needing password
|
||||||
if (cmd.requires_wallet and storage.is_encrypted() and server is None)\
|
if (cmd.requires_wallet and storage.is_encrypted() and server is None)\
|
||||||
or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())):
|
or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())):
|
||||||
if config.get('password'):
|
if storage.is_encrypted_with_hw_device():
|
||||||
|
# this case is handled later in the control flow
|
||||||
|
password = None
|
||||||
|
elif config.get('password'):
|
||||||
password = config.get('password')
|
password = config.get('password')
|
||||||
else:
|
else:
|
||||||
password = prompt_password('Password:', False)
|
password = prompt_password('Password:', False)
|
||||||
|
@ -258,19 +268,57 @@ def init_cmdline(config_options, server):
|
||||||
return cmd, password
|
return cmd, password
|
||||||
|
|
||||||
|
|
||||||
def run_offline_command(config, config_options):
|
def get_connected_hw_devices(plugins):
|
||||||
|
support = plugins.get_hardware_support()
|
||||||
|
if not support:
|
||||||
|
print_msg('No hardware wallet support found on your system.')
|
||||||
|
sys.exit(1)
|
||||||
|
# scan devices
|
||||||
|
devices = []
|
||||||
|
devmgr = plugins.device_manager
|
||||||
|
for name, description, plugin in support:
|
||||||
|
try:
|
||||||
|
u = devmgr.unpaired_device_infos(None, plugin)
|
||||||
|
except:
|
||||||
|
devmgr.print_error("error", name)
|
||||||
|
continue
|
||||||
|
devices += list(map(lambda x: (name, x), u))
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
def get_password_for_hw_device_encrypted_storage(plugins):
|
||||||
|
devices = get_connected_hw_devices(plugins)
|
||||||
|
if len(devices) == 0:
|
||||||
|
print_msg("Error: No connected hw device found. Can not decrypt this wallet.")
|
||||||
|
sys.exit(1)
|
||||||
|
elif len(devices) > 1:
|
||||||
|
print_msg("Warning: multiple hardware devices detected. "
|
||||||
|
"The first one will be used to decrypt the wallet.")
|
||||||
|
# FIXME we use the "first" device, in case of multiple ones
|
||||||
|
name, device_info = devices[0]
|
||||||
|
plugin = plugins.get_plugin(name)
|
||||||
|
derivation = get_derivation_used_for_hw_device_encryption()
|
||||||
|
xpub = plugin.get_xpub(device_info.device.id_, derivation, 'standard', plugin.handler)
|
||||||
|
password = keystore.Xpub.get_pubkey_from_xpub(xpub, ())
|
||||||
|
return password
|
||||||
|
|
||||||
|
|
||||||
|
def run_offline_command(config, config_options, plugins):
|
||||||
cmdname = config.get('cmd')
|
cmdname = config.get('cmd')
|
||||||
cmd = known_commands[cmdname]
|
cmd = known_commands[cmdname]
|
||||||
password = config_options.get('password')
|
password = config_options.get('password')
|
||||||
if cmd.requires_wallet:
|
if cmd.requires_wallet:
|
||||||
storage = WalletStorage(config.get_wallet_path())
|
storage = WalletStorage(config.get_wallet_path())
|
||||||
if storage.is_encrypted():
|
if storage.is_encrypted():
|
||||||
|
if storage.is_encrypted_with_hw_device():
|
||||||
|
password = get_password_for_hw_device_encrypted_storage(plugins)
|
||||||
|
config_options['password'] = password
|
||||||
storage.decrypt(password)
|
storage.decrypt(password)
|
||||||
wallet = Wallet(storage)
|
wallet = Wallet(storage)
|
||||||
else:
|
else:
|
||||||
wallet = None
|
wallet = None
|
||||||
# check password
|
# check password
|
||||||
if cmd.requires_password and storage.get('use_encryption'):
|
if cmd.requires_password and wallet.has_password():
|
||||||
try:
|
try:
|
||||||
seed = wallet.check_password(password)
|
seed = wallet.check_password(password)
|
||||||
except InvalidPassword:
|
except InvalidPassword:
|
||||||
|
@ -281,7 +329,8 @@ def run_offline_command(config, config_options):
|
||||||
# arguments passed to function
|
# arguments passed to function
|
||||||
args = [config.get(x) for x in cmd.params]
|
args = [config.get(x) for x in cmd.params]
|
||||||
# decode json arguments
|
# decode json arguments
|
||||||
args = list(map(json_decode, args))
|
if cmdname not in ('setconfig',):
|
||||||
|
args = list(map(json_decode, args))
|
||||||
# options
|
# options
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for x in cmd.options:
|
for x in cmd.options:
|
||||||
|
@ -298,8 +347,10 @@ def init_plugins(config, gui_name):
|
||||||
from electrum.plugins import Plugins
|
from electrum.plugins import Plugins
|
||||||
return Plugins(config, is_local or is_android, gui_name)
|
return Plugins(config, is_local or is_android, gui_name)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The hook will only be used in the Qt GUI right now
|
||||||
|
util.setup_thread_excepthook()
|
||||||
# on osx, delete Process Serial Number arg generated for apps launched in Finder
|
# on osx, delete Process Serial Number arg generated for apps launched in Finder
|
||||||
sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv))
|
sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv))
|
||||||
|
|
||||||
|
@ -364,7 +415,7 @@ if __name__ == '__main__':
|
||||||
cmdname = config.get('cmd')
|
cmdname = config.get('cmd')
|
||||||
|
|
||||||
if config.get('testnet'):
|
if config.get('testnet'):
|
||||||
bitcoin.NetworkConstants.set_testnet()
|
constants.set_testnet()
|
||||||
|
|
||||||
# run non-RPC commands separately
|
# run non-RPC commands separately
|
||||||
if cmdname in ['create', 'restore']:
|
if cmdname in ['create', 'restore']:
|
||||||
|
@ -430,8 +481,8 @@ if __name__ == '__main__':
|
||||||
print_msg("Daemon not running; try 'electrum daemon start'")
|
print_msg("Daemon not running; try 'electrum daemon start'")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
init_plugins(config, 'cmdline')
|
plugins = init_plugins(config, 'cmdline')
|
||||||
result = run_offline_command(config, config_options)
|
result = run_offline_command(config, config_options, plugins)
|
||||||
# print result
|
# print result
|
||||||
if isinstance(result, str):
|
if isinstance(result, str):
|
||||||
print_msg(result)
|
print_msg(result)
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Kivy GUI
|
||||||
|
|
||||||
|
The Kivy GUI is used with Electrum on Android devices. To generate an APK file, follow these instructions.
|
||||||
|
|
||||||
|
## 1. Install python-for-android (p4a)
|
||||||
|
p4a is used to package Electrum, Python, SDL and a bootstrap Java app into an APK file.
|
||||||
|
We patched p4a to add some functionality we need for Electrum. Until those changes are
|
||||||
|
merged into p4a, you need to merge them locally (into the master branch):
|
||||||
|
|
||||||
|
2. [kivy/python-for-android#1217](https://github.com/kivy/python-for-android/pull/1217)
|
||||||
|
|
||||||
|
Something like this should work:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /opt
|
||||||
|
git clone https://github.com/kivy/python-for-android
|
||||||
|
cd python-for-android
|
||||||
|
git remote add agilewalker https://github.com/agilewalker/python-for-android
|
||||||
|
git checkout a036f4442b6a23
|
||||||
|
git fetch agilewalker
|
||||||
|
git merge agilewalker/master
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Install buildozer
|
||||||
|
2.1 Buildozer is a frontend to p4a. Luckily we don't need to patch it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /opt
|
||||||
|
git clone https://github.com/kivy/buildozer
|
||||||
|
cd buildozer
|
||||||
|
sudo python3 setup.py install
|
||||||
|
```
|
||||||
|
|
||||||
|
2.2 Download the [Crystax NDK](https://www.crystax.net/en/download) manually.
|
||||||
|
Extract into `/opt/crystax-ndk-10.3.2`
|
||||||
|
|
||||||
|
## 3. Update the Android SDK build tools
|
||||||
|
|
||||||
|
### Method 1: Using the GUI
|
||||||
|
|
||||||
|
Start the Android SDK manager in GUI mode:
|
||||||
|
|
||||||
|
~/.buildozer/android/platform/android-sdk-20/tools/android
|
||||||
|
|
||||||
|
Check the latest SDK available and install it.
|
||||||
|
Close the SDK manager.
|
||||||
|
Reopen the SDK manager, scroll to the bottom and install the latest build tools (probably v27)
|
||||||
|
Install "Android Support Library Repository" from the SDK manager.
|
||||||
|
|
||||||
|
### Method 2: Using the command line:
|
||||||
|
|
||||||
|
Repeat the following command until there is nothing to install:
|
||||||
|
|
||||||
|
~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t tools,platform-tools
|
||||||
|
|
||||||
|
Install Build Tools, android API 19 and Android Support Library:
|
||||||
|
|
||||||
|
~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t build-tools-27.0.3,android-19,extra-android-m2repository
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 5. Create the UI Atlas
|
||||||
|
In the `gui/kivy` directory of Electrum, run `make theming`.
|
||||||
|
|
||||||
|
## 6. Download Electrum dependencies
|
||||||
|
Run `contrib/make_packages`.
|
||||||
|
|
||||||
|
## 7. Build the APK
|
||||||
|
Run `contrib/make_apk`.
|
||||||
|
|
||||||
|
# FAQ
|
||||||
|
## Why do I get errors like `package me.dm7.barcodescanner.zxing does not exist` while compiling?
|
||||||
|
Update your Android build tools to version 27 like described above.
|
||||||
|
|
||||||
|
## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling?
|
||||||
|
Make sure that your p4a installation includes commit a3cc78a6d1a107cd3b6bd28db8b80f89e3ecddd2.
|
||||||
|
Also make sure you have recent SDK tools and platform-tools
|
||||||
|
|
||||||
|
## I changed something but I don't see any differences on the phone. What did I do wrong?
|
||||||
|
You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}`
|
|
@ -1,24 +0,0 @@
|
||||||
Before compiling, create packages: `contrib/make_packages`
|
|
||||||
|
|
||||||
Commands::
|
|
||||||
|
|
||||||
`make theming` to make a atlas out of a list of pngs
|
|
||||||
|
|
||||||
`make apk` to make a apk
|
|
||||||
|
|
||||||
|
|
||||||
If something in included modules like kivy or any other module changes
|
|
||||||
then you need to rebuild the distribution. To do so:
|
|
||||||
|
|
||||||
rm -rf .buildozer/android/platform/python-for-android/dist
|
|
||||||
|
|
||||||
|
|
||||||
how to build with ssl:
|
|
||||||
|
|
||||||
rm -rf .buildozer/android/platform/build/
|
|
||||||
./contrib/make_apk
|
|
||||||
pushd /opt/electrum/.buildozer/android/platform/build/build/libs_collections/Electrum/armeabi-v7a
|
|
||||||
cp libssl1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libssl.so
|
|
||||||
cp libcrypto1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libcrypto.so
|
|
||||||
popd
|
|
||||||
./contrib/make_apk
|
|
|
@ -1,21 +1,22 @@
|
||||||
import gettext
|
import gettext
|
||||||
|
|
||||||
|
|
||||||
class _(str):
|
class _(str):
|
||||||
|
|
||||||
observers = set()
|
observers = set()
|
||||||
lang = None
|
lang = None
|
||||||
|
|
||||||
def __new__(cls, s, *args, **kwargs):
|
def __new__(cls, s):
|
||||||
if _.lang is None:
|
if _.lang is None:
|
||||||
_.switch_lang('en')
|
_.switch_lang('en')
|
||||||
t = _.translate(s, *args, **kwargs)
|
t = _.translate(s)
|
||||||
o = super(_, cls).__new__(cls, t)
|
o = super(_, cls).__new__(cls, t)
|
||||||
o.source_text = s
|
o.source_text = s
|
||||||
return o
|
return o
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def translate(s, *args, **kwargs):
|
def translate(s, *args, **kwargs):
|
||||||
return _.lang(s).format(args, kwargs)
|
return _.lang(s)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bind(label):
|
def bind(label):
|
||||||
|
|
|
@ -239,7 +239,7 @@
|
||||||
self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu()
|
self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu()
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
Color:
|
||||||
rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.3, 0.3, 0.3, 1)
|
rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.15, 0.15, 0.17, 1)
|
||||||
Rectangle:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
|
@ -397,7 +397,7 @@
|
||||||
slide: 1
|
slide: 1
|
||||||
CleanHeader:
|
CleanHeader:
|
||||||
id: history_tab
|
id: history_tab
|
||||||
text: _('History')
|
text: _('Balance')
|
||||||
slide: 2
|
slide: 2
|
||||||
CleanHeader:
|
CleanHeader:
|
||||||
id: receive_tab
|
id: receive_tab
|
||||||
|
|
|
@ -82,6 +82,10 @@ class ElectrumWindow(App):
|
||||||
server_port = StringProperty('')
|
server_port = StringProperty('')
|
||||||
num_chains = NumericProperty(0)
|
num_chains = NumericProperty(0)
|
||||||
blockchain_name = StringProperty('')
|
blockchain_name = StringProperty('')
|
||||||
|
fee_status = StringProperty('Fee')
|
||||||
|
balance = StringProperty('')
|
||||||
|
fiat_balance = StringProperty('')
|
||||||
|
is_fiat = BooleanProperty(False)
|
||||||
blockchain_checkpoint = NumericProperty(0)
|
blockchain_checkpoint = NumericProperty(0)
|
||||||
|
|
||||||
auto_connect = BooleanProperty(False)
|
auto_connect = BooleanProperty(False)
|
||||||
|
@ -95,8 +99,8 @@ class ElectrumWindow(App):
|
||||||
from .uix.dialogs.choice_dialog import ChoiceDialog
|
from .uix.dialogs.choice_dialog import ChoiceDialog
|
||||||
protocol = 's'
|
protocol = 's'
|
||||||
def cb2(host):
|
def cb2(host):
|
||||||
from electrum.bitcoin import NetworkConstants
|
from electrum import constants
|
||||||
pp = servers.get(host, NetworkConstants.DEFAULT_PORTS)
|
pp = servers.get(host, constants.net.DEFAULT_PORTS)
|
||||||
port = pp.get(protocol, '')
|
port = pp.get(protocol, '')
|
||||||
popup.ids.host.text = host
|
popup.ids.host.text = host
|
||||||
popup.ids.port.text = port
|
popup.ids.port.text = port
|
||||||
|
@ -175,8 +179,10 @@ class ElectrumWindow(App):
|
||||||
def btc_to_fiat(self, amount_str):
|
def btc_to_fiat(self, amount_str):
|
||||||
if not amount_str:
|
if not amount_str:
|
||||||
return ''
|
return ''
|
||||||
|
if not self.fx.is_enabled():
|
||||||
|
return ''
|
||||||
rate = self.fx.exchange_rate()
|
rate = self.fx.exchange_rate()
|
||||||
if not rate:
|
if rate.is_nan():
|
||||||
return ''
|
return ''
|
||||||
fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
|
fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
|
||||||
return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')
|
return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')
|
||||||
|
@ -185,7 +191,7 @@ class ElectrumWindow(App):
|
||||||
if not fiat_amount:
|
if not fiat_amount:
|
||||||
return ''
|
return ''
|
||||||
rate = self.fx.exchange_rate()
|
rate = self.fx.exchange_rate()
|
||||||
if not rate:
|
if rate.is_nan():
|
||||||
return ''
|
return ''
|
||||||
satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
|
satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
|
||||||
return format_satoshis_plain(satoshis, self.decimal_point())
|
return format_satoshis_plain(satoshis, self.decimal_point())
|
||||||
|
@ -271,6 +277,7 @@ class ElectrumWindow(App):
|
||||||
# cached dialogs
|
# cached dialogs
|
||||||
self._settings_dialog = None
|
self._settings_dialog = None
|
||||||
self._password_dialog = None
|
self._password_dialog = None
|
||||||
|
self.fee_status = self.electrum_config.get_fee_status()
|
||||||
|
|
||||||
def wallet_name(self):
|
def wallet_name(self):
|
||||||
return os.path.basename(self.wallet.storage.path) if self.wallet else ' '
|
return os.path.basename(self.wallet.storage.path) if self.wallet else ' '
|
||||||
|
@ -457,6 +464,7 @@ class ElectrumWindow(App):
|
||||||
if self.network:
|
if self.network:
|
||||||
interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces']
|
interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces']
|
||||||
self.network.register_callback(self.on_network_event, interests)
|
self.network.register_callback(self.on_network_event, interests)
|
||||||
|
self.network.register_callback(self.on_fee, ['fee'])
|
||||||
self.network.register_callback(self.on_quotes, ['on_quotes'])
|
self.network.register_callback(self.on_quotes, ['on_quotes'])
|
||||||
self.network.register_callback(self.on_history, ['on_history'])
|
self.network.register_callback(self.on_history, ['on_history'])
|
||||||
# URI passed in config
|
# URI passed in config
|
||||||
|
@ -631,17 +639,17 @@ class ElectrumWindow(App):
|
||||||
if not self.wallet.up_to_date or server_height == 0:
|
if not self.wallet.up_to_date or server_height == 0:
|
||||||
status = _("Synchronizing...")
|
status = _("Synchronizing...")
|
||||||
elif server_lag > 1:
|
elif server_lag > 1:
|
||||||
status = _("Server lagging (%d blocks)"%server_lag)
|
status = _("Server lagging")
|
||||||
else:
|
else:
|
||||||
c, u, x = self.wallet.get_balance()
|
status = ''
|
||||||
text = self.format_amount(c+x+u)
|
|
||||||
status = str(text.strip() + ' ' + self.base_unit)
|
|
||||||
else:
|
else:
|
||||||
status = _("Disconnected")
|
status = _("Disconnected")
|
||||||
|
self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '')
|
||||||
n = self.wallet.basename()
|
# balance
|
||||||
self.status = '[size=15dp]%s[/size]\n%s' %(n, status)
|
c, u, x = self.wallet.get_balance()
|
||||||
#fiat_balance = self.fx.format_amount_and_units(c+u+x) or ''
|
text = self.format_amount(c+x+u)
|
||||||
|
self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
|
||||||
|
self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy
|
||||||
|
|
||||||
def get_max_amount(self):
|
def get_max_amount(self):
|
||||||
inputs = self.wallet.get_spendable_coins(None, self.electrum_config)
|
inputs = self.wallet.get_spendable_coins(None, self.electrum_config)
|
||||||
|
@ -684,9 +692,6 @@ class ElectrumWindow(App):
|
||||||
def on_resume(self):
|
def on_resume(self):
|
||||||
if self.nfcscanner:
|
if self.nfcscanner:
|
||||||
self.nfcscanner.nfc_enable()
|
self.nfcscanner.nfc_enable()
|
||||||
# workaround p4a bug:
|
|
||||||
# show an empty info bubble, to refresh the display
|
|
||||||
self.show_info_bubble('', duration=0.1, pos=(0,0), width=1, arrow_pos=None)
|
|
||||||
|
|
||||||
def on_size(self, instance, value):
|
def on_size(self, instance, value):
|
||||||
width, height = value
|
width, height = value
|
||||||
|
@ -831,6 +836,16 @@ class ElectrumWindow(App):
|
||||||
popup = AmountDialog(show_max, amount, cb)
|
popup = AmountDialog(show_max, amount, cb)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
|
def fee_dialog(self, label, dt):
|
||||||
|
from .uix.dialogs.fee_dialog import FeeDialog
|
||||||
|
def cb():
|
||||||
|
self.fee_status = self.electrum_config.get_fee_status()
|
||||||
|
fee_dialog = FeeDialog(self, self.electrum_config, cb)
|
||||||
|
fee_dialog.open()
|
||||||
|
|
||||||
|
def on_fee(self, event, *arg):
|
||||||
|
self.fee_status = self.electrum_config.get_fee_status()
|
||||||
|
|
||||||
def protected(self, msg, f, args):
|
def protected(self, msg, f, args):
|
||||||
if self.wallet.has_password():
|
if self.wallet.has_password():
|
||||||
self.password_dialog(msg, f, args)
|
self.password_dialog(msg, f, args)
|
||||||
|
@ -846,7 +861,7 @@ class ElectrumWindow(App):
|
||||||
def _delete_wallet(self, b):
|
def _delete_wallet(self, b):
|
||||||
if b:
|
if b:
|
||||||
basename = os.path.basename(self.wallet.storage.path)
|
basename = os.path.basename(self.wallet.storage.path)
|
||||||
self.protected(_("Enter your PIN code to confirm deletion of %s") % basename, self.__delete_wallet, ())
|
self.protected(_("Enter your PIN code to confirm deletion of {}").format(basename), self.__delete_wallet, ())
|
||||||
|
|
||||||
def __delete_wallet(self, pw):
|
def __delete_wallet(self, pw):
|
||||||
wallet_path = self.get_wallet_path()
|
wallet_path = self.get_wallet_path()
|
||||||
|
@ -928,6 +943,10 @@ class ElectrumWindow(App):
|
||||||
return
|
return
|
||||||
if not self.wallet.can_export():
|
if not self.wallet.can_export():
|
||||||
return
|
return
|
||||||
key = str(self.wallet.export_private_key(addr, password)[0])
|
try:
|
||||||
pk_label.data = key
|
key = str(self.wallet.export_private_key(addr, password)[0])
|
||||||
|
pk_label.data = key
|
||||||
|
except InvalidPassword:
|
||||||
|
self.show_error("Invalid PIN")
|
||||||
|
return
|
||||||
self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))
|
self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))
|
||||||
|
|
|
@ -31,7 +31,7 @@ version.filename = %(source.dir)s/contrib/versions.py
|
||||||
#version = 1.9.8
|
#version = 1.9.8
|
||||||
|
|
||||||
# (list) Application requirements
|
# (list) Application requirements
|
||||||
requirements = python3crystax, android, openssl, plyer, kivy==master
|
requirements = python3crystax==3.6, android, openssl, plyer, kivy==master
|
||||||
|
|
||||||
# (str) Presplash of the application
|
# (str) Presplash of the application
|
||||||
#presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png
|
#presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png
|
||||||
|
|
|
@ -13,21 +13,35 @@ Builder.load_string('''
|
||||||
anchor_x: 'center'
|
anchor_x: 'center'
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'vertical'
|
orientation: 'vertical'
|
||||||
size_hint: 0.8, 1
|
size_hint: 0.9, 1
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, 0.2
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '80dp'
|
height: '80dp'
|
||||||
Label:
|
Button:
|
||||||
id: a
|
background_color: 0, 0, 0, 0
|
||||||
btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else ''
|
id: btc
|
||||||
fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else ''
|
text: kb.amount + ' ' + app.base_unit
|
||||||
text1: ((self.fiat_text if kb.is_fiat else self.btc_text) if app.fiat_unit else self.btc_text) if self.btc_text else ''
|
color: (0.7, 0.7, 1, 1) if kb.is_fiat else (1, 1, 1, 1)
|
||||||
text2: ((self.btc_text if kb.is_fiat else self.fiat_text) if app.fiat_unit else '') if self.btc_text else ''
|
|
||||||
text: self.text1 + "\\n" + "[color=#8888ff]" + self.text2 + "[/color]"
|
|
||||||
halign: 'right'
|
halign: 'right'
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
font_size: '22dp'
|
font_size: '20dp'
|
||||||
height: '80dp'
|
height: '48dp'
|
||||||
|
on_release:
|
||||||
|
kb.is_fiat = False
|
||||||
|
Button:
|
||||||
|
background_color: 0, 0, 0, 0
|
||||||
|
id: fiat
|
||||||
|
text: kb.fiat_amount + ' ' + app.fiat_unit
|
||||||
|
color: (1, 1, 1, 1) if kb.is_fiat else (0.7, 0.7, 1, 1)
|
||||||
|
halign: 'right'
|
||||||
|
size_hint: 1, None
|
||||||
|
font_size: '20dp'
|
||||||
|
height: '48dp'
|
||||||
|
disabled: not app.fx.is_enabled()
|
||||||
|
on_release:
|
||||||
|
kb.is_fiat = True
|
||||||
Widget:
|
Widget:
|
||||||
size_hint: 1, 0.2
|
size_hint: 1, 0.2
|
||||||
GridLayout:
|
GridLayout:
|
||||||
|
@ -65,6 +79,9 @@ Builder.load_string('''
|
||||||
text: '0'
|
text: '0'
|
||||||
KButton:
|
KButton:
|
||||||
text: '<'
|
text: '<'
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '48dp'
|
||||||
Button:
|
Button:
|
||||||
id: but_max
|
id: but_max
|
||||||
opacity: 1 if root.show_max else 0
|
opacity: 1 if root.show_max else 0
|
||||||
|
@ -75,13 +92,6 @@ Builder.load_string('''
|
||||||
on_release:
|
on_release:
|
||||||
kb.is_fiat = False
|
kb.is_fiat = False
|
||||||
kb.amount = app.get_max_amount()
|
kb.amount = app.get_max_amount()
|
||||||
Button:
|
|
||||||
id: button_fiat
|
|
||||||
size_hint: 1, None
|
|
||||||
height: '48dp'
|
|
||||||
text: (app.base_unit if not kb.is_fiat else app.fiat_unit) if app.fiat_unit else ''
|
|
||||||
on_release:
|
|
||||||
if app.fiat_unit: popup.toggle_fiat(kb)
|
|
||||||
Button:
|
Button:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
|
@ -102,7 +112,7 @@ Builder.load_string('''
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
text: _('OK')
|
text: _('OK')
|
||||||
on_release:
|
on_release:
|
||||||
root.callback(a.btc_text)
|
root.callback(btc.text if kb.amount else '')
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
@ -117,9 +127,6 @@ class AmountDialog(Factory.Popup):
|
||||||
if amount:
|
if amount:
|
||||||
self.ids.kb.amount = amount
|
self.ids.kb.amount = amount
|
||||||
|
|
||||||
def toggle_fiat(self, a):
|
|
||||||
a.is_fiat = not a.is_fiat
|
|
||||||
|
|
||||||
def update_amount(self, c):
|
def update_amount(self, c):
|
||||||
kb = self.ids.kb
|
kb = self.ids.kb
|
||||||
amount = kb.fiat_amount if kb.is_fiat else kb.amount
|
amount = kb.fiat_amount if kb.is_fiat else kb.amount
|
||||||
|
|
|
@ -3,7 +3,6 @@ from kivy.factory import Factory
|
||||||
from kivy.properties import ObjectProperty
|
from kivy.properties import ObjectProperty
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
|
|
||||||
from electrum.util import fee_levels
|
|
||||||
from electrum_gui.kivy.i18n import _
|
from electrum_gui.kivy.i18n import _
|
||||||
|
|
||||||
Builder.load_string('''
|
Builder.load_string('''
|
||||||
|
@ -29,7 +28,11 @@ Builder.load_string('''
|
||||||
text: _('New Fee')
|
text: _('New Fee')
|
||||||
value: ''
|
value: ''
|
||||||
Label:
|
Label:
|
||||||
id: tooltip
|
id: tooltip1
|
||||||
|
text: ''
|
||||||
|
size_hint_y: None
|
||||||
|
Label:
|
||||||
|
id: tooltip2
|
||||||
text: ''
|
text: ''
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
Slider:
|
Slider:
|
||||||
|
@ -72,39 +75,39 @@ class BumpFeeDialog(Factory.Popup):
|
||||||
self.tx_size = size
|
self.tx_size = size
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.config = app.electrum_config
|
self.config = app.electrum_config
|
||||||
self.fee_step = self.config.max_fee_rate() / 10
|
self.mempool = self.config.use_mempool_fees()
|
||||||
self.dynfees = self.config.get('dynamic_fees', True) and self.app.network
|
self.dynfees = self.config.is_dynfee() and self.app.network and self.config.has_dynamic_fees_ready()
|
||||||
self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee)
|
self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee)
|
||||||
self.update_slider()
|
self.update_slider()
|
||||||
self.update_text()
|
self.update_text()
|
||||||
|
|
||||||
def update_text(self):
|
def update_text(self):
|
||||||
value = int(self.ids.slider.value)
|
fee = self.get_fee()
|
||||||
self.ids.new_fee.value = self.app.format_amount_and_units(self.get_fee())
|
self.ids.new_fee.value = self.app.format_amount_and_units(fee)
|
||||||
if self.dynfees:
|
pos = int(self.ids.slider.value)
|
||||||
value = int(self.ids.slider.value)
|
fee_rate = self.get_fee_rate()
|
||||||
self.ids.tooltip.text = fee_levels[value]
|
text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, fee_rate)
|
||||||
|
self.ids.tooltip1.text = text
|
||||||
|
self.ids.tooltip2.text = tooltip
|
||||||
|
|
||||||
def update_slider(self):
|
def update_slider(self):
|
||||||
slider = self.ids.slider
|
slider = self.ids.slider
|
||||||
|
maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool)
|
||||||
|
slider.range = (0, maxp)
|
||||||
|
slider.step = 1
|
||||||
|
slider.value = pos
|
||||||
|
|
||||||
|
def get_fee_rate(self):
|
||||||
|
pos = int(self.ids.slider.value)
|
||||||
if self.dynfees:
|
if self.dynfees:
|
||||||
slider.range = (0, 4)
|
fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(pos)
|
||||||
slider.step = 1
|
|
||||||
slider.value = 3
|
|
||||||
else:
|
else:
|
||||||
slider.range = (1, 10)
|
fee_rate = self.config.static_fee(pos)
|
||||||
slider.step = 1
|
return fee_rate
|
||||||
rate = self.init_fee*1000//self.tx_size
|
|
||||||
slider.value = min( rate * 2 // self.fee_step, 10)
|
|
||||||
|
|
||||||
def get_fee(self):
|
def get_fee(self):
|
||||||
value = int(self.ids.slider.value)
|
fee_rate = self.get_fee_rate()
|
||||||
if self.dynfees:
|
return int(fee_rate * self.tx_size // 1000)
|
||||||
if self.config.has_fee_estimates():
|
|
||||||
dynfee = self.config.dynfee(value)
|
|
||||||
return int(dynfee * self.tx_size // 1000)
|
|
||||||
else:
|
|
||||||
return int(value*self.fee_step * self.tx_size // 1000)
|
|
||||||
|
|
||||||
def on_ok(self):
|
def on_ok(self):
|
||||||
new_fee = self.get_fee()
|
new_fee = self.get_fee()
|
||||||
|
|
|
@ -3,7 +3,6 @@ from kivy.factory import Factory
|
||||||
from kivy.properties import ObjectProperty
|
from kivy.properties import ObjectProperty
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
|
|
||||||
from electrum.util import fee_levels
|
|
||||||
from electrum_gui.kivy.i18n import _
|
from electrum_gui.kivy.i18n import _
|
||||||
|
|
||||||
Builder.load_string('''
|
Builder.load_string('''
|
||||||
|
@ -12,29 +11,46 @@ Builder.load_string('''
|
||||||
title: _('Transaction Fees')
|
title: _('Transaction Fees')
|
||||||
size_hint: 0.8, 0.8
|
size_hint: 0.8, 0.8
|
||||||
pos_hint: {'top':0.9}
|
pos_hint: {'top':0.9}
|
||||||
|
method: 0
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'vertical'
|
orientation: 'vertical'
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'horizontal'
|
orientation: 'horizontal'
|
||||||
size_hint: 1, 0.5
|
size_hint: 1, 0.5
|
||||||
Label:
|
Label:
|
||||||
id: fee_per_kb
|
text: _('Method') + ':'
|
||||||
|
Button:
|
||||||
|
text: _('Mempool') if root.method == 2 else _('ETA') if root.method == 1 else _('Static')
|
||||||
|
background_color: (0,0,0,0)
|
||||||
|
bold: True
|
||||||
|
on_release:
|
||||||
|
root.method = (root.method + 1) % 3
|
||||||
|
root.update_slider()
|
||||||
|
root.update_text()
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'horizontal'
|
||||||
|
size_hint: 1, 0.5
|
||||||
|
Label:
|
||||||
|
text: (_('Target') if root.method > 0 else _('Fee')) + ':'
|
||||||
|
Label:
|
||||||
|
id: fee_target
|
||||||
text: ''
|
text: ''
|
||||||
Slider:
|
Slider:
|
||||||
id: slider
|
id: slider
|
||||||
range: 0, 4
|
range: 0, 4
|
||||||
step: 1
|
step: 1
|
||||||
on_value: root.on_slider(self.value)
|
on_value: root.on_slider(self.value)
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, 0.5
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'horizontal'
|
orientation: 'horizontal'
|
||||||
size_hint: 1, 0.5
|
size_hint: 1, 0.5
|
||||||
Label:
|
TopLabel:
|
||||||
text: _('Dynamic Fees')
|
id: fee_estimate
|
||||||
CheckBox:
|
text: ''
|
||||||
id: dynfees
|
font_size: '14dp'
|
||||||
on_active: root.on_checkbox(self.active)
|
|
||||||
Widget:
|
Widget:
|
||||||
size_hint: 1, 1
|
size_hint: 1, 0.5
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'horizontal'
|
orientation: 'horizontal'
|
||||||
size_hint: 1, 0.5
|
size_hint: 1, 0.5
|
||||||
|
@ -60,53 +76,57 @@ class FeeDialog(Factory.Popup):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.fee_rate = self.config.fee_per_kb()
|
self.fee_rate = self.config.fee_per_kb()
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.dynfees = self.config.get('dynamic_fees', True)
|
mempool = self.config.use_mempool_fees()
|
||||||
self.ids.dynfees.active = self.dynfees
|
dynfees = self.config.is_dynfee()
|
||||||
|
self.method = (2 if mempool else 1) if dynfees else 0
|
||||||
self.update_slider()
|
self.update_slider()
|
||||||
self.update_text()
|
self.update_text()
|
||||||
|
|
||||||
def update_text(self):
|
def update_text(self):
|
||||||
value = int(self.ids.slider.value)
|
pos = int(self.ids.slider.value)
|
||||||
self.ids.fee_per_kb.text = self.get_fee_text(value)
|
dynfees, mempool = self.get_method()
|
||||||
|
if self.method == 2:
|
||||||
|
fee_rate = self.config.depth_to_fee(pos)
|
||||||
|
target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate)
|
||||||
|
msg = 'In the current network conditions, in order to be positioned %s, a transaction will require a fee of %s.' % (target, estimate)
|
||||||
|
elif self.method == 1:
|
||||||
|
fee_rate = self.config.eta_to_fee(pos)
|
||||||
|
target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate)
|
||||||
|
msg = 'In the last few days, transactions that confirmed %s usually paid a fee of at least %s.' % (target.lower(), estimate)
|
||||||
|
else:
|
||||||
|
fee_rate = self.config.static_fee(pos)
|
||||||
|
target, estimate = self.config.get_fee_text(pos, dynfees, True, fee_rate)
|
||||||
|
msg = 'In the current network conditions, a transaction paying %s would be positioned %s.' % (target, estimate)
|
||||||
|
|
||||||
|
self.ids.fee_target.text = target
|
||||||
|
self.ids.fee_estimate.text = msg
|
||||||
|
|
||||||
|
def get_method(self):
|
||||||
|
dynfees = self.method > 0
|
||||||
|
mempool = self.method == 2
|
||||||
|
return dynfees, mempool
|
||||||
|
|
||||||
def update_slider(self):
|
def update_slider(self):
|
||||||
slider = self.ids.slider
|
slider = self.ids.slider
|
||||||
if self.dynfees:
|
dynfees, mempool = self.get_method()
|
||||||
slider.range = (0, 4)
|
maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool)
|
||||||
slider.step = 1
|
slider.range = (0, maxp)
|
||||||
slider.value = self.config.get('fee_level', 2)
|
slider.step = 1
|
||||||
else:
|
slider.value = pos
|
||||||
slider.range = (0, 9)
|
|
||||||
slider.step = 1
|
|
||||||
slider.value = self.config.static_fee_index(self.fee_rate)
|
|
||||||
|
|
||||||
def get_fee_text(self, value):
|
|
||||||
if self.ids.dynfees.active:
|
|
||||||
tooltip = fee_levels[value]
|
|
||||||
if self.config.has_fee_estimates():
|
|
||||||
dynfee = self.config.dynfee(value)
|
|
||||||
tooltip += '\n' + (self.app.format_amount_and_units(dynfee)) + '/kB'
|
|
||||||
else:
|
|
||||||
fee_rate = self.config.static_fee(value)
|
|
||||||
tooltip = self.app.format_amount_and_units(fee_rate) + '/kB'
|
|
||||||
if self.config.has_fee_estimates():
|
|
||||||
i = self.config.reverse_dynfee(fee_rate)
|
|
||||||
tooltip += '\n' + (_('low fee') if i < 0 else 'Within %d blocks'%i)
|
|
||||||
return tooltip
|
|
||||||
|
|
||||||
def on_ok(self):
|
def on_ok(self):
|
||||||
value = int(self.ids.slider.value)
|
value = int(self.ids.slider.value)
|
||||||
self.config.set_key('dynamic_fees', self.dynfees, False)
|
dynfees, mempool = self.get_method()
|
||||||
if self.dynfees:
|
self.config.set_key('dynamic_fees', dynfees, False)
|
||||||
self.config.set_key('fee_level', value, True)
|
self.config.set_key('mempool_fees', mempool, False)
|
||||||
|
if dynfees:
|
||||||
|
if mempool:
|
||||||
|
self.config.set_key('depth_level', value, True)
|
||||||
|
else:
|
||||||
|
self.config.set_key('fee_level', value, True)
|
||||||
else:
|
else:
|
||||||
self.config.set_key('fee_per_kb', self.config.static_fee(value), True)
|
self.config.set_key('fee_per_kb', self.config.static_fee(value), True)
|
||||||
self.callback()
|
self.callback()
|
||||||
|
|
||||||
def on_slider(self, value):
|
def on_slider(self, value):
|
||||||
self.update_text()
|
self.update_text()
|
||||||
|
|
||||||
def on_checkbox(self, b):
|
|
||||||
self.dynfees = b
|
|
||||||
self.update_slider()
|
|
||||||
self.update_text()
|
|
||||||
|
|
|
@ -106,4 +106,6 @@ class FxDialog(Factory.Popup):
|
||||||
if ccy != self.fx.get_currency():
|
if ccy != self.fx.get_currency():
|
||||||
self.fx.set_currency(ccy)
|
self.fx.set_currency(ccy)
|
||||||
self.app.fiat_unit = ccy
|
self.app.fiat_unit = ccy
|
||||||
|
else:
|
||||||
|
self.app.is_fiat = False
|
||||||
Clock.schedule_once(lambda dt: self.add_exchanges())
|
Clock.schedule_once(lambda dt: self.add_exchanges())
|
||||||
|
|
|
@ -135,7 +135,7 @@ Builder.load_string('''
|
||||||
height: self.minimum_height
|
height: self.minimum_height
|
||||||
Label:
|
Label:
|
||||||
color: root.text_color
|
color: root.text_color
|
||||||
text: _('From %d cosigners')%n.value
|
text: _('From {} cosigners').format(n.value)
|
||||||
Slider:
|
Slider:
|
||||||
id: n
|
id: n
|
||||||
range: 2, 5
|
range: 2, 5
|
||||||
|
@ -143,7 +143,7 @@ Builder.load_string('''
|
||||||
value: 2
|
value: 2
|
||||||
Label:
|
Label:
|
||||||
color: root.text_color
|
color: root.text_color
|
||||||
text: _('Require %d signatures')%m.value
|
text: _('Require {} signatures').format(m.value)
|
||||||
Slider:
|
Slider:
|
||||||
id: m
|
id: m
|
||||||
range: 1, n.value
|
range: 1, n.value
|
||||||
|
@ -613,7 +613,7 @@ class RestoreSeedDialog(WizardDialog):
|
||||||
for c in line.children:
|
for c in line.children:
|
||||||
if isinstance(c, Button):
|
if isinstance(c, Button):
|
||||||
if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
|
if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
|
||||||
c.disabled = (c.text.lower() not in p) and last_word
|
c.disabled = (c.text.lower() not in p) and bool(last_word)
|
||||||
elif c.text == ' ':
|
elif c.text == ' ':
|
||||||
c.disabled = not enable_space
|
c.disabled = not enable_space
|
||||||
|
|
||||||
|
@ -807,7 +807,7 @@ class InstallWizard(BaseWizard, Widget):
|
||||||
popup.init(message, callback)
|
popup.init(message, callback)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
def request_password(self, run_next):
|
def request_password(self, run_next, force_disable_encrypt_cb=False):
|
||||||
def callback(pin):
|
def callback(pin):
|
||||||
if pin:
|
if pin:
|
||||||
self.run('confirm_password', pin, run_next)
|
self.run('confirm_password', pin, run_next)
|
||||||
|
|
|
@ -8,7 +8,6 @@ from electrum.i18n import languages
|
||||||
from electrum_gui.kivy.i18n import _
|
from electrum_gui.kivy.i18n import _
|
||||||
from electrum.plugins import run_hook
|
from electrum.plugins import run_hook
|
||||||
from electrum import coinchooser
|
from electrum import coinchooser
|
||||||
from electrum.util import fee_levels
|
|
||||||
|
|
||||||
from .choice_dialog import ChoiceDialog
|
from .choice_dialog import ChoiceDialog
|
||||||
|
|
||||||
|
@ -49,12 +48,6 @@ Builder.load_string('''
|
||||||
description: _("Base unit for Zclassic amounts.")
|
description: _("Base unit for Zclassic amounts.")
|
||||||
action: partial(root.unit_dialog, self)
|
action: partial(root.unit_dialog, self)
|
||||||
CardSeparator
|
CardSeparator
|
||||||
SettingsItem:
|
|
||||||
status: root.fee_status()
|
|
||||||
title: _('Fees') + ': ' + self.status
|
|
||||||
description: _("Fees paid to the Zclassic miners.")
|
|
||||||
action: partial(root.fee_dialog, self)
|
|
||||||
CardSeparator
|
|
||||||
SettingsItem:
|
SettingsItem:
|
||||||
status: root.fx_status()
|
status: root.fx_status()
|
||||||
title: _('Fiat Currency') + ': ' + self.status
|
title: _('Fiat Currency') + ': ' + self.status
|
||||||
|
@ -113,7 +106,6 @@ class SettingsDialog(Factory.Popup):
|
||||||
layout.bind(minimum_height=layout.setter('height'))
|
layout.bind(minimum_height=layout.setter('height'))
|
||||||
# cached dialogs
|
# cached dialogs
|
||||||
self._fx_dialog = None
|
self._fx_dialog = None
|
||||||
self._fee_dialog = None
|
|
||||||
self._proxy_dialog = None
|
self._proxy_dialog = None
|
||||||
self._language_dialog = None
|
self._language_dialog = None
|
||||||
self._unit_dialog = None
|
self._unit_dialog = None
|
||||||
|
@ -204,18 +196,7 @@ class SettingsDialog(Factory.Popup):
|
||||||
d.open()
|
d.open()
|
||||||
|
|
||||||
def fee_status(self):
|
def fee_status(self):
|
||||||
if self.config.get('dynamic_fees', True):
|
return self.config.get_fee_status()
|
||||||
return fee_levels[self.config.get('fee_level', 2)]
|
|
||||||
else:
|
|
||||||
return self.app.format_amount_and_units(self.config.fee_per_kb()) + '/kB'
|
|
||||||
|
|
||||||
def fee_dialog(self, label, dt):
|
|
||||||
if self._fee_dialog is None:
|
|
||||||
from .fee_dialog import FeeDialog
|
|
||||||
def cb():
|
|
||||||
label.status = self.fee_status()
|
|
||||||
self._fee_dialog = FeeDialog(self.app, self.config, cb)
|
|
||||||
self._fee_dialog.open()
|
|
||||||
|
|
||||||
def boolean_dialog(self, name, title, message, dt):
|
def boolean_dialog(self, name, title, message, dt):
|
||||||
from .checkbox_dialog import CheckBoxDialog
|
from .checkbox_dialog import CheckBoxDialog
|
||||||
|
|
|
@ -20,6 +20,7 @@ Builder.load_string('''
|
||||||
can_rbf: False
|
can_rbf: False
|
||||||
fee_str: ''
|
fee_str: ''
|
||||||
date_str: ''
|
date_str: ''
|
||||||
|
date_label:''
|
||||||
amount_str: ''
|
amount_str: ''
|
||||||
tx_hash: ''
|
tx_hash: ''
|
||||||
status_str: ''
|
status_str: ''
|
||||||
|
@ -46,7 +47,7 @@ Builder.load_string('''
|
||||||
text: _('Description') if root.description else ''
|
text: _('Description') if root.description else ''
|
||||||
value: root.description
|
value: root.description
|
||||||
BoxLabel:
|
BoxLabel:
|
||||||
text: _('Date') if root.date_str else ''
|
text: root.date_label
|
||||||
value: root.date_str
|
value: root.date_str
|
||||||
BoxLabel:
|
BoxLabel:
|
||||||
text: _('Amount sent') if root.is_mine else _('Amount received')
|
text: _('Amount sent') if root.is_mine else _('Amount received')
|
||||||
|
@ -110,10 +111,13 @@ class TxDialog(Factory.Popup):
|
||||||
tx_hash, self.status_str, self.description, self.can_broadcast, self.can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx)
|
tx_hash, self.status_str, self.description, self.can_broadcast, self.can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx)
|
||||||
self.tx_hash = tx_hash or ''
|
self.tx_hash = tx_hash or ''
|
||||||
if timestamp:
|
if timestamp:
|
||||||
|
self.date_label = _('Date')
|
||||||
self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
||||||
elif exp_n:
|
elif exp_n:
|
||||||
self.date_str = _('Within %d blocks') % exp_n if exp_n > 0 else _('unknown (low fee)')
|
self.date_label = _('Mempool depth')
|
||||||
|
self.date_str = _('{} from tip').format('%.2f MB'%(exp_n/1000000))
|
||||||
else:
|
else:
|
||||||
|
self.date_label = ''
|
||||||
self.date_str = ''
|
self.date_str = ''
|
||||||
|
|
||||||
if amount is None:
|
if amount is None:
|
||||||
|
|
|
@ -87,9 +87,9 @@ class CScreen(Factory.Screen):
|
||||||
self.add_widget(self.context_menu)
|
self.add_widget(self.context_menu)
|
||||||
|
|
||||||
|
|
||||||
|
# note: this list needs to be kept in sync with another in qt
|
||||||
TX_ICONS = [
|
TX_ICONS = [
|
||||||
"close",
|
"unconfirmed",
|
||||||
"close",
|
|
||||||
"close",
|
"close",
|
||||||
"unconfirmed",
|
"unconfirmed",
|
||||||
"close",
|
"close",
|
||||||
|
@ -143,14 +143,13 @@ class HistoryScreen(CScreen):
|
||||||
ri.icon = icon
|
ri.icon = icon
|
||||||
ri.date = status_str
|
ri.date = status_str
|
||||||
ri.message = label
|
ri.message = label
|
||||||
ri.value = value or 0
|
|
||||||
ri.amount = self.app.format_amount(value, True) if value is not None else '--'
|
|
||||||
ri.confirmations = conf
|
ri.confirmations = conf
|
||||||
if self.app.fiat_unit and date:
|
if value is not None:
|
||||||
rate = self.app.fx.history_rate(date)
|
ri.is_mine = value < 0
|
||||||
if rate:
|
if value < 0: value = - value
|
||||||
s = self.app.fx.value_str(value, rate)
|
ri.amount = self.app.format_amount_and_units(value)
|
||||||
ri.quote_text = '' if s is None else s + ' ' + self.app.fiat_unit
|
if self.app.fiat_unit and date:
|
||||||
|
ri.quote_text = self.app.fx.historical_value_str(value, date) + ' ' + self.app.fx.ccy
|
||||||
return ri
|
return ri
|
||||||
|
|
||||||
def update(self, see_all=False):
|
def update(self, see_all=False):
|
||||||
|
@ -162,13 +161,8 @@ class HistoryScreen(CScreen):
|
||||||
count = 0
|
count = 0
|
||||||
for item in history:
|
for item in history:
|
||||||
ri = self.get_card(*item)
|
ri = self.get_card(*item)
|
||||||
count += 1
|
|
||||||
history_card.add_widget(ri)
|
history_card.add_widget(ri)
|
||||||
|
|
||||||
if count == 0:
|
|
||||||
msg = _('This screen shows your list of transactions. It is currently empty.')
|
|
||||||
history_card.add_widget(EmptyLabel(text=msg))
|
|
||||||
|
|
||||||
|
|
||||||
class SendScreen(CScreen):
|
class SendScreen(CScreen):
|
||||||
|
|
||||||
|
@ -379,6 +373,8 @@ class ReceiveScreen(CScreen):
|
||||||
|
|
||||||
def save_request(self):
|
def save_request(self):
|
||||||
addr = self.screen.address
|
addr = self.screen.address
|
||||||
|
if not addr:
|
||||||
|
return
|
||||||
amount = self.screen.amount
|
amount = self.screen.amount
|
||||||
message = self.screen.message
|
message = self.screen.message
|
||||||
amount = self.app.get_amount(amount) if amount else 0
|
amount = self.app.get_amount(amount) if amount else 0
|
||||||
|
@ -521,7 +517,12 @@ class AddressScreen(CScreen):
|
||||||
def update(self):
|
def update(self):
|
||||||
self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)]
|
self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)]
|
||||||
wallet = self.app.wallet
|
wallet = self.app.wallet
|
||||||
_list = wallet.get_change_addresses() if self.screen.show_change else wallet.get_receiving_addresses()
|
if self.screen.show_change == 0:
|
||||||
|
_list = wallet.get_receiving_addresses()
|
||||||
|
elif self.screen.show_change == 1:
|
||||||
|
_list = wallet.get_change_addresses()
|
||||||
|
else:
|
||||||
|
_list = wallet.get_addresses()
|
||||||
search = self.screen.message
|
search = self.screen.message
|
||||||
container = self.screen.ids.search_container
|
container = self.screen.ids.search_container
|
||||||
container.clear_widgets()
|
container.clear_widgets()
|
||||||
|
|
|
@ -24,33 +24,18 @@
|
||||||
shorten: True
|
shorten: True
|
||||||
Widget
|
Widget
|
||||||
AddressLabel:
|
AddressLabel:
|
||||||
text: root.memo
|
text: (root.amount if root.status == 'Funded' else root.status) + ' ' + root.memo
|
||||||
color: .699, .699, .699, 1
|
color: .699, .699, .699, 1
|
||||||
font_size: '13sp'
|
font_size: '13sp'
|
||||||
shorten: True
|
shorten: True
|
||||||
Widget
|
Widget
|
||||||
BoxLayout:
|
|
||||||
spacing: '8dp'
|
|
||||||
height: '32dp'
|
|
||||||
orientation: 'vertical'
|
|
||||||
Widget
|
|
||||||
AddressLabel:
|
|
||||||
text: root.amount
|
|
||||||
halign: 'right'
|
|
||||||
font_size: '15sp'
|
|
||||||
Widget
|
|
||||||
AddressLabel:
|
|
||||||
text: root.status
|
|
||||||
halign: 'right'
|
|
||||||
font_size: '13sp'
|
|
||||||
color: .699, .699, .699, 1
|
|
||||||
|
|
||||||
AddressScreen:
|
AddressScreen:
|
||||||
id: addr_screen
|
id: addr_screen
|
||||||
name: 'address'
|
name: 'address'
|
||||||
message: ''
|
message: ''
|
||||||
pr_status: 'Pending'
|
pr_status: 'Pending'
|
||||||
show_change: False
|
show_change: 0
|
||||||
show_used: 0
|
show_used: 0
|
||||||
on_message:
|
on_message:
|
||||||
self.parent.update()
|
self.parent.update()
|
||||||
|
@ -70,9 +55,9 @@ AddressScreen:
|
||||||
spacing: '5dp'
|
spacing: '5dp'
|
||||||
AddressButton:
|
AddressButton:
|
||||||
id: search
|
id: search
|
||||||
text: _('Change') if root.show_change else _('Receiving')
|
text: {0:_('Receiving'), 1:_('Change'), 2:_('All')}[root.show_change]
|
||||||
on_release:
|
on_release:
|
||||||
root.show_change = not root.show_change
|
root.show_change = (root.show_change + 1) % 3
|
||||||
Clock.schedule_once(lambda dt: app.address_screen.update())
|
Clock.schedule_once(lambda dt: app.address_screen.update())
|
||||||
AddressFilter:
|
AddressFilter:
|
||||||
opacity: 1
|
opacity: 1
|
||||||
|
@ -103,4 +88,3 @@ AddressScreen:
|
||||||
id: search_container
|
id: search_container
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
height: self.minimum_height
|
height: self.minimum_height
|
||||||
spacing: '2dp'
|
|
||||||
|
|
|
@ -19,56 +19,57 @@
|
||||||
<HistoryItem@CardItem>
|
<HistoryItem@CardItem>
|
||||||
icon: 'atlas://gui/kivy/theming/light/important'
|
icon: 'atlas://gui/kivy/theming/light/important'
|
||||||
message: ''
|
message: ''
|
||||||
value: 0
|
is_mine: True
|
||||||
amount: '--'
|
amount: '--'
|
||||||
amount_color: '#FF6657' if self.value < 0 else '#2EA442'
|
action: _('Sent') if self.is_mine else _('Received')
|
||||||
|
amount_color: '#FF6657' if self.is_mine else '#2EA442'
|
||||||
confirmations: 0
|
confirmations: 0
|
||||||
date: ''
|
date: ''
|
||||||
quote_text: ''
|
quote_text: ''
|
||||||
spacing: '9dp'
|
|
||||||
Image:
|
Image:
|
||||||
id: icon
|
id: icon
|
||||||
source: root.icon
|
source: root.icon
|
||||||
size_hint: None, 1
|
size_hint: None, 1
|
||||||
width: self.height *.54
|
allow_stretch: True
|
||||||
|
width: self.height*1.5
|
||||||
mipmap: True
|
mipmap: True
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'vertical'
|
orientation: 'vertical'
|
||||||
Widget
|
Widget
|
||||||
CardLabel:
|
CardLabel:
|
||||||
text: root.date
|
text:
|
||||||
font_size: '14sp'
|
u'[color={color}]{s}[/color]'.format(s='<<' if root.is_mine else '>>', color=root.amount_color)\
|
||||||
|
+ ' ' + root.action + ' ' + (root.quote_text if app.is_fiat else root.amount)
|
||||||
|
font_size: '15sp'
|
||||||
CardLabel:
|
CardLabel:
|
||||||
color: .699, .699, .699, 1
|
color: .699, .699, .699, 1
|
||||||
font_size: '13sp'
|
font_size: '14sp'
|
||||||
shorten: True
|
shorten: True
|
||||||
text: root.message
|
text: root.date + ' ' + root.message
|
||||||
Widget
|
Widget
|
||||||
CardLabel:
|
|
||||||
halign: 'right'
|
|
||||||
font_size: '15sp'
|
|
||||||
size_hint: None, 1
|
|
||||||
width: '110sp'
|
|
||||||
markup: True
|
|
||||||
font_name: font_light
|
|
||||||
text:
|
|
||||||
u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\
|
|
||||||
u'[color=#B2B3B3][size=13sp]{qt}[/size]'\
|
|
||||||
u'[/color]'.format(amount_color=root.amount_color,\
|
|
||||||
amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\
|
|
||||||
unit=app.base_unit)
|
|
||||||
|
|
||||||
|
|
||||||
HistoryScreen:
|
HistoryScreen:
|
||||||
name: 'history'
|
name: 'history'
|
||||||
content: content
|
content: content
|
||||||
ScrollView:
|
BoxLayout:
|
||||||
id: content
|
orientation: 'vertical'
|
||||||
do_scroll_x: False
|
Button:
|
||||||
GridLayout
|
background_color: 0, 0, 0, 0
|
||||||
id: history_container
|
text: app.fiat_balance if app.is_fiat else app.balance
|
||||||
cols: 1
|
markup: True
|
||||||
size_hint: 1, None
|
color: .9, .9, .9, 1
|
||||||
height: self.minimum_height
|
font_size: '30dp'
|
||||||
padding: '12dp'
|
bold: True
|
||||||
spacing: '2dp'
|
size_hint: 1, 0.25
|
||||||
|
on_release: app.is_fiat = not app.is_fiat if app.fx.is_enabled() else False
|
||||||
|
ScrollView:
|
||||||
|
id: content
|
||||||
|
do_scroll_x: False
|
||||||
|
size_hint: 1, 0.75
|
||||||
|
GridLayout
|
||||||
|
id: history_container
|
||||||
|
cols: 1
|
||||||
|
size_hint: 1, None
|
||||||
|
height: self.minimum_height
|
||||||
|
|
|
@ -11,7 +11,7 @@ Popup:
|
||||||
height: self.minimum_height
|
height: self.minimum_height
|
||||||
padding: '10dp'
|
padding: '10dp'
|
||||||
SettingsItem:
|
SettingsItem:
|
||||||
value: _("%d connections.")% app.num_nodes if app.num_nodes else _("Not connected")
|
value: _("{} connections.").format(app.num_nodes) if app.num_nodes else _("Not connected")
|
||||||
title: _("Status") + ': ' + self.value
|
title: _("Status") + ': ' + self.value
|
||||||
description: _("Connections with Electrum servers")
|
description: _("Connections with Electrum servers")
|
||||||
action: lambda x: None
|
action: lambda x: None
|
||||||
|
@ -46,7 +46,7 @@ Popup:
|
||||||
|
|
||||||
CardSeparator
|
CardSeparator
|
||||||
SettingsItem:
|
SettingsItem:
|
||||||
title: _('Fork detected at block %d')%app.blockchain_checkpoint if app.num_chains>1 else _('No fork detected')
|
title: _('Fork detected at block {}').format(app.blockchain_checkpoint) if app.num_chains>1 else _('No fork detected')
|
||||||
fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name
|
fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name
|
||||||
description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain')
|
description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain')
|
||||||
action: app.choose_blockchain_dialog
|
action: app.choose_blockchain_dialog
|
||||||
|
|
|
@ -71,6 +71,24 @@ SendScreen:
|
||||||
text: s.message if s.message else (_('No Description') if root.is_pr else _('Description'))
|
text: s.message if s.message else (_('No Description') if root.is_pr else _('Description'))
|
||||||
disabled: root.is_pr
|
disabled: root.is_pr
|
||||||
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
|
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
|
||||||
|
CardSeparator:
|
||||||
|
opacity: int(not root.is_pr)
|
||||||
|
color: blue_bottom.foreground_color
|
||||||
|
BoxLayout:
|
||||||
|
size_hint: 1, None
|
||||||
|
height: blue_bottom.item_height
|
||||||
|
spacing: '5dp'
|
||||||
|
Image:
|
||||||
|
source: 'atlas://gui/kivy/theming/light/star_big_inactive'
|
||||||
|
opacity: 0.7
|
||||||
|
size_hint: None, None
|
||||||
|
size: '22dp', '22dp'
|
||||||
|
pos_hint: {'center_y': .5}
|
||||||
|
BlueButton:
|
||||||
|
id: fee_e
|
||||||
|
default_text: _('Fee')
|
||||||
|
text: app.fee_status
|
||||||
|
on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True))
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -94,6 +95,8 @@ class ElectrumGui:
|
||||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
|
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
|
||||||
if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
|
if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
|
||||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
|
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
|
||||||
|
if hasattr(QGuiApplication, 'setDesktopFileName'):
|
||||||
|
QGuiApplication.setDesktopFileName('electrum.desktop')
|
||||||
self.config = config
|
self.config = config
|
||||||
self.daemon = daemon
|
self.daemon = daemon
|
||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
|
@ -190,8 +193,10 @@ class ElectrumGui:
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
wallet = self.daemon.load_wallet(path, None)
|
wallet = self.daemon.load_wallet(path, None)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e))
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
d = QMessageBox(QMessageBox.Warning, _('Error'),
|
||||||
|
_('Cannot load wallet:') + '\n' + str(e))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
return
|
return
|
||||||
if not wallet:
|
if not wallet:
|
||||||
|
@ -208,7 +213,14 @@ class ElectrumGui:
|
||||||
return
|
return
|
||||||
wallet.start_threads(self.daemon.network)
|
wallet.start_threads(self.daemon.network)
|
||||||
self.daemon.add_wallet(wallet)
|
self.daemon.add_wallet(wallet)
|
||||||
w = self.create_window_for_wallet(wallet)
|
try:
|
||||||
|
w = self.create_window_for_wallet(wallet)
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
d = QMessageBox(QMessageBox.Warning, _('Error'),
|
||||||
|
_('Cannot create window for wallet:') + '\n' + str(e))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
if uri:
|
if uri:
|
||||||
w.pay_to_URI(uri)
|
w.pay_to_URI(uri)
|
||||||
w.bring_to_top()
|
w.bring_to_top()
|
||||||
|
@ -241,8 +253,7 @@ class ElectrumGui:
|
||||||
return
|
return
|
||||||
except GoBack:
|
except GoBack:
|
||||||
return
|
return
|
||||||
except:
|
except BaseException as e:
|
||||||
import traceback
|
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
return
|
return
|
||||||
self.timer.start()
|
self.timer.start()
|
||||||
|
|
|
@ -24,47 +24,56 @@
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
from .util import *
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import block_explorer_URL
|
from electrum.util import block_explorer_URL
|
||||||
from electrum.plugins import run_hook
|
from electrum.plugins import run_hook
|
||||||
from electrum.bitcoin import is_address
|
from electrum.bitcoin import is_address
|
||||||
|
|
||||||
|
from .util import *
|
||||||
|
|
||||||
|
|
||||||
class AddressList(MyTreeWidget):
|
class AddressList(MyTreeWidget):
|
||||||
filter_columns = [0, 1, 2] # Address, Label, Balance
|
filter_columns = [0, 1, 2, 3] # Type, Address, Label, Balance
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
MyTreeWidget.__init__(self, parent, self.create_menu, [], 1)
|
MyTreeWidget.__init__(self, parent, self.create_menu, [], 2)
|
||||||
self.refresh_headers()
|
self.refresh_headers()
|
||||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
self.show_change = False
|
self.setSortingEnabled(True)
|
||||||
|
self.show_change = 0
|
||||||
self.show_used = 0
|
self.show_used = 0
|
||||||
self.change_button = QComboBox(self)
|
self.change_button = QComboBox(self)
|
||||||
self.change_button.currentIndexChanged.connect(self.toggle_change)
|
self.change_button.currentIndexChanged.connect(self.toggle_change)
|
||||||
for t in [_('Receiving'), _('Change')]:
|
for t in [_('All'), _('Receiving'), _('Change')]:
|
||||||
self.change_button.addItem(t)
|
self.change_button.addItem(t)
|
||||||
self.used_button = QComboBox(self)
|
self.used_button = QComboBox(self)
|
||||||
self.used_button.currentIndexChanged.connect(self.toggle_used)
|
self.used_button.currentIndexChanged.connect(self.toggle_used)
|
||||||
for t in [_('All'), _('Unused'), _('Funded'), _('Used')]:
|
for t in [_('All'), _('Unused'), _('Funded'), _('Used')]:
|
||||||
self.used_button.addItem(t)
|
self.used_button.addItem(t)
|
||||||
|
|
||||||
def get_list_header(self):
|
def get_toolbar_buttons(self):
|
||||||
return QLabel(_("Filter ")), self.change_button, self.used_button
|
return QLabel(_("Filter:")), self.change_button, self.used_button
|
||||||
|
|
||||||
|
def on_hide_toolbar(self):
|
||||||
|
self.show_change = 0
|
||||||
|
self.show_used = 0
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def save_toolbar_state(self, state, config):
|
||||||
|
config.set_key('show_toolbar_addresses', state)
|
||||||
|
|
||||||
def refresh_headers(self):
|
def refresh_headers(self):
|
||||||
headers = [ _('Address'), _('Label'), _('Balance')]
|
headers = [_('Type'), _('Address'), _('Label'), _('Balance')]
|
||||||
fx = self.parent.fx
|
fx = self.parent.fx
|
||||||
if fx and fx.get_fiat_address_config():
|
if fx and fx.get_fiat_address_config():
|
||||||
headers.extend([_(fx.get_currency()+' Balance')])
|
headers.extend([_(fx.get_currency()+' Balance')])
|
||||||
headers.extend([_('Tx')])
|
headers.extend([_('Tx')])
|
||||||
self.update_headers(headers)
|
self.update_headers(headers)
|
||||||
|
|
||||||
def toggle_change(self, show):
|
def toggle_change(self, state):
|
||||||
show = bool(show)
|
if state == self.show_change:
|
||||||
if show == self.show_change:
|
|
||||||
return
|
return
|
||||||
self.show_change = show
|
self.show_change = state
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def toggle_used(self, state):
|
def toggle_used(self, state):
|
||||||
|
@ -77,10 +86,15 @@ class AddressList(MyTreeWidget):
|
||||||
self.wallet = self.parent.wallet
|
self.wallet = self.parent.wallet
|
||||||
item = self.currentItem()
|
item = self.currentItem()
|
||||||
current_address = item.data(0, Qt.UserRole) if item else None
|
current_address = item.data(0, Qt.UserRole) if item else None
|
||||||
addr_list = self.wallet.get_change_addresses() if self.show_change else self.wallet.get_receiving_addresses()
|
if self.show_change == 1:
|
||||||
|
addr_list = self.wallet.get_receiving_addresses()
|
||||||
|
elif self.show_change == 2:
|
||||||
|
addr_list = self.wallet.get_change_addresses()
|
||||||
|
else:
|
||||||
|
addr_list = self.wallet.get_addresses()
|
||||||
self.clear()
|
self.clear()
|
||||||
for address in addr_list:
|
for address in addr_list:
|
||||||
num = len(self.wallet.history.get(address,[]))
|
num = len(self.wallet.get_address_history(address))
|
||||||
is_used = self.wallet.is_used(address)
|
is_used = self.wallet.is_used(address)
|
||||||
label = self.wallet.labels.get(address, '')
|
label = self.wallet.labels.get(address, '')
|
||||||
c, u, x = self.wallet.get_addr_balance(address)
|
c, u, x = self.wallet.get_addr_balance(address)
|
||||||
|
@ -91,23 +105,29 @@ class AddressList(MyTreeWidget):
|
||||||
continue
|
continue
|
||||||
if self.show_used == 3 and not is_used:
|
if self.show_used == 3 and not is_used:
|
||||||
continue
|
continue
|
||||||
balance_text = self.parent.format_amount(balance)
|
balance_text = self.parent.format_amount(balance, whitespaces=True)
|
||||||
fx = self.parent.fx
|
fx = self.parent.fx
|
||||||
if fx and fx.get_fiat_address_config():
|
if fx and fx.get_fiat_address_config():
|
||||||
rate = fx.exchange_rate()
|
rate = fx.exchange_rate()
|
||||||
fiat_balance = fx.value_str(balance, rate)
|
fiat_balance = fx.value_str(balance, rate)
|
||||||
address_item = QTreeWidgetItem([address, label, balance_text, fiat_balance, "%d"%num])
|
address_item = SortableTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num])
|
||||||
address_item.setTextAlignment(3, Qt.AlignRight)
|
address_item.setTextAlignment(4, Qt.AlignRight)
|
||||||
|
address_item.setFont(4, QFont(MONOSPACE_FONT))
|
||||||
else:
|
else:
|
||||||
address_item = QTreeWidgetItem([address, label, balance_text, "%d"%num])
|
address_item = SortableTreeWidgetItem(['', address, label, balance_text, "%d"%num])
|
||||||
address_item.setTextAlignment(2, Qt.AlignRight)
|
address_item.setFont(3, QFont(MONOSPACE_FONT))
|
||||||
address_item.setFont(0, QFont(MONOSPACE_FONT))
|
if self.wallet.is_change(address):
|
||||||
address_item.setData(0, Qt.UserRole, address)
|
address_item.setText(0, _('change'))
|
||||||
address_item.setData(0, Qt.UserRole+1, True) # label can be edited
|
address_item.setBackground(0, ColorScheme.YELLOW.as_color(True))
|
||||||
|
else:
|
||||||
|
address_item.setText(0, _('receiving'))
|
||||||
|
address_item.setBackground(0, ColorScheme.GREEN.as_color(True))
|
||||||
|
address_item.setFont(1, QFont(MONOSPACE_FONT))
|
||||||
|
address_item.setData(1, Qt.UserRole, address)
|
||||||
if self.wallet.is_frozen(address):
|
if self.wallet.is_frozen(address):
|
||||||
address_item.setBackground(0, ColorScheme.BLUE.as_color(True))
|
address_item.setBackground(1, ColorScheme.BLUE.as_color(True))
|
||||||
if self.wallet.is_beyond_limit(address, self.show_change):
|
if self.wallet.is_beyond_limit(address):
|
||||||
address_item.setBackground(0, ColorScheme.RED.as_color(True))
|
address_item.setBackground(1, ColorScheme.RED.as_color(True))
|
||||||
self.addChild(address_item)
|
self.addChild(address_item)
|
||||||
if address == current_address:
|
if address == current_address:
|
||||||
self.setCurrentItem(address_item)
|
self.setCurrentItem(address_item)
|
||||||
|
@ -118,7 +138,7 @@ class AddressList(MyTreeWidget):
|
||||||
can_delete = self.wallet.can_delete_address()
|
can_delete = self.wallet.can_delete_address()
|
||||||
selected = self.selectedItems()
|
selected = self.selectedItems()
|
||||||
multi_select = len(selected) > 1
|
multi_select = len(selected) > 1
|
||||||
addrs = [item.text(0) for item in selected]
|
addrs = [item.text(1) for item in selected]
|
||||||
if not addrs:
|
if not addrs:
|
||||||
return
|
return
|
||||||
if not multi_select:
|
if not multi_select:
|
||||||
|
@ -135,10 +155,10 @@ class AddressList(MyTreeWidget):
|
||||||
if not multi_select:
|
if not multi_select:
|
||||||
column_title = self.headerItem().text(col)
|
column_title = self.headerItem().text(col)
|
||||||
copy_text = item.text(col)
|
copy_text = item.text(col)
|
||||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(copy_text))
|
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text))
|
||||||
menu.addAction(_('Details'), lambda: self.parent.show_address(addr))
|
menu.addAction(_('Details'), lambda: self.parent.show_address(addr))
|
||||||
if col in self.editable_columns:
|
if col in self.editable_columns:
|
||||||
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, col))
|
menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, col))
|
||||||
menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr))
|
menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr))
|
||||||
if self.wallet.can_export():
|
if self.wallet.can_export():
|
||||||
menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr))
|
menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr))
|
||||||
|
|
|
@ -106,12 +106,7 @@ class BTCAmountEdit(AmountEdit):
|
||||||
|
|
||||||
class FeerateEdit(BTCAmountEdit):
|
class FeerateEdit(BTCAmountEdit):
|
||||||
def _base_unit(self):
|
def _base_unit(self):
|
||||||
p = self.decimal_point()
|
return 'zat/byte'
|
||||||
if p == 2:
|
|
||||||
return 'mZCL/kB'
|
|
||||||
if p == 0:
|
|
||||||
return 'zat/byte'
|
|
||||||
raise Exception('Unknown base unit')
|
|
||||||
|
|
||||||
def get_amount(self):
|
def get_amount(self):
|
||||||
sat_per_byte_amount = BTCAmountEdit.get_amount(self)
|
sat_per_byte_amount = BTCAmountEdit.get_amount(self)
|
||||||
|
|
|
@ -203,7 +203,8 @@ class Console(QtWidgets.QPlainTextEdit):
|
||||||
self.skip = not self.skip
|
self.skip = not self.skip
|
||||||
|
|
||||||
if type(self.namespace.get(command)) == type(lambda:None):
|
if type(self.namespace.get(command)) == type(lambda:None):
|
||||||
self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command))
|
self.appendPlainText("'{}' is a function. Type '{}()' to use it in the Python console."
|
||||||
|
.format(command, command))
|
||||||
self.newPrompt()
|
self.newPrompt()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ from PyQt5.QtGui import *
|
||||||
from PyQt5.QtCore import *
|
from PyQt5.QtCore import *
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem)
|
QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem)
|
||||||
from .util import MyTreeWidget
|
from .util import MyTreeWidget, import_meta_gui, export_meta_gui
|
||||||
|
|
||||||
|
|
||||||
class ContactList(MyTreeWidget):
|
class ContactList(MyTreeWidget):
|
||||||
|
@ -53,12 +53,10 @@ class ContactList(MyTreeWidget):
|
||||||
self.parent.set_contact(item.text(0), item.text(1))
|
self.parent.set_contact(item.text(0), item.text(1))
|
||||||
|
|
||||||
def import_contacts(self):
|
def import_contacts(self):
|
||||||
wallet_folder = self.parent.get_wallet_folder()
|
import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update)
|
||||||
filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
|
|
||||||
if not filename:
|
def export_contacts(self):
|
||||||
return
|
export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file)
|
||||||
self.parent.contacts.import_file(filename)
|
|
||||||
self.on_update()
|
|
||||||
|
|
||||||
def create_menu(self, position):
|
def create_menu(self, position):
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
|
@ -66,16 +64,17 @@ class ContactList(MyTreeWidget):
|
||||||
if not selected:
|
if not selected:
|
||||||
menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog())
|
menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog())
|
||||||
menu.addAction(_("Import file"), lambda: self.import_contacts())
|
menu.addAction(_("Import file"), lambda: self.import_contacts())
|
||||||
|
menu.addAction(_("Export file"), lambda: self.export_contacts())
|
||||||
else:
|
else:
|
||||||
names = [item.text(0) for item in selected]
|
names = [item.text(0) for item in selected]
|
||||||
keys = [item.text(1) for item in selected]
|
keys = [item.text(1) for item in selected]
|
||||||
column = self.currentColumn()
|
column = self.currentColumn()
|
||||||
column_title = self.headerItem().text(column)
|
column_title = self.headerItem().text(column)
|
||||||
column_data = '\n'.join([item.text(column) for item in selected])
|
column_data = '\n'.join([item.text(column) for item in selected])
|
||||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
|
||||||
if column in self.editable_columns:
|
if column in self.editable_columns:
|
||||||
item = self.currentItem()
|
item = self.currentItem()
|
||||||
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
|
menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column))
|
||||||
menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys))
|
menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys))
|
||||||
menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys))
|
menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys))
|
||||||
URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)]
|
URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)]
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Electrum - lightweight Bitcoin client
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation files
|
||||||
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
import json
|
||||||
|
import locale
|
||||||
|
import platform
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from PyQt5.QtCore import QObject
|
||||||
|
import PyQt5.QtCore as QtCore
|
||||||
|
from PyQt5.QtGui import QIcon
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum import ELECTRUM_VERSION, bitcoin, constants
|
||||||
|
|
||||||
|
issue_template = """<h2>Traceback</h2>
|
||||||
|
<pre>
|
||||||
|
{traceback}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h2>Additional information</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Electrum version: {app_version}</li>
|
||||||
|
<li>Operating system: {os}</li>
|
||||||
|
<li>Wallet type: {wallet_type}</li>
|
||||||
|
<li>Locale: {locale}</li>
|
||||||
|
</ul>
|
||||||
|
"""
|
||||||
|
report_server = "https://crashhub.electrum.org/crash"
|
||||||
|
|
||||||
|
|
||||||
|
class Exception_Window(QWidget):
|
||||||
|
_active_window = None
|
||||||
|
|
||||||
|
def __init__(self, main_window, exctype, value, tb):
|
||||||
|
self.exc_args = (exctype, value, tb)
|
||||||
|
self.main_window = main_window
|
||||||
|
QWidget.__init__(self)
|
||||||
|
self.setWindowTitle('Electrum - ' + _('An Error Occured'))
|
||||||
|
self.setMinimumSize(600, 300)
|
||||||
|
|
||||||
|
main_box = QVBoxLayout()
|
||||||
|
|
||||||
|
heading = QLabel('<h2>' + _('Sorry!') + '</h2>')
|
||||||
|
main_box.addWidget(heading)
|
||||||
|
main_box.addWidget(QLabel(_('Something went wrong while executing Electrum.')))
|
||||||
|
|
||||||
|
main_box.addWidget(QLabel(
|
||||||
|
_('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug '
|
||||||
|
'information:')))
|
||||||
|
|
||||||
|
collapse_info = QPushButton(_("Show report contents"))
|
||||||
|
collapse_info.clicked.connect(lambda: QMessageBox.about(self, "Report contents", self.get_report_string()))
|
||||||
|
main_box.addWidget(collapse_info)
|
||||||
|
|
||||||
|
main_box.addWidget(QLabel(_("Please briefly describe what led to the error (optional):")))
|
||||||
|
|
||||||
|
self.description_textfield = QTextEdit()
|
||||||
|
self.description_textfield.setFixedHeight(50)
|
||||||
|
main_box.addWidget(self.description_textfield)
|
||||||
|
|
||||||
|
main_box.addWidget(QLabel(_("Do you want to send this report?")))
|
||||||
|
|
||||||
|
buttons = QHBoxLayout()
|
||||||
|
|
||||||
|
report_button = QPushButton(_('Send Bug Report'))
|
||||||
|
report_button.clicked.connect(self.send_report)
|
||||||
|
report_button.setIcon(QIcon(":icons/tab_send.png"))
|
||||||
|
buttons.addWidget(report_button)
|
||||||
|
|
||||||
|
never_button = QPushButton(_('Never'))
|
||||||
|
never_button.clicked.connect(self.show_never)
|
||||||
|
buttons.addWidget(never_button)
|
||||||
|
|
||||||
|
close_button = QPushButton(_('Not Now'))
|
||||||
|
close_button.clicked.connect(self.close)
|
||||||
|
buttons.addWidget(close_button)
|
||||||
|
|
||||||
|
main_box.addLayout(buttons)
|
||||||
|
|
||||||
|
self.setLayout(main_box)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def send_report(self):
|
||||||
|
if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in report_server:
|
||||||
|
# Gah! Some kind of altcoin wants to send us crash reports.
|
||||||
|
self.main_window.show_critical(_("Please report this issue manually."))
|
||||||
|
return
|
||||||
|
report = self.get_traceback_info()
|
||||||
|
report.update(self.get_additional_info())
|
||||||
|
report = json.dumps(report)
|
||||||
|
try:
|
||||||
|
response = requests.post(report_server, data=report, timeout=20)
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' +
|
||||||
|
str(e) + '\n' +
|
||||||
|
_("Please report this issue manually."))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
QMessageBox.about(self, "Crash report", response.text)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
Exception_Window._active_window = None
|
||||||
|
sys.__excepthook__(*self.exc_args)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def show_never(self):
|
||||||
|
self.main_window.config.set_key("show_crash_reporter", False)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self.on_close()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def get_traceback_info(self):
|
||||||
|
exc_string = str(self.exc_args[1])
|
||||||
|
stack = traceback.extract_tb(self.exc_args[2])
|
||||||
|
readable_trace = "".join(traceback.format_list(stack))
|
||||||
|
id = {
|
||||||
|
"file": stack[-1].filename,
|
||||||
|
"name": stack[-1].name,
|
||||||
|
"type": self.exc_args[0].__name__
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"exc_string": exc_string,
|
||||||
|
"stack": readable_trace,
|
||||||
|
"id": id
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_additional_info(self):
|
||||||
|
args = {
|
||||||
|
"app_version": ELECTRUM_VERSION,
|
||||||
|
"os": platform.platform(),
|
||||||
|
"wallet_type": "unknown",
|
||||||
|
"locale": locale.getdefaultlocale()[0],
|
||||||
|
"description": self.description_textfield.toPlainText()
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
args["wallet_type"] = self.main_window.wallet.wallet_type
|
||||||
|
except:
|
||||||
|
# Maybe the wallet isn't loaded yet
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
args["app_version"] = self.get_git_version()
|
||||||
|
except:
|
||||||
|
# This is probably not running from source
|
||||||
|
pass
|
||||||
|
return args
|
||||||
|
|
||||||
|
def get_report_string(self):
|
||||||
|
info = self.get_additional_info()
|
||||||
|
info["traceback"] = "".join(traceback.format_exception(*self.exc_args))
|
||||||
|
return issue_template.format(**info)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_git_version():
|
||||||
|
dir = os.path.dirname(os.path.realpath(sys.argv[0]))
|
||||||
|
version = subprocess.check_output(['git', 'describe', '--always'], cwd=dir)
|
||||||
|
return str(version, "utf8").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _show_window(*args):
|
||||||
|
if not Exception_Window._active_window:
|
||||||
|
Exception_Window._active_window = Exception_Window(*args)
|
||||||
|
|
||||||
|
|
||||||
|
class Exception_Hook(QObject):
|
||||||
|
_report_exception = QtCore.pyqtSignal(object, object, object, object)
|
||||||
|
|
||||||
|
def __init__(self, main_window, *args, **kwargs):
|
||||||
|
super(Exception_Hook, self).__init__(*args, **kwargs)
|
||||||
|
if not main_window.config.get("show_crash_reporter", default=True):
|
||||||
|
return
|
||||||
|
self.main_window = main_window
|
||||||
|
sys.excepthook = self.handler
|
||||||
|
self._report_exception.connect(_show_window)
|
||||||
|
|
||||||
|
def handler(self, *args):
|
||||||
|
self._report_exception.emit(self.main_window, *args)
|
|
@ -1,6 +1,4 @@
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
|
|
||||||
from PyQt5.QtGui import *
|
from PyQt5.QtGui import *
|
||||||
from PyQt5.QtCore import *
|
from PyQt5.QtCore import *
|
||||||
from PyQt5.QtWidgets import QSlider, QToolTip
|
from PyQt5.QtWidgets import QSlider, QToolTip
|
||||||
|
@ -18,47 +16,43 @@ class FeeSlider(QSlider):
|
||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
self.update()
|
self.update()
|
||||||
self.valueChanged.connect(self.moved)
|
self.valueChanged.connect(self.moved)
|
||||||
|
self._active = True
|
||||||
|
|
||||||
def moved(self, pos):
|
def moved(self, pos):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
fee_rate = self.config.dynfee(pos) if self.dyn else self.config.static_fee(pos)
|
if self.dyn:
|
||||||
|
fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos)
|
||||||
|
else:
|
||||||
|
fee_rate = self.config.static_fee(pos)
|
||||||
tooltip = self.get_tooltip(pos, fee_rate)
|
tooltip = self.get_tooltip(pos, fee_rate)
|
||||||
QToolTip.showText(QCursor.pos(), tooltip, self)
|
QToolTip.showText(QCursor.pos(), tooltip, self)
|
||||||
self.setToolTip(tooltip)
|
self.setToolTip(tooltip)
|
||||||
self.callback(self.dyn, pos, fee_rate)
|
self.callback(self.dyn, pos, fee_rate)
|
||||||
|
|
||||||
def get_tooltip(self, pos, fee_rate):
|
def get_tooltip(self, pos, fee_rate):
|
||||||
from electrum.util import fee_levels
|
mempool = self.config.use_mempool_fees()
|
||||||
rate_str = self.window.format_fee_rate(fee_rate) if fee_rate else _('unknown')
|
target, estimate = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate)
|
||||||
if self.dyn:
|
if self.dyn:
|
||||||
tooltip = fee_levels[pos] + '\n' + rate_str
|
return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate
|
||||||
else:
|
else:
|
||||||
tooltip = 'Fixed rate: ' + rate_str
|
return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate
|
||||||
if self.config.has_fee_estimates():
|
|
||||||
i = self.config.reverse_dynfee(fee_rate)
|
|
||||||
tooltip += '\n' + (_('Low fee') if i < 0 else 'Within %d blocks'%i)
|
|
||||||
return tooltip
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.dyn = self.config.is_dynfee()
|
self.dyn = self.config.is_dynfee()
|
||||||
if self.dyn:
|
mempool = self.config.use_mempool_fees()
|
||||||
pos = self.config.get('fee_level', 2)
|
maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool)
|
||||||
fee_rate = self.config.dynfee(pos)
|
self.setRange(0, maxp)
|
||||||
self.setRange(0, 4)
|
self.setValue(pos)
|
||||||
self.setValue(pos)
|
|
||||||
else:
|
|
||||||
fee_rate = self.config.fee_per_kb()
|
|
||||||
pos = self.config.static_fee_index(fee_rate)
|
|
||||||
self.setRange(0, 9)
|
|
||||||
self.setValue(pos)
|
|
||||||
tooltip = self.get_tooltip(pos, fee_rate)
|
tooltip = self.get_tooltip(pos, fee_rate)
|
||||||
self.setToolTip(tooltip)
|
self.setToolTip(tooltip)
|
||||||
|
|
||||||
def activate(self):
|
def activate(self):
|
||||||
|
self._active = True
|
||||||
self.setStyleSheet('')
|
self.setStyleSheet('')
|
||||||
|
|
||||||
def deactivate(self):
|
def deactivate(self):
|
||||||
|
self._active = False
|
||||||
# TODO it would be nice to find a platform-independent solution
|
# TODO it would be nice to find a platform-independent solution
|
||||||
# that makes the slider look as if it was disabled
|
# that makes the slider look as if it was disabled
|
||||||
self.setStyleSheet(
|
self.setStyleSheet(
|
||||||
|
@ -79,3 +73,6 @@ class FeeSlider(QSlider):
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return self._active
|
||||||
|
|
|
@ -24,19 +24,24 @@
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from electrum.wallet import AddTransactionException, TX_HEIGHT_LOCAL
|
||||||
from .util import *
|
from .util import *
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import block_explorer_URL
|
from electrum.util import block_explorer_URL, profiler
|
||||||
from electrum.util import timestamp_to_datetime, profiler
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from electrum.plot import plot_history, NothingToPlotException
|
||||||
|
except:
|
||||||
|
plot_history = None
|
||||||
|
|
||||||
|
# note: this list needs to be kept in sync with another in kivy
|
||||||
TX_ICONS = [
|
TX_ICONS = [
|
||||||
"warning.png",
|
"unconfirmed.png",
|
||||||
"warning.png",
|
|
||||||
"warning.png",
|
"warning.png",
|
||||||
"unconfirmed.png",
|
"unconfirmed.png",
|
||||||
"unconfirmed.png",
|
"offline_tx.png",
|
||||||
"clock1.png",
|
"clock1.png",
|
||||||
"clock2.png",
|
"clock2.png",
|
||||||
"clock3.png",
|
"clock3.png",
|
||||||
|
@ -46,51 +51,204 @@ TX_ICONS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class HistoryList(MyTreeWidget):
|
class HistoryList(MyTreeWidget, AcceptFileDragDrop):
|
||||||
filter_columns = [2, 3, 4] # Date, Description, Amount
|
filter_columns = [2, 3, 4] # Date, Description, Amount
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
|
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
|
||||||
|
AcceptFileDragDrop.__init__(self, ".txn")
|
||||||
self.refresh_headers()
|
self.refresh_headers()
|
||||||
self.setColumnHidden(1, True)
|
self.setColumnHidden(1, True)
|
||||||
|
self.setSortingEnabled(True)
|
||||||
|
self.sortByColumn(0, Qt.AscendingOrder)
|
||||||
|
self.start_timestamp = None
|
||||||
|
self.end_timestamp = None
|
||||||
|
self.years = []
|
||||||
|
self.create_toolbar_buttons()
|
||||||
|
|
||||||
|
def format_date(self, d):
|
||||||
|
return str(datetime.date(d.year, d.month, d.day)) if d else _('None')
|
||||||
|
|
||||||
def refresh_headers(self):
|
def refresh_headers(self):
|
||||||
headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')]
|
headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')]
|
||||||
fx = self.parent.fx
|
fx = self.parent.fx
|
||||||
if fx and fx.show_history():
|
if fx and fx.show_history():
|
||||||
headers.extend(['%s '%fx.ccy + _('Amount'), '%s '%fx.ccy + _('Balance')])
|
headers.extend(['%s '%fx.ccy + _('Value')])
|
||||||
|
self.editable_columns |= {6}
|
||||||
|
if fx.get_history_capital_gains_config():
|
||||||
|
headers.extend(['%s '%fx.ccy + _('Acquisition price')])
|
||||||
|
headers.extend(['%s '%fx.ccy + _('Capital Gains')])
|
||||||
|
else:
|
||||||
|
self.editable_columns -= {6}
|
||||||
self.update_headers(headers)
|
self.update_headers(headers)
|
||||||
|
|
||||||
def get_domain(self):
|
def get_domain(self):
|
||||||
'''Replaced in address_dialog.py'''
|
'''Replaced in address_dialog.py'''
|
||||||
return self.wallet.get_addresses()
|
return self.wallet.get_addresses()
|
||||||
|
|
||||||
|
def on_combo(self, x):
|
||||||
|
s = self.period_combo.itemText(x)
|
||||||
|
x = s == _('Custom')
|
||||||
|
self.start_button.setEnabled(x)
|
||||||
|
self.end_button.setEnabled(x)
|
||||||
|
if s == _('All'):
|
||||||
|
self.start_timestamp = None
|
||||||
|
self.end_timestamp = None
|
||||||
|
self.start_button.setText("-")
|
||||||
|
self.end_button.setText("-")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
year = int(s)
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
start_date = datetime.datetime(year, 1, 1)
|
||||||
|
end_date = datetime.datetime(year+1, 1, 1)
|
||||||
|
self.start_timestamp = time.mktime(start_date.timetuple())
|
||||||
|
self.end_timestamp = time.mktime(end_date.timetuple())
|
||||||
|
self.start_button.setText(_('From') + ' ' + self.format_date(start_date))
|
||||||
|
self.end_button.setText(_('To') + ' ' + self.format_date(end_date))
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def create_toolbar_buttons(self):
|
||||||
|
self.period_combo = QComboBox()
|
||||||
|
self.start_button = QPushButton('-')
|
||||||
|
self.start_button.pressed.connect(self.select_start_date)
|
||||||
|
self.start_button.setEnabled(False)
|
||||||
|
self.end_button = QPushButton('-')
|
||||||
|
self.end_button.pressed.connect(self.select_end_date)
|
||||||
|
self.end_button.setEnabled(False)
|
||||||
|
self.period_combo.addItems([_('All'), _('Custom')])
|
||||||
|
self.period_combo.activated.connect(self.on_combo)
|
||||||
|
|
||||||
|
def get_toolbar_buttons(self):
|
||||||
|
return self.period_combo, self.start_button, self.end_button
|
||||||
|
|
||||||
|
def on_hide_toolbar(self):
|
||||||
|
self.start_timestamp = None
|
||||||
|
self.end_timestamp = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def save_toolbar_state(self, state, config):
|
||||||
|
config.set_key('show_toolbar_history', state)
|
||||||
|
|
||||||
|
def select_start_date(self):
|
||||||
|
self.start_timestamp = self.select_date(self.start_button)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def select_end_date(self):
|
||||||
|
self.end_timestamp = self.select_date(self.end_button)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def select_date(self, button):
|
||||||
|
d = WindowModalDialog(self, _("Select date"))
|
||||||
|
d.setMinimumSize(600, 150)
|
||||||
|
d.date = None
|
||||||
|
vbox = QVBoxLayout()
|
||||||
|
def on_date(date):
|
||||||
|
d.date = date
|
||||||
|
cal = QCalendarWidget()
|
||||||
|
cal.setGridVisible(True)
|
||||||
|
cal.clicked[QDate].connect(on_date)
|
||||||
|
vbox.addWidget(cal)
|
||||||
|
vbox.addLayout(Buttons(OkButton(d), CancelButton(d)))
|
||||||
|
d.setLayout(vbox)
|
||||||
|
if d.exec_():
|
||||||
|
if d.date is None:
|
||||||
|
return None
|
||||||
|
date = d.date.toPyDate()
|
||||||
|
button.setText(self.format_date(date))
|
||||||
|
return time.mktime(date.timetuple())
|
||||||
|
|
||||||
|
def show_summary(self):
|
||||||
|
h = self.summary
|
||||||
|
start_date = h.get('start_date')
|
||||||
|
end_date = h.get('end_date')
|
||||||
|
format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit()
|
||||||
|
d = WindowModalDialog(self, _("Summary"))
|
||||||
|
d.setMinimumSize(600, 150)
|
||||||
|
vbox = QVBoxLayout()
|
||||||
|
grid = QGridLayout()
|
||||||
|
grid.addWidget(QLabel(_("Start")), 0, 0)
|
||||||
|
grid.addWidget(QLabel(self.format_date(start_date)), 0, 1)
|
||||||
|
grid.addWidget(QLabel(_("End")), 1, 0)
|
||||||
|
grid.addWidget(QLabel(self.format_date(end_date)), 1, 1)
|
||||||
|
grid.addWidget(QLabel(_("Initial balance")), 2, 0)
|
||||||
|
grid.addWidget(QLabel(format_amount(h['start_balance'])), 2, 1)
|
||||||
|
grid.addWidget(QLabel(str(h.get('start_fiat_balance'))), 2, 2)
|
||||||
|
grid.addWidget(QLabel(_("Final balance")), 4, 0)
|
||||||
|
grid.addWidget(QLabel(format_amount(h['end_balance'])), 4, 1)
|
||||||
|
grid.addWidget(QLabel(str(h.get('end_fiat_balance'))), 4, 2)
|
||||||
|
grid.addWidget(QLabel(_("Income")), 5, 0)
|
||||||
|
grid.addWidget(QLabel(format_amount(h.get('income'))), 5, 1)
|
||||||
|
grid.addWidget(QLabel(str(h.get('fiat_income'))), 5, 2)
|
||||||
|
grid.addWidget(QLabel(_("Expenditures")), 6, 0)
|
||||||
|
grid.addWidget(QLabel(format_amount(h.get('expenditures'))), 6, 1)
|
||||||
|
grid.addWidget(QLabel(str(h.get('fiat_expenditures'))), 6, 2)
|
||||||
|
grid.addWidget(QLabel(_("Capital gains")), 7, 0)
|
||||||
|
grid.addWidget(QLabel(str(h.get('capital_gains'))), 7, 2)
|
||||||
|
grid.addWidget(QLabel(_("Unrealized gains")), 8, 0)
|
||||||
|
grid.addWidget(QLabel(str(h.get('unrealized_gains', ''))), 8, 2)
|
||||||
|
vbox.addLayout(grid)
|
||||||
|
vbox.addLayout(Buttons(CloseButton(d)))
|
||||||
|
d.setLayout(vbox)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def plot_history_dialog(self):
|
||||||
|
if plot_history is None:
|
||||||
|
self.parent.show_message(
|
||||||
|
_("Can't plot history.") + '\n' +
|
||||||
|
_("Perhaps some dependencies are missing...") + " (matplotlib?)")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
plt = plot_history(self.transactions)
|
||||||
|
plt.show()
|
||||||
|
except NothingToPlotException as e:
|
||||||
|
self.parent.show_message(str(e))
|
||||||
|
|
||||||
@profiler
|
@profiler
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.wallet = self.parent.wallet
|
self.wallet = self.parent.wallet
|
||||||
h = self.wallet.get_history(self.get_domain())
|
fx = self.parent.fx
|
||||||
|
r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=self.start_timestamp, to_timestamp=self.end_timestamp, fx=fx)
|
||||||
|
self.transactions = r['transactions']
|
||||||
|
self.summary = r['summary']
|
||||||
|
if not self.years and self.transactions:
|
||||||
|
from datetime import date
|
||||||
|
start_date = self.transactions[0].get('date') or date.today()
|
||||||
|
end_date = self.transactions[-1].get('date') or date.today()
|
||||||
|
self.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
|
||||||
|
self.period_combo.insertItems(1, self.years)
|
||||||
item = self.currentItem()
|
item = self.currentItem()
|
||||||
current_tx = item.data(0, Qt.UserRole) if item else None
|
current_tx = item.data(0, Qt.UserRole) if item else None
|
||||||
self.clear()
|
self.clear()
|
||||||
fx = self.parent.fx
|
|
||||||
if fx: fx.history_used_spot = False
|
if fx: fx.history_used_spot = False
|
||||||
for h_item in h:
|
for tx_item in self.transactions:
|
||||||
tx_hash, height, conf, timestamp, value, balance = h_item
|
tx_hash = tx_item['txid']
|
||||||
|
height = tx_item['height']
|
||||||
|
conf = tx_item['confirmations']
|
||||||
|
timestamp = tx_item['timestamp']
|
||||||
|
value = tx_item['value'].value
|
||||||
|
balance = tx_item['balance'].value
|
||||||
|
label = tx_item['label']
|
||||||
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
|
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
|
||||||
has_invoice = self.wallet.invoices.paid.get(tx_hash)
|
has_invoice = self.wallet.invoices.paid.get(tx_hash)
|
||||||
icon = QIcon(":icons/" + TX_ICONS[status])
|
icon = QIcon(":icons/" + TX_ICONS[status])
|
||||||
v_str = self.parent.format_amount(value, True, whitespaces=True)
|
v_str = self.parent.format_amount(value, True, whitespaces=True)
|
||||||
balance_str = self.parent.format_amount(balance, whitespaces=True)
|
balance_str = self.parent.format_amount(balance, whitespaces=True)
|
||||||
label = self.wallet.get_label(tx_hash)
|
|
||||||
entry = ['', tx_hash, status_str, label, v_str, balance_str]
|
entry = ['', tx_hash, status_str, label, v_str, balance_str]
|
||||||
if fx and fx.show_history():
|
fiat_value = None
|
||||||
date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp)
|
if value is not None and fx and fx.show_history():
|
||||||
for amount in [value, balance]:
|
fiat_value = tx_item['fiat_value'].value
|
||||||
text = fx.historical_value_str(amount, date)
|
value_str = fx.format_fiat(fiat_value)
|
||||||
entry.append(text)
|
entry.append(value_str)
|
||||||
item = QTreeWidgetItem(entry)
|
# fixme: should use is_mine
|
||||||
|
if value < 0:
|
||||||
|
entry.append(fx.format_fiat(tx_item['acquisition_price'].value))
|
||||||
|
entry.append(fx.format_fiat(tx_item['capital_gain'].value))
|
||||||
|
item = SortableTreeWidgetItem(entry)
|
||||||
item.setIcon(0, icon)
|
item.setIcon(0, icon)
|
||||||
item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else ""))
|
item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else ""))
|
||||||
|
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
|
||||||
if has_invoice:
|
if has_invoice:
|
||||||
item.setIcon(3, QIcon(":icons/seal"))
|
item.setIcon(3, QIcon(":icons/seal"))
|
||||||
for i in range(len(entry)):
|
for i in range(len(entry)):
|
||||||
|
@ -101,12 +259,27 @@ class HistoryList(MyTreeWidget):
|
||||||
if value and value < 0:
|
if value and value < 0:
|
||||||
item.setForeground(3, QBrush(QColor("#BC1E1E")))
|
item.setForeground(3, QBrush(QColor("#BC1E1E")))
|
||||||
item.setForeground(4, QBrush(QColor("#BC1E1E")))
|
item.setForeground(4, QBrush(QColor("#BC1E1E")))
|
||||||
|
if fiat_value and not tx_item['fiat_default']:
|
||||||
|
item.setForeground(6, QBrush(QColor("#1E1EFF")))
|
||||||
if tx_hash:
|
if tx_hash:
|
||||||
item.setData(0, Qt.UserRole, tx_hash)
|
item.setData(0, Qt.UserRole, tx_hash)
|
||||||
self.insertTopLevelItem(0, item)
|
self.insertTopLevelItem(0, item)
|
||||||
if current_tx == tx_hash:
|
if current_tx == tx_hash:
|
||||||
self.setCurrentItem(item)
|
self.setCurrentItem(item)
|
||||||
|
|
||||||
|
def on_edited(self, item, column, prior):
|
||||||
|
'''Called only when the text actually changes'''
|
||||||
|
key = item.data(0, Qt.UserRole)
|
||||||
|
text = item.text(column)
|
||||||
|
# fixme
|
||||||
|
if column == 3:
|
||||||
|
self.parent.wallet.set_label(key, text)
|
||||||
|
self.update_labels()
|
||||||
|
self.parent.update_completions()
|
||||||
|
elif column == 6:
|
||||||
|
self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text)
|
||||||
|
self.on_update()
|
||||||
|
|
||||||
def on_doubleclick(self, item, column):
|
def on_doubleclick(self, item, column):
|
||||||
if self.permit_edit(item, column):
|
if self.permit_edit(item, column):
|
||||||
super(HistoryList, self).on_doubleclick(item, column)
|
super(HistoryList, self).on_doubleclick(item, column)
|
||||||
|
@ -131,6 +304,7 @@ class HistoryList(MyTreeWidget):
|
||||||
if items:
|
if items:
|
||||||
item = items[0]
|
item = items[0]
|
||||||
item.setIcon(0, icon)
|
item.setIcon(0, icon)
|
||||||
|
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
|
||||||
item.setText(2, status_str)
|
item.setText(2, status_str)
|
||||||
|
|
||||||
def create_menu(self, position):
|
def create_menu(self, position):
|
||||||
|
@ -148,20 +322,18 @@ class HistoryList(MyTreeWidget):
|
||||||
else:
|
else:
|
||||||
column_title = self.headerItem().text(column)
|
column_title = self.headerItem().text(column)
|
||||||
column_data = item.text(column)
|
column_data = item.text(column)
|
||||||
|
|
||||||
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
|
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
|
||||||
height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
|
height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
|
||||||
tx = self.wallet.transactions.get(tx_hash)
|
tx = self.wallet.transactions.get(tx_hash)
|
||||||
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
||||||
is_unconfirmed = height <= 0
|
is_unconfirmed = height <= 0
|
||||||
pr_key = self.wallet.invoices.paid.get(tx_hash)
|
pr_key = self.wallet.invoices.paid.get(tx_hash)
|
||||||
|
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
|
if height == TX_HEIGHT_LOCAL:
|
||||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
|
||||||
if column in self.editable_columns:
|
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
|
||||||
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
|
for c in self.editable_columns:
|
||||||
|
menu.addAction(_("Edit {}").format(self.headerItem().text(c)), lambda: self.editItem(item, c))
|
||||||
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
|
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
|
||||||
if is_unconfirmed and tx:
|
if is_unconfirmed and tx:
|
||||||
rbf = is_mine and not tx.is_final()
|
rbf = is_mine and not tx.is_final()
|
||||||
|
@ -176,3 +348,73 @@ class HistoryList(MyTreeWidget):
|
||||||
if tx_URL:
|
if tx_URL:
|
||||||
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
|
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
|
||||||
menu.exec_(self.viewport().mapToGlobal(position))
|
menu.exec_(self.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
|
def remove_local_tx(self, delete_tx):
|
||||||
|
to_delete = {delete_tx}
|
||||||
|
to_delete |= self.wallet.get_depending_transactions(delete_tx)
|
||||||
|
question = _("Are you sure you want to remove this transaction?")
|
||||||
|
if len(to_delete) > 1:
|
||||||
|
question = _(
|
||||||
|
"Are you sure you want to remove this transaction and {} child transactions?".format(len(to_delete) - 1)
|
||||||
|
)
|
||||||
|
answer = QMessageBox.question(self.parent, _("Please confirm"), question, QMessageBox.Yes, QMessageBox.No)
|
||||||
|
if answer == QMessageBox.No:
|
||||||
|
return
|
||||||
|
for tx in to_delete:
|
||||||
|
self.wallet.remove_transaction(tx)
|
||||||
|
self.wallet.save_transactions(write=True)
|
||||||
|
# need to update at least: history_list, utxo_list, address_list
|
||||||
|
self.parent.need_update.set()
|
||||||
|
|
||||||
|
def onFileAdded(self, fn):
|
||||||
|
try:
|
||||||
|
with open(fn) as f:
|
||||||
|
tx = self.parent.tx_from_text(f.read())
|
||||||
|
self.parent.save_transaction_into_wallet(tx)
|
||||||
|
except IOError as e:
|
||||||
|
self.parent.show_error(e)
|
||||||
|
|
||||||
|
def export_history_dialog(self):
|
||||||
|
d = WindowModalDialog(self, _('Export History'))
|
||||||
|
d.setMinimumSize(400, 200)
|
||||||
|
vbox = QVBoxLayout(d)
|
||||||
|
defaultname = os.path.expanduser('~/electrum-history.csv')
|
||||||
|
select_msg = _('Select file to export your wallet transactions to')
|
||||||
|
hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
|
||||||
|
vbox.addLayout(hbox)
|
||||||
|
vbox.addStretch(1)
|
||||||
|
hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
|
||||||
|
vbox.addLayout(hbox)
|
||||||
|
#run_hook('export_history_dialog', self, hbox)
|
||||||
|
self.update()
|
||||||
|
if not d.exec_():
|
||||||
|
return
|
||||||
|
filename = filename_e.text()
|
||||||
|
if not filename:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.do_export_history(self.wallet, filename, csv_button.isChecked())
|
||||||
|
except (IOError, os.error) as reason:
|
||||||
|
export_error_label = _("Electrum was unable to produce a transaction export.")
|
||||||
|
self.parent.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
|
||||||
|
return
|
||||||
|
self.parent.show_message(_("Your wallet history has been successfully exported."))
|
||||||
|
|
||||||
|
def do_export_history(self, wallet, fileName, is_csv):
|
||||||
|
history = self.transactions
|
||||||
|
lines = []
|
||||||
|
for item in history:
|
||||||
|
if is_csv:
|
||||||
|
lines.append([item['txid'], item.get('label', ''), item['confirmations'], item['value'], item['date']])
|
||||||
|
else:
|
||||||
|
lines.append(item)
|
||||||
|
with open(fileName, "w+") as f:
|
||||||
|
if is_csv:
|
||||||
|
import csv
|
||||||
|
transaction = csv.writer(f, lineterminator='\n')
|
||||||
|
transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
|
||||||
|
for line in lines:
|
||||||
|
transaction.writerow(line)
|
||||||
|
else:
|
||||||
|
from electrum.util import json_encode
|
||||||
|
f.write(json_encode(history))
|
||||||
|
|
|
@ -10,29 +10,25 @@ from PyQt5.QtWidgets import *
|
||||||
|
|
||||||
from electrum import Wallet, WalletStorage
|
from electrum import Wallet, WalletStorage
|
||||||
from electrum.util import UserCancelled, InvalidPassword
|
from electrum.util import UserCancelled, InvalidPassword
|
||||||
from electrum.base_wizard import BaseWizard
|
from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
|
|
||||||
from .seed_dialog import SeedLayout, KeysLayout
|
from .seed_dialog import SeedLayout, KeysLayout
|
||||||
from .network_dialog import NetworkChoiceLayout
|
from .network_dialog import NetworkChoiceLayout
|
||||||
from .util import *
|
from .util import *
|
||||||
from .password_dialog import PasswordLayout, PW_NEW
|
from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
|
||||||
|
|
||||||
|
|
||||||
class GoBack(Exception):
|
class GoBack(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
MSG_GENERATING_WAIT = _("Generating your addresses, please wait...")
|
|
||||||
MSG_ENTER_ANYTHING = _("Please enter a seed phrase, a master key, a list of "
|
|
||||||
"Zclassic addresses, or a list of private keys")
|
|
||||||
MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or xprv):")
|
|
||||||
MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:")
|
|
||||||
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
|
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
|
||||||
+ _("Leave this field empty if you want to disable encryption.")
|
+ _("Leave this field empty if you want to disable encryption.")
|
||||||
MSG_RESTORE_PASSPHRASE = \
|
MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
|
||||||
_("Please enter your seed derivation passphrase. "
|
+ _("Your wallet file does not contain secrets, mostly just metadata. ") \
|
||||||
"Note: this is NOT your encryption password. "
|
+ _("It also contains your master public key that allows watching your addresses.") + '\n\n'\
|
||||||
"Leave this field empty if you did not use one or are unsure.")
|
+ _("Note: If you enable this setting, you will need your hardware device to open your wallet.")
|
||||||
|
|
||||||
|
|
||||||
class CosignWidget(QWidget):
|
class CosignWidget(QWidget):
|
||||||
|
@ -196,12 +192,18 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||||
msg =_("This file does not exist.") + '\n' \
|
msg =_("This file does not exist.") + '\n' \
|
||||||
+ _("Press 'Next' to create this wallet, or choose another file.")
|
+ _("Press 'Next' to create this wallet, or choose another file.")
|
||||||
pw = False
|
pw = False
|
||||||
elif self.storage.file_exists() and self.storage.is_encrypted():
|
|
||||||
msg = _("This file is encrypted.") + '\n' + _('Enter your password or choose another file.')
|
|
||||||
pw = True
|
|
||||||
else:
|
else:
|
||||||
msg = _("Press 'Next' to open this wallet.")
|
if self.storage.is_encrypted_with_user_pw():
|
||||||
pw = False
|
msg = _("This file is encrypted with a password.") + '\n' \
|
||||||
|
+ _('Enter your password or choose another file.')
|
||||||
|
pw = True
|
||||||
|
elif self.storage.is_encrypted_with_hw_device():
|
||||||
|
msg = _("This file is encrypted using a hardware device.") + '\n' \
|
||||||
|
+ _("Press 'Next' to choose device to decrypt.")
|
||||||
|
pw = False
|
||||||
|
else:
|
||||||
|
msg = _("Press 'Next' to open this wallet.")
|
||||||
|
pw = False
|
||||||
else:
|
else:
|
||||||
msg = _('Cannot read file')
|
msg = _('Cannot read file')
|
||||||
pw = False
|
pw = False
|
||||||
|
@ -227,23 +229,46 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||||
if not self.storage.file_exists():
|
if not self.storage.file_exists():
|
||||||
break
|
break
|
||||||
if self.storage.file_exists() and self.storage.is_encrypted():
|
if self.storage.file_exists() and self.storage.is_encrypted():
|
||||||
password = self.pw_e.text()
|
if self.storage.is_encrypted_with_user_pw():
|
||||||
try:
|
password = self.pw_e.text()
|
||||||
self.storage.decrypt(password)
|
try:
|
||||||
break
|
self.storage.decrypt(password)
|
||||||
except InvalidPassword as e:
|
break
|
||||||
QMessageBox.information(None, _('Error'), str(e))
|
except InvalidPassword as e:
|
||||||
continue
|
QMessageBox.information(None, _('Error'), str(e))
|
||||||
except BaseException as e:
|
continue
|
||||||
traceback.print_exc(file=sys.stdout)
|
except BaseException as e:
|
||||||
QMessageBox.information(None, _('Error'), str(e))
|
traceback.print_exc(file=sys.stdout)
|
||||||
return
|
QMessageBox.information(None, _('Error'), str(e))
|
||||||
|
return
|
||||||
|
elif self.storage.is_encrypted_with_hw_device():
|
||||||
|
try:
|
||||||
|
self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET)
|
||||||
|
except InvalidPassword as e:
|
||||||
|
# FIXME if we get here because of mistyped passphrase
|
||||||
|
# then that passphrase gets "cached"
|
||||||
|
QMessageBox.information(
|
||||||
|
None, _('Error'),
|
||||||
|
_('Failed to decrypt using this hardware device.') + '\n' +
|
||||||
|
_('If you use a passphrase, make sure it is correct.'))
|
||||||
|
self.stack = []
|
||||||
|
return self.run_and_get_wallet()
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
QMessageBox.information(None, _('Error'), str(e))
|
||||||
|
return
|
||||||
|
if self.storage.is_past_initial_decryption():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise Exception('Unexpected encryption version')
|
||||||
|
|
||||||
path = self.storage.path
|
path = self.storage.path
|
||||||
if self.storage.requires_split():
|
if self.storage.requires_split():
|
||||||
self.hide()
|
self.hide()
|
||||||
msg = _("The wallet '%s' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n"
|
msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n"
|
||||||
"Do you want to split your wallet into multiple files?"%path)
|
"Do you want to split your wallet into multiple files?").format(path)
|
||||||
if not self.question(msg):
|
if not self.question(msg):
|
||||||
return
|
return
|
||||||
file_list = '\n'.join(self.storage.split_accounts())
|
file_list = '\n'.join(self.storage.split_accounts())
|
||||||
|
@ -261,10 +286,10 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||||
action = self.storage.get_action()
|
action = self.storage.get_action()
|
||||||
if action and action != 'new':
|
if action and action != 'new':
|
||||||
self.hide()
|
self.hide()
|
||||||
msg = _("The file '%s' contains an incompletely created wallet.\n"
|
msg = _("The file '{}' contains an incompletely created wallet.\n"
|
||||||
"Do you want to complete its creation now?") % path
|
"Do you want to complete its creation now?").format(path)
|
||||||
if not self.question(msg):
|
if not self.question(msg):
|
||||||
if self.question(_("Do you want to delete '%s'?") % path):
|
if self.question(_("Do you want to delete '{}'?").format(path)):
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
self.show_warning(_('The file was removed'))
|
self.show_warning(_('The file was removed'))
|
||||||
return
|
return
|
||||||
|
@ -386,17 +411,25 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||||
self.exec_layout(slayout)
|
self.exec_layout(slayout)
|
||||||
return slayout.is_ext
|
return slayout.is_ext
|
||||||
|
|
||||||
def pw_layout(self, msg, kind):
|
def pw_layout(self, msg, kind, force_disable_encrypt_cb):
|
||||||
playout = PasswordLayout(None, msg, kind, self.next_button)
|
playout = PasswordLayout(None, msg, kind, self.next_button,
|
||||||
|
force_disable_encrypt_cb=force_disable_encrypt_cb)
|
||||||
playout.encrypt_cb.setChecked(True)
|
playout.encrypt_cb.setChecked(True)
|
||||||
self.exec_layout(playout.layout())
|
self.exec_layout(playout.layout())
|
||||||
return playout.new_password(), playout.encrypt_cb.isChecked()
|
return playout.new_password(), playout.encrypt_cb.isChecked()
|
||||||
|
|
||||||
@wizard_dialog
|
@wizard_dialog
|
||||||
def request_password(self, run_next):
|
def request_password(self, run_next, force_disable_encrypt_cb=False):
|
||||||
"""Request the user enter a new password and confirm it. Return
|
"""Request the user enter a new password and confirm it. Return
|
||||||
the password or None for no password."""
|
the password or None for no password."""
|
||||||
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW)
|
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb)
|
||||||
|
|
||||||
|
@wizard_dialog
|
||||||
|
def request_storage_encryption(self, run_next):
|
||||||
|
playout = PasswordLayoutForHW(None, MSG_HW_STORAGE_ENCRYPTION, PW_NEW, self.next_button)
|
||||||
|
playout.encrypt_cb.setChecked(True)
|
||||||
|
self.exec_layout(playout.layout())
|
||||||
|
return playout.encrypt_cb.isChecked()
|
||||||
|
|
||||||
def show_restore(self, wallet, network):
|
def show_restore(self, wallet, network):
|
||||||
# FIXME: these messages are shown after the install wizard is
|
# FIXME: these messages are shown after the install wizard is
|
||||||
|
@ -437,7 +470,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||||
self.accept_signal.emit()
|
self.accept_signal.emit()
|
||||||
|
|
||||||
def waiting_dialog(self, task, msg):
|
def waiting_dialog(self, task, msg):
|
||||||
self.please_wait.setText(MSG_GENERATING_WAIT)
|
self.please_wait.setText(msg)
|
||||||
self.refresh_gui()
|
self.refresh_gui()
|
||||||
t = threading.Thread(target = task)
|
t = threading.Thread(target = task)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
|
@ -23,10 +23,11 @@
|
||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
from .util import *
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import format_time
|
from electrum.util import format_time
|
||||||
|
|
||||||
|
from .util import *
|
||||||
|
|
||||||
|
|
||||||
class InvoiceList(MyTreeWidget):
|
class InvoiceList(MyTreeWidget):
|
||||||
filter_columns = [0, 1, 2, 3] # Date, Requested By, Description, Amount
|
filter_columns = [0, 1, 2, 3] # Date, Requested By, Description, Amount
|
||||||
|
@ -57,12 +58,10 @@ class InvoiceList(MyTreeWidget):
|
||||||
self.parent.invoices_label.setVisible(len(inv_list))
|
self.parent.invoices_label.setVisible(len(inv_list))
|
||||||
|
|
||||||
def import_invoices(self):
|
def import_invoices(self):
|
||||||
wallet_folder = self.parent.get_wallet_folder()
|
import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update)
|
||||||
filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
|
|
||||||
if not filename:
|
def export_invoices(self):
|
||||||
return
|
export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
|
||||||
self.parent.invoices.import_file(filename)
|
|
||||||
self.on_update()
|
|
||||||
|
|
||||||
def create_menu(self, position):
|
def create_menu(self, position):
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
|
@ -76,7 +75,7 @@ class InvoiceList(MyTreeWidget):
|
||||||
pr = self.parent.invoices.get(key)
|
pr = self.parent.invoices.get(key)
|
||||||
status = self.parent.invoices.get_status(key)
|
status = self.parent.invoices.get_status(key)
|
||||||
if column_data:
|
if column_data:
|
||||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
|
||||||
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
|
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
|
||||||
if status == PR_UNPAID:
|
if status == PR_UNPAID:
|
||||||
menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(key))
|
menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(key))
|
||||||
|
|
|
@ -32,34 +32,32 @@ from decimal import Decimal
|
||||||
import base64
|
import base64
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
from PyQt5.QtGui import *
|
from PyQt5.QtGui import *
|
||||||
|
from PyQt5.QtCore import *
|
||||||
|
import PyQt5.QtCore as QtCore
|
||||||
|
|
||||||
|
from .exception_window import Exception_Hook
|
||||||
from PyQt5.QtWidgets import *
|
from PyQt5.QtWidgets import *
|
||||||
|
|
||||||
from electrum.util import bh2u, bfh
|
|
||||||
|
|
||||||
from electrum import keystore, simple_config
|
from electrum import keystore, simple_config
|
||||||
from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS, NetworkConstants
|
from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS
|
||||||
|
from electrum import constants
|
||||||
from electrum.plugins import run_hook
|
from electrum.plugins import run_hook
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import (format_time, format_satoshis, PrintError,
|
from electrum.util import (format_time, format_satoshis, PrintError,
|
||||||
format_satoshis_plain, NotEnoughFunds,
|
format_satoshis_plain, NotEnoughFunds,
|
||||||
UserCancelled, NoDynamicFeeEstimates)
|
UserCancelled, NoDynamicFeeEstimates, profiler,
|
||||||
|
export_meta, import_meta, bh2u, bfh)
|
||||||
from electrum import Transaction
|
from electrum import Transaction
|
||||||
from electrum import util, bitcoin, commands, coinchooser
|
from electrum import util, bitcoin, commands, coinchooser
|
||||||
from electrum import paymentrequest
|
from electrum import paymentrequest
|
||||||
from electrum.wallet import Multisig_Wallet
|
from electrum.wallet import Multisig_Wallet, AddTransactionException
|
||||||
try:
|
|
||||||
from electrum.plot import plot_history
|
|
||||||
except:
|
|
||||||
plot_history = None
|
|
||||||
|
|
||||||
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
|
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
|
||||||
from .qrcodewidget import QRCodeWidget, QRDialog
|
from .qrcodewidget import QRCodeWidget, QRDialog
|
||||||
from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
|
from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
|
||||||
from .transaction_dialog import show_transaction
|
from .transaction_dialog import show_transaction
|
||||||
from .fee_slider import FeeSlider
|
from .fee_slider import FeeSlider
|
||||||
|
|
||||||
from .util import *
|
from .util import *
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,6 +100,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
self.gui_object = gui_object
|
self.gui_object = gui_object
|
||||||
self.config = config = gui_object.config
|
self.config = config = gui_object.config
|
||||||
|
|
||||||
|
self.setup_exception_hook()
|
||||||
|
|
||||||
self.network = gui_object.daemon.network
|
self.network = gui_object.daemon.network
|
||||||
self.fx = gui_object.daemon.fx
|
self.fx = gui_object.daemon.fx
|
||||||
self.invoices = wallet.invoices
|
self.invoices = wallet.invoices
|
||||||
|
@ -123,9 +124,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.create_status_bar()
|
self.create_status_bar()
|
||||||
self.need_update = threading.Event()
|
self.need_update = threading.Event()
|
||||||
|
|
||||||
self.decimal_point = config.get('decimal_point', 8)
|
self.decimal_point = config.get('decimal_point', 5)
|
||||||
self.fee_unit = config.get('fee_unit', 0)
|
self.num_zeros = int(config.get('num_zeros',0))
|
||||||
self.num_zeros = int(config.get('num_zeros', 0))
|
|
||||||
|
|
||||||
self.completions = QStringListModel()
|
self.completions = QStringListModel()
|
||||||
|
|
||||||
|
@ -203,6 +203,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
def on_history(self, b):
|
def on_history(self, b):
|
||||||
self.new_fx_history_signal.emit()
|
self.new_fx_history_signal.emit()
|
||||||
|
|
||||||
|
def setup_exception_hook(self):
|
||||||
|
Exception_Hook(self)
|
||||||
|
|
||||||
def on_fx_history(self):
|
def on_fx_history(self):
|
||||||
self.history_list.refresh_headers()
|
self.history_list.refresh_headers()
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
|
@ -283,7 +286,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.need_update.set()
|
self.need_update.set()
|
||||||
self.gui_object.network_updated_signal_obj.network_updated_signal \
|
self.gui_object.network_updated_signal_obj.network_updated_signal \
|
||||||
.emit(event, args)
|
.emit(event, args)
|
||||||
|
|
||||||
elif event == 'new_transaction':
|
elif event == 'new_transaction':
|
||||||
self.tx_notifications.append(args[0])
|
self.tx_notifications.append(args[0])
|
||||||
self.notify_transactions_signal.emit()
|
self.notify_transactions_signal.emit()
|
||||||
|
@ -305,6 +307,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if self.config.is_dynfee():
|
if self.config.is_dynfee():
|
||||||
self.fee_slider.update()
|
self.fee_slider.update()
|
||||||
self.do_update_fee()
|
self.do_update_fee()
|
||||||
|
elif event == 'fee_histogram':
|
||||||
|
if self.config.is_dynfee():
|
||||||
|
self.fee_slider.update()
|
||||||
|
self.do_update_fee()
|
||||||
|
# todo: update only unconfirmed tx
|
||||||
|
self.history_list.update()
|
||||||
else:
|
else:
|
||||||
self.print_error("unexpected network_qt signal:", event, args)
|
self.print_error("unexpected network_qt signal:", event, args)
|
||||||
|
|
||||||
|
@ -325,6 +333,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.print_error('close_wallet', self.wallet.storage.path)
|
self.print_error('close_wallet', self.wallet.storage.path)
|
||||||
run_hook('close_wallet', self.wallet)
|
run_hook('close_wallet', self.wallet)
|
||||||
|
|
||||||
|
@profiler
|
||||||
def load_wallet(self, wallet):
|
def load_wallet(self, wallet):
|
||||||
wallet.thread = TaskThread(self, self.on_error)
|
wallet.thread = TaskThread(self, self.on_error)
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
|
@ -363,7 +372,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.setGeometry(100, 100, 840, 400)
|
self.setGeometry(100, 100, 840, 400)
|
||||||
|
|
||||||
def watching_only_changed(self):
|
def watching_only_changed(self):
|
||||||
name = "[TESTNET] Zclassic Electrum" if NetworkConstants.TESTNET else "Zclassic Electrum"
|
name = "Zclassic Electrum TESTNET" if constants.net.TESTNET else "Zclassic Electrum"
|
||||||
title = '%s %s - %s' % (name, self.wallet.electrum_version,
|
title = '%s %s - %s' % (name, self.wallet.electrum_version,
|
||||||
self.wallet.basename())
|
self.wallet.basename())
|
||||||
extra = [self.wallet.storage.get('wallet_type', '?')]
|
extra = [self.wallet.storage.get('wallet_type', '?')]
|
||||||
|
@ -372,7 +381,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
extra.append(_('watching only'))
|
extra.append(_('watching only'))
|
||||||
title += ' [%s]'% ', '.join(extra)
|
title += ' [%s]'% ', '.join(extra)
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
self.password_menu.setEnabled(self.wallet.can_change_password())
|
self.password_menu.setEnabled(self.wallet.may_have_password())
|
||||||
self.import_privkey_menu.setVisible(self.wallet.can_import_privkey())
|
self.import_privkey_menu.setVisible(self.wallet.can_import_privkey())
|
||||||
self.import_address_menu.setVisible(self.wallet.can_import_address())
|
self.import_address_menu.setVisible(self.wallet.can_import_address())
|
||||||
self.export_menu.setEnabled(self.wallet.can_export())
|
self.export_menu.setEnabled(self.wallet.can_export())
|
||||||
|
@ -467,17 +476,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.import_address_menu = wallet_menu.addAction(_("Import Addresses"), self.import_addresses)
|
self.import_address_menu = wallet_menu.addAction(_("Import Addresses"), self.import_addresses)
|
||||||
wallet_menu.addSeparator()
|
wallet_menu.addSeparator()
|
||||||
|
|
||||||
|
addresses_menu = wallet_menu.addMenu(_("&Addresses"))
|
||||||
|
addresses_menu.addAction(_("&Filter"), lambda: self.address_list.toggle_toolbar(self.config))
|
||||||
labels_menu = wallet_menu.addMenu(_("&Labels"))
|
labels_menu = wallet_menu.addMenu(_("&Labels"))
|
||||||
labels_menu.addAction(_("&Import"), self.do_import_labels)
|
labels_menu.addAction(_("&Import"), self.do_import_labels)
|
||||||
labels_menu.addAction(_("&Export"), self.do_export_labels)
|
labels_menu.addAction(_("&Export"), self.do_export_labels)
|
||||||
|
history_menu = wallet_menu.addMenu(_("&History"))
|
||||||
|
history_menu.addAction(_("&Filter"), lambda: self.history_list.toggle_toolbar(self.config))
|
||||||
|
history_menu.addAction(_("&Summary"), self.history_list.show_summary)
|
||||||
|
history_menu.addAction(_("&Plot"), self.history_list.plot_history_dialog)
|
||||||
|
history_menu.addAction(_("&Export"), self.history_list.export_history_dialog)
|
||||||
contacts_menu = wallet_menu.addMenu(_("Contacts"))
|
contacts_menu = wallet_menu.addMenu(_("Contacts"))
|
||||||
contacts_menu.addAction(_("&New"), self.new_contact_dialog)
|
contacts_menu.addAction(_("&New"), self.new_contact_dialog)
|
||||||
contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts())
|
contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts())
|
||||||
|
contacts_menu.addAction(_("Export"), lambda: self.contact_list.export_contacts())
|
||||||
invoices_menu = wallet_menu.addMenu(_("Invoices"))
|
invoices_menu = wallet_menu.addMenu(_("Invoices"))
|
||||||
invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices())
|
invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices())
|
||||||
hist_menu = wallet_menu.addMenu(_("&History"))
|
invoices_menu.addAction(_("Export"), lambda: self.invoice_list.export_invoices())
|
||||||
hist_menu.addAction("Plot", self.plot_history_dialog).setEnabled(plot_history is not None)
|
|
||||||
hist_menu.addAction("Export", self.export_history_dialog)
|
|
||||||
|
|
||||||
wallet_menu.addSeparator()
|
wallet_menu.addSeparator()
|
||||||
wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F"))
|
wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F"))
|
||||||
|
@ -559,24 +574,24 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
return
|
return
|
||||||
self.print_error("Notifying GUI")
|
self.print_error("Notifying GUI")
|
||||||
if len(self.tx_notifications) > 0:
|
if len(self.tx_notifications) > 0:
|
||||||
# Combine the transactions if there are more then three
|
# Combine the transactions if there are at least three
|
||||||
tx_amount = len(self.tx_notifications)
|
num_txns = len(self.tx_notifications)
|
||||||
if(tx_amount >= 3):
|
if num_txns >= 3:
|
||||||
total_amount = 0
|
total_amount = 0
|
||||||
for tx in self.tx_notifications:
|
for tx in self.tx_notifications:
|
||||||
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
||||||
if(v > 0):
|
if v > 0:
|
||||||
total_amount += v
|
total_amount += v
|
||||||
self.notify(_("%(txs)s new transactions received: Total amount received in the new transactions %(amount)s") \
|
self.notify(_("{} new transactions received: Total amount received in the new transactions {}")
|
||||||
% { 'txs' : tx_amount, 'amount' : self.format_amount_and_units(total_amount)})
|
.format(num_txns, self.format_amount_and_units(total_amount)))
|
||||||
self.tx_notifications = []
|
self.tx_notifications = []
|
||||||
else:
|
else:
|
||||||
for tx in self.tx_notifications:
|
for tx in self.tx_notifications:
|
||||||
if tx:
|
if tx:
|
||||||
self.tx_notifications.remove(tx)
|
self.tx_notifications.remove(tx)
|
||||||
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
||||||
if(v > 0):
|
if v > 0:
|
||||||
self.notify(_("Inbound Transaction - %(amount)s") % { 'amount' : self.format_amount_and_units(v)})
|
self.notify(_("New Transaction: {}").format(self.format_amount_and_units(v)))
|
||||||
|
|
||||||
def notify(self, message):
|
def notify(self, message):
|
||||||
if self.tray:
|
if self.tray:
|
||||||
|
@ -625,16 +640,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
def format_amount_and_units(self, amount):
|
def format_amount_and_units(self, amount):
|
||||||
text = self.format_amount(amount) + ' '+ self.base_unit()
|
text = self.format_amount(amount) + ' '+ self.base_unit()
|
||||||
x = self.fx.format_amount_and_units(amount)
|
x = self.fx.format_amount_and_units(amount) if self.fx else None
|
||||||
if text and x:
|
if text and x:
|
||||||
text += ' (%s)'%x
|
text += ' (%s)'%x
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def format_fee_rate(self, fee_rate):
|
def format_fee_rate(self, fee_rate):
|
||||||
if self.fee_unit == 0:
|
return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' zat/byte'
|
||||||
return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' zat/byte'
|
|
||||||
else:
|
|
||||||
return self.format_amount(fee_rate) + ' ' + self.base_unit() + '/kB'
|
|
||||||
|
|
||||||
def get_decimal_point(self):
|
def get_decimal_point(self):
|
||||||
return self.decimal_point
|
return self.decimal_point
|
||||||
|
@ -704,7 +716,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
text = _("Synchronizing...")
|
text = _("Synchronizing...")
|
||||||
icon = QIcon(":icons/status_waiting.png")
|
icon = QIcon(":icons/status_waiting.png")
|
||||||
elif server_lag > 1:
|
elif server_lag > 1:
|
||||||
text = _("Server is lagging (%d blocks)"%server_lag)
|
text = _("Server is lagging ({} blocks)").format(server_lag)
|
||||||
icon = QIcon(":icons/status_lagging.png")
|
icon = QIcon(":icons/status_lagging.png")
|
||||||
else:
|
else:
|
||||||
c, u, x = self.wallet.get_balance()
|
c, u, x = self.wallet.get_balance()
|
||||||
|
@ -749,7 +761,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
from .history_list import HistoryList
|
from .history_list import HistoryList
|
||||||
self.history_list = l = HistoryList(self)
|
self.history_list = l = HistoryList(self)
|
||||||
l.searchable_list = l
|
l.searchable_list = l
|
||||||
return l
|
toolbar = l.create_toolbar(self.config)
|
||||||
|
toolbar_shown = self.config.get('show_toolbar_history', False)
|
||||||
|
l.show_toolbar(toolbar_shown)
|
||||||
|
return self.create_list_tab(l, toolbar)
|
||||||
|
|
||||||
def show_address(self, addr):
|
def show_address(self, addr):
|
||||||
from . import address_dialog
|
from . import address_dialog
|
||||||
|
@ -883,14 +898,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if alias_addr:
|
if alias_addr:
|
||||||
if self.wallet.is_mine(alias_addr):
|
if self.wallet.is_mine(alias_addr):
|
||||||
msg = _('This payment request will be signed.') + '\n' + _('Please enter your password')
|
msg = _('This payment request will be signed.') + '\n' + _('Please enter your password')
|
||||||
password = self.password_dialog(msg)
|
password = None
|
||||||
if password:
|
if self.wallet.has_keystore_encryption():
|
||||||
try:
|
password = self.password_dialog(msg)
|
||||||
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
|
if not password:
|
||||||
except Exception as e:
|
|
||||||
self.show_error(str(e))
|
|
||||||
return
|
return
|
||||||
else:
|
try:
|
||||||
|
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
|
||||||
|
except Exception as e:
|
||||||
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
@ -905,11 +921,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
i = self.expires_combo.currentIndex()
|
i = self.expires_combo.currentIndex()
|
||||||
expiration = list(map(lambda x: x[1], expiration_values))[i]
|
expiration = list(map(lambda x: x[1], expiration_values))[i]
|
||||||
req = self.wallet.make_payment_request(addr, amount, message, expiration)
|
req = self.wallet.make_payment_request(addr, amount, message, expiration)
|
||||||
self.wallet.add_payment_request(req, self.config)
|
try:
|
||||||
self.sign_payment_request(addr)
|
self.wallet.add_payment_request(req, self.config)
|
||||||
self.request_list.update()
|
except Exception as e:
|
||||||
self.address_list.update()
|
traceback.print_exc(file=sys.stderr)
|
||||||
self.save_request_button.setEnabled(False)
|
self.show_error(_('Error adding payment request') + ':\n' + str(e))
|
||||||
|
else:
|
||||||
|
self.sign_payment_request(addr)
|
||||||
|
self.save_request_button.setEnabled(False)
|
||||||
|
finally:
|
||||||
|
self.request_list.update()
|
||||||
|
self.address_list.update()
|
||||||
|
|
||||||
def view_and_paste(self, title, msg, data):
|
def view_and_paste(self, title, msg, data):
|
||||||
dialog = WindowModalDialog(self, title)
|
dialog = WindowModalDialog(self, title)
|
||||||
|
@ -1005,6 +1027,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if self.qr_window and self.qr_window.isVisible():
|
if self.qr_window and self.qr_window.isVisible():
|
||||||
self.qr_window.set_content(addr, amount, message, uri)
|
self.qr_window.set_content(addr, amount, message, uri)
|
||||||
|
|
||||||
|
def set_feerounding_text(self, num_satoshis_added):
|
||||||
|
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
|
||||||
|
.format(num_satoshis_added))
|
||||||
|
|
||||||
def create_send_tab(self):
|
def create_send_tab(self):
|
||||||
# A 4-column grid layout. All the stretch is in the last column.
|
# A 4-column grid layout. All the stretch is in the last column.
|
||||||
# The exchange rate plugin adds a fiat widget in column 2
|
# The exchange rate plugin adds a fiat widget in column 2
|
||||||
|
@ -1070,12 +1096,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
def fee_cb(dyn, pos, fee_rate):
|
def fee_cb(dyn, pos, fee_rate):
|
||||||
if dyn:
|
if dyn:
|
||||||
self.config.set_key('fee_level', pos, False)
|
if self.config.use_mempool_fees():
|
||||||
|
self.config.set_key('depth_level', pos, False)
|
||||||
|
else:
|
||||||
|
self.config.set_key('fee_level', pos, False)
|
||||||
else:
|
else:
|
||||||
self.config.set_key('fee_per_kb', fee_rate, False)
|
self.config.set_key('fee_per_kb', fee_rate, False)
|
||||||
|
|
||||||
if fee_rate:
|
if fee_rate:
|
||||||
self.feerate_e.setAmount(fee_rate // 1000)
|
self.feerate_e.setAmount(fee_rate // 1000)
|
||||||
|
else:
|
||||||
|
self.feerate_e.setAmount(None)
|
||||||
self.fee_e.setModified(False)
|
self.fee_e.setModified(False)
|
||||||
|
|
||||||
self.fee_slider.activate()
|
self.fee_slider.activate()
|
||||||
|
@ -1108,7 +1139,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.size_e.setFixedWidth(140)
|
self.size_e.setFixedWidth(140)
|
||||||
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
|
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
|
||||||
|
|
||||||
self.feerate_e = FeerateEdit(lambda: 2 if self.fee_unit else 0)
|
self.feerate_e = FeerateEdit(lambda: 0)
|
||||||
|
self.feerate_e.setAmount(self.config.fee_per_byte())
|
||||||
self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False))
|
self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False))
|
||||||
self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True))
|
self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True))
|
||||||
|
|
||||||
|
@ -1116,6 +1148,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.fee_e.textEdited.connect(partial(on_fee_or_feerate, self.fee_e, False))
|
self.fee_e.textEdited.connect(partial(on_fee_or_feerate, self.fee_e, False))
|
||||||
self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True))
|
self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True))
|
||||||
|
|
||||||
|
def feerounding_onclick():
|
||||||
|
text = (self.feerounding_text + '\n\n' +
|
||||||
|
_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +
|
||||||
|
_('At most 100 satoshis might be lost due to this rounding.') + ' ' +
|
||||||
|
_("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' +
|
||||||
|
_('Also, dust is not kept as change, but added to the fee.'))
|
||||||
|
QMessageBox.information(self, 'Fee rounding', text)
|
||||||
|
|
||||||
|
self.feerounding_icon = QPushButton(QIcon(':icons/info.png'), '')
|
||||||
|
self.feerounding_icon.setFixedWidth(20)
|
||||||
|
self.feerounding_icon.setFlat(True)
|
||||||
|
self.feerounding_icon.clicked.connect(feerounding_onclick)
|
||||||
|
self.feerounding_icon.setVisible(False)
|
||||||
|
|
||||||
self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e)
|
self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e)
|
||||||
|
|
||||||
vbox_feelabel = QVBoxLayout()
|
vbox_feelabel = QVBoxLayout()
|
||||||
|
@ -1129,14 +1175,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
hbox.addWidget(self.feerate_e)
|
hbox.addWidget(self.feerate_e)
|
||||||
hbox.addWidget(self.size_e)
|
hbox.addWidget(self.size_e)
|
||||||
hbox.addWidget(self.fee_e)
|
hbox.addWidget(self.fee_e)
|
||||||
|
hbox.addWidget(self.feerounding_icon, Qt.AlignLeft)
|
||||||
|
hbox.addStretch(1)
|
||||||
|
|
||||||
vbox_feecontrol = QVBoxLayout()
|
vbox_feecontrol = QVBoxLayout()
|
||||||
vbox_feecontrol.addWidget(self.fee_adv_controls)
|
vbox_feecontrol.addWidget(self.fee_adv_controls)
|
||||||
vbox_feecontrol.addWidget(self.fee_slider)
|
vbox_feecontrol.addWidget(self.fee_slider)
|
||||||
|
|
||||||
grid.addLayout(vbox_feecontrol, 5, 1, 1, 3)
|
grid.addLayout(vbox_feecontrol, 5, 1, 1, -1)
|
||||||
|
|
||||||
if not self.config.get('show_fee', True):
|
if not self.config.get('show_fee', False):
|
||||||
self.fee_adv_controls.setVisible(False)
|
self.fee_adv_controls.setVisible(False)
|
||||||
|
|
||||||
self.preview_button = EnterButton(_("Preview"), self.do_preview)
|
self.preview_button = EnterButton(_("Preview"), self.do_preview)
|
||||||
|
@ -1233,9 +1281,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
'''Recalculate the fee. If the fee was manually input, retain it, but
|
'''Recalculate the fee. If the fee was manually input, retain it, but
|
||||||
still build the TX to see if there are enough funds.
|
still build the TX to see if there are enough funds.
|
||||||
'''
|
'''
|
||||||
if not self.config.get('offline') and self.config.is_dynfee() and not self.config.has_fee_estimates():
|
|
||||||
self.statusBar().showMessage(_('Waiting for fee estimates...'))
|
|
||||||
return False
|
|
||||||
freeze_fee = self.is_send_fee_frozen()
|
freeze_fee = self.is_send_fee_frozen()
|
||||||
freeze_feerate = self.is_send_feerate_frozen()
|
freeze_feerate = self.is_send_feerate_frozen()
|
||||||
amount = '!' if self.is_max else self.amount_e.get_amount()
|
amount = '!' if self.is_max else self.amount_e.get_amount()
|
||||||
|
@ -1258,15 +1303,22 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
try:
|
try:
|
||||||
tx = make_tx(fee_estimator)
|
tx = make_tx(fee_estimator)
|
||||||
self.not_enough_funds = False
|
self.not_enough_funds = False
|
||||||
except NotEnoughFunds:
|
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
|
||||||
self.not_enough_funds = True
|
|
||||||
if not freeze_fee:
|
if not freeze_fee:
|
||||||
self.fee_e.setAmount(None)
|
self.fee_e.setAmount(None)
|
||||||
return
|
if not freeze_feerate:
|
||||||
except NoDynamicFeeEstimates:
|
self.feerate_e.setAmount(None)
|
||||||
tx = make_tx(0)
|
self.feerounding_icon.setVisible(False)
|
||||||
size = tx.estimated_size()
|
|
||||||
self.size_e.setAmount(size)
|
if isinstance(e, NotEnoughFunds):
|
||||||
|
self.not_enough_funds = True
|
||||||
|
elif isinstance(e, NoDynamicFeeEstimates):
|
||||||
|
try:
|
||||||
|
tx = make_tx(0)
|
||||||
|
size = tx.estimated_size()
|
||||||
|
self.size_e.setAmount(size)
|
||||||
|
except BaseException:
|
||||||
|
pass
|
||||||
return
|
return
|
||||||
except BaseException:
|
except BaseException:
|
||||||
traceback.print_exc(file=sys.stderr)
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
@ -1276,12 +1328,32 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.size_e.setAmount(size)
|
self.size_e.setAmount(size)
|
||||||
|
|
||||||
fee = tx.get_fee()
|
fee = tx.get_fee()
|
||||||
if not freeze_fee:
|
fee = None if self.not_enough_funds else fee
|
||||||
fee = None if self.not_enough_funds else fee
|
|
||||||
self.fee_e.setAmount(fee)
|
# Displayed fee/fee_rate values are set according to user input.
|
||||||
if not freeze_feerate:
|
# Due to rounding or dropping dust in CoinChooser,
|
||||||
fee_rate = fee // size if fee is not None else None
|
# actual fees often differ somewhat.
|
||||||
self.feerate_e.setAmount(fee_rate)
|
if freeze_feerate or self.fee_slider.is_active():
|
||||||
|
displayed_feerate = self.feerate_e.get_amount()
|
||||||
|
displayed_feerate = displayed_feerate // 1000 if displayed_feerate else 0
|
||||||
|
displayed_fee = displayed_feerate * size
|
||||||
|
self.fee_e.setAmount(displayed_fee)
|
||||||
|
else:
|
||||||
|
if freeze_fee:
|
||||||
|
displayed_fee = self.fee_e.get_amount()
|
||||||
|
else:
|
||||||
|
# fallback to actual fee if nothing is frozen
|
||||||
|
displayed_fee = fee
|
||||||
|
self.fee_e.setAmount(displayed_fee)
|
||||||
|
displayed_fee = displayed_fee if displayed_fee else 0
|
||||||
|
displayed_feerate = displayed_fee // size if displayed_fee is not None else None
|
||||||
|
self.feerate_e.setAmount(displayed_feerate)
|
||||||
|
|
||||||
|
# show/hide fee rounding icon
|
||||||
|
feerounding = (fee - displayed_fee) if fee else 0
|
||||||
|
self.set_feerounding_text(feerounding)
|
||||||
|
self.feerounding_icon.setToolTip(self.feerounding_text)
|
||||||
|
self.feerounding_icon.setVisible(bool(feerounding))
|
||||||
|
|
||||||
if self.is_max:
|
if self.is_max:
|
||||||
amount = tx.output_value()
|
amount = tx.output_value()
|
||||||
|
@ -1331,7 +1403,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
def request_password(self, *args, **kwargs):
|
def request_password(self, *args, **kwargs):
|
||||||
parent = self.top_level_window()
|
parent = self.top_level_window()
|
||||||
password = None
|
password = None
|
||||||
while self.wallet.has_password():
|
while self.wallet.has_keystore_encryption():
|
||||||
password = self.password_dialog(parent=parent)
|
password = self.password_dialog(parent=parent)
|
||||||
if password is None:
|
if password is None:
|
||||||
# User cancelled password input
|
# User cancelled password input
|
||||||
|
@ -1360,7 +1432,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
fee_estimator = self.fee_e.get_amount()
|
fee_estimator = self.fee_e.get_amount()
|
||||||
elif self.is_send_feerate_frozen():
|
elif self.is_send_feerate_frozen():
|
||||||
amount = self.feerate_e.get_amount()
|
amount = self.feerate_e.get_amount()
|
||||||
amount = 0 if amount is None else float(amount)
|
amount = 0 if amount is None else amount
|
||||||
fee_estimator = partial(
|
fee_estimator = partial(
|
||||||
simple_config.SimpleConfig.estimate_fee_for_feerate, amount)
|
simple_config.SimpleConfig.estimate_fee_for_feerate, amount)
|
||||||
else:
|
else:
|
||||||
|
@ -1369,7 +1441,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
def read_send_tab(self):
|
def read_send_tab(self):
|
||||||
if self.payment_request and self.payment_request.has_expired():
|
if self.payment_request and self.payment_request.has_expired():
|
||||||
self.show_error(_('Payment request has expired.'))
|
self.show_error(_('Payment request has expired'))
|
||||||
return
|
return
|
||||||
label = self.message_e.text()
|
label = self.message_e.text()
|
||||||
|
|
||||||
|
@ -1384,7 +1456,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
if self.payto_e.is_alias and self.payto_e.validated is False:
|
if self.payto_e.is_alias and self.payto_e.validated is False:
|
||||||
alias = self.payto_e.toPlainText()
|
alias = self.payto_e.toPlainText()
|
||||||
msg = _('WARNING: the alias "%s" could not be validated via an additional security check, DNSSEC, and thus may not be correct.'%alias) + '\n'
|
msg = _('WARNING: the alias "{}" could not be validated via an additional '
|
||||||
|
'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n'
|
||||||
msg += _('Do you wish to continue?')
|
msg += _('Do you wish to continue?')
|
||||||
if not self.question(msg):
|
if not self.question(msg):
|
||||||
return
|
return
|
||||||
|
@ -1439,13 +1512,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
tx.set_rbf(True)
|
tx.set_rbf(True)
|
||||||
|
|
||||||
if fee < self.wallet.relayfee() * tx.estimated_size() / 1000:
|
if fee < self.wallet.relayfee() * tx.estimated_size() / 1000:
|
||||||
self.show_error(_("This transaction requires a higher fee, or it will not be propagated by the network"))
|
self.show_error('\n'.join([
|
||||||
|
_("This transaction requires a higher fee, or it will not be propagated by your current server"),
|
||||||
|
_("Try to raise your transaction fee, or use a server with a lower relay fee.")
|
||||||
|
]))
|
||||||
return
|
return
|
||||||
|
|
||||||
if preview:
|
if preview:
|
||||||
self.show_transaction(tx, tx_desc)
|
self.show_transaction(tx, tx_desc)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not self.network:
|
||||||
|
self.show_error(_("You can't broadcast a transaction without a live network connection."))
|
||||||
|
return
|
||||||
|
|
||||||
# confirmation dialog
|
# confirmation dialog
|
||||||
msg = [
|
msg = [
|
||||||
_("Amount to be sent") + ": " + self.format_amount_and_units(amount),
|
_("Amount to be sent") + ": " + self.format_amount_and_units(amount),
|
||||||
|
@ -1457,11 +1537,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
x_fee_address, x_fee_amount = x_fee
|
x_fee_address, x_fee_amount = x_fee
|
||||||
msg.append( _("Additional fees") + ": " + self.format_amount_and_units(x_fee_amount) )
|
msg.append( _("Additional fees") + ": " + self.format_amount_and_units(x_fee_amount) )
|
||||||
|
|
||||||
confirm_rate = 2 * self.config.max_fee_rate()
|
confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
|
||||||
if fee > confirm_rate * tx.estimated_size() / 1000:
|
if fee > confirm_rate * tx.estimated_size() / 1000:
|
||||||
msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
|
msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
|
||||||
|
|
||||||
if self.wallet.has_password():
|
if self.wallet.has_keystore_encryption():
|
||||||
msg.append("")
|
msg.append("")
|
||||||
msg.append(_("Enter your password to proceed"))
|
msg.append(_("Enter your password to proceed"))
|
||||||
password = self.password_dialog('\n'.join(msg))
|
password = self.password_dialog('\n'.join(msg))
|
||||||
|
@ -1514,7 +1594,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
pr = self.payment_request
|
pr = self.payment_request
|
||||||
if pr and pr.has_expired():
|
if pr and pr.has_expired():
|
||||||
self.payment_request = None
|
self.payment_request = None
|
||||||
return False, _("Payment request has expired.")
|
return False, _("Payment request has expired")
|
||||||
status, msg = self.network.broadcast(tx)
|
status, msg = self.network.broadcast(tx)
|
||||||
if pr and status is True:
|
if pr and status is True:
|
||||||
self.invoices.set_paid(pr, tx.txid())
|
self.invoices.set_paid(pr, tx.txid())
|
||||||
|
@ -1606,7 +1686,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.payment_request_error_signal.emit()
|
self.payment_request_error_signal.emit()
|
||||||
|
|
||||||
def pay_to_URI(self, URI):
|
def pay_to_URI(self, URI):
|
||||||
if not URI or not isinstance(URI, str):
|
if not URI:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
out = util.parse_URI(URI, self.on_pr)
|
out = util.parse_URI(URI, self.on_pr)
|
||||||
|
@ -1646,7 +1726,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
e.setText('')
|
e.setText('')
|
||||||
e.setFrozen(False)
|
e.setFrozen(False)
|
||||||
self.fee_slider.activate()
|
self.fee_slider.activate()
|
||||||
|
self.feerate_e.setAmount(self.config.fee_per_byte())
|
||||||
self.size_e.setAmount(0)
|
self.size_e.setAmount(0)
|
||||||
|
self.feerounding_icon.setVisible(False)
|
||||||
self.set_pay_from([])
|
self.set_pay_from([])
|
||||||
self.tx_external_keypairs = {}
|
self.tx_external_keypairs = {}
|
||||||
self.update_status()
|
self.update_status()
|
||||||
|
@ -1658,26 +1740,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.utxo_list.update()
|
self.utxo_list.update()
|
||||||
self.update_fee()
|
self.update_fee()
|
||||||
|
|
||||||
def create_list_tab(self, l, list_header=None):
|
def create_list_tab(self, l, toolbar=None):
|
||||||
w = QWidget()
|
w = QWidget()
|
||||||
w.searchable_list = l
|
w.searchable_list = l
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
w.setLayout(vbox)
|
w.setLayout(vbox)
|
||||||
vbox.setContentsMargins(0, 0, 0, 0)
|
vbox.setContentsMargins(0, 0, 0, 0)
|
||||||
vbox.setSpacing(0)
|
vbox.setSpacing(0)
|
||||||
if list_header:
|
if toolbar:
|
||||||
hbox = QHBoxLayout()
|
vbox.addLayout(toolbar)
|
||||||
for b in list_header:
|
|
||||||
hbox.addWidget(b)
|
|
||||||
hbox.addStretch()
|
|
||||||
vbox.addLayout(hbox)
|
|
||||||
vbox.addWidget(l)
|
vbox.addWidget(l)
|
||||||
return w
|
return w
|
||||||
|
|
||||||
def create_addresses_tab(self):
|
def create_addresses_tab(self):
|
||||||
from .address_list import AddressList
|
from .address_list import AddressList
|
||||||
self.address_list = l = AddressList(self)
|
self.address_list = l = AddressList(self)
|
||||||
return self.create_list_tab(l, l.get_list_header())
|
toolbar = l.create_toolbar(self.config)
|
||||||
|
toolbar_shown = self.config.get('show_toolbar_addresses', False)
|
||||||
|
l.show_toolbar(toolbar_shown)
|
||||||
|
return self.create_list_tab(l, toolbar)
|
||||||
|
|
||||||
def create_utxo_tab(self):
|
def create_utxo_tab(self):
|
||||||
from .utxo_list import UTXOList
|
from .utxo_list import UTXOList
|
||||||
|
@ -1741,8 +1822,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def delete_contacts(self, labels):
|
def delete_contacts(self, labels):
|
||||||
if not self.question(_("Remove %s from your list of contacts?")
|
if not self.question(_("Remove {} from your list of contacts?")
|
||||||
% " + ".join(labels)):
|
.format(" + ".join(labels))):
|
||||||
return
|
return
|
||||||
for label in labels:
|
for label in labels:
|
||||||
self.contacts.pop(label)
|
self.contacts.pop(label)
|
||||||
|
@ -1862,17 +1943,37 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
def update_buttons_on_seed(self):
|
def update_buttons_on_seed(self):
|
||||||
self.seed_button.setVisible(self.wallet.has_seed())
|
self.seed_button.setVisible(self.wallet.has_seed())
|
||||||
self.password_button.setVisible(self.wallet.can_change_password())
|
self.password_button.setVisible(self.wallet.may_have_password())
|
||||||
self.send_button.setVisible(not self.wallet.is_watching_only())
|
self.send_button.setVisible(not self.wallet.is_watching_only())
|
||||||
|
|
||||||
def change_password_dialog(self):
|
def change_password_dialog(self):
|
||||||
from .password_dialog import ChangePasswordDialog
|
from electrum.storage import STO_EV_XPUB_PW
|
||||||
d = ChangePasswordDialog(self, self.wallet)
|
if self.wallet.get_available_storage_encryption_version() == STO_EV_XPUB_PW:
|
||||||
ok, password, new_password, encrypt_file = d.run()
|
from .password_dialog import ChangePasswordDialogForHW
|
||||||
|
d = ChangePasswordDialogForHW(self, self.wallet)
|
||||||
|
ok, encrypt_file = d.run()
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
hw_dev_pw = self.wallet.keystore.get_password_for_storage_encryption()
|
||||||
|
except UserCancelled:
|
||||||
|
return
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
self.show_error(str(e))
|
||||||
|
return
|
||||||
|
old_password = hw_dev_pw if self.wallet.has_password() else None
|
||||||
|
new_password = hw_dev_pw if encrypt_file else None
|
||||||
|
else:
|
||||||
|
from .password_dialog import ChangePasswordDialogForSW
|
||||||
|
d = ChangePasswordDialogForSW(self, self.wallet)
|
||||||
|
ok, old_password, new_password, encrypt_file = d.run()
|
||||||
|
|
||||||
if not ok:
|
if not ok:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.wallet.update_password(password, new_password, encrypt_file)
|
self.wallet.update_password(old_password, new_password, encrypt_file)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.show_error(str(e))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
|
@ -1880,11 +1981,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
self.show_error(_('Failed to update password'))
|
self.show_error(_('Failed to update password'))
|
||||||
return
|
return
|
||||||
msg = _('Password was updated successfully') if new_password else _('Password is disabled, this wallet is not protected')
|
msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected')
|
||||||
self.show_message(msg, title=_("Success"))
|
self.show_message(msg, title=_("Success"))
|
||||||
self.update_lock_icon()
|
self.update_lock_icon()
|
||||||
|
|
||||||
def toggle_search(self):
|
def toggle_search(self):
|
||||||
|
tab = self.tabs.currentWidget()
|
||||||
|
#if hasattr(tab, 'searchable_list'):
|
||||||
|
# tab.searchable_list.toggle_toolbar()
|
||||||
|
#return
|
||||||
self.search_box.setHidden(not self.search_box.isHidden())
|
self.search_box.setHidden(not self.search_box.isHidden())
|
||||||
if not self.search_box.isHidden():
|
if not self.search_box.isHidden():
|
||||||
self.search_box.setFocus(1)
|
self.search_box.setFocus(1)
|
||||||
|
@ -2017,8 +2122,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
rds_e = ShowQRTextEdit(text=redeem_script)
|
rds_e = ShowQRTextEdit(text=redeem_script)
|
||||||
rds_e.addCopyButton(self.app)
|
rds_e.addCopyButton(self.app)
|
||||||
vbox.addWidget(rds_e)
|
vbox.addWidget(rds_e)
|
||||||
if xtype in ['p2wpkh', 'p2wsh', 'p2wpkh-p2sh', 'p2wsh-p2sh']:
|
|
||||||
vbox.addWidget(WWLabel(_("Warning: the format of private keys associated to segwit addresses may not be compatible with other wallets")))
|
|
||||||
vbox.addLayout(Buttons(CloseButton(d)))
|
vbox.addLayout(Buttons(CloseButton(d)))
|
||||||
d.setLayout(vbox)
|
d.setLayout(vbox)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
@ -2036,6 +2139,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if not bitcoin.is_address(address):
|
if not bitcoin.is_address(address):
|
||||||
self.show_message(_('Invalid Zclassic address.'))
|
self.show_message(_('Invalid Zclassic address.'))
|
||||||
return
|
return
|
||||||
|
if self.wallet.is_watching_only():
|
||||||
|
self.show_message(_('This is a watching-only wallet.'))
|
||||||
|
return
|
||||||
if not self.wallet.is_mine(address):
|
if not self.wallet.is_mine(address):
|
||||||
self.show_message(_('Address not in wallet.'))
|
self.show_message(_('Address not in wallet.'))
|
||||||
return
|
return
|
||||||
|
@ -2047,7 +2153,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
task = partial(self.wallet.sign_message, address, message, password)
|
task = partial(self.wallet.sign_message, address, message, password)
|
||||||
|
|
||||||
def show_signed_message(sig):
|
def show_signed_message(sig):
|
||||||
signature.setText(base64.b64encode(sig).decode('ascii'))
|
try:
|
||||||
|
signature.setText(base64.b64encode(sig).decode('ascii'))
|
||||||
|
except RuntimeError:
|
||||||
|
# (signature) wrapped C/C++ object has been deleted
|
||||||
|
pass
|
||||||
|
|
||||||
self.wallet.thread.add(task, on_success=show_signed_message)
|
self.wallet.thread.add(task, on_success=show_signed_message)
|
||||||
|
|
||||||
def do_verify(self, address, message, signature):
|
def do_verify(self, address, message, signature):
|
||||||
|
@ -2106,9 +2217,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
|
def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
|
||||||
|
if self.wallet.is_watching_only():
|
||||||
|
self.show_message(_('This is a watching-only wallet.'))
|
||||||
|
return
|
||||||
cyphertext = encrypted_e.toPlainText()
|
cyphertext = encrypted_e.toPlainText()
|
||||||
task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password)
|
task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password)
|
||||||
self.wallet.thread.add(task, on_success=lambda text: message_e.setText(text.decode('utf-8')))
|
|
||||||
|
def setText(text):
|
||||||
|
try:
|
||||||
|
message_e.setText(text.decode('utf-8'))
|
||||||
|
except RuntimeError:
|
||||||
|
# (message_e) wrapped C/C++ object has been deleted
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.wallet.thread.add(task, on_success=setText)
|
||||||
|
|
||||||
def do_encrypt(self, message_e, pubkey_e, encrypted_e):
|
def do_encrypt(self, message_e, pubkey_e, encrypted_e):
|
||||||
message = message_e.toPlainText()
|
message = message_e.toPlainText()
|
||||||
|
@ -2207,25 +2329,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
return self.tx_from_text(file_content)
|
return self.tx_from_text(file_content)
|
||||||
|
|
||||||
def do_process_from_text(self):
|
def do_process_from_text(self):
|
||||||
from electrum.transaction import SerializationError
|
|
||||||
text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
|
text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
|
||||||
if not text:
|
if not text:
|
||||||
return
|
return
|
||||||
try:
|
tx = self.tx_from_text(text)
|
||||||
tx = self.tx_from_text(text)
|
if tx:
|
||||||
if tx:
|
self.show_transaction(tx)
|
||||||
self.show_transaction(tx)
|
|
||||||
except SerializationError as e:
|
|
||||||
self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
|
|
||||||
|
|
||||||
def do_process_from_file(self):
|
def do_process_from_file(self):
|
||||||
from electrum.transaction import SerializationError
|
tx = self.read_tx_from_file()
|
||||||
try:
|
if tx:
|
||||||
tx = self.read_tx_from_file()
|
self.show_transaction(tx)
|
||||||
if tx:
|
|
||||||
self.show_transaction(tx)
|
|
||||||
except SerializationError as e:
|
|
||||||
self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
|
|
||||||
|
|
||||||
def do_process_from_txid(self):
|
def do_process_from_txid(self):
|
||||||
from electrum import transaction
|
from electrum import transaction
|
||||||
|
@ -2251,7 +2365,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
_('It can not be "backed up" by simply exporting these private keys.'))
|
_('It can not be "backed up" by simply exporting these private keys.'))
|
||||||
|
|
||||||
d = WindowModalDialog(self, _('Private keys'))
|
d = WindowModalDialog(self, _('Private keys'))
|
||||||
d.setMinimumSize(850, 300)
|
d.setMinimumSize(980, 300)
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
|
|
||||||
msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
|
msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
|
||||||
|
@ -2344,102 +2458,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
f.write(json.dumps(pklist, indent = 4))
|
f.write(json.dumps(pklist, indent = 4))
|
||||||
|
|
||||||
def do_import_labels(self):
|
def do_import_labels(self):
|
||||||
labelsFile = self.getOpenFileName(_("Open Labels File"), "*.json")
|
def import_labels(path):
|
||||||
if not labelsFile: return
|
def _validate(data):
|
||||||
try:
|
return data # TODO
|
||||||
with open(labelsFile, 'r') as f:
|
|
||||||
data = f.read()
|
def import_labels_assign(data):
|
||||||
for key, value in json.loads(data).items():
|
for key, value in data.items():
|
||||||
self.wallet.set_label(key, value)
|
self.wallet.set_label(key, value)
|
||||||
self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile))
|
import_meta(path, _validate, import_labels_assign)
|
||||||
except (IOError, os.error) as reason:
|
|
||||||
self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason))
|
def on_import():
|
||||||
self.address_list.update()
|
self.need_update.set()
|
||||||
self.history_list.update()
|
import_meta_gui(self, _('labels'), import_labels, on_import)
|
||||||
|
|
||||||
def do_export_labels(self):
|
def do_export_labels(self):
|
||||||
labels = self.wallet.labels
|
def export_labels(filename):
|
||||||
try:
|
export_meta(self.wallet.labels, filename)
|
||||||
fileName = self.getSaveFileName(_("Select destination file for your labels:"), 'electrum_labels.json', "*.json")
|
export_meta_gui(self, _('labels'), export_labels)
|
||||||
if fileName:
|
|
||||||
with open(fileName, 'w+') as f:
|
|
||||||
json.dump(labels, f, indent=4, sort_keys=True)
|
|
||||||
self.show_message(_("Your labels were exported to") + " '%s'" % str(fileName))
|
|
||||||
except (IOError, os.error) as reason:
|
|
||||||
self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason))
|
|
||||||
|
|
||||||
def export_history_dialog(self):
|
|
||||||
d = WindowModalDialog(self, _('Export History'))
|
|
||||||
d.setMinimumSize(400, 200)
|
|
||||||
vbox = QVBoxLayout(d)
|
|
||||||
defaultname = os.path.expanduser('~/electrum-history.csv')
|
|
||||||
select_msg = _('Select destination file for your wallet transaction history:')
|
|
||||||
hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
|
|
||||||
vbox.addLayout(hbox)
|
|
||||||
vbox.addStretch(1)
|
|
||||||
hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
|
|
||||||
vbox.addLayout(hbox)
|
|
||||||
run_hook('export_history_dialog', self, hbox)
|
|
||||||
self.update()
|
|
||||||
if not d.exec_():
|
|
||||||
return
|
|
||||||
filename = filename_e.text()
|
|
||||||
if not filename:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self.do_export_history(self.wallet, filename, csv_button.isChecked())
|
|
||||||
except (IOError, os.error) as reason:
|
|
||||||
export_error_label = _("Electrum was unable to produce a transaction export.")
|
|
||||||
self.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
|
|
||||||
return
|
|
||||||
self.show_message(_("Your wallet history has been successfully exported."))
|
|
||||||
|
|
||||||
def plot_history_dialog(self):
|
|
||||||
if plot_history is None:
|
|
||||||
return
|
|
||||||
wallet = self.wallet
|
|
||||||
history = wallet.get_history()
|
|
||||||
if len(history) > 0:
|
|
||||||
plt = plot_history(self.wallet, history)
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
def do_export_history(self, wallet, fileName, is_csv):
|
|
||||||
history = wallet.get_history()
|
|
||||||
lines = []
|
|
||||||
for item in history:
|
|
||||||
tx_hash, height, confirmations, timestamp, value, balance = item
|
|
||||||
if height>0:
|
|
||||||
if timestamp is not None:
|
|
||||||
time_string = format_time(timestamp)
|
|
||||||
else:
|
|
||||||
time_string = _("unverified")
|
|
||||||
else:
|
|
||||||
time_string = _("unconfirmed")
|
|
||||||
|
|
||||||
if value is not None:
|
|
||||||
value_string = format_satoshis(value, True)
|
|
||||||
else:
|
|
||||||
value_string = '--'
|
|
||||||
|
|
||||||
if tx_hash:
|
|
||||||
label = wallet.get_label(tx_hash)
|
|
||||||
else:
|
|
||||||
label = ""
|
|
||||||
|
|
||||||
if is_csv:
|
|
||||||
lines.append([tx_hash, label, confirmations, value_string, time_string])
|
|
||||||
else:
|
|
||||||
lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
|
|
||||||
|
|
||||||
with open(fileName, "w+") as f:
|
|
||||||
if is_csv:
|
|
||||||
transaction = csv.writer(f, lineterminator='\n')
|
|
||||||
transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
|
|
||||||
for line in lines:
|
|
||||||
transaction.writerow(line)
|
|
||||||
else:
|
|
||||||
import json
|
|
||||||
f.write(json.dumps(lines, indent = 4))
|
|
||||||
|
|
||||||
def sweep_key_dialog(self):
|
def sweep_key_dialog(self):
|
||||||
d = WindowModalDialog(self, title=_('Sweep private keys'))
|
d = WindowModalDialog(self, title=_('Sweep private keys'))
|
||||||
|
@ -2590,6 +2625,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
nz.valueChanged.connect(on_nz)
|
nz.valueChanged.connect(on_nz)
|
||||||
gui_widgets.append((nz_label, nz))
|
gui_widgets.append((nz_label, nz))
|
||||||
|
|
||||||
|
msg = '\n'.join([
|
||||||
|
_('Time based: fee rate is based on average confirmation time estimates'),
|
||||||
|
_('Mempool based: fee rate is targetting a depth in the memory pool')
|
||||||
|
]
|
||||||
|
)
|
||||||
|
fee_type_label = HelpLabel(_('Fee estimation') + ':', msg)
|
||||||
|
fee_type_combo = QComboBox()
|
||||||
|
fee_type_combo.addItems([_('Static'), _('ETA'), _('Mempool')])
|
||||||
|
fee_type_combo.setCurrentIndex(1 if self.config.use_mempool_fees() else 0)
|
||||||
|
def on_fee_type(x):
|
||||||
|
self.config.set_key('mempool_fees', x==2)
|
||||||
|
self.config.set_key('dynamic_fees', x>0)
|
||||||
|
self.fee_slider.update()
|
||||||
|
fee_type_combo.currentIndexChanged.connect(on_fee_type)
|
||||||
|
fee_widgets.append((fee_type_label, fee_type_combo))
|
||||||
|
|
||||||
use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
|
use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
|
||||||
use_rbf_cb.setChecked(self.config.get('use_rbf', True))
|
use_rbf_cb.setChecked(self.config.get('use_rbf', True))
|
||||||
|
@ -2602,18 +2652,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
use_rbf_cb.stateChanged.connect(on_use_rbf)
|
use_rbf_cb.stateChanged.connect(on_use_rbf)
|
||||||
fee_widgets.append((use_rbf_cb, None))
|
fee_widgets.append((use_rbf_cb, None))
|
||||||
|
|
||||||
self.fee_unit = self.config.get('fee_unit', 0)
|
|
||||||
fee_unit_label = HelpLabel(_('Fee Unit') + ':', '')
|
|
||||||
fee_unit_combo = QComboBox()
|
|
||||||
fee_unit_combo.addItems([_('zat/byte'), _('mZCL/kB')])
|
|
||||||
fee_unit_combo.setCurrentIndex(self.fee_unit)
|
|
||||||
def on_fee_unit(x):
|
|
||||||
self.fee_unit = x
|
|
||||||
self.config.set_key('fee_unit', x)
|
|
||||||
self.fee_slider.update()
|
|
||||||
fee_unit_combo.currentIndexChanged.connect(on_fee_unit)
|
|
||||||
fee_widgets.append((fee_unit_label, fee_unit_combo))
|
|
||||||
|
|
||||||
msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
|
msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
|
||||||
+ _('The following alias providers are available:') + '\n'\
|
+ _('The following alias providers are available:') + '\n'\
|
||||||
+ '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
|
+ '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
|
||||||
|
@ -2672,7 +2710,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
unit_combo = QComboBox()
|
unit_combo = QComboBox()
|
||||||
unit_combo.addItems(units)
|
unit_combo.addItems(units)
|
||||||
unit_combo.setCurrentIndex(units.index(self.base_unit()))
|
unit_combo.setCurrentIndex(units.index(self.base_unit()))
|
||||||
def on_unit(x):
|
def on_unit(x, nz):
|
||||||
unit_result = units[unit_combo.currentIndex()]
|
unit_result = units[unit_combo.currentIndex()]
|
||||||
if self.base_unit() == unit_result:
|
if self.base_unit() == unit_result:
|
||||||
return
|
return
|
||||||
|
@ -2687,13 +2725,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
else:
|
else:
|
||||||
raise Exception('Unknown base unit')
|
raise Exception('Unknown base unit')
|
||||||
self.config.set_key('decimal_point', self.decimal_point, True)
|
self.config.set_key('decimal_point', self.decimal_point, True)
|
||||||
|
nz.setMaximum(self.decimal_point)
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
self.request_list.update()
|
self.request_list.update()
|
||||||
self.address_list.update()
|
self.address_list.update()
|
||||||
for edit, amount in zip(edits, amounts):
|
for edit, amount in zip(edits, amounts):
|
||||||
edit.setAmount(amount)
|
edit.setAmount(amount)
|
||||||
self.update_status()
|
self.update_status()
|
||||||
unit_combo.currentIndexChanged.connect(on_unit)
|
unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
|
||||||
gui_widgets.append((unit_label, unit_combo))
|
gui_widgets.append((unit_label, unit_combo))
|
||||||
|
|
||||||
block_explorers = sorted(util.block_explorer_info().keys())
|
block_explorers = sorted(util.block_explorer_info().keys())
|
||||||
|
@ -2783,8 +2822,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
unconf_cb.stateChanged.connect(on_unconf)
|
unconf_cb.stateChanged.connect(on_unconf)
|
||||||
tx_widgets.append((unconf_cb, None))
|
tx_widgets.append((unconf_cb, None))
|
||||||
|
|
||||||
|
def on_outrounding(x):
|
||||||
|
self.config.set_key('coin_chooser_output_rounding', bool(x))
|
||||||
|
enable_outrounding = self.config.get('coin_chooser_output_rounding', False)
|
||||||
|
outrounding_cb = QCheckBox(_('Enable output value rounding'))
|
||||||
|
outrounding_cb.setToolTip(
|
||||||
|
_('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
|
||||||
|
_('This might improve your privacy somewhat.') + '\n' +
|
||||||
|
_('If enabled, at most 100 zatoshis might be lost due to this, per transaction.'))
|
||||||
|
outrounding_cb.setChecked(enable_outrounding)
|
||||||
|
outrounding_cb.stateChanged.connect(on_outrounding)
|
||||||
|
tx_widgets.append((outrounding_cb, None))
|
||||||
|
|
||||||
# Fiat Currency
|
# Fiat Currency
|
||||||
hist_checkbox = QCheckBox()
|
hist_checkbox = QCheckBox()
|
||||||
|
hist_capgains_checkbox = QCheckBox()
|
||||||
fiat_address_checkbox = QCheckBox()
|
fiat_address_checkbox = QCheckBox()
|
||||||
ccy_combo = QComboBox()
|
ccy_combo = QComboBox()
|
||||||
ex_combo = QComboBox()
|
ex_combo = QComboBox()
|
||||||
|
@ -2806,6 +2858,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if not self.fx: return
|
if not self.fx: return
|
||||||
fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config())
|
fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config())
|
||||||
|
|
||||||
|
def update_history_capgains_cb():
|
||||||
|
if not self.fx: return
|
||||||
|
hist_capgains_checkbox.setChecked(self.fx.get_history_capital_gains_config())
|
||||||
|
hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked())
|
||||||
|
|
||||||
def update_exchanges():
|
def update_exchanges():
|
||||||
if not self.fx: return
|
if not self.fx: return
|
||||||
b = self.fx.is_enabled()
|
b = self.fx.is_enabled()
|
||||||
|
@ -2844,6 +2901,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if self.fx.is_enabled() and checked:
|
if self.fx.is_enabled() and checked:
|
||||||
# reset timeout to get historical rates
|
# reset timeout to get historical rates
|
||||||
self.fx.timeout = 0
|
self.fx.timeout = 0
|
||||||
|
update_history_capgains_cb()
|
||||||
|
|
||||||
|
def on_history_capgains(checked):
|
||||||
|
if not self.fx: return
|
||||||
|
self.fx.set_history_capital_gains_config(checked)
|
||||||
|
self.history_list.refresh_headers()
|
||||||
|
|
||||||
def on_fiat_address(checked):
|
def on_fiat_address(checked):
|
||||||
if not self.fx: return
|
if not self.fx: return
|
||||||
|
@ -2853,16 +2916,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
update_currencies()
|
update_currencies()
|
||||||
update_history_cb()
|
update_history_cb()
|
||||||
|
update_history_capgains_cb()
|
||||||
update_fiat_address_cb()
|
update_fiat_address_cb()
|
||||||
update_exchanges()
|
update_exchanges()
|
||||||
ccy_combo.currentIndexChanged.connect(on_currency)
|
ccy_combo.currentIndexChanged.connect(on_currency)
|
||||||
hist_checkbox.stateChanged.connect(on_history)
|
hist_checkbox.stateChanged.connect(on_history)
|
||||||
|
hist_capgains_checkbox.stateChanged.connect(on_history_capgains)
|
||||||
fiat_address_checkbox.stateChanged.connect(on_fiat_address)
|
fiat_address_checkbox.stateChanged.connect(on_fiat_address)
|
||||||
ex_combo.currentIndexChanged.connect(on_exchange)
|
ex_combo.currentIndexChanged.connect(on_exchange)
|
||||||
|
|
||||||
fiat_widgets = []
|
fiat_widgets = []
|
||||||
fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
|
fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
|
||||||
fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
|
fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
|
||||||
|
fiat_widgets.append((QLabel(_('Show capital gains in history')), hist_capgains_checkbox))
|
||||||
fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox))
|
fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox))
|
||||||
fiat_widgets.append((QLabel(_('Source')), ex_combo))
|
fiat_widgets.append((QLabel(_('Source')), ex_combo))
|
||||||
|
|
||||||
|
@ -3017,6 +3083,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0)
|
grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0)
|
||||||
grid.addWidget(output_amount, 2, 1)
|
grid.addWidget(output_amount, 2, 1)
|
||||||
fee_e = BTCAmountEdit(self.get_decimal_point)
|
fee_e = BTCAmountEdit(self.get_decimal_point)
|
||||||
|
# FIXME with dyn fees, without estimates, there are all kinds of crashes here
|
||||||
def f(x):
|
def f(x):
|
||||||
a = max_fee - fee_e.get_amount()
|
a = max_fee - fee_e.get_amount()
|
||||||
output_amount.setText((self.format_amount(a) + ' ' + self.base_unit()) if a else '')
|
output_amount.setText((self.format_amount(a) + ' ' + self.base_unit()) if a else '')
|
||||||
|
@ -3081,3 +3148,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if is_final:
|
if is_final:
|
||||||
new_tx.set_rbf(False)
|
new_tx.set_rbf(False)
|
||||||
self.show_transaction(new_tx, tx_label)
|
self.show_transaction(new_tx, tx_label)
|
||||||
|
|
||||||
|
def save_transaction_into_wallet(self, tx):
|
||||||
|
try:
|
||||||
|
if not self.wallet.add_transaction(tx.txid(), tx):
|
||||||
|
self.show_error(_("Transaction could not be saved.") + "\n" +
|
||||||
|
_("It conflicts with current history."))
|
||||||
|
return False
|
||||||
|
except AddTransactionException as e:
|
||||||
|
self.show_error(e)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.wallet.save_transactions(write=True)
|
||||||
|
# need to update at least: history_list, utxo_list, address_list
|
||||||
|
self.need_update.set()
|
||||||
|
self.msg_box(QPixmap(":icons/offline_tx.png"), None, _('Success'), _("Transaction added to wallet history"))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,9 @@ from PyQt5.QtWidgets import *
|
||||||
import PyQt5.QtCore as QtCore
|
import PyQt5.QtCore as QtCore
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.bitcoin import NetworkConstants
|
from electrum import constants
|
||||||
from electrum.util import print_error
|
from electrum.util import print_error
|
||||||
|
from electrum.network import serialize_server, deserialize_server
|
||||||
|
|
||||||
from .util import *
|
from .util import *
|
||||||
|
|
||||||
|
@ -145,7 +146,7 @@ class ServerListWidget(QTreeWidget):
|
||||||
menu.exec_(self.viewport().mapToGlobal(position))
|
menu.exec_(self.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
def set_server(self, s):
|
def set_server(self, s):
|
||||||
host, port, protocol = s.split(':')
|
host, port, protocol = deserialize_server(s)
|
||||||
self.parent.server_host.setText(host)
|
self.parent.server_host.setText(host)
|
||||||
self.parent.server_port.setText(port)
|
self.parent.server_port.setText(port)
|
||||||
self.parent.set_server()
|
self.parent.set_server()
|
||||||
|
@ -170,7 +171,7 @@ class ServerListWidget(QTreeWidget):
|
||||||
port = d.get(protocol)
|
port = d.get(protocol)
|
||||||
if port:
|
if port:
|
||||||
x = QTreeWidgetItem([_host, port])
|
x = QTreeWidgetItem([_host, port])
|
||||||
server = _host+':'+port+':'+protocol
|
server = serialize_server(_host, port, protocol)
|
||||||
x.setData(1, Qt.UserRole, server)
|
x.setData(1, Qt.UserRole, server)
|
||||||
self.addTopLevelItem(x)
|
self.addTopLevelItem(x)
|
||||||
|
|
||||||
|
@ -395,7 +396,7 @@ class NetworkChoiceLayout(object):
|
||||||
def change_protocol(self, use_ssl):
|
def change_protocol(self, use_ssl):
|
||||||
p = 's' if use_ssl else 't'
|
p = 's' if use_ssl else 't'
|
||||||
host = self.server_host.text()
|
host = self.server_host.text()
|
||||||
pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS)
|
pp = self.servers.get(host, constants.net.DEFAULT_PORTS)
|
||||||
if p not in pp.keys():
|
if p not in pp.keys():
|
||||||
p = list(pp.keys())[0]
|
p = list(pp.keys())[0]
|
||||||
port = pp[p]
|
port = pp[p]
|
||||||
|
@ -411,7 +412,7 @@ class NetworkChoiceLayout(object):
|
||||||
def follow_server(self, server):
|
def follow_server(self, server):
|
||||||
self.network.switch_to_interface(server)
|
self.network.switch_to_interface(server)
|
||||||
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
|
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
|
||||||
host, port, protocol = server.split(':')
|
host, port, protocol = deserialize_server(server)
|
||||||
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
|
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
@ -420,7 +421,7 @@ class NetworkChoiceLayout(object):
|
||||||
self.change_server(str(x.text(0)), self.protocol)
|
self.change_server(str(x.text(0)), self.protocol)
|
||||||
|
|
||||||
def change_server(self, host, protocol):
|
def change_server(self, host, protocol):
|
||||||
pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS)
|
pp = self.servers.get(host, constants.net.DEFAULT_PORTS)
|
||||||
if protocol and protocol not in protocol_letters:
|
if protocol and protocol not in protocol_letters:
|
||||||
protocol = None
|
protocol = None
|
||||||
if protocol:
|
if protocol:
|
||||||
|
@ -444,7 +445,6 @@ class NetworkChoiceLayout(object):
|
||||||
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
|
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
|
||||||
host = str(self.server_host.text())
|
host = str(self.server_host.text())
|
||||||
port = str(self.server_port.text())
|
port = str(self.server_port.text())
|
||||||
protocol = 't' if self.config.get('nossl') else 's'
|
|
||||||
auto_connect = self.autoconnect_cb.isChecked()
|
auto_connect = self.autoconnect_cb.isChecked()
|
||||||
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
|
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ class PasswordLayout(object):
|
||||||
|
|
||||||
titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")]
|
titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")]
|
||||||
|
|
||||||
def __init__(self, wallet, msg, kind, OK_button):
|
def __init__(self, wallet, msg, kind, OK_button, force_disable_encrypt_cb=False):
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
|
|
||||||
self.pw = QLineEdit()
|
self.pw = QLineEdit()
|
||||||
|
@ -126,7 +126,8 @@ class PasswordLayout(object):
|
||||||
def enable_OK():
|
def enable_OK():
|
||||||
ok = self.new_pw.text() == self.conf_pw.text()
|
ok = self.new_pw.text() == self.conf_pw.text()
|
||||||
OK_button.setEnabled(ok)
|
OK_button.setEnabled(ok)
|
||||||
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text()))
|
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text())
|
||||||
|
and not force_disable_encrypt_cb)
|
||||||
self.new_pw.textChanged.connect(enable_OK)
|
self.new_pw.textChanged.connect(enable_OK)
|
||||||
self.conf_pw.textChanged.connect(enable_OK)
|
self.conf_pw.textChanged.connect(enable_OK)
|
||||||
|
|
||||||
|
@ -163,11 +164,84 @@ class PasswordLayout(object):
|
||||||
return pw
|
return pw
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordDialog(WindowModalDialog):
|
class PasswordLayoutForHW(object):
|
||||||
|
|
||||||
|
def __init__(self, wallet, msg, kind, OK_button):
|
||||||
|
self.wallet = wallet
|
||||||
|
|
||||||
|
self.kind = kind
|
||||||
|
self.OK_button = OK_button
|
||||||
|
|
||||||
|
vbox = QVBoxLayout()
|
||||||
|
label = QLabel(msg + "\n")
|
||||||
|
label.setWordWrap(True)
|
||||||
|
|
||||||
|
grid = QGridLayout()
|
||||||
|
grid.setSpacing(8)
|
||||||
|
grid.setColumnMinimumWidth(0, 150)
|
||||||
|
grid.setColumnMinimumWidth(1, 100)
|
||||||
|
grid.setColumnStretch(1,1)
|
||||||
|
|
||||||
|
logo_grid = QGridLayout()
|
||||||
|
logo_grid.setSpacing(8)
|
||||||
|
logo_grid.setColumnMinimumWidth(0, 70)
|
||||||
|
logo_grid.setColumnStretch(1,1)
|
||||||
|
|
||||||
|
logo = QLabel()
|
||||||
|
logo.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
|
logo_grid.addWidget(logo, 0, 0)
|
||||||
|
logo_grid.addWidget(label, 0, 1, 1, 2)
|
||||||
|
vbox.addLayout(logo_grid)
|
||||||
|
|
||||||
|
if wallet and wallet.has_storage_encryption():
|
||||||
|
lockfile = ":icons/lock.png"
|
||||||
|
else:
|
||||||
|
lockfile = ":icons/unlock.png"
|
||||||
|
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
|
||||||
|
|
||||||
|
vbox.addLayout(grid)
|
||||||
|
|
||||||
|
self.encrypt_cb = QCheckBox(_('Encrypt wallet file'))
|
||||||
|
grid.addWidget(self.encrypt_cb, 1, 0, 1, 2)
|
||||||
|
|
||||||
|
self.vbox = vbox
|
||||||
|
|
||||||
|
def title(self):
|
||||||
|
return _("Toggle Encryption")
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
return self.vbox
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordDialogBase(WindowModalDialog):
|
||||||
|
|
||||||
def __init__(self, parent, wallet):
|
def __init__(self, parent, wallet):
|
||||||
WindowModalDialog.__init__(self, parent)
|
WindowModalDialog.__init__(self, parent)
|
||||||
is_encrypted = wallet.storage.is_encrypted()
|
is_encrypted = wallet.has_storage_encryption()
|
||||||
|
OK_button = OkButton(self)
|
||||||
|
|
||||||
|
self.create_password_layout(wallet, is_encrypted, OK_button)
|
||||||
|
|
||||||
|
self.setWindowTitle(self.playout.title())
|
||||||
|
vbox = QVBoxLayout(self)
|
||||||
|
vbox.addLayout(self.playout.layout())
|
||||||
|
vbox.addStretch(1)
|
||||||
|
vbox.addLayout(Buttons(CancelButton(self), OK_button))
|
||||||
|
self.playout.encrypt_cb.setChecked(is_encrypted)
|
||||||
|
|
||||||
|
def create_password_layout(self, wallet, is_encrypted, OK_button):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordDialogForSW(ChangePasswordDialogBase):
|
||||||
|
|
||||||
|
def __init__(self, parent, wallet):
|
||||||
|
ChangePasswordDialogBase.__init__(self, parent, wallet)
|
||||||
|
if not wallet.has_password():
|
||||||
|
self.playout.encrypt_cb.setChecked(True)
|
||||||
|
|
||||||
|
def create_password_layout(self, wallet, is_encrypted, OK_button):
|
||||||
if not wallet.has_password():
|
if not wallet.has_password():
|
||||||
msg = _('Your wallet is not protected.')
|
msg = _('Your wallet is not protected.')
|
||||||
msg += ' ' + _('Use this dialog to add a password to your wallet.')
|
msg += ' ' + _('Use this dialog to add a password to your wallet.')
|
||||||
|
@ -177,14 +251,9 @@ class ChangePasswordDialog(WindowModalDialog):
|
||||||
else:
|
else:
|
||||||
msg = _('Your wallet is password protected and encrypted.')
|
msg = _('Your wallet is password protected and encrypted.')
|
||||||
msg += ' ' + _('Use this dialog to change your password.')
|
msg += ' ' + _('Use this dialog to change your password.')
|
||||||
OK_button = OkButton(self)
|
self.playout = PasswordLayout(
|
||||||
self.playout = PasswordLayout(wallet, msg, PW_CHANGE, OK_button)
|
wallet, msg, PW_CHANGE, OK_button,
|
||||||
self.setWindowTitle(self.playout.title())
|
force_disable_encrypt_cb=not wallet.can_have_keystore_encryption())
|
||||||
vbox = QVBoxLayout(self)
|
|
||||||
vbox.addLayout(self.playout.layout())
|
|
||||||
vbox.addStretch(1)
|
|
||||||
vbox.addLayout(Buttons(CancelButton(self), OK_button))
|
|
||||||
self.playout.encrypt_cb.setChecked(is_encrypted or not wallet.has_password())
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if not self.exec_():
|
if not self.exec_():
|
||||||
|
@ -192,6 +261,26 @@ class ChangePasswordDialog(WindowModalDialog):
|
||||||
return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked()
|
return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked()
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordDialogForHW(ChangePasswordDialogBase):
|
||||||
|
|
||||||
|
def __init__(self, parent, wallet):
|
||||||
|
ChangePasswordDialogBase.__init__(self, parent, wallet)
|
||||||
|
|
||||||
|
def create_password_layout(self, wallet, is_encrypted, OK_button):
|
||||||
|
if not is_encrypted:
|
||||||
|
msg = _('Your wallet file is NOT encrypted.')
|
||||||
|
else:
|
||||||
|
msg = _('Your wallet file is encrypted.')
|
||||||
|
msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.')
|
||||||
|
msg += '\n' + _('Use this dialog to toggle encryption.')
|
||||||
|
self.playout = PasswordLayoutForHW(wallet, msg, PW_CHANGE, OK_button)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if not self.exec_():
|
||||||
|
return False, None
|
||||||
|
return True, self.playout.encrypt_cb.isChecked()
|
||||||
|
|
||||||
|
|
||||||
class PasswordDialog(WindowModalDialog):
|
class PasswordDialog(WindowModalDialog):
|
||||||
|
|
||||||
def __init__(self, parent=None, msg=None):
|
def __init__(self, parent=None, msg=None):
|
||||||
|
|
|
@ -115,7 +115,7 @@ class RequestList(MyTreeWidget):
|
||||||
column_title = self.headerItem().text(column)
|
column_title = self.headerItem().text(column)
|
||||||
column_data = item.text(column)
|
column_data = item.text(column)
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
|
||||||
menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr)))
|
menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr)))
|
||||||
menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
|
menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
|
||||||
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))
|
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
from PyQt5.QtCore import *
|
from PyQt5.QtCore import *
|
||||||
from PyQt5.QtGui import *
|
from PyQt5.QtGui import *
|
||||||
|
@ -33,16 +34,27 @@ from PyQt5.QtWidgets import *
|
||||||
from electrum.bitcoin import base_encode
|
from electrum.bitcoin import base_encode
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.plugins import run_hook
|
from electrum.plugins import run_hook
|
||||||
|
from electrum import simple_config
|
||||||
|
|
||||||
from electrum.util import bfh
|
from electrum.util import bfh
|
||||||
|
from electrum.wallet import AddTransactionException
|
||||||
|
from electrum.transaction import SerializationError
|
||||||
|
|
||||||
from .util import *
|
from .util import *
|
||||||
|
|
||||||
dialogs = [] # Otherwise python randomly garbage collects the dialogs...
|
dialogs = [] # Otherwise python randomly garbage collects the dialogs...
|
||||||
|
|
||||||
|
|
||||||
def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
|
def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
|
||||||
d = TxDialog(tx, parent, desc, prompt_if_unsaved)
|
try:
|
||||||
dialogs.append(d)
|
d = TxDialog(tx, parent, desc, prompt_if_unsaved)
|
||||||
d.show()
|
except SerializationError as e:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
|
||||||
|
else:
|
||||||
|
dialogs.append(d)
|
||||||
|
d.show()
|
||||||
|
|
||||||
|
|
||||||
class TxDialog(QDialog, MessageBoxMixin):
|
class TxDialog(QDialog, MessageBoxMixin):
|
||||||
|
|
||||||
|
@ -56,7 +68,10 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
# e.g. the FX plugin. If this happens during or after a long
|
# e.g. the FX plugin. If this happens during or after a long
|
||||||
# sign operation the signatures are lost.
|
# sign operation the signatures are lost.
|
||||||
self.tx = copy.deepcopy(tx)
|
self.tx = copy.deepcopy(tx)
|
||||||
self.tx.deserialize()
|
try:
|
||||||
|
self.tx.deserialize()
|
||||||
|
except BaseException as e:
|
||||||
|
raise SerializationError(e)
|
||||||
self.main_window = parent
|
self.main_window = parent
|
||||||
self.wallet = parent.wallet
|
self.wallet = parent.wallet
|
||||||
self.prompt_if_unsaved = prompt_if_unsaved
|
self.prompt_if_unsaved = prompt_if_unsaved
|
||||||
|
@ -98,8 +113,17 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
self.broadcast_button = b = QPushButton(_("Broadcast"))
|
self.broadcast_button = b = QPushButton(_("Broadcast"))
|
||||||
b.clicked.connect(self.do_broadcast)
|
b.clicked.connect(self.do_broadcast)
|
||||||
|
|
||||||
self.save_button = b = QPushButton(_("Save"))
|
self.save_button = QPushButton(_("Save"))
|
||||||
b.clicked.connect(self.save)
|
save_button_disabled = not tx.is_complete()
|
||||||
|
self.save_button.setDisabled(save_button_disabled)
|
||||||
|
if save_button_disabled:
|
||||||
|
self.save_button.setToolTip(_("Please sign this transaction in order to save it"))
|
||||||
|
else:
|
||||||
|
self.save_button.setToolTip("")
|
||||||
|
self.save_button.clicked.connect(self.save)
|
||||||
|
|
||||||
|
self.export_button = b = QPushButton(_("Export"))
|
||||||
|
b.clicked.connect(self.export)
|
||||||
|
|
||||||
self.cancel_button = b = QPushButton(_("Close"))
|
self.cancel_button = b = QPushButton(_("Close"))
|
||||||
b.clicked.connect(self.close)
|
b.clicked.connect(self.close)
|
||||||
|
@ -112,9 +136,9 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
self.copy_button = CopyButton(lambda: str(self.tx), parent.app)
|
self.copy_button = CopyButton(lambda: str(self.tx), parent.app)
|
||||||
|
|
||||||
# Action buttons
|
# Action buttons
|
||||||
self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button]
|
self.buttons = [self.sign_button, self.broadcast_button, self.save_button, self.cancel_button]
|
||||||
# Transaction sharing buttons
|
# Transaction sharing buttons
|
||||||
self.sharing_buttons = [self.copy_button, self.qr_button, self.save_button]
|
self.sharing_buttons = [self.copy_button, self.qr_button, self.export_button]
|
||||||
|
|
||||||
run_hook('transaction_dialog', self)
|
run_hook('transaction_dialog', self)
|
||||||
|
|
||||||
|
@ -136,11 +160,14 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
if (self.prompt_if_unsaved and not self.saved
|
if (self.prompt_if_unsaved and not self.saved
|
||||||
and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
|
and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
|
||||||
event.ignore()
|
event.ignore()
|
||||||
else:
|
else:
|
||||||
event.accept()
|
event.accept()
|
||||||
dialogs.remove(self)
|
try:
|
||||||
|
dialogs.remove(self)
|
||||||
|
except ValueError:
|
||||||
|
pass # was not in list already
|
||||||
|
|
||||||
def show_qr(self):
|
def show_qr(self):
|
||||||
text = bfh(str(self.tx))
|
text = bfh(str(self.tx))
|
||||||
|
@ -155,6 +182,8 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
if success:
|
if success:
|
||||||
self.prompt_if_unsaved = True
|
self.prompt_if_unsaved = True
|
||||||
self.saved = False
|
self.saved = False
|
||||||
|
self.save_button.setDisabled(False)
|
||||||
|
self.save_button.setToolTip("")
|
||||||
self.update()
|
self.update()
|
||||||
self.main_window.pop_top_level_window(self)
|
self.main_window.pop_top_level_window(self)
|
||||||
|
|
||||||
|
@ -163,12 +192,18 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
self.main_window.sign_tx(self.tx, sign_done)
|
self.main_window.sign_tx(self.tx, sign_done)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
if self.main_window.save_transaction_into_wallet(self.tx):
|
||||||
|
self.save_button.setDisabled(True)
|
||||||
|
self.saved = True
|
||||||
|
|
||||||
|
|
||||||
|
def export(self):
|
||||||
name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn'
|
name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn'
|
||||||
fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn")
|
fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn")
|
||||||
if fileName:
|
if fileName:
|
||||||
with open(fileName, "w+") as f:
|
with open(fileName, "w+") as f:
|
||||||
f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n')
|
f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n')
|
||||||
self.show_message(_("Transaction saved successfully"))
|
self.show_message(_("Transaction exported successfully"))
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
|
@ -191,11 +226,11 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
|
|
||||||
if timestamp:
|
if timestamp:
|
||||||
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
||||||
self.date_label.setText(_("Date: %s")%time_str)
|
self.date_label.setText(_("Date: {}").format(time_str))
|
||||||
self.date_label.show()
|
self.date_label.show()
|
||||||
elif exp_n:
|
elif exp_n:
|
||||||
text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)')
|
text = '%.2f MB'%(exp_n/1000000)
|
||||||
self.date_label.setText(_('Expected confirmation time') + ': ' + text)
|
self.date_label.setText(_('Position in mempool') + ': ' + text + ' ' + _('from tip'))
|
||||||
self.date_label.show()
|
self.date_label.show()
|
||||||
else:
|
else:
|
||||||
self.date_label.hide()
|
self.date_label.hide()
|
||||||
|
@ -206,9 +241,13 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
else:
|
else:
|
||||||
amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit
|
amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit
|
||||||
size_str = _("Size:") + ' %d bytes'% size
|
size_str = _("Size:") + ' %d bytes'% size
|
||||||
fee_str = _("Fee") + ': %s'% (format_amount(fee) + ' ' + base_unit if fee is not None else _('Unknown'))
|
fee_str = _("Fee") + ': %s' % (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
|
||||||
if fee is not None:
|
if fee is not None:
|
||||||
fee_str += ' ( %s ) '% self.main_window.format_fee_rate(fee/size*1000)
|
fee_rate = fee/size*1000
|
||||||
|
fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate)
|
||||||
|
confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
|
||||||
|
if fee_rate > confirm_rate:
|
||||||
|
fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!'
|
||||||
self.amount_label.setText(amount_str)
|
self.amount_label.setText(amount_str)
|
||||||
self.fee_label.setText(fee_str)
|
self.fee_label.setText(fee_str)
|
||||||
self.size_label.setText(size_str)
|
self.size_label.setText(size_str)
|
||||||
|
@ -224,7 +263,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
|
rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
|
||||||
rec.setToolTip(_("Wallet receive address"))
|
rec.setToolTip(_("Wallet receive address"))
|
||||||
chg = QTextCharFormat()
|
chg = QTextCharFormat()
|
||||||
chg.setBackground(QBrush(QColor("yellow")))
|
chg.setBackground(QBrush(ColorScheme.YELLOW.as_color(background=True)))
|
||||||
chg.setToolTip(_("Wallet change address"))
|
chg.setToolTip(_("Wallet change address"))
|
||||||
|
|
||||||
def text_format(addr):
|
def text_format(addr):
|
||||||
|
@ -254,7 +293,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
if _addr:
|
if _addr:
|
||||||
addr = _addr
|
addr = _addr
|
||||||
if addr is None:
|
if addr is None:
|
||||||
addr = _('Unknown')
|
addr = _('unknown')
|
||||||
cursor.insertText(addr, text_format(addr))
|
cursor.insertText(addr, text_format(addr))
|
||||||
if x.get('value'):
|
if x.get('value'):
|
||||||
cursor.insertText(format_amount(x['value']), ext)
|
cursor.insertText(format_amount(x['value']), ext)
|
||||||
|
|
174
gui/qt/util.py
174
gui/qt/util.py
|
@ -6,11 +6,15 @@ import queue
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from electrum.i18n import _
|
|
||||||
from PyQt5.QtGui import *
|
from PyQt5.QtGui import *
|
||||||
from PyQt5.QtCore import *
|
from PyQt5.QtCore import *
|
||||||
from PyQt5.QtWidgets import *
|
from PyQt5.QtWidgets import *
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum.util import FileImportFailed, FileExportFailed
|
||||||
|
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
|
||||||
|
|
||||||
|
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
MONOSPACE_FONT = 'Lucida Console'
|
MONOSPACE_FONT = 'Lucida Console'
|
||||||
elif platform.system() == 'Darwin':
|
elif platform.system() == 'Darwin':
|
||||||
|
@ -21,8 +25,6 @@ else:
|
||||||
|
|
||||||
dialogs = []
|
dialogs = []
|
||||||
|
|
||||||
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
|
|
||||||
|
|
||||||
pr_icons = {
|
pr_icons = {
|
||||||
PR_UNPAID:":icons/unpaid.png",
|
PR_UNPAID:":icons/unpaid.png",
|
||||||
PR_PAID:":icons/confirmed.png",
|
PR_PAID:":icons/confirmed.png",
|
||||||
|
@ -200,9 +202,14 @@ class MessageBoxMixin(object):
|
||||||
def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok,
|
def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok,
|
||||||
defaultButton=QMessageBox.NoButton):
|
defaultButton=QMessageBox.NoButton):
|
||||||
parent = parent or self.top_level_window()
|
parent = parent or self.top_level_window()
|
||||||
d = QMessageBox(icon, title, str(text), buttons, parent)
|
if type(icon) is QPixmap:
|
||||||
|
d = QMessageBox(QMessageBox.Information, title, str(text), buttons, parent)
|
||||||
|
d.setIconPixmap(icon)
|
||||||
|
else:
|
||||||
|
d = QMessageBox(icon, title, str(text), buttons, parent)
|
||||||
d.setWindowModality(Qt.WindowModal)
|
d.setWindowModality(Qt.WindowModal)
|
||||||
d.setDefaultButton(defaultButton)
|
d.setDefaultButton(defaultButton)
|
||||||
|
d.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||||
return d.exec_()
|
return d.exec_()
|
||||||
|
|
||||||
class WindowModalDialog(QDialog, MessageBoxMixin):
|
class WindowModalDialog(QDialog, MessageBoxMixin):
|
||||||
|
@ -216,7 +223,7 @@ class WindowModalDialog(QDialog, MessageBoxMixin):
|
||||||
|
|
||||||
|
|
||||||
class WaitingDialog(WindowModalDialog):
|
class WaitingDialog(WindowModalDialog):
|
||||||
'''Shows a please wait dialog whilst runnning a task. It is not
|
'''Shows a please wait dialog whilst running a task. It is not
|
||||||
necessary to maintain a reference to this dialog.'''
|
necessary to maintain a reference to this dialog.'''
|
||||||
def __init__(self, parent, message, task, on_success=None, on_error=None):
|
def __init__(self, parent, message, task, on_success=None, on_error=None):
|
||||||
assert parent
|
assert parent
|
||||||
|
@ -228,6 +235,7 @@ class WaitingDialog(WindowModalDialog):
|
||||||
self.accepted.connect(self.on_accepted)
|
self.accepted.connect(self.on_accepted)
|
||||||
self.show()
|
self.show()
|
||||||
self.thread = TaskThread(self)
|
self.thread = TaskThread(self)
|
||||||
|
self.thread.finished.connect(self.deleteLater) # see #3956
|
||||||
self.thread.add(task, on_success, self.accept, on_error)
|
self.thread.add(task, on_success, self.accept, on_error)
|
||||||
|
|
||||||
def wait(self):
|
def wait(self):
|
||||||
|
@ -254,7 +262,7 @@ def line_dialog(parent, title, label, ok_label, default=None):
|
||||||
def text_dialog(parent, title, label, ok_label, default=None, allow_multi=False):
|
def text_dialog(parent, title, label, ok_label, default=None, allow_multi=False):
|
||||||
from .qrtextedit import ScanQRTextEdit
|
from .qrtextedit import ScanQRTextEdit
|
||||||
dialog = WindowModalDialog(parent, title)
|
dialog = WindowModalDialog(parent, title)
|
||||||
dialog.setMinimumWidth(500)
|
dialog.setMinimumWidth(600)
|
||||||
l = QVBoxLayout()
|
l = QVBoxLayout()
|
||||||
dialog.setLayout(l)
|
dialog.setLayout(l)
|
||||||
l.addWidget(QLabel(label))
|
l.addWidget(QLabel(label))
|
||||||
|
@ -389,13 +397,18 @@ class MyTreeWidget(QTreeWidget):
|
||||||
self.editor = None
|
self.editor = None
|
||||||
self.pending_update = False
|
self.pending_update = False
|
||||||
if editable_columns is None:
|
if editable_columns is None:
|
||||||
editable_columns = [stretch_column]
|
editable_columns = {stretch_column}
|
||||||
|
else:
|
||||||
|
editable_columns = set(editable_columns)
|
||||||
self.editable_columns = editable_columns
|
self.editable_columns = editable_columns
|
||||||
self.setItemDelegate(ElectrumItemDelegate(self))
|
self.setItemDelegate(ElectrumItemDelegate(self))
|
||||||
self.itemDoubleClicked.connect(self.on_doubleclick)
|
self.itemDoubleClicked.connect(self.on_doubleclick)
|
||||||
self.update_headers(headers)
|
self.update_headers(headers)
|
||||||
self.current_filter = ""
|
self.current_filter = ""
|
||||||
|
|
||||||
|
self.setRootIsDecorated(False) # remove left margin
|
||||||
|
self.toolbar_shown = False
|
||||||
|
|
||||||
def update_headers(self, headers):
|
def update_headers(self, headers):
|
||||||
self.setColumnCount(len(headers))
|
self.setColumnCount(len(headers))
|
||||||
self.setHeaderLabels(headers)
|
self.setHeaderLabels(headers)
|
||||||
|
@ -406,11 +419,15 @@ class MyTreeWidget(QTreeWidget):
|
||||||
|
|
||||||
def editItem(self, item, column):
|
def editItem(self, item, column):
|
||||||
if column in self.editable_columns:
|
if column in self.editable_columns:
|
||||||
self.editing_itemcol = (item, column, item.text(column))
|
try:
|
||||||
# Calling setFlags causes on_changed events for some reason
|
self.editing_itemcol = (item, column, item.text(column))
|
||||||
item.setFlags(item.flags() | Qt.ItemIsEditable)
|
# Calling setFlags causes on_changed events for some reason
|
||||||
QTreeWidget.editItem(self, item, column)
|
item.setFlags(item.flags() | Qt.ItemIsEditable)
|
||||||
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
QTreeWidget.editItem(self, item, column)
|
||||||
|
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
||||||
|
except RuntimeError:
|
||||||
|
# (item) wrapped C/C++ object has been deleted
|
||||||
|
pass
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None:
|
if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None:
|
||||||
|
@ -478,8 +495,12 @@ class MyTreeWidget(QTreeWidget):
|
||||||
self.pending_update = True
|
self.pending_update = True
|
||||||
else:
|
else:
|
||||||
self.setUpdatesEnabled(False)
|
self.setUpdatesEnabled(False)
|
||||||
|
scroll_pos = self.verticalScrollBar().value()
|
||||||
self.on_update()
|
self.on_update()
|
||||||
self.setUpdatesEnabled(True)
|
self.setUpdatesEnabled(True)
|
||||||
|
# To paint the list before resetting the scroll position
|
||||||
|
self.parent.app.processEvents()
|
||||||
|
self.verticalScrollBar().setValue(scroll_pos)
|
||||||
if self.current_filter:
|
if self.current_filter:
|
||||||
self.filter(self.current_filter)
|
self.filter(self.current_filter)
|
||||||
|
|
||||||
|
@ -503,6 +524,37 @@ class MyTreeWidget(QTreeWidget):
|
||||||
item.setHidden(all([item.text(column).lower().find(p) == -1
|
item.setHidden(all([item.text(column).lower().find(p) == -1
|
||||||
for column in columns]))
|
for column in columns]))
|
||||||
|
|
||||||
|
def create_toolbar(self, config=None):
|
||||||
|
hbox = QHBoxLayout()
|
||||||
|
buttons = self.get_toolbar_buttons()
|
||||||
|
for b in buttons:
|
||||||
|
b.setVisible(False)
|
||||||
|
hbox.addWidget(b)
|
||||||
|
hide_button = QPushButton('x')
|
||||||
|
hide_button.setVisible(False)
|
||||||
|
hide_button.pressed.connect(lambda: self.show_toolbar(False, config))
|
||||||
|
self.toolbar_buttons = buttons + (hide_button,)
|
||||||
|
hbox.addStretch()
|
||||||
|
hbox.addWidget(hide_button)
|
||||||
|
return hbox
|
||||||
|
|
||||||
|
def save_toolbar_state(self, state, config):
|
||||||
|
pass # implemented in subclasses
|
||||||
|
|
||||||
|
def show_toolbar(self, state, config=None):
|
||||||
|
if state == self.toolbar_shown:
|
||||||
|
return
|
||||||
|
self.toolbar_shown = state
|
||||||
|
if config:
|
||||||
|
self.save_toolbar_state(state, config)
|
||||||
|
for b in self.toolbar_buttons:
|
||||||
|
b.setVisible(state)
|
||||||
|
if not state:
|
||||||
|
self.on_hide_toolbar()
|
||||||
|
|
||||||
|
def toggle_toolbar(self, config=None):
|
||||||
|
self.show_toolbar(not self.toolbar_shown, config)
|
||||||
|
|
||||||
|
|
||||||
class ButtonsWidget(QWidget):
|
class ButtonsWidget(QWidget):
|
||||||
|
|
||||||
|
@ -589,12 +641,12 @@ class TaskThread(QThread):
|
||||||
except BaseException:
|
except BaseException:
|
||||||
self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
|
self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
|
||||||
|
|
||||||
def on_done(self, result, cb_done, cb):
|
def on_done(self, result, cb_done, cb_result):
|
||||||
# This runs in the parent's thread.
|
# This runs in the parent's thread.
|
||||||
if cb_done:
|
if cb_done:
|
||||||
cb_done()
|
cb_done()
|
||||||
if cb:
|
if cb_result:
|
||||||
cb(result)
|
cb_result(result)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.tasks.put(None)
|
self.tasks.put(None)
|
||||||
|
@ -621,6 +673,7 @@ class ColorScheme:
|
||||||
dark_scheme = False
|
dark_scheme = False
|
||||||
|
|
||||||
GREEN = ColorSchemeItem("#117c11", "#8af296")
|
GREEN = ColorSchemeItem("#117c11", "#8af296")
|
||||||
|
YELLOW = ColorSchemeItem("#ffff00", "#ffff00")
|
||||||
RED = ColorSchemeItem("#7c1111", "#f18c8c")
|
RED = ColorSchemeItem("#7c1111", "#f18c8c")
|
||||||
BLUE = ColorSchemeItem("#123b7c", "#8cb3f2")
|
BLUE = ColorSchemeItem("#123b7c", "#8cb3f2")
|
||||||
DEFAULT = ColorSchemeItem("black", "white")
|
DEFAULT = ColorSchemeItem("black", "white")
|
||||||
|
@ -635,6 +688,97 @@ class ColorScheme:
|
||||||
if ColorScheme.has_dark_background(widget):
|
if ColorScheme.has_dark_background(widget):
|
||||||
ColorScheme.dark_scheme = True
|
ColorScheme.dark_scheme = True
|
||||||
|
|
||||||
|
|
||||||
|
class AcceptFileDragDrop:
|
||||||
|
def __init__(self, file_type=""):
|
||||||
|
assert isinstance(self, QWidget)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.file_type = file_type
|
||||||
|
|
||||||
|
def validateEvent(self, event):
|
||||||
|
if not event.mimeData().hasUrls():
|
||||||
|
event.ignore()
|
||||||
|
return False
|
||||||
|
for url in event.mimeData().urls():
|
||||||
|
if not url.toLocalFile().endswith(self.file_type):
|
||||||
|
event.ignore()
|
||||||
|
return False
|
||||||
|
event.accept()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def dragEnterEvent(self, event):
|
||||||
|
self.validateEvent(event)
|
||||||
|
|
||||||
|
def dragMoveEvent(self, event):
|
||||||
|
if self.validateEvent(event):
|
||||||
|
event.setDropAction(Qt.CopyAction)
|
||||||
|
|
||||||
|
def dropEvent(self, event):
|
||||||
|
if self.validateEvent(event):
|
||||||
|
for url in event.mimeData().urls():
|
||||||
|
self.onFileAdded(url.toLocalFile())
|
||||||
|
|
||||||
|
def onFileAdded(self, fn):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
def import_meta_gui(electrum_window, title, importer, on_success):
|
||||||
|
filter_ = "JSON (*.json);;All files (*)"
|
||||||
|
filename = electrum_window.getOpenFileName(_("Open {} file").format(title), filter_)
|
||||||
|
if not filename:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
importer(filename)
|
||||||
|
except FileImportFailed as e:
|
||||||
|
electrum_window.show_critical(str(e))
|
||||||
|
else:
|
||||||
|
electrum_window.show_message(_("Your {} were successfully imported").format(title))
|
||||||
|
on_success()
|
||||||
|
|
||||||
|
|
||||||
|
def export_meta_gui(electrum_window, title, exporter):
|
||||||
|
filter_ = "JSON (*.json);;All files (*)"
|
||||||
|
filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title),
|
||||||
|
'electrum_{}.json'.format(title), filter_)
|
||||||
|
if not filename:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
exporter(filename)
|
||||||
|
except FileExportFailed as e:
|
||||||
|
electrum_window.show_critical(str(e))
|
||||||
|
else:
|
||||||
|
electrum_window.show_message(_("Your {0} were exported to '{1}'")
|
||||||
|
.format(title, str(filename)))
|
||||||
|
|
||||||
|
|
||||||
|
def get_parent_main_window(widget):
|
||||||
|
"""Returns a reference to the ElectrumWindow this widget belongs to."""
|
||||||
|
from .main_window import ElectrumWindow
|
||||||
|
for _ in range(100):
|
||||||
|
if widget is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(widget, ElectrumWindow):
|
||||||
|
widget = widget.parentWidget()
|
||||||
|
else:
|
||||||
|
return widget
|
||||||
|
return None
|
||||||
|
|
||||||
|
class SortableTreeWidgetItem(QTreeWidgetItem):
|
||||||
|
DataRole = Qt.UserRole + 1
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
column = self.treeWidget().sortColumn()
|
||||||
|
if None not in [x.data(column, self.DataRole) for x in [self, other]]:
|
||||||
|
# We have set custom data to sort by
|
||||||
|
return self.data(column, self.DataRole) < other.data(column, self.DataRole)
|
||||||
|
try:
|
||||||
|
# Is the value something numeric?
|
||||||
|
return float(self.text(column)) < float(other.text(column))
|
||||||
|
except ValueError:
|
||||||
|
# If not, we will just do string comparison
|
||||||
|
return self.text(column) < other.text(column)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))
|
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))
|
||||||
|
|
|
@ -32,6 +32,7 @@ class UTXOList(MyTreeWidget):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1)
|
MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1)
|
||||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
|
self.setSortingEnabled(True)
|
||||||
|
|
||||||
def get_name(self, x):
|
def get_name(self, x):
|
||||||
return x.get('prevout_hash') + ":%d"%x.get('prevout_n')
|
return x.get('prevout_hash') + ":%d"%x.get('prevout_n')
|
||||||
|
@ -46,9 +47,10 @@ class UTXOList(MyTreeWidget):
|
||||||
height = x.get('height')
|
height = x.get('height')
|
||||||
name = self.get_name(x)
|
name = self.get_name(x)
|
||||||
label = self.wallet.get_label(x.get('prevout_hash'))
|
label = self.wallet.get_label(x.get('prevout_hash'))
|
||||||
amount = self.parent.format_amount(x['value'])
|
amount = self.parent.format_amount(x['value'], whitespaces=True)
|
||||||
utxo_item = QTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]])
|
utxo_item = SortableTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]])
|
||||||
utxo_item.setFont(0, QFont(MONOSPACE_FONT))
|
utxo_item.setFont(0, QFont(MONOSPACE_FONT))
|
||||||
|
utxo_item.setFont(2, QFont(MONOSPACE_FONT))
|
||||||
utxo_item.setFont(4, QFont(MONOSPACE_FONT))
|
utxo_item.setFont(4, QFont(MONOSPACE_FONT))
|
||||||
utxo_item.setData(0, Qt.UserRole, name)
|
utxo_item.setData(0, Qt.UserRole, name)
|
||||||
if self.wallet.is_frozen(address):
|
if self.wallet.is_frozen(address):
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<file>icons/electrum_light_icon.png</file>
|
<file>icons/electrum_light_icon.png</file>
|
||||||
<file>icons/electrum_dark_icon.png</file>
|
<file>icons/electrum_dark_icon.png</file>
|
||||||
<file>icons/file.png</file>
|
<file>icons/file.png</file>
|
||||||
|
<file>icons/info.png</file>
|
||||||
<file>icons/keepkey.png</file>
|
<file>icons/keepkey.png</file>
|
||||||
<file>icons/keepkey_unpaired.png</file>
|
<file>icons/keepkey_unpaired.png</file>
|
||||||
<file>icons/key.png</file>
|
<file>icons/key.png</file>
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
<file>icons/lock.png</file>
|
<file>icons/lock.png</file>
|
||||||
<file>icons/microphone.png</file>
|
<file>icons/microphone.png</file>
|
||||||
<file>icons/network.png</file>
|
<file>icons/network.png</file>
|
||||||
|
<file>icons/offline_tx.png</file>
|
||||||
<file>icons/qrcode.png</file>
|
<file>icons/qrcode.png</file>
|
||||||
<file>icons/qrcode_white.png</file>
|
<file>icons/qrcode_white.png</file>
|
||||||
<file>icons/preferences.png</file>
|
<file>icons/preferences.png</file>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 463 B |
BIN
icons/unpaid.png
BIN
icons/unpaid.png
Binary file not shown.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 23 KiB |
|
@ -24,12 +24,19 @@
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from . import keystore
|
from . import keystore
|
||||||
from .keystore import bip44_derivation
|
from .keystore import bip44_derivation
|
||||||
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types
|
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types
|
||||||
|
from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
from .util import UserCancelled
|
||||||
|
|
||||||
|
# hardware device setup purpose
|
||||||
|
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
|
||||||
|
|
||||||
class ScriptTypeNotSupported(Exception): pass
|
class ScriptTypeNotSupported(Exception): pass
|
||||||
|
|
||||||
|
@ -123,7 +130,7 @@ class BaseWizard(object):
|
||||||
choices = [
|
choices = [
|
||||||
('choose_seed_type', _('Create a new seed')),
|
('choose_seed_type', _('Create a new seed')),
|
||||||
('restore_from_seed', _('I already have a seed')),
|
('restore_from_seed', _('I already have a seed')),
|
||||||
('restore_from_key', _('Use public or private keys')),
|
('restore_from_key', _('Use a master key')),
|
||||||
]
|
]
|
||||||
if not self.is_kivy:
|
if not self.is_kivy:
|
||||||
choices.append(('choose_hw_device', _('Use a hardware device')))
|
choices.append(('choose_hw_device', _('Use a hardware device')))
|
||||||
|
@ -146,17 +153,22 @@ class BaseWizard(object):
|
||||||
is_valid=v, allow_multi=True)
|
is_valid=v, allow_multi=True)
|
||||||
|
|
||||||
def on_import(self, text):
|
def on_import(self, text):
|
||||||
|
# create a temporary wallet and exploit that modifications
|
||||||
|
# will be reflected on self.storage
|
||||||
if keystore.is_address_list(text):
|
if keystore.is_address_list(text):
|
||||||
self.wallet = Imported_Wallet(self.storage)
|
w = Imported_Wallet(self.storage)
|
||||||
for x in text.split():
|
for x in text.split():
|
||||||
self.wallet.import_address(x)
|
w.import_address(x)
|
||||||
elif keystore.is_private_key_list(text):
|
elif keystore.is_private_key_list(text):
|
||||||
k = keystore.Imported_KeyStore({})
|
k = keystore.Imported_KeyStore({})
|
||||||
self.storage.put('keystore', k.dump())
|
self.storage.put('keystore', k.dump())
|
||||||
self.wallet = Imported_Wallet(self.storage)
|
w = Imported_Wallet(self.storage)
|
||||||
for x in text.split():
|
for x in text.split():
|
||||||
self.wallet.import_private_key(x, None)
|
w.import_private_key(x, None)
|
||||||
self.terminate()
|
self.keystores.append(w.keystore)
|
||||||
|
else:
|
||||||
|
return self.terminate()
|
||||||
|
return self.run('create_wallet')
|
||||||
|
|
||||||
def restore_from_key(self):
|
def restore_from_key(self):
|
||||||
if self.wallet_type == 'standard':
|
if self.wallet_type == 'standard':
|
||||||
|
@ -175,7 +187,7 @@ class BaseWizard(object):
|
||||||
k = keystore.from_master_key(text)
|
k = keystore.from_master_key(text)
|
||||||
self.on_keystore(k)
|
self.on_keystore(k)
|
||||||
|
|
||||||
def choose_hw_device(self):
|
def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET):
|
||||||
title = _('Hardware Keystore')
|
title = _('Hardware Keystore')
|
||||||
# check available plugins
|
# check available plugins
|
||||||
support = self.plugins.get_hardware_support()
|
support = self.plugins.get_hardware_support()
|
||||||
|
@ -184,7 +196,7 @@ class BaseWizard(object):
|
||||||
_('No hardware wallet support found on your system.'),
|
_('No hardware wallet support found on your system.'),
|
||||||
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
|
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
|
||||||
])
|
])
|
||||||
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device())
|
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
|
||||||
return
|
return
|
||||||
# scan devices
|
# scan devices
|
||||||
devices = []
|
devices = []
|
||||||
|
@ -204,34 +216,51 @@ class BaseWizard(object):
|
||||||
_('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ',
|
_('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ',
|
||||||
_('On Linux, you might have to add a new permission to your udev rules.'),
|
_('On Linux, you might have to add a new permission to your udev rules.'),
|
||||||
])
|
])
|
||||||
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device())
|
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
|
||||||
return
|
return
|
||||||
# select device
|
# select device
|
||||||
self.devices = devices
|
self.devices = devices
|
||||||
choices = []
|
choices = []
|
||||||
for name, info in devices:
|
for name, info in devices:
|
||||||
state = _("initialized") if info.initialized else _("wiped")
|
state = _("initialized") if info.initialized else _("wiped")
|
||||||
label = info.label or _("An unnamed %s")%name
|
label = info.label or _("An unnamed {}").format(name)
|
||||||
descr = "%s [%s, %s]" % (label, name, state)
|
descr = "%s [%s, %s]" % (label, name, state)
|
||||||
choices.append(((name, info), descr))
|
choices.append(((name, info), descr))
|
||||||
msg = _('Select a device') + ':'
|
msg = _('Select a device') + ':'
|
||||||
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_device)
|
self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose))
|
||||||
|
|
||||||
def on_device(self, name, device_info):
|
def on_device(self, name, device_info, *, purpose):
|
||||||
self.plugin = self.plugins.get_plugin(name)
|
self.plugin = self.plugins.get_plugin(name)
|
||||||
try:
|
try:
|
||||||
self.plugin.setup_device(device_info, self)
|
self.plugin.setup_device(device_info, self, purpose)
|
||||||
|
except OSError as e:
|
||||||
|
self.show_error(_('We encountered an error while connecting to your device:')
|
||||||
|
+ '\n' + str(e) + '\n'
|
||||||
|
+ _('To try to fix this, we will now re-pair with your device.') + '\n'
|
||||||
|
+ _('Please try again.'))
|
||||||
|
devmgr = self.plugins.device_manager
|
||||||
|
devmgr.unpair_id(device_info.device.id_)
|
||||||
|
self.choose_hw_device(purpose)
|
||||||
|
return
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.show_error(str(e))
|
self.show_error(str(e))
|
||||||
self.choose_hw_device()
|
self.choose_hw_device(purpose)
|
||||||
return
|
return
|
||||||
if self.wallet_type=='multisig':
|
if purpose == HWD_SETUP_NEW_WALLET:
|
||||||
# There is no general standard for HD multisig.
|
if self.wallet_type=='multisig':
|
||||||
# This is partially compatible with BIP45; assumes index=0
|
# There is no general standard for HD multisig.
|
||||||
self.on_hw_derivation(name, device_info, "m/45'/0")
|
# This is partially compatible with BIP45; assumes index=0
|
||||||
|
self.on_hw_derivation(name, device_info, "m/45'/0")
|
||||||
|
else:
|
||||||
|
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
|
||||||
|
self.derivation_dialog(f)
|
||||||
|
elif purpose == HWD_SETUP_DECRYPT_WALLET:
|
||||||
|
derivation = get_derivation_used_for_hw_device_encryption()
|
||||||
|
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self)
|
||||||
|
password = keystore.Xpub.get_pubkey_from_xpub(xpub, ())
|
||||||
|
self.storage.decrypt(password)
|
||||||
else:
|
else:
|
||||||
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
|
raise Exception('unknown purpose: %s' % purpose)
|
||||||
self.derivation_dialog(f)
|
|
||||||
|
|
||||||
def derivation_dialog(self, f):
|
def derivation_dialog(self, f):
|
||||||
default = bip44_derivation(0, bip43_purpose=44)
|
default = bip44_derivation(0, bip43_purpose=44)
|
||||||
|
@ -364,13 +393,45 @@ class BaseWizard(object):
|
||||||
self.run('create_wallet')
|
self.run('create_wallet')
|
||||||
|
|
||||||
def create_wallet(self):
|
def create_wallet(self):
|
||||||
if any(k.may_have_password() for k in self.keystores):
|
encrypt_keystore = any(k.may_have_password() for k in self.keystores)
|
||||||
self.request_password(run_next=self.on_password)
|
# note: the following condition ("if") is duplicated logic from
|
||||||
|
# wallet.get_available_storage_encryption_version()
|
||||||
|
if self.wallet_type == 'standard' and isinstance(self.keystores[0], keystore.Hardware_KeyStore):
|
||||||
|
# offer encrypting with a pw derived from the hw device
|
||||||
|
k = self.keystores[0]
|
||||||
|
try:
|
||||||
|
k.handler = self.plugin.create_handler(self)
|
||||||
|
password = k.get_password_for_storage_encryption()
|
||||||
|
except UserCancelled:
|
||||||
|
devmgr = self.plugins.device_manager
|
||||||
|
devmgr.unpair_xpub(k.xpub)
|
||||||
|
self.choose_hw_device()
|
||||||
|
return
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
self.show_error(str(e))
|
||||||
|
return
|
||||||
|
self.request_storage_encryption(
|
||||||
|
run_next=lambda encrypt_storage: self.on_password(
|
||||||
|
password,
|
||||||
|
encrypt_storage=encrypt_storage,
|
||||||
|
storage_enc_version=STO_EV_XPUB_PW,
|
||||||
|
encrypt_keystore=False))
|
||||||
else:
|
else:
|
||||||
self.on_password(None, False)
|
# prompt the user to set an arbitrary password
|
||||||
|
self.request_password(
|
||||||
|
run_next=lambda password, encrypt_storage: self.on_password(
|
||||||
|
password,
|
||||||
|
encrypt_storage=encrypt_storage,
|
||||||
|
storage_enc_version=STO_EV_USER_PW,
|
||||||
|
encrypt_keystore=encrypt_keystore),
|
||||||
|
force_disable_encrypt_cb=not encrypt_keystore)
|
||||||
|
|
||||||
def on_password(self, password, encrypt):
|
def on_password(self, password, *, encrypt_storage,
|
||||||
self.storage.set_password(password, encrypt)
|
storage_enc_version=STO_EV_USER_PW, encrypt_keystore):
|
||||||
|
self.storage.set_keystore_encryption(bool(password) and encrypt_keystore)
|
||||||
|
if encrypt_storage:
|
||||||
|
self.storage.set_password(password, enc_version=storage_enc_version)
|
||||||
for k in self.keystores:
|
for k in self.keystores:
|
||||||
if k.may_have_password():
|
if k.may_have_password():
|
||||||
k.update_password(None, password)
|
k.update_password(None, password)
|
||||||
|
@ -386,6 +447,13 @@ class BaseWizard(object):
|
||||||
self.storage.write()
|
self.storage.write()
|
||||||
self.wallet = Multisig_Wallet(self.storage)
|
self.wallet = Multisig_Wallet(self.storage)
|
||||||
self.run('create_addresses')
|
self.run('create_addresses')
|
||||||
|
elif self.wallet_type == 'imported':
|
||||||
|
if len(self.keystores) > 0:
|
||||||
|
keys = self.keystores[0].dump()
|
||||||
|
self.storage.put('keystore', keys)
|
||||||
|
self.wallet = Imported_Wallet(self.storage)
|
||||||
|
self.wallet.storage.write()
|
||||||
|
self.terminate()
|
||||||
|
|
||||||
def show_xpub_and_add_cosigners(self, xpub):
|
def show_xpub_and_add_cosigners(self, xpub):
|
||||||
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
|
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
|
||||||
|
@ -444,5 +512,5 @@ class BaseWizard(object):
|
||||||
self.wallet.synchronize()
|
self.wallet.synchronize()
|
||||||
self.wallet.storage.write()
|
self.wallet.storage.write()
|
||||||
self.terminate()
|
self.terminate()
|
||||||
msg = _("Electrum is generating your addresses, please wait.")
|
msg = _("Electrum is generating your addresses, please wait...")
|
||||||
self.waiting_dialog(task, msg)
|
self.waiting_dialog(task, msg)
|
||||||
|
|
196
lib/bitcoin.py
196
lib/bitcoin.py
|
@ -37,89 +37,11 @@ from .util import bfh, bh2u, to_string
|
||||||
from . import version
|
from . import version
|
||||||
from .util import print_error, InvalidPassword, assert_bytes, to_bytes, inv_dict
|
from .util import print_error, InvalidPassword, assert_bytes, to_bytes, inv_dict
|
||||||
from . import segwit_addr
|
from . import segwit_addr
|
||||||
|
from . import constants
|
||||||
|
|
||||||
def read_json(filename, default):
|
|
||||||
path = os.path.join(os.path.dirname(__file__), filename)
|
|
||||||
try:
|
|
||||||
with open(path, 'r') as f:
|
|
||||||
r = json.loads(f.read())
|
|
||||||
except:
|
|
||||||
r = default
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Version numbers for BIP32 extended keys
|
|
||||||
# standard: xprv, xpub
|
|
||||||
# segwit in p2sh: yprv, ypub
|
|
||||||
# native segwit: zprv, zpub
|
|
||||||
XPRV_HEADERS = {
|
|
||||||
'standard': 0x0488ade4,
|
|
||||||
'p2wpkh-p2sh': 0x049d7878,
|
|
||||||
'p2wsh-p2sh': 0x295b005,
|
|
||||||
'p2wpkh': 0x4b2430c,
|
|
||||||
'p2wsh': 0x2aa7a99
|
|
||||||
}
|
|
||||||
XPUB_HEADERS = {
|
|
||||||
'standard': 0x0488b21e,
|
|
||||||
'p2wpkh-p2sh': 0x049d7cb2,
|
|
||||||
'p2wsh-p2sh': 0x295b43f,
|
|
||||||
'p2wpkh': 0x4b24746,
|
|
||||||
'p2wsh': 0x2aa7ed3
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkConstants:
|
|
||||||
|
|
||||||
# https://github.com/z-classic/zclassic/blob/master/src/chainparams.cpp#L103
|
|
||||||
@classmethod
|
|
||||||
def set_mainnet(cls):
|
|
||||||
cls.TESTNET = False
|
|
||||||
cls.WIF_PREFIX = 0x80
|
|
||||||
cls.ADDRTYPE_P2PKH = [0x1C, 0xB8]
|
|
||||||
cls.ADDRTYPE_P2SH = [0x1C, 0xBD]
|
|
||||||
cls.ADDRTYPE_SHIELDED = [0x16, 0x9A]
|
|
||||||
cls.SEGWIT_HRP = "bc" #TODO zcl has no segwit
|
|
||||||
cls.GENESIS = "0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602"
|
|
||||||
cls.DEFAULT_PORTS = {'t': '50001', 's': '50002'}
|
|
||||||
cls.DEFAULT_SERVERS = read_json('servers.json', {})
|
|
||||||
cls.CHECKPOINTS = read_json('checkpoints.json', [])
|
|
||||||
cls.EQUIHASH_N = 200
|
|
||||||
cls.EQUIHASH_K = 9
|
|
||||||
cls.HEADERS_URL = "http://headers.zcl-electrum.com/blockchain_headers"
|
|
||||||
|
|
||||||
cls.CHUNK_SIZE = 200
|
|
||||||
|
|
||||||
# https://github.com/z-classic/zclassic/blob/master/src/chainparams.cpp#L234
|
|
||||||
@classmethod
|
|
||||||
def set_testnet(cls):
|
|
||||||
cls.TESTNET = True
|
|
||||||
cls.WIF_PREFIX = 0xef
|
|
||||||
cls.ADDRTYPE_P2PKH = [0x1D, 0x25]
|
|
||||||
cls.ADDRTYPE_P2SH = [0x1C, 0xBA]
|
|
||||||
cls.ADDRTYPE_SHIELDED = [0x16, 0xB6]
|
|
||||||
cls.SEGWIT_HRP = "tb" #TODO zcl has no segwit
|
|
||||||
cls.GENESIS = "03e1c4bb705c871bf9bfda3e74b7f8f86bff267993c215a89d5795e3708e5e1f"
|
|
||||||
cls.DEFAULT_PORTS = {'t': '51001', 's': '51002'}
|
|
||||||
cls.DEFAULT_SERVERS = read_json('servers_testnet.json', {})
|
|
||||||
cls.CHECKPOINTS = read_json('checkpoints_testnet.json', [])
|
|
||||||
cls.EQUIHASH_N = 200
|
|
||||||
cls.EQUIHASH_K = 9
|
|
||||||
|
|
||||||
#cls.HEADERS_URL = "http://35.224.186.7/blockchain_headers"
|
|
||||||
|
|
||||||
cls.CHUNK_SIZE = 200
|
|
||||||
|
|
||||||
NetworkConstants.set_mainnet()
|
|
||||||
|
|
||||||
################################## transactions
|
################################## transactions
|
||||||
|
|
||||||
FEE_STEP = 10000
|
|
||||||
DEFAULT_FEE_RATE = 10000
|
|
||||||
MAX_FEE_RATE = 300000
|
|
||||||
FEE_TARGETS = [25, 10, 5, 2]
|
|
||||||
|
|
||||||
COINBASE_MATURITY = 100
|
COINBASE_MATURITY = 100
|
||||||
COIN = 100000000
|
COIN = 100000000
|
||||||
|
|
||||||
|
@ -332,11 +254,11 @@ def ser_uint256(u):
|
||||||
return rs
|
return rs
|
||||||
|
|
||||||
def sha256(x):
|
def sha256(x):
|
||||||
if isinstance(x, str):
|
x = to_bytes(x, 'utf8')
|
||||||
x = x.encode('utf8')
|
|
||||||
return bytes(hashlib.sha256(x).digest())
|
return bytes(hashlib.sha256(x).digest())
|
||||||
|
|
||||||
def Hash(x):
|
def Hash(x):
|
||||||
|
x = to_bytes(x, 'utf8')
|
||||||
out = bytes(sha256(sha256(x)))
|
out = bytes(sha256(sha256(x)))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@ -429,16 +351,16 @@ def b58_address_to_hash160(addr):
|
||||||
|
|
||||||
|
|
||||||
def hash160_to_p2pkh(h160):
|
def hash160_to_p2pkh(h160):
|
||||||
return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2PKH)
|
return hash160_to_b58_address(h160, constants.net.ADDRTYPE_P2PKH)
|
||||||
|
|
||||||
def hash160_to_p2sh(h160):
|
def hash160_to_p2sh(h160):
|
||||||
return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2SH)
|
return hash160_to_b58_address(h160, constants.net.ADDRTYPE_P2SH)
|
||||||
|
|
||||||
def public_key_to_p2pkh(public_key):
|
def public_key_to_p2pkh(public_key):
|
||||||
return hash160_to_p2pkh(hash_160(public_key))
|
return hash160_to_p2pkh(hash_160(public_key))
|
||||||
|
|
||||||
def hash_to_segwit_addr(h):
|
def hash_to_segwit_addr(h):
|
||||||
return segwit_addr.encode(NetworkConstants.SEGWIT_HRP, 0, h)
|
return segwit_addr.encode(constants.net.SEGWIT_HRP, 0, h)
|
||||||
|
|
||||||
def public_key_to_p2wpkh(public_key):
|
def public_key_to_p2wpkh(public_key):
|
||||||
return hash_to_segwit_addr(hash_160(public_key))
|
return hash_to_segwit_addr(hash_160(public_key))
|
||||||
|
@ -484,7 +406,7 @@ def script_to_address(script):
|
||||||
return addr
|
return addr
|
||||||
|
|
||||||
def address_to_script(addr):
|
def address_to_script(addr):
|
||||||
witver, witprog = segwit_addr.decode(NetworkConstants.SEGWIT_HRP, addr)
|
witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
|
||||||
if witprog is not None:
|
if witprog is not None:
|
||||||
assert (0 <= witver <= 16)
|
assert (0 <= witver <= 16)
|
||||||
OP_n = witver + 0x50 if witver > 0 else 0
|
OP_n = witver + 0x50 if witver > 0 else 0
|
||||||
|
@ -492,11 +414,11 @@ def address_to_script(addr):
|
||||||
script += push_script(bh2u(bytes(witprog)))
|
script += push_script(bh2u(bytes(witprog)))
|
||||||
return script
|
return script
|
||||||
addrtype, hash_160 = b58_address_to_hash160(addr)
|
addrtype, hash_160 = b58_address_to_hash160(addr)
|
||||||
if addrtype == NetworkConstants.ADDRTYPE_P2PKH:
|
if addrtype == constants.net.ADDRTYPE_P2PKH:
|
||||||
script = '76a9' # op_dup, op_hash_160
|
script = '76a9' # op_dup, op_hash_160
|
||||||
script += push_script(bh2u(hash_160))
|
script += push_script(bh2u(hash_160))
|
||||||
script += '88ac' # op_equalverify, op_checksig
|
script += '88ac' # op_equalverify, op_checksig
|
||||||
elif addrtype == NetworkConstants.ADDRTYPE_P2SH:
|
elif addrtype == constants.net.ADDRTYPE_P2SH:
|
||||||
script = 'a9' # op_hash_160
|
script = 'a9' # op_hash_160
|
||||||
script += push_script(bh2u(hash_160))
|
script += push_script(bh2u(hash_160))
|
||||||
script += '87' # op_equal
|
script += '87' # op_equal
|
||||||
|
@ -600,9 +522,8 @@ def DecodeBase58Check(psz):
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
# backwards compat
|
||||||
# extended key export format for segwit
|
# extended WIF for segwit (used in 3.0.x; but still used internally)
|
||||||
|
|
||||||
SCRIPT_TYPES = {
|
SCRIPT_TYPES = {
|
||||||
'p2pkh':0,
|
'p2pkh':0,
|
||||||
'p2wpkh':1,
|
'p2wpkh':1,
|
||||||
|
@ -613,26 +534,43 @@ SCRIPT_TYPES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def serialize_privkey(secret, compressed, txin_type):
|
def serialize_privkey(secret, compressed, txin_type, internal_use=False):
|
||||||
prefix = bytes([(SCRIPT_TYPES[txin_type]+NetworkConstants.WIF_PREFIX)&255])
|
if internal_use:
|
||||||
|
prefix = bytes([(SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255])
|
||||||
|
else:
|
||||||
|
prefix = bytes([constants.net.WIF_PREFIX])
|
||||||
suffix = b'\01' if compressed else b''
|
suffix = b'\01' if compressed else b''
|
||||||
vchIn = prefix + secret + suffix
|
vchIn = prefix + secret + suffix
|
||||||
return EncodeBase58Check(vchIn)
|
base58_wif = EncodeBase58Check(vchIn)
|
||||||
|
if internal_use:
|
||||||
|
return base58_wif
|
||||||
|
else:
|
||||||
|
return '{}:{}'.format(txin_type, base58_wif)
|
||||||
|
|
||||||
|
|
||||||
def deserialize_privkey(key):
|
def deserialize_privkey(key):
|
||||||
# whether the pubkey is compressed should be visible from the keystore
|
|
||||||
vch = DecodeBase58Check(key)
|
|
||||||
if is_minikey(key):
|
if is_minikey(key):
|
||||||
return 'p2pkh', minikey_to_private_key(key), True
|
return 'p2pkh', minikey_to_private_key(key), True
|
||||||
elif vch:
|
|
||||||
txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - NetworkConstants.WIF_PREFIX]
|
txin_type = None
|
||||||
assert len(vch) in [33, 34]
|
if ':' in key:
|
||||||
compressed = len(vch) == 34
|
txin_type, key = key.split(sep=':', maxsplit=1)
|
||||||
return txin_type, vch[1:33], compressed
|
assert txin_type in SCRIPT_TYPES
|
||||||
else:
|
vch = DecodeBase58Check(key)
|
||||||
|
if not vch:
|
||||||
raise BaseException("cannot deserialize", key)
|
raise BaseException("cannot deserialize", key)
|
||||||
|
|
||||||
|
if txin_type is None:
|
||||||
|
# keys exported in version 3.0.x encoded script type in first byte
|
||||||
|
txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - constants.net.WIF_PREFIX]
|
||||||
|
else:
|
||||||
|
assert vch[0] == constants.net.WIF_PREFIX
|
||||||
|
|
||||||
|
assert len(vch) in [33, 34]
|
||||||
|
compressed = len(vch) == 34
|
||||||
|
return txin_type, vch[1:33], compressed
|
||||||
|
|
||||||
|
|
||||||
def regenerate_key(pk):
|
def regenerate_key(pk):
|
||||||
assert len(pk) == 32
|
assert len(pk) == 32
|
||||||
return EC_KEY(pk)
|
return EC_KEY(pk)
|
||||||
|
@ -662,7 +600,7 @@ def address_from_private_key(sec):
|
||||||
|
|
||||||
def is_segwit_address(addr):
|
def is_segwit_address(addr):
|
||||||
try:
|
try:
|
||||||
witver, witprog = segwit_addr.decode(NetworkConstants.SEGWIT_HRP, addr)
|
witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
return witprog is not None
|
return witprog is not None
|
||||||
|
@ -672,7 +610,7 @@ def is_b58_address(addr):
|
||||||
addrtype, h = b58_address_to_hash160(addr)
|
addrtype, h = b58_address_to_hash160(addr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
if addrtype not in [NetworkConstants.ADDRTYPE_P2PKH, NetworkConstants.ADDRTYPE_P2SH]:
|
if addrtype not in [constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH]:
|
||||||
return False
|
return False
|
||||||
return addr == hash160_to_b58_address(h, addrtype)
|
return addr == hash160_to_b58_address(h, addrtype)
|
||||||
|
|
||||||
|
@ -735,8 +673,8 @@ def verify_message(address, sig, message):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def encrypt_message(message, pubkey):
|
def encrypt_message(message, pubkey, magic=b'BIE1'):
|
||||||
return EC_KEY.encrypt_message(message, bfh(pubkey))
|
return EC_KEY.encrypt_message(message, bfh(pubkey), magic)
|
||||||
|
|
||||||
|
|
||||||
def chunks(l, n):
|
def chunks(l, n):
|
||||||
|
@ -881,7 +819,7 @@ class EC_KEY(object):
|
||||||
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac
|
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def encrypt_message(self, message, pubkey):
|
def encrypt_message(self, message, pubkey, magic=b'BIE1'):
|
||||||
assert_bytes(message)
|
assert_bytes(message)
|
||||||
|
|
||||||
pk = ser_to_point(pubkey)
|
pk = ser_to_point(pubkey)
|
||||||
|
@ -895,20 +833,20 @@ class EC_KEY(object):
|
||||||
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
||||||
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
|
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
|
||||||
ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True))
|
ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True))
|
||||||
encrypted = b'BIE1' + ephemeral_pubkey + ciphertext
|
encrypted = magic + ephemeral_pubkey + ciphertext
|
||||||
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
|
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
|
||||||
|
|
||||||
return base64.b64encode(encrypted + mac)
|
return base64.b64encode(encrypted + mac)
|
||||||
|
|
||||||
def decrypt_message(self, encrypted):
|
def decrypt_message(self, encrypted, magic=b'BIE1'):
|
||||||
encrypted = base64.b64decode(encrypted)
|
encrypted = base64.b64decode(encrypted)
|
||||||
if len(encrypted) < 85:
|
if len(encrypted) < 85:
|
||||||
raise Exception('invalid ciphertext: length')
|
raise Exception('invalid ciphertext: length')
|
||||||
magic = encrypted[:4]
|
magic_found = encrypted[:4]
|
||||||
ephemeral_pubkey = encrypted[4:37]
|
ephemeral_pubkey = encrypted[4:37]
|
||||||
ciphertext = encrypted[37:-32]
|
ciphertext = encrypted[37:-32]
|
||||||
mac = encrypted[-32:]
|
mac = encrypted[-32:]
|
||||||
if magic != b'BIE1':
|
if magic_found != magic:
|
||||||
raise Exception('invalid ciphertext: invalid magic bytes')
|
raise Exception('invalid ciphertext: invalid magic bytes')
|
||||||
try:
|
try:
|
||||||
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
|
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
|
||||||
|
@ -984,25 +922,35 @@ def _CKD_pub(cK, c, s):
|
||||||
return cK_n, c_n
|
return cK_n, c_n
|
||||||
|
|
||||||
|
|
||||||
def xprv_header(xtype):
|
def xprv_header(xtype, *, net=None):
|
||||||
return bfh("%08x" % XPRV_HEADERS[xtype])
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
|
return bfh("%08x" % net.XPRV_HEADERS[xtype])
|
||||||
|
|
||||||
|
|
||||||
def xpub_header(xtype):
|
def xpub_header(xtype, *, net=None):
|
||||||
return bfh("%08x" % XPUB_HEADERS[xtype])
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
|
return bfh("%08x" % net.XPUB_HEADERS[xtype])
|
||||||
|
|
||||||
|
|
||||||
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4):
|
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4,
|
||||||
xprv = xprv_header(xtype) + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
|
child_number=b'\x00'*4, *, net=None):
|
||||||
|
xprv = xprv_header(xtype, net=net) \
|
||||||
|
+ bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
|
||||||
return EncodeBase58Check(xprv)
|
return EncodeBase58Check(xprv)
|
||||||
|
|
||||||
|
|
||||||
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4):
|
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
|
||||||
xpub = xpub_header(xtype) + bytes([depth]) + fingerprint + child_number + c + cK
|
child_number=b'\x00'*4, *, net=None):
|
||||||
|
xpub = xpub_header(xtype, net=net) \
|
||||||
|
+ bytes([depth]) + fingerprint + child_number + c + cK
|
||||||
return EncodeBase58Check(xpub)
|
return EncodeBase58Check(xpub)
|
||||||
|
|
||||||
|
|
||||||
def deserialize_xkey(xkey, prv):
|
def deserialize_xkey(xkey, prv, *, net=None):
|
||||||
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
xkey = DecodeBase58Check(xkey)
|
xkey = DecodeBase58Check(xkey)
|
||||||
if len(xkey) != 78:
|
if len(xkey) != 78:
|
||||||
raise BaseException('Invalid length')
|
raise BaseException('Invalid length')
|
||||||
|
@ -1011,7 +959,7 @@ def deserialize_xkey(xkey, prv):
|
||||||
child_number = xkey[9:13]
|
child_number = xkey[9:13]
|
||||||
c = xkey[13:13+32]
|
c = xkey[13:13+32]
|
||||||
header = int('0x' + bh2u(xkey[0:4]), 16)
|
header = int('0x' + bh2u(xkey[0:4]), 16)
|
||||||
headers = XPRV_HEADERS if prv else XPUB_HEADERS
|
headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
|
||||||
if header not in headers.values():
|
if header not in headers.values():
|
||||||
raise BaseException('Invalid xpub format', hex(header))
|
raise BaseException('Invalid xpub format', hex(header))
|
||||||
xtype = list(headers.keys())[list(headers.values()).index(header)]
|
xtype = list(headers.keys())[list(headers.values()).index(header)]
|
||||||
|
@ -1020,11 +968,11 @@ def deserialize_xkey(xkey, prv):
|
||||||
return xtype, depth, fingerprint, child_number, c, K_or_k
|
return xtype, depth, fingerprint, child_number, c, K_or_k
|
||||||
|
|
||||||
|
|
||||||
def deserialize_xpub(xkey):
|
def deserialize_xpub(xkey, *, net=None):
|
||||||
return deserialize_xkey(xkey, False)
|
return deserialize_xkey(xkey, False, net=net)
|
||||||
|
|
||||||
def deserialize_xprv(xkey):
|
def deserialize_xprv(xkey, *, net=None):
|
||||||
return deserialize_xkey(xkey, True)
|
return deserialize_xkey(xkey, True, net=net)
|
||||||
|
|
||||||
def xpub_type(x):
|
def xpub_type(x):
|
||||||
return deserialize_xpub(x)[0]
|
return deserialize_xpub(x)[0]
|
||||||
|
|
|
@ -27,6 +27,7 @@ from io import BytesIO
|
||||||
|
|
||||||
from . import util
|
from . import util
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
|
from . import constants
|
||||||
from .bitcoin import *
|
from .bitcoin import *
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
@ -84,7 +85,10 @@ def hash_header(header):
|
||||||
return '0' * 64
|
return '0' * 64
|
||||||
if header.get('prev_block_hash') is None:
|
if header.get('prev_block_hash') is None:
|
||||||
header['prev_block_hash'] = '00'*64
|
header['prev_block_hash'] = '00'*64
|
||||||
return hash_to_str(Hash(serialize_header(header)))
|
'''
|
||||||
|
TODO 32?
|
||||||
|
'''
|
||||||
|
return hash_encode(Hash(bfh(serialize_header(header))))
|
||||||
|
|
||||||
|
|
||||||
blockchains = {}
|
blockchains = {}
|
||||||
|
@ -131,7 +135,7 @@ class Blockchain(util.PrintError):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.catch_up = None # interface catching up
|
self.catch_up = None # interface catching up
|
||||||
self.checkpoint = checkpoint
|
self.checkpoint = checkpoint
|
||||||
self.checkpoints = NetworkConstants.CHECKPOINTS
|
self.checkpoints = constants.net.CHECKPOINTS
|
||||||
self.parent_id = parent_id
|
self.parent_id = parent_id
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -175,34 +179,34 @@ class Blockchain(util.PrintError):
|
||||||
|
|
||||||
def update_size(self):
|
def update_size(self):
|
||||||
p = self.path()
|
p = self.path()
|
||||||
self._size = os.path.getsize(p)//bitcoin.HEADER_SIZE if os.path.exists(p) else 0
|
self._size = os.path.getsize(p)//80 if os.path.exists(p) else 0
|
||||||
|
|
||||||
def verify_header(self, header, prev_header):
|
def verify_header(self, header, prev_hash, target):
|
||||||
if prev_header:
|
_hash = hash_header(header)
|
||||||
prev_hash = hash_header(prev_header)
|
if prev_hash != header.get('prev_block_hash'):
|
||||||
if prev_hash != header.get('prev_block_hash'):
|
raise BaseException("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
|
||||||
raise BaseException("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
|
if constants.net.TESTNET:
|
||||||
_powhash = sha256_header(header)
|
return
|
||||||
target = self.bits_to_target(header['bits'])
|
bits = self.target_to_bits(target)
|
||||||
if _powhash > target:
|
if bits != header.get('bits'):
|
||||||
raise BaseException("insufficient proof of work: %s vs target %s" % (int('0x' + _powhash, 16), target))
|
raise BaseException("bits mismatch: %s vs %s" % (bits, header.get('bits')))
|
||||||
|
if int('0x' + _hash, 16) > target:
|
||||||
|
raise BaseException("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target))
|
||||||
nonce = uint256_from_bytes(str_to_hash(header.get('nonce')))
|
nonce = uint256_from_bytes(str_to_hash(header.get('nonce')))
|
||||||
n_solution = vector_from_bytes(base64.b64decode(header.get('n_solution').encode('utf8')))
|
n_solution = vector_from_bytes(base64.b64decode(header.get('n_solution').encode('utf8')))
|
||||||
if not is_gbp_valid(serialize_header(header), nonce, n_solution,
|
if not is_gbp_valid(serialize_header(header), nonce, n_solution,
|
||||||
NetworkConstants.EQUIHASH_N, NetworkConstants.EQUIHASH_K):
|
constants.net.EQUIHASH_N, constants.net.EQUIHASH_K):
|
||||||
raise BaseException("Equihash invalid")
|
raise BaseException("Equihash invalid")
|
||||||
|
|
||||||
def verify_chunk(self, index, data):
|
def verify_chunk(self, index, data):
|
||||||
num = len(data) // bitcoin.HEADER_SIZE
|
num = len(data) // bitcoin.HEADER_SIZE
|
||||||
prev_header = None
|
prev_hash = self.get_hash(index * constants.net.CHUNK_SIZE - 1)
|
||||||
if index != 0:
|
target = self.get_target(index-1)
|
||||||
prev_header = self.read_header(index * NetworkConstants.CHUNK_SIZE - 1)
|
|
||||||
|
|
||||||
for i in range(num):
|
for i in range(num):
|
||||||
raw_header = data[i*bitcoin.HEADER_SIZE:(i+1) * bitcoin.HEADER_SIZE]
|
raw_header = data[i*bitcoin.HEADER_SIZE:(i+1) * bitcoin.HEADER_SIZE]
|
||||||
header = deserialize_header(raw_header, index*NetworkConstants.CHUNK_SIZE + i)
|
header = deserialize_header(raw_header, index*constants.net.CHUNK_SIZE + i)
|
||||||
self.verify_header(header, prev_header)
|
self.verify_header(header, prev_hash, target)
|
||||||
prev_header = header
|
prev_hash = hash_header(header)
|
||||||
|
|
||||||
def path(self):
|
def path(self):
|
||||||
d = util.get_headers_dir(self.config)
|
d = util.get_headers_dir(self.config)
|
||||||
|
@ -211,11 +215,12 @@ class Blockchain(util.PrintError):
|
||||||
|
|
||||||
def save_chunk(self, index, chunk):
|
def save_chunk(self, index, chunk):
|
||||||
filename = self.path()
|
filename = self.path()
|
||||||
d = (index * NetworkConstants.CHUNK_SIZE - self.checkpoint) * bitcoin.HEADER_SIZE
|
d = (index * constants.net.CHUNK_SIZE - self.checkpoint) * bitcoin.HEADER_SIZE
|
||||||
if d < 0:
|
if d < 0:
|
||||||
chunk = chunk[-d:]
|
chunk = chunk[-d:]
|
||||||
d = 0
|
d = 0
|
||||||
self.write(chunk, d)
|
truncate = index >= len(self.checkpoints)
|
||||||
|
self.write(chunk, d, truncate)
|
||||||
self.swap_with_parent()
|
self.swap_with_parent()
|
||||||
|
|
||||||
def swap_with_parent(self):
|
def swap_with_parent(self):
|
||||||
|
@ -253,11 +258,11 @@ class Blockchain(util.PrintError):
|
||||||
blockchains[self.checkpoint] = self
|
blockchains[self.checkpoint] = self
|
||||||
blockchains[parent.checkpoint] = parent
|
blockchains[parent.checkpoint] = parent
|
||||||
|
|
||||||
def write(self, data, offset):
|
def write(self, data, offset, truncate=True):
|
||||||
filename = self.path()
|
filename = self.path()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
with open(filename, 'rb+') as f:
|
with open(filename, 'rb+') as f:
|
||||||
if offset != self._size*bitcoin.HEADER_SIZE:
|
if truncate and offset != self._size*bitcoin.HEADER_SIZE:
|
||||||
f.seek(offset)
|
f.seek(offset)
|
||||||
f.truncate()
|
f.truncate()
|
||||||
f.seek(offset)
|
f.seek(offset)
|
||||||
|
@ -268,7 +273,7 @@ class Blockchain(util.PrintError):
|
||||||
|
|
||||||
def save_header(self, header):
|
def save_header(self, header):
|
||||||
delta = header.get('block_height') - self.checkpoint
|
delta = header.get('block_height') - self.checkpoint
|
||||||
data = serialize_header(header)
|
data = bfh(serialize_header(header))
|
||||||
assert delta == self.size()
|
assert delta == self.size()
|
||||||
assert len(data) == bitcoin.HEADER_SIZE
|
assert len(data) == bitcoin.HEADER_SIZE
|
||||||
self.write(data, delta*bitcoin.HEADER_SIZE)
|
self.write(data, delta*bitcoin.HEADER_SIZE)
|
||||||
|
@ -289,25 +294,52 @@ class Blockchain(util.PrintError):
|
||||||
with open(name, 'rb') as f:
|
with open(name, 'rb') as f:
|
||||||
f.seek(delta * bitcoin.HEADER_SIZE)
|
f.seek(delta * bitcoin.HEADER_SIZE)
|
||||||
h = f.read(bitcoin.HEADER_SIZE)
|
h = f.read(bitcoin.HEADER_SIZE)
|
||||||
|
if h == bytes([0])*bitcoin.HEADER_SIZE:
|
||||||
|
return None
|
||||||
return deserialize_header(h, height)
|
return deserialize_header(h, height)
|
||||||
|
|
||||||
def get_hash(self, height):
|
def get_hash(self, height):
|
||||||
return self.hash_header(self.read_header(height))
|
if height == -1:
|
||||||
|
return '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
elif height == 0:
|
||||||
|
return constants.net.GENESIS
|
||||||
|
elif height < len(self.checkpoints) * constants.net.CHUNK_SIZE:
|
||||||
|
assert (height+1) % constants.net.CHUNK_SIZE == 0, height
|
||||||
|
index = height // constants.net.CHUNK_SIZE
|
||||||
|
h, t = self.checkpoints[index]
|
||||||
|
return h
|
||||||
|
else:
|
||||||
|
return hash_header(self.read_header(height))
|
||||||
|
|
||||||
def hash_header(self, header):
|
def get_target(self, index):
|
||||||
return hash_header(header)
|
# compute target from chunk x, used in chunk x+1
|
||||||
|
if constants.net.TESTNET:
|
||||||
|
return 0
|
||||||
|
if index == -1:
|
||||||
|
return MAX_TARGET
|
||||||
|
if index < len(self.checkpoints):
|
||||||
|
h, t = self.checkpoints[index]
|
||||||
|
return t
|
||||||
|
# new target
|
||||||
|
first = self.read_header(index * constants.net.CHUNK_SIZE)
|
||||||
|
last = self.read_header(index * constants.net.CHUNK_SIZE + (constants.net.CHUNK_SIZE - 1))
|
||||||
|
bits = last.get('bits')
|
||||||
|
target = self.bits_to_target(bits)
|
||||||
|
nActualTimespan = last.get('timestamp') - first.get('timestamp')
|
||||||
|
nTargetTimespan = 14 * 24 * 60 * 60
|
||||||
|
nActualTimespan = max(nActualTimespan, nTargetTimespan // 4)
|
||||||
|
nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)
|
||||||
|
new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan)
|
||||||
|
return new_target
|
||||||
|
|
||||||
def bits_to_target(self, bits):
|
def bits_to_target(self, bits):
|
||||||
bitsN = (bits >> 24) & 0xff
|
bitsN = (bits >> 24) & 0xff
|
||||||
# if not (bitsN >= 0x03 and bitsN <= 0x1d):
|
if not (bitsN >= 0x03 and bitsN <= 0x1d):
|
||||||
# raise BaseException("First part of bits should be in [0x03, 0x1d]")
|
raise BaseException("First part of bits should be in [0x03, 0x1d]")
|
||||||
bitsBase = bits & 0xffffff
|
bitsBase = bits & 0xffffff
|
||||||
# if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff):
|
if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff):
|
||||||
# raise BaseException("Second part of bits should be in [0x8000, 0x7fffff]")
|
raise BaseException("Second part of bits should be in [0x8000, 0x7fffff]")
|
||||||
if bitsN <= 3:
|
return bitsBase << (8 * (bitsN-3))
|
||||||
return bitsBase >> (8 * (3 - bitsN))
|
|
||||||
else:
|
|
||||||
return bitsBase << (8 * (bitsN - 3))
|
|
||||||
|
|
||||||
def target_to_bits(self, target):
|
def target_to_bits(self, target):
|
||||||
c = ("%064x" % target)[2:]
|
c = ("%064x" % target)[2:]
|
||||||
|
@ -320,38 +352,42 @@ class Blockchain(util.PrintError):
|
||||||
return bitsN << 24 | bitsBase
|
return bitsN << 24 | bitsBase
|
||||||
|
|
||||||
def can_connect(self, header, check_height=True):
|
def can_connect(self, header, check_height=True):
|
||||||
# import pdb; pdb.set_trace()
|
|
||||||
height = header['block_height']
|
height = header['block_height']
|
||||||
if check_height and self.height() != height - 1:
|
if check_height and self.height() != height - 1:
|
||||||
self.print_error("cannot connect at height", height)
|
self.print_error("cannot connect at height", height)
|
||||||
return False
|
return False
|
||||||
if height == 0:
|
if height == 0:
|
||||||
return hash_header(header) == NetworkConstants.GENESIS
|
return hash_header(header) == constants.net.GENESIS
|
||||||
try:
|
try:
|
||||||
prev_header = self.read_header(height - 1)
|
prev_hash = self.get_hash(height - 1)
|
||||||
prev_hash = self.hash_header(prev_header)
|
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
if prev_hash != header.get('prev_block_hash'):
|
if prev_hash != header.get('prev_block_hash'):
|
||||||
return False
|
return False
|
||||||
|
target = self.get_target(height // constants.net.CHUNK_SIZE - 1)
|
||||||
try:
|
try:
|
||||||
self.verify_header(header, prev_header)
|
self.verify_header(header, prev_hash, target)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
self.print_error('verify_header failed', str(e))
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def connect_chunk(self, idx, hexdata):
|
def connect_chunk(self, idx, hexdata):
|
||||||
try:
|
try:
|
||||||
data = bytes.fromhex(hexdata)
|
data = bfh(hexdata)
|
||||||
self.verify_chunk(idx, data)
|
self.verify_chunk(idx, data)
|
||||||
self.print_error("validated chunk %d" % idx)
|
#self.print_error("validated chunk %d" % idx)
|
||||||
self.save_chunk(idx, data)
|
self.save_chunk(idx, data)
|
||||||
return True
|
return True
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
import traceback
|
self.print_error('verify_chunk %d failed'%idx, str(e))
|
||||||
traceback.print_exc()
|
|
||||||
self.print_error('verify_chunk failed', str(e))
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_checkpoints(self):
|
||||||
|
# for each chunk, store the hash of the last block and the target after the chunk
|
||||||
|
cp = []
|
||||||
|
n = self.height() // 2016
|
||||||
|
for index in range(n):
|
||||||
|
h = self.get_hash((index+1) * 2016 -1)
|
||||||
|
target = self.get_target(index)
|
||||||
|
cp.append((h, target))
|
||||||
|
return cp
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from math import floor, log10
|
from math import floor, log10
|
||||||
|
|
||||||
from .bitcoin import sha256, COIN, TYPE_ADDRESS
|
from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
|
||||||
from .transaction import Transaction
|
from .transaction import Transaction
|
||||||
from .util import NotEnoughFunds, PrintError
|
from .util import NotEnoughFunds, PrintError
|
||||||
|
|
||||||
|
@ -87,6 +87,8 @@ def strip_unneeded(bkts, sufficient_funds):
|
||||||
|
|
||||||
class CoinChooserBase(PrintError):
|
class CoinChooserBase(PrintError):
|
||||||
|
|
||||||
|
enable_output_value_rounding = False
|
||||||
|
|
||||||
def keys(self, coins):
|
def keys(self, coins):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -135,7 +137,13 @@ class CoinChooserBase(PrintError):
|
||||||
zeroes = [trailing_zeroes(i) for i in output_amounts]
|
zeroes = [trailing_zeroes(i) for i in output_amounts]
|
||||||
min_zeroes = min(zeroes)
|
min_zeroes = min(zeroes)
|
||||||
max_zeroes = max(zeroes)
|
max_zeroes = max(zeroes)
|
||||||
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
|
|
||||||
|
if n > 1:
|
||||||
|
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
|
||||||
|
else:
|
||||||
|
# if there is only one change output, this will ensure that we aim
|
||||||
|
# to have one that is exactly as precise as the most precise output
|
||||||
|
zeroes = [min_zeroes]
|
||||||
|
|
||||||
# Calculate change; randomize it a bit if using more than 1 output
|
# Calculate change; randomize it a bit if using more than 1 output
|
||||||
remaining = change_amount
|
remaining = change_amount
|
||||||
|
@ -150,8 +158,10 @@ class CoinChooserBase(PrintError):
|
||||||
n -= 1
|
n -= 1
|
||||||
|
|
||||||
# Last change output. Round down to maximum precision but lose
|
# Last change output. Round down to maximum precision but lose
|
||||||
# no more than 100 satoshis to fees (2dp)
|
# no more than 10**max_dp_to_round_for_privacy
|
||||||
N = pow(10, min(2, zeroes[0]))
|
# e.g. a max of 2 decimal places means losing 100 satoshis to fees
|
||||||
|
max_dp_to_round_for_privacy = 2 if self.enable_output_value_rounding else 0
|
||||||
|
N = pow(10, min(max_dp_to_round_for_privacy, zeroes[0]))
|
||||||
amount = (remaining // N) * N
|
amount = (remaining // N) * N
|
||||||
amounts.append(amount)
|
amounts.append(amount)
|
||||||
|
|
||||||
|
@ -230,6 +240,13 @@ class CoinChooserBase(PrintError):
|
||||||
tx.add_inputs([coin for b in buckets for coin in b.coins])
|
tx.add_inputs([coin for b in buckets for coin in b.coins])
|
||||||
tx_weight = get_tx_weight(buckets)
|
tx_weight = get_tx_weight(buckets)
|
||||||
|
|
||||||
|
# change is sent back to sending address unless specified
|
||||||
|
if not change_addrs:
|
||||||
|
change_addrs = [tx.inputs()[0]['address']]
|
||||||
|
# note: this is not necessarily the final "first input address"
|
||||||
|
# because the inputs had not been sorted at this point
|
||||||
|
assert is_address(change_addrs[0])
|
||||||
|
|
||||||
# This takes a count of change outputs and returns a tx fee
|
# This takes a count of change outputs and returns a tx fee
|
||||||
output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
|
output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
|
||||||
fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
|
fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
|
||||||
|
@ -273,6 +290,8 @@ class CoinChooserRandom(CoinChooserBase):
|
||||||
candidates.add(tuple(sorted(permutation[:count + 1])))
|
candidates.add(tuple(sorted(permutation[:count + 1])))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
# FIXME this assumes that the effective value of any bkt is >= 0
|
||||||
|
# we should make sure not to choose buckets with <= 0 eff. val.
|
||||||
raise NotEnoughFunds()
|
raise NotEnoughFunds()
|
||||||
|
|
||||||
candidates = [[buckets[n] for n in c] for c in candidates]
|
candidates = [[buckets[n] for n in c] for c in candidates]
|
||||||
|
@ -284,7 +303,7 @@ class CoinChooserRandom(CoinChooserBase):
|
||||||
Any bucket can be:
|
Any bucket can be:
|
||||||
1. "confirmed" if it only contains confirmed coins; else
|
1. "confirmed" if it only contains confirmed coins; else
|
||||||
2. "unconfirmed" if it does not contain coins with unconfirmed parents
|
2. "unconfirmed" if it does not contain coins with unconfirmed parents
|
||||||
3. "unconfirmed parent" otherwise
|
3. other: e.g. "unconfirmed parent" or "local"
|
||||||
|
|
||||||
This method tries to only use buckets of type 1, and if the coins there
|
This method tries to only use buckets of type 1, and if the coins there
|
||||||
are not enough, tries to use the next type but while also selecting
|
are not enough, tries to use the next type but while also selecting
|
||||||
|
@ -292,9 +311,9 @@ class CoinChooserRandom(CoinChooserBase):
|
||||||
"""
|
"""
|
||||||
conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0]
|
conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0]
|
||||||
unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0]
|
unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0]
|
||||||
unconf_par_buckets = [bkt for bkt in buckets if bkt.min_height == -1]
|
other_buckets = [bkt for bkt in buckets if bkt.min_height < 0]
|
||||||
|
|
||||||
bucket_sets = [conf_buckets, unconf_buckets, unconf_par_buckets]
|
bucket_sets = [conf_buckets, unconf_buckets, other_buckets]
|
||||||
already_selected_buckets = []
|
already_selected_buckets = []
|
||||||
|
|
||||||
for bkts_choose_from in bucket_sets:
|
for bkts_choose_from in bucket_sets:
|
||||||
|
@ -368,4 +387,6 @@ def get_name(config):
|
||||||
|
|
||||||
def get_coin_chooser(config):
|
def get_coin_chooser(config):
|
||||||
klass = COIN_CHOOSERS[get_name(config)]
|
klass = COIN_CHOOSERS[get_name(config)]
|
||||||
return klass()
|
coinchooser = klass()
|
||||||
|
coinchooser.enable_output_value_rounding = config.get('coin_chooser_output_rounding', False)
|
||||||
|
return coinchooser
|
||||||
|
|
110
lib/commands.py
110
lib/commands.py
|
@ -34,7 +34,7 @@ from functools import wraps
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from .import util
|
from .import util
|
||||||
from .util import bfh, bh2u, format_satoshis
|
from .util import bfh, bh2u, format_satoshis, json_decode, print_error
|
||||||
from .import bitcoin
|
from .import bitcoin
|
||||||
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
|
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
@ -82,7 +82,7 @@ def command(s):
|
||||||
password = kwargs.get('password')
|
password = kwargs.get('password')
|
||||||
if c.requires_wallet and wallet is None:
|
if c.requires_wallet and wallet is None:
|
||||||
raise BaseException("wallet not loaded. Use 'electrum daemon load_wallet'")
|
raise BaseException("wallet not loaded. Use 'electrum daemon load_wallet'")
|
||||||
if c.requires_password and password is None and wallet.storage.get('use_encryption'):
|
if c.requires_password and password is None and wallet.has_password():
|
||||||
return {'error': 'Password required' }
|
return {'error': 'Password required' }
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return func_wrapper
|
return func_wrapper
|
||||||
|
@ -138,6 +138,8 @@ class Commands:
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def password(self, password=None, new_password=None):
|
def password(self, password=None, new_password=None):
|
||||||
"""Change wallet password. """
|
"""Change wallet password. """
|
||||||
|
if self.wallet.storage.is_encrypted_with_hw_device() and new_password:
|
||||||
|
raise Exception("Can't change the password of a wallet encrypted with a hw device.")
|
||||||
b = self.wallet.storage.is_encrypted()
|
b = self.wallet.storage.is_encrypted()
|
||||||
self.wallet.update_password(password, new_password, b)
|
self.wallet.update_password(password, new_password, b)
|
||||||
self.wallet.storage.write()
|
self.wallet.storage.write()
|
||||||
|
@ -151,10 +153,8 @@ class Commands:
|
||||||
@command('')
|
@command('')
|
||||||
def setconfig(self, key, value):
|
def setconfig(self, key, value):
|
||||||
"""Set a configuration variable. 'value' may be a string or a Python expression."""
|
"""Set a configuration variable. 'value' may be a string or a Python expression."""
|
||||||
try:
|
if key not in ('rpcuser', 'rpcpassword'):
|
||||||
value = ast.literal_eval(value)
|
value = json_decode(value)
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.config.set_key(key, value)
|
self.config.set_key(key, value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -177,7 +177,8 @@ class Commands:
|
||||||
"""Return the transaction history of any address. Note: This is a
|
"""Return the transaction history of any address. Note: This is a
|
||||||
walletless server query, results are not checked by SPV.
|
walletless server query, results are not checked by SPV.
|
||||||
"""
|
"""
|
||||||
return self.network.synchronous_get(('blockchain.address.get_history', [address]))
|
sh = bitcoin.address_to_scripthash(address)
|
||||||
|
return self.network.synchronous_get(('blockchain.scripthash.get_history', [sh]))
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def listunspent(self):
|
def listunspent(self):
|
||||||
|
@ -194,7 +195,8 @@ class Commands:
|
||||||
"""Returns the UTXO list of any address. Note: This
|
"""Returns the UTXO list of any address. Note: This
|
||||||
is a walletless server query, results are not checked by SPV.
|
is a walletless server query, results are not checked by SPV.
|
||||||
"""
|
"""
|
||||||
return self.network.synchronous_get(('blockchain.address.listunspent', [address]))
|
sh = bitcoin.address_to_scripthash(address)
|
||||||
|
return self.network.synchronous_get(('blockchain.scripthash.listunspent', [sh]))
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
def serialize(self, jsontx):
|
def serialize(self, jsontx):
|
||||||
|
@ -316,20 +318,12 @@ class Commands:
|
||||||
"""Return the balance of any address. Note: This is a walletless
|
"""Return the balance of any address. Note: This is a walletless
|
||||||
server query, results are not checked by SPV.
|
server query, results are not checked by SPV.
|
||||||
"""
|
"""
|
||||||
out = self.network.synchronous_get(('blockchain.address.get_balance', [address]))
|
sh = bitcoin.address_to_scripthash(address)
|
||||||
|
out = self.network.synchronous_get(('blockchain.scripthash.get_balance', [sh]))
|
||||||
out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
|
out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
|
||||||
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
|
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@command('n')
|
|
||||||
def getproof(self, address):
|
|
||||||
"""Get Merkle branch of an address in the UTXO set"""
|
|
||||||
p = self.network.synchronous_get(('blockchain.address.get_proof', [address]))
|
|
||||||
out = []
|
|
||||||
for i,s in p:
|
|
||||||
out.append(i)
|
|
||||||
return out
|
|
||||||
|
|
||||||
@command('n')
|
@command('n')
|
||||||
def getmerkle(self, txid, height):
|
def getmerkle(self, txid, height):
|
||||||
"""Get Merkle branch of a transaction included in a block. Electrum
|
"""Get Merkle branch of a transaction included in a block. Electrum
|
||||||
|
@ -448,46 +442,20 @@ class Commands:
|
||||||
return tx.as_dict()
|
return tx.as_dict()
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def history(self):
|
def history(self, year=None, show_addresses=False, show_fiat=False):
|
||||||
"""Wallet history. Returns the transaction history of your wallet."""
|
"""Wallet history. Returns the transaction history of your wallet."""
|
||||||
balance = 0
|
kwargs = {'show_addresses': show_addresses}
|
||||||
out = []
|
if year:
|
||||||
for item in self.wallet.get_history():
|
import time
|
||||||
tx_hash, height, conf, timestamp, value, balance = item
|
start_date = datetime.datetime(year, 1, 1)
|
||||||
if timestamp:
|
end_date = datetime.datetime(year+1, 1, 1)
|
||||||
date = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
|
||||||
else:
|
kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
|
||||||
date = "----"
|
if show_fiat:
|
||||||
label = self.wallet.get_label(tx_hash)
|
from .exchange_rate import FxThread
|
||||||
tx = self.wallet.transactions.get(tx_hash)
|
fx = FxThread(self.config, None)
|
||||||
tx.deserialize()
|
kwargs['fx'] = fx
|
||||||
input_addresses = []
|
return self.wallet.get_full_history(**kwargs)
|
||||||
output_addresses = []
|
|
||||||
for x in tx.inputs():
|
|
||||||
if x['type'] == 'coinbase': continue
|
|
||||||
addr = x.get('address')
|
|
||||||
if addr == None: continue
|
|
||||||
if addr == "(pubkey)":
|
|
||||||
prevout_hash = x.get('prevout_hash')
|
|
||||||
prevout_n = x.get('prevout_n')
|
|
||||||
_addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n)
|
|
||||||
if _addr:
|
|
||||||
addr = _addr
|
|
||||||
input_addresses.append(addr)
|
|
||||||
for addr, v in tx.get_outputs():
|
|
||||||
output_addresses.append(addr)
|
|
||||||
out.append({
|
|
||||||
'txid': tx_hash,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'date': date,
|
|
||||||
'input_addresses': input_addresses,
|
|
||||||
'output_addresses': output_addresses,
|
|
||||||
'label': label,
|
|
||||||
'value': str(Decimal(value)/COIN) if value is not None else None,
|
|
||||||
'height': height,
|
|
||||||
'confirmations': conf
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def setlabel(self, key, label):
|
def setlabel(self, key, label):
|
||||||
|
@ -631,6 +599,15 @@ class Commands:
|
||||||
out = self.wallet.get_payment_request(addr, self.config)
|
out = self.wallet.get_payment_request(addr, self.config)
|
||||||
return self._format_request(out)
|
return self._format_request(out)
|
||||||
|
|
||||||
|
@command('w')
|
||||||
|
def addtransaction(self, tx):
|
||||||
|
""" Add a transaction to the wallet history """
|
||||||
|
tx = Transaction(tx)
|
||||||
|
if not self.wallet.add_transaction(tx.txid(), tx):
|
||||||
|
return False
|
||||||
|
self.wallet.save_transactions()
|
||||||
|
return tx.txid()
|
||||||
|
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def signrequest(self, address, password=None):
|
def signrequest(self, address, password=None):
|
||||||
"Sign payment request with an OpenAlias"
|
"Sign payment request with an OpenAlias"
|
||||||
|
@ -658,13 +635,15 @@ class Commands:
|
||||||
import urllib.request
|
import urllib.request
|
||||||
headers = {'content-type':'application/json'}
|
headers = {'content-type':'application/json'}
|
||||||
data = {'address':address, 'status':x.get('result')}
|
data = {'address':address, 'status':x.get('result')}
|
||||||
|
serialized_data = util.to_bytes(json.dumps(data))
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(URL, json.dumps(data), headers)
|
req = urllib.request.Request(URL, serialized_data, headers)
|
||||||
response_stream = urllib.request.urlopen(req, timeout=5)
|
response_stream = urllib.request.urlopen(req, timeout=5)
|
||||||
util.print_error('Got Response for %s' % address)
|
util.print_error('Got Response for %s' % address)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
util.print_error(str(e))
|
util.print_error(str(e))
|
||||||
self.network.send([('blockchain.address.subscribe', [address])], callback)
|
h = self.network.addr_to_scripthash(address)
|
||||||
|
self.network.send([('blockchain.scripthash.subscribe', [h])], callback)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@command('wn')
|
@command('wn')
|
||||||
|
@ -672,6 +651,12 @@ class Commands:
|
||||||
""" return wallet synchronization status """
|
""" return wallet synchronization status """
|
||||||
return self.wallet.is_up_to_date()
|
return self.wallet.is_up_to_date()
|
||||||
|
|
||||||
|
@command('n')
|
||||||
|
def getfeerate(self):
|
||||||
|
"""Return current optimal fee rate per kilobyte, according
|
||||||
|
to config settings (static/dynamic)"""
|
||||||
|
return self.config.fee_per_kb()
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
def help(self):
|
def help(self):
|
||||||
# for the python console
|
# for the python console
|
||||||
|
@ -727,6 +712,9 @@ command_options = {
|
||||||
'pending': (None, "Show only pending requests."),
|
'pending': (None, "Show only pending requests."),
|
||||||
'expired': (None, "Show only expired requests."),
|
'expired': (None, "Show only expired requests."),
|
||||||
'paid': (None, "Show only paid requests."),
|
'paid': (None, "Show only paid requests."),
|
||||||
|
'show_addresses': (None, "Show input and output addresses"),
|
||||||
|
'show_fiat': (None, "Show fiat value of transactions"),
|
||||||
|
'year': (None, "Show history for a given year"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -737,6 +725,7 @@ arg_types = {
|
||||||
'num': int,
|
'num': int,
|
||||||
'nbits': int,
|
'nbits': int,
|
||||||
'imax': int,
|
'imax': int,
|
||||||
|
'year': int,
|
||||||
'entropy': int,
|
'entropy': int,
|
||||||
'tx': tx_from_str,
|
'tx': tx_from_str,
|
||||||
'pubkeys': json_loads,
|
'pubkeys': json_loads,
|
||||||
|
@ -800,7 +789,7 @@ def subparser_call(self, parser, namespace, values, option_string=None):
|
||||||
parser = self._name_parser_map[parser_name]
|
parser = self._name_parser_map[parser_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
tup = parser_name, ', '.join(self._name_parser_map)
|
tup = parser_name, ', '.join(self._name_parser_map)
|
||||||
msg = _('unknown parser %r (choices: %s)') % tup
|
msg = _('unknown parser {!r} (choices: {})').format(*tup)
|
||||||
raise ArgumentError(self, msg)
|
raise ArgumentError(self, msg)
|
||||||
# parse all the remaining options into the namespace
|
# parse all the remaining options into the namespace
|
||||||
# store any unrecognized options on the object, so that the top
|
# store any unrecognized options on the object, so that the top
|
||||||
|
@ -825,7 +814,6 @@ def add_global_options(parser):
|
||||||
group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
|
group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
|
||||||
group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
|
group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
|
||||||
group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
|
group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
|
||||||
group.add_argument("--nossl", action="store_true", dest="nossl", default=False, help="Disable SSL")
|
|
||||||
|
|
||||||
def get_parser():
|
def get_parser():
|
||||||
# create main parser
|
# create main parser
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Electrum - lightweight Bitcoin client
|
||||||
|
# Copyright (C) 2018 The Electrum developers
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation files
|
||||||
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(filename, default):
|
||||||
|
path = os.path.join(os.path.dirname(__file__), filename)
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
r = json.loads(f.read())
|
||||||
|
except:
|
||||||
|
r = default
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class BitcoinMainnet:
|
||||||
|
|
||||||
|
TESTNET = False
|
||||||
|
WIF_PREFIX = 0x80
|
||||||
|
ADDRTYPE_P2PKH = [0x1C, 0xB8]
|
||||||
|
ADDRTYPE_P2SH = [0x1C, 0xBD]
|
||||||
|
ADDRTYPE_SHIELDED = [0x16, 0x9A]
|
||||||
|
SEGWIT_HRP = "bc" # (No ZCL Segwit)
|
||||||
|
GENESIS = "0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602"
|
||||||
|
DEFAULT_PORTS = {'t': '50001', 's': '50002'}
|
||||||
|
DEFAULT_SERVERS = read_json('servers.json', {})
|
||||||
|
CHECKPOINTS = read_json('checkpoints.json', [])
|
||||||
|
|
||||||
|
EQUIHASH_N = 200
|
||||||
|
EQUIHASH_K = 9
|
||||||
|
|
||||||
|
CHUNK_SIZE = 200
|
||||||
|
|
||||||
|
HEADERS_URL = "http://headers.zcl-electrum.com/blockchain_headers"
|
||||||
|
|
||||||
|
XPRV_HEADERS = {
|
||||||
|
'standard': 0x0488ade4, # xprv
|
||||||
|
'p2wpkh-p2sh': 0x049d7878, # yprv
|
||||||
|
'p2wsh-p2sh': 0x0295b005, # Yprv
|
||||||
|
'p2wpkh': 0x04b2430c, # zprv
|
||||||
|
'p2wsh': 0x02aa7a99, # Zprv
|
||||||
|
}
|
||||||
|
XPUB_HEADERS = {
|
||||||
|
'standard': 0x0488b21e, # xpub
|
||||||
|
'p2wpkh-p2sh': 0x049d7cb2, # ypub
|
||||||
|
'p2wsh-p2sh': 0x0295b43f, # Ypub
|
||||||
|
'p2wpkh': 0x04b24746, # zpub
|
||||||
|
'p2wsh': 0x02aa7ed3, # Zpub
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BitcoinTestnet:
|
||||||
|
|
||||||
|
TESTNET = True
|
||||||
|
WIF_PREFIX = 0xef
|
||||||
|
ADDRTYPE_P2PKH = [0x1D, 0x25]
|
||||||
|
ADDRTYPE_P2SH = [0x1C, 0xBA]
|
||||||
|
ADDTYPE_SHIELDED = [0x16, 0xB6]
|
||||||
|
SEGWIT_HRP = "tb" # (ZCL has no Segwit)
|
||||||
|
GENESIS = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
|
||||||
|
DEFAULT_PORTS = {'t': '51001', 's': '51002'}
|
||||||
|
DEFAULT_SERVERS = read_json('servers_testnet.json', {})
|
||||||
|
CHECKPOINTS = read_json('checkpoints_testnet.json', [])
|
||||||
|
|
||||||
|
EQUIHASH_N = 200
|
||||||
|
EQUIHASH_K = 9
|
||||||
|
|
||||||
|
CHUNK_SIZE = 200
|
||||||
|
|
||||||
|
XPRV_HEADERS = {
|
||||||
|
'standard': 0x04358394, # tprv
|
||||||
|
'p2wpkh-p2sh': 0x044a4e28, # uprv
|
||||||
|
'p2wsh-p2sh': 0x024285b5, # Uprv
|
||||||
|
'p2wpkh': 0x045f18bc, # vprv
|
||||||
|
'p2wsh': 0x02575048, # Vprv
|
||||||
|
}
|
||||||
|
XPUB_HEADERS = {
|
||||||
|
'standard': 0x043587cf, # tpub
|
||||||
|
'p2wpkh-p2sh': 0x044a5262, # upub
|
||||||
|
'p2wsh-p2sh': 0x024285ef, # Upub
|
||||||
|
'p2wpkh': 0x045f1cf6, # vpub
|
||||||
|
'p2wsh': 0x02575483, # Vpub
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# don't import net directly, import the module instead (so that net is singleton)
|
||||||
|
net = BitcoinMainnet
|
||||||
|
|
||||||
|
|
||||||
|
def set_mainnet():
|
||||||
|
global net
|
||||||
|
net = BitcoinMainnet
|
||||||
|
|
||||||
|
|
||||||
|
def set_testnet():
|
||||||
|
global net
|
||||||
|
net = BitcoinTestnet
|
|
@ -23,9 +23,12 @@
|
||||||
import re
|
import re
|
||||||
import dns
|
import dns
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from . import dnssec
|
from . import dnssec
|
||||||
|
from .util import export_meta, import_meta
|
||||||
|
|
||||||
|
|
||||||
class Contacts(dict):
|
class Contacts(dict):
|
||||||
|
@ -48,14 +51,15 @@ class Contacts(dict):
|
||||||
self.storage.put('contacts', dict(self))
|
self.storage.put('contacts', dict(self))
|
||||||
|
|
||||||
def import_file(self, path):
|
def import_file(self, path):
|
||||||
try:
|
import_meta(path, self._validate, self.load_meta)
|
||||||
with open(path, 'r') as f:
|
|
||||||
d = self._validate(json.loads(f.read()))
|
def load_meta(self, data):
|
||||||
except:
|
self.update(data)
|
||||||
return
|
|
||||||
self.update(d)
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def export_file(self, filename):
|
||||||
|
export_meta(self, filename)
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
dict.__setitem__(self, key, value)
|
dict.__setitem__(self, key, value)
|
||||||
self.save()
|
self.save()
|
||||||
|
@ -113,13 +117,13 @@ class Contacts(dict):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _validate(self, data):
|
def _validate(self, data):
|
||||||
for k,v in list(data.items()):
|
for k, v in list(data.items()):
|
||||||
if k == 'contacts':
|
if k == 'contacts':
|
||||||
return self._validate(v)
|
return self._validate(v)
|
||||||
if not bitcoin.is_address(k):
|
if not bitcoin.is_address(k):
|
||||||
data.pop(k)
|
data.pop(k)
|
||||||
else:
|
else:
|
||||||
_type,_ = v
|
_type, _ = v
|
||||||
if _type != 'address':
|
if _type != 'address':
|
||||||
data.pop(k)
|
data.pop(k)
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"CoinMarketCap": [
|
"CoinMarketCap": [
|
||||||
"USD",
|
"USD",
|
||||||
"EUR",
|
"EUR",
|
||||||
"CNY"
|
"RUB",
|
||||||
|
"USD"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
import ast
|
import ast
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
# from jsonrpc import JSONRPCResponseManager
|
# from jsonrpc import JSONRPCResponseManager
|
||||||
import jsonrpclib
|
import jsonrpclib
|
||||||
|
@ -121,13 +123,12 @@ class Daemon(DaemonThread):
|
||||||
self.config = config
|
self.config = config
|
||||||
if config.get('offline'):
|
if config.get('offline'):
|
||||||
self.network = None
|
self.network = None
|
||||||
self.fx = None
|
|
||||||
else:
|
else:
|
||||||
self.network = Network(config)
|
self.network = Network(config)
|
||||||
self.network.start()
|
self.network.start()
|
||||||
self.fx = FxThread(config, self.network)
|
self.fx = FxThread(config, self.network)
|
||||||
|
if self.network:
|
||||||
self.network.add_jobs([self.fx])
|
self.network.add_jobs([self.fx])
|
||||||
|
|
||||||
self.gui = None
|
self.gui = None
|
||||||
self.wallets = {}
|
self.wallets = {}
|
||||||
# Setup JSONRPC server
|
# Setup JSONRPC server
|
||||||
|
@ -173,7 +174,7 @@ class Daemon(DaemonThread):
|
||||||
path = config.get_wallet_path()
|
path = config.get_wallet_path()
|
||||||
wallet = self.load_wallet(path, config.get('password'))
|
wallet = self.load_wallet(path, config.get('password'))
|
||||||
self.cmd_runner.wallet = wallet
|
self.cmd_runner.wallet = wallet
|
||||||
response = True
|
response = wallet is not None
|
||||||
elif sub == 'close_wallet':
|
elif sub == 'close_wallet':
|
||||||
path = config.get_wallet_path()
|
path = config.get_wallet_path()
|
||||||
if path in self.wallets:
|
if path in self.wallets:
|
||||||
|
@ -301,4 +302,8 @@ class Daemon(DaemonThread):
|
||||||
gui_name = 'qt'
|
gui_name = 'qt'
|
||||||
gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui'])
|
gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui'])
|
||||||
self.gui = gui.ElectrumGui(config, self, plugins)
|
self.gui = gui.ElectrumGui(config, self, plugins)
|
||||||
self.gui.main()
|
try:
|
||||||
|
self.gui.main()
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
# app will exit now
|
||||||
|
|
|
@ -2,9 +2,12 @@ from datetime import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import requests
|
import requests
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import time
|
import time
|
||||||
import csv
|
import csv
|
||||||
|
import decimal
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from .bitcoin import COIN
|
from .bitcoin import COIN
|
||||||
|
@ -13,15 +16,12 @@ from .util import PrintError, ThreadJob
|
||||||
|
|
||||||
|
|
||||||
# See https://en.wikipedia.org/wiki/ISO_4217
|
# See https://en.wikipedia.org/wiki/ISO_4217
|
||||||
CCY_PRECISIONS = {}
|
CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
|
||||||
'''
|
'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,
|
||||||
{'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
|
'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3,
|
||||||
'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,
|
'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0,
|
||||||
'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3,
|
'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
|
||||||
'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0,
|
'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
|
||||||
'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
|
|
||||||
'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
|
|
||||||
'''
|
|
||||||
|
|
||||||
class ExchangeBase(PrintError):
|
class ExchangeBase(PrintError):
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class ExchangeBase(PrintError):
|
||||||
def get_json(self, site, get_string):
|
def get_json(self, site, get_string):
|
||||||
# APIs must have https
|
# APIs must have https
|
||||||
url = ''.join(['https://', site, get_string])
|
url = ''.join(['https://', site, get_string])
|
||||||
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'})
|
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}, timeout=10)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def get_csv(self, site, get_string):
|
def get_csv(self, site, get_string):
|
||||||
|
@ -60,28 +60,54 @@ class ExchangeBase(PrintError):
|
||||||
t.setDaemon(True)
|
t.setDaemon(True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def get_historical_rates_safe(self, ccy):
|
def read_historical_rates(self, ccy, cache_dir):
|
||||||
|
filename = os.path.join(cache_dir, self.name() + '_'+ ccy)
|
||||||
|
if os.path.exists(filename):
|
||||||
|
timestamp = os.stat(filename).st_mtime
|
||||||
|
try:
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
h = json.loads(f.read())
|
||||||
|
h['timestamp'] = timestamp
|
||||||
|
except:
|
||||||
|
h = None
|
||||||
|
else:
|
||||||
|
h = None
|
||||||
|
if h:
|
||||||
|
self.history[ccy] = h
|
||||||
|
self.on_history()
|
||||||
|
return h
|
||||||
|
|
||||||
|
def get_historical_rates_safe(self, ccy, cache_dir):
|
||||||
try:
|
try:
|
||||||
self.print_error("requesting fx history for", ccy)
|
self.print_error("requesting fx history for", ccy)
|
||||||
self.history[ccy] = self.historical_rates(ccy)
|
h = self.request_history(ccy)
|
||||||
self.print_error("received fx history for", ccy)
|
self.print_error("received fx history for", ccy)
|
||||||
self.on_history()
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.print_error("failed fx history:", e)
|
self.print_error("failed fx history:", e)
|
||||||
|
return
|
||||||
|
filename = os.path.join(cache_dir, self.name() + '_' + ccy)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(json.dumps(h))
|
||||||
|
h['timestamp'] = time.time()
|
||||||
|
self.history[ccy] = h
|
||||||
|
self.on_history()
|
||||||
|
|
||||||
def get_historical_rates(self, ccy):
|
def get_historical_rates(self, ccy, cache_dir):
|
||||||
result = self.history.get(ccy)
|
if ccy not in self.history_ccys():
|
||||||
if not result and ccy in self.history_ccys():
|
return
|
||||||
t = Thread(target=self.get_historical_rates_safe, args=(ccy,))
|
h = self.history.get(ccy)
|
||||||
|
if h is None:
|
||||||
|
h = self.read_historical_rates(ccy, cache_dir)
|
||||||
|
if h is None or h['timestamp'] < time.time() - 24*3600:
|
||||||
|
t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir))
|
||||||
t.setDaemon(True)
|
t.setDaemon(True)
|
||||||
t.start()
|
t.start()
|
||||||
return result
|
|
||||||
|
|
||||||
def history_ccys(self):
|
def history_ccys(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def historical_rate(self, ccy, d_t):
|
def historical_rate(self, ccy, d_t):
|
||||||
return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'))
|
return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'), 'NaN')
|
||||||
|
|
||||||
def get_currencies(self):
|
def get_currencies(self):
|
||||||
rates = self.get_rates('')
|
rates = self.get_rates('')
|
||||||
|
@ -126,7 +152,9 @@ def get_exchanges_and_currencies():
|
||||||
exchange = klass(None, None)
|
exchange = klass(None, None)
|
||||||
try:
|
try:
|
||||||
d[name] = exchange.get_currencies()
|
d[name] = exchange.get_currencies()
|
||||||
|
print(name, "ok")
|
||||||
except:
|
except:
|
||||||
|
print(name, "error")
|
||||||
continue
|
continue
|
||||||
with open(path, 'w') as f:
|
with open(path, 'w') as f:
|
||||||
f.write(json.dumps(d, indent=4, sort_keys=True))
|
f.write(json.dumps(d, indent=4, sort_keys=True))
|
||||||
|
@ -157,7 +185,10 @@ class FxThread(ThreadJob):
|
||||||
self.history_used_spot = False
|
self.history_used_spot = False
|
||||||
self.ccy_combo = None
|
self.ccy_combo = None
|
||||||
self.hist_checkbox = None
|
self.hist_checkbox = None
|
||||||
|
self.cache_dir = os.path.join(config.path, 'cache')
|
||||||
self.set_exchange(self.config_exchange())
|
self.set_exchange(self.config_exchange())
|
||||||
|
if not os.path.exists(self.cache_dir):
|
||||||
|
os.mkdir(self.cache_dir)
|
||||||
|
|
||||||
def get_currencies(self, h):
|
def get_currencies(self, h):
|
||||||
d = get_exchanges_by_ccy(h)
|
d = get_exchanges_by_ccy(h)
|
||||||
|
@ -170,13 +201,17 @@ class FxThread(ThreadJob):
|
||||||
def ccy_amount_str(self, amount, commas):
|
def ccy_amount_str(self, amount, commas):
|
||||||
prec = CCY_PRECISIONS.get(self.ccy, 2)
|
prec = CCY_PRECISIONS.get(self.ccy, 2)
|
||||||
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
|
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
|
||||||
return fmt_str.format(round(amount, prec))
|
try:
|
||||||
|
rounded_amount = round(amount, prec)
|
||||||
|
except decimal.InvalidOperation:
|
||||||
|
rounded_amount = amount
|
||||||
|
return fmt_str.format(rounded_amount)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# This runs from the plugins thread which catches exceptions
|
# This runs from the plugins thread which catches exceptions
|
||||||
if self.is_enabled():
|
if self.is_enabled():
|
||||||
if self.timeout ==0 and self.show_history():
|
if self.timeout ==0 and self.show_history():
|
||||||
self.exchange.get_historical_rates(self.ccy)
|
self.exchange.get_historical_rates(self.ccy, self.cache_dir)
|
||||||
if self.timeout <= time.time():
|
if self.timeout <= time.time():
|
||||||
self.timeout = time.time() + 150
|
self.timeout = time.time() + 150
|
||||||
self.exchange.update(self.ccy)
|
self.exchange.update(self.ccy)
|
||||||
|
@ -193,6 +228,12 @@ class FxThread(ThreadJob):
|
||||||
def set_history_config(self, b):
|
def set_history_config(self, b):
|
||||||
self.config.set_key('history_rates', bool(b))
|
self.config.set_key('history_rates', bool(b))
|
||||||
|
|
||||||
|
def get_history_capital_gains_config(self):
|
||||||
|
return bool(self.config.get('history_rates_capital_gains', False))
|
||||||
|
|
||||||
|
def set_history_capital_gains_config(self, b):
|
||||||
|
self.config.set_key('history_rates_capital_gains', bool(b))
|
||||||
|
|
||||||
def get_fiat_address_config(self):
|
def get_fiat_address_config(self):
|
||||||
return bool(self.config.get('fiat_address'))
|
return bool(self.config.get('fiat_address'))
|
||||||
|
|
||||||
|
@ -224,45 +265,65 @@ class FxThread(ThreadJob):
|
||||||
# A new exchange means new fx quotes, initially empty. Force
|
# A new exchange means new fx quotes, initially empty. Force
|
||||||
# a quote refresh
|
# a quote refresh
|
||||||
self.timeout = 0
|
self.timeout = 0
|
||||||
|
self.exchange.read_historical_rates(self.ccy, self.cache_dir)
|
||||||
|
|
||||||
def on_quotes(self):
|
def on_quotes(self):
|
||||||
self.network.trigger_callback('on_quotes')
|
if self.network:
|
||||||
|
self.network.trigger_callback('on_quotes')
|
||||||
|
|
||||||
def on_history(self):
|
def on_history(self):
|
||||||
self.network.trigger_callback('on_history')
|
if self.network:
|
||||||
|
self.network.trigger_callback('on_history')
|
||||||
|
|
||||||
def exchange_rate(self):
|
def exchange_rate(self):
|
||||||
'''Returns None, or the exchange rate as a Decimal'''
|
'''Returns None, or the exchange rate as a Decimal'''
|
||||||
rate = self.exchange.quotes.get(self.ccy)
|
rate = self.exchange.quotes.get(self.ccy)
|
||||||
if rate:
|
if rate is None:
|
||||||
return Decimal(rate)
|
return Decimal('NaN')
|
||||||
|
return Decimal(rate)
|
||||||
|
|
||||||
|
def format_amount(self, btc_balance):
|
||||||
|
rate = self.exchange_rate()
|
||||||
|
return '' if rate.is_nan() else "%s" % self.value_str(btc_balance, rate)
|
||||||
|
|
||||||
def format_amount_and_units(self, btc_balance):
|
def format_amount_and_units(self, btc_balance):
|
||||||
rate = self.exchange_rate()
|
rate = self.exchange_rate()
|
||||||
return '' if rate is None else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
|
return '' if rate.is_nan() else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
|
||||||
|
|
||||||
def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):
|
def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):
|
||||||
rate = self.exchange_rate()
|
rate = self.exchange_rate()
|
||||||
return _(" (No exchange rate available)") if rate is None else " 1 %s=%s %s" % (base_unit,
|
return _(" (No FX rate available)") if rate.is_nan() else " 1 %s~%s %s" % (base_unit,
|
||||||
self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
|
self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
|
||||||
|
|
||||||
|
def fiat_value(self, satoshis, rate):
|
||||||
|
return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate)
|
||||||
|
|
||||||
def value_str(self, satoshis, rate):
|
def value_str(self, satoshis, rate):
|
||||||
if satoshis is None: # Can happen with incomplete history
|
return self.format_fiat(self.fiat_value(satoshis, rate))
|
||||||
return _("Unknown")
|
|
||||||
if rate:
|
def format_fiat(self, value):
|
||||||
value = Decimal(satoshis) / COIN * Decimal(rate)
|
if value.is_nan():
|
||||||
return "%s" % (self.ccy_amount_str(value, True))
|
return _("No data")
|
||||||
return _("No data")
|
return "%s" % (self.ccy_amount_str(value, True))
|
||||||
|
|
||||||
def history_rate(self, d_t):
|
def history_rate(self, d_t):
|
||||||
|
if d_t is None:
|
||||||
|
return Decimal('NaN')
|
||||||
rate = self.exchange.historical_rate(self.ccy, d_t)
|
rate = self.exchange.historical_rate(self.ccy, d_t)
|
||||||
# Frequently there is no rate for today, until tomorrow :)
|
# Frequently there is no rate for today, until tomorrow :)
|
||||||
# Use spot quotes in that case
|
# Use spot quotes in that case
|
||||||
if rate is None and (datetime.today().date() - d_t.date()).days <= 2:
|
if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2:
|
||||||
rate = self.exchange.quotes.get(self.ccy)
|
rate = self.exchange.quotes.get(self.ccy, 'NaN')
|
||||||
self.history_used_spot = True
|
self.history_used_spot = True
|
||||||
return rate
|
return Decimal(rate)
|
||||||
|
|
||||||
def historical_value_str(self, satoshis, d_t):
|
def historical_value_str(self, satoshis, d_t):
|
||||||
rate = self.history_rate(d_t)
|
return self.format_fiat(self.historical_value(satoshis, d_t))
|
||||||
return self.value_str(satoshis, rate)
|
|
||||||
|
def historical_value(self, satoshis, d_t):
|
||||||
|
return self.fiat_value(satoshis, self.history_rate(d_t))
|
||||||
|
|
||||||
|
def timestamp_rate(self, timestamp):
|
||||||
|
from electrum.util import timestamp_to_datetime
|
||||||
|
date = timestamp_to_datetime(timestamp)
|
||||||
|
return self.history_rate(date)
|
||||||
|
|
|
@ -31,9 +31,7 @@ import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from .util import print_error, get_cert_path
|
from .util import print_error
|
||||||
|
|
||||||
ca_path = get_cert_path()
|
|
||||||
|
|
||||||
from . import util
|
from . import util
|
||||||
from . import x509
|
from . import x509
|
||||||
|
|
|
@ -28,7 +28,7 @@ from unicodedata import normalize
|
||||||
|
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from .bitcoin import *
|
from .bitcoin import *
|
||||||
|
from . import constants
|
||||||
from .util import PrintError, InvalidPassword, hfu
|
from .util import PrintError, InvalidPassword, hfu
|
||||||
from .mnemonic import Mnemonic, load_wordlist
|
from .mnemonic import Mnemonic, load_wordlist
|
||||||
from .plugins import run_hook
|
from .plugins import run_hook
|
||||||
|
@ -45,6 +45,10 @@ class KeyStore(PrintError):
|
||||||
def can_import(self):
|
def can_import(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def may_have_password(self):
|
||||||
|
"""Returns whether the keystore can be encrypted with a password."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_tx_derivations(self, tx):
|
def get_tx_derivations(self, tx):
|
||||||
keypairs = {}
|
keypairs = {}
|
||||||
for txin in tx.inputs():
|
for txin in tx.inputs():
|
||||||
|
@ -116,9 +120,6 @@ class Imported_KeyStore(Software_KeyStore):
|
||||||
def is_deterministic(self):
|
def is_deterministic(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_change_password(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_master_public_key(self):
|
def get_master_public_key(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -138,7 +139,10 @@ class Imported_KeyStore(Software_KeyStore):
|
||||||
def import_privkey(self, sec, password):
|
def import_privkey(self, sec, password):
|
||||||
txin_type, privkey, compressed = deserialize_privkey(sec)
|
txin_type, privkey, compressed = deserialize_privkey(sec)
|
||||||
pubkey = public_key_from_private_key(privkey, compressed)
|
pubkey = public_key_from_private_key(privkey, compressed)
|
||||||
self.keypairs[pubkey] = pw_encode(sec, password)
|
# re-serialize the key so the internal storage format is consistent
|
||||||
|
serialized_privkey = serialize_privkey(
|
||||||
|
privkey, compressed, txin_type, internal_use=True)
|
||||||
|
self.keypairs[pubkey] = pw_encode(serialized_privkey, password)
|
||||||
return txin_type, pubkey
|
return txin_type, pubkey
|
||||||
|
|
||||||
def delete_imported_key(self, key):
|
def delete_imported_key(self, key):
|
||||||
|
@ -196,9 +200,6 @@ class Deterministic_KeyStore(Software_KeyStore):
|
||||||
def is_watching_only(self):
|
def is_watching_only(self):
|
||||||
return not self.has_seed()
|
return not self.has_seed()
|
||||||
|
|
||||||
def can_change_password(self):
|
|
||||||
return not self.is_watching_only()
|
|
||||||
|
|
||||||
def add_seed(self, seed):
|
def add_seed(self, seed):
|
||||||
if self.seed:
|
if self.seed:
|
||||||
raise Exception("a seed exists")
|
raise Exception("a seed exists")
|
||||||
|
@ -522,9 +523,13 @@ class Hardware_KeyStore(KeyStore, Xpub):
|
||||||
assert not self.has_seed()
|
assert not self.has_seed()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_change_password(self):
|
def get_password_for_storage_encryption(self):
|
||||||
return False
|
from .storage import get_derivation_used_for_hw_device_encryption
|
||||||
|
client = self.plugin.get_client(self)
|
||||||
|
derivation = get_derivation_used_for_hw_device_encryption()
|
||||||
|
xpub = client.get_xpub(derivation, "standard")
|
||||||
|
password = self.get_pubkey_from_xpub(xpub, ())
|
||||||
|
return password
|
||||||
|
|
||||||
|
|
||||||
def bip39_normalize_passphrase(passphrase):
|
def bip39_normalize_passphrase(passphrase):
|
||||||
|
@ -683,7 +688,7 @@ is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
|
||||||
|
|
||||||
|
|
||||||
def bip44_derivation(account_id, bip43_purpose=44):
|
def bip44_derivation(account_id, bip43_purpose=44):
|
||||||
coin = 1 if bitcoin.NetworkConstants.TESTNET else 147 # ZCL - SLIP0044
|
coin = 1 if constants.net.TESTNET else 147 # ZCL - SLIP0044
|
||||||
return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
|
return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
|
||||||
|
|
||||||
def from_seed(seed, passphrase, is_p2sh):
|
def from_seed(seed, passphrase, is_p2sh):
|
||||||
|
|
120
lib/network.py
120
lib/network.py
|
@ -37,9 +37,11 @@ import socks
|
||||||
from . import util
|
from . import util
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from .bitcoin import *
|
from .bitcoin import *
|
||||||
|
from . import constants
|
||||||
from .interface import Connection, Interface
|
from .interface import Connection, Interface
|
||||||
from . import blockchain
|
from . import blockchain
|
||||||
from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
|
from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
|
||||||
|
from .i18n import _
|
||||||
|
|
||||||
|
|
||||||
NODES_RETRY_INTERVAL = 60
|
NODES_RETRY_INTERVAL = 60
|
||||||
|
@ -59,7 +61,7 @@ def parse_servers(result):
|
||||||
for v in item[2]:
|
for v in item[2]:
|
||||||
if re.match("[st]\d*", v):
|
if re.match("[st]\d*", v):
|
||||||
protocol, port = v[0], v[1:]
|
protocol, port = v[0], v[1:]
|
||||||
if port == '': port = NetworkConstants.DEFAULT_PORTS[protocol]
|
if port == '': port = constants.net.DEFAULT_PORTS[protocol]
|
||||||
out[protocol] = port
|
out[protocol] = port
|
||||||
elif re.match("v(.?)+", v):
|
elif re.match("v(.?)+", v):
|
||||||
version = v[1:]
|
version = v[1:]
|
||||||
|
@ -93,7 +95,7 @@ def filter_protocol(hostmap, protocol = 's'):
|
||||||
|
|
||||||
def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
|
def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
|
||||||
if hostmap is None:
|
if hostmap is None:
|
||||||
hostmap = NetworkConstants.DEFAULT_SERVERS
|
hostmap = constants.net.DEFAULT_SERVERS
|
||||||
eligible = list(set(filter_protocol(hostmap, protocol)) - exclude_set)
|
eligible = list(set(filter_protocol(hostmap, protocol)) - exclude_set)
|
||||||
return random.choice(eligible) if eligible else None
|
return random.choice(eligible) if eligible else None
|
||||||
|
|
||||||
|
@ -137,7 +139,7 @@ def deserialize_proxy(s):
|
||||||
|
|
||||||
|
|
||||||
def deserialize_server(server_str):
|
def deserialize_server(server_str):
|
||||||
host, port, protocol = str(server_str).split(':')
|
host, port, protocol = str(server_str).rsplit(':', 2)
|
||||||
assert protocol in 'st'
|
assert protocol in 'st'
|
||||||
int(port) # Throw if cannot be converted to int
|
int(port) # Throw if cannot be converted to int
|
||||||
return host, port, protocol
|
return host, port, protocol
|
||||||
|
@ -171,17 +173,17 @@ class Network(util.DaemonThread):
|
||||||
self.blockchain_index = config.get('blockchain_index', 0)
|
self.blockchain_index = config.get('blockchain_index', 0)
|
||||||
if self.blockchain_index not in self.blockchains.keys():
|
if self.blockchain_index not in self.blockchains.keys():
|
||||||
self.blockchain_index = 0
|
self.blockchain_index = 0
|
||||||
self.protocol = 't' if self.config.get('nossl') else 's'
|
|
||||||
# Server for addresses and transactions
|
# Server for addresses and transactions
|
||||||
self.default_server = self.config.get('server')
|
self.default_server = self.config.get('server', None)
|
||||||
# Sanitize default server
|
# Sanitize default server
|
||||||
try:
|
if self.default_server:
|
||||||
host, port, protocol = deserialize_server(self.default_server)
|
try:
|
||||||
assert protocol == self.protocol
|
deserialize_server(self.default_server)
|
||||||
except:
|
except:
|
||||||
self.default_server = None
|
self.print_error('Warning: failed to parse server-string; falling back to random.')
|
||||||
|
self.default_server = None
|
||||||
if not self.default_server:
|
if not self.default_server:
|
||||||
self.default_server = pick_random_server(protocol=self.protocol)
|
self.default_server = pick_random_server()
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.pending_sends = []
|
self.pending_sends = []
|
||||||
self.message_id = 0
|
self.message_id = 0
|
||||||
|
@ -220,7 +222,8 @@ class Network(util.DaemonThread):
|
||||||
self.connecting = set()
|
self.connecting = set()
|
||||||
self.requested_chunks = set()
|
self.requested_chunks = set()
|
||||||
self.socket_queue = queue.Queue()
|
self.socket_queue = queue.Queue()
|
||||||
self.start_network(self.protocol, deserialize_proxy(self.config.get('proxy')))
|
self.start_network(deserialize_server(self.default_server)[2],
|
||||||
|
deserialize_proxy(self.config.get('proxy')))
|
||||||
|
|
||||||
def register_callback(self, callback, events):
|
def register_callback(self, callback, events):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -305,6 +308,9 @@ class Network(util.DaemonThread):
|
||||||
# Resend unanswered requests
|
# Resend unanswered requests
|
||||||
requests = self.unanswered_requests.values()
|
requests = self.unanswered_requests.values()
|
||||||
self.unanswered_requests = {}
|
self.unanswered_requests = {}
|
||||||
|
if self.interface.ping_required():
|
||||||
|
params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
|
||||||
|
self.queue_request('server.version', params, self.interface)
|
||||||
for request in requests:
|
for request in requests:
|
||||||
message_id = self.queue_request(request[0], request[1])
|
message_id = self.queue_request(request[0], request[1])
|
||||||
self.unanswered_requests[message_id] = request
|
self.unanswered_requests[message_id] = request
|
||||||
|
@ -313,15 +319,14 @@ class Network(util.DaemonThread):
|
||||||
self.queue_request('server.peers.subscribe', [])
|
self.queue_request('server.peers.subscribe', [])
|
||||||
self.request_fee_estimates()
|
self.request_fee_estimates()
|
||||||
self.queue_request('blockchain.relayfee', [])
|
self.queue_request('blockchain.relayfee', [])
|
||||||
if self.interface.ping_required():
|
|
||||||
params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
|
|
||||||
self.queue_request('server.version', params, self.interface)
|
|
||||||
for h in self.subscribed_addresses:
|
for h in self.subscribed_addresses:
|
||||||
self.queue_request('blockchain.scripthash.subscribe', [h])
|
self.queue_request('blockchain.scripthash.subscribe', [h])
|
||||||
|
|
||||||
def request_fee_estimates(self):
|
def request_fee_estimates(self):
|
||||||
|
from .simple_config import FEE_ETA_TARGETS
|
||||||
self.config.requested_fee_estimates()
|
self.config.requested_fee_estimates()
|
||||||
for i in bitcoin.FEE_TARGETS:
|
self.queue_request('mempool.get_fee_histogram', [])
|
||||||
|
for i in FEE_ETA_TARGETS:
|
||||||
self.queue_request('blockchain.estimatefee', [i])
|
self.queue_request('blockchain.estimatefee', [i])
|
||||||
|
|
||||||
def get_status_value(self, key):
|
def get_status_value(self, key):
|
||||||
|
@ -331,6 +336,8 @@ class Network(util.DaemonThread):
|
||||||
value = self.banner
|
value = self.banner
|
||||||
elif key == 'fee':
|
elif key == 'fee':
|
||||||
value = self.config.fee_estimates
|
value = self.config.fee_estimates
|
||||||
|
elif key == 'fee_histogram':
|
||||||
|
value = self.config.mempool_fees
|
||||||
elif key == 'updated':
|
elif key == 'updated':
|
||||||
value = (self.get_local_height(), self.get_server_height())
|
value = (self.get_local_height(), self.get_server_height())
|
||||||
elif key == 'servers':
|
elif key == 'servers':
|
||||||
|
@ -358,7 +365,7 @@ class Network(util.DaemonThread):
|
||||||
return list(self.interfaces.keys())
|
return list(self.interfaces.keys())
|
||||||
|
|
||||||
def get_servers(self):
|
def get_servers(self):
|
||||||
out = NetworkConstants.DEFAULT_SERVERS
|
out = constants.net.DEFAULT_SERVERS
|
||||||
if self.irc_servers:
|
if self.irc_servers:
|
||||||
out.update(filter_version(self.irc_servers.copy()))
|
out.update(filter_version(self.irc_servers.copy()))
|
||||||
else:
|
else:
|
||||||
|
@ -542,6 +549,11 @@ class Network(util.DaemonThread):
|
||||||
elif method == 'server.donation_address':
|
elif method == 'server.donation_address':
|
||||||
if error is None:
|
if error is None:
|
||||||
self.donation_address = result
|
self.donation_address = result
|
||||||
|
elif method == 'mempool.get_fee_histogram':
|
||||||
|
if error is None:
|
||||||
|
self.print_error('fee_histogram', result)
|
||||||
|
self.config.mempool_fees = result
|
||||||
|
self.notify('fee_histogram')
|
||||||
elif method == 'blockchain.estimatefee':
|
elif method == 'blockchain.estimatefee':
|
||||||
if error is None and result > 0:
|
if error is None and result > 0:
|
||||||
i = params[0]
|
i = params[0]
|
||||||
|
@ -767,29 +779,33 @@ class Network(util.DaemonThread):
|
||||||
error = response.get('error')
|
error = response.get('error')
|
||||||
result = response.get('result')
|
result = response.get('result')
|
||||||
params = response.get('params')
|
params = response.get('params')
|
||||||
|
blockchain = interface.blockchain
|
||||||
if result is None or params is None or error is not None:
|
if result is None or params is None or error is not None:
|
||||||
interface.print_error(error or 'bad response')
|
interface.print_error(error or 'bad response')
|
||||||
return
|
return
|
||||||
index = params[0]
|
index = params[0]
|
||||||
# Ignore unsolicited chunks
|
# Ignore unsolicited chunks
|
||||||
if index not in self.requested_chunks:
|
if index not in self.requested_chunks:
|
||||||
|
interface.print_error("received chunk %d (unsolicited)" % index)
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
interface.print_error("received chunk %d" % index)
|
||||||
self.requested_chunks.remove(index)
|
self.requested_chunks.remove(index)
|
||||||
connect = interface.blockchain.connect_chunk(index, result)
|
connect = blockchain.connect_chunk(index, result)
|
||||||
if not connect:
|
if not connect:
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
return
|
return
|
||||||
# If not finished, get the next chunk
|
# If not finished, get the next chunk
|
||||||
if interface.blockchain.height() < interface.tip:
|
if index >= len(blockchain.checkpoints) and blockchain.height() < interface.tip:
|
||||||
self.request_chunk(interface, index+1)
|
self.request_chunk(interface, index+1)
|
||||||
else:
|
else:
|
||||||
interface.mode = 'default'
|
interface.mode = 'default'
|
||||||
interface.print_error('catch up done', interface.blockchain.height())
|
interface.print_error('catch up done', blockchain.height())
|
||||||
interface.blockchain.catch_up = None
|
blockchain.catch_up = None
|
||||||
self.notify('updated')
|
self.notify('updated')
|
||||||
|
|
||||||
def request_header(self, interface, height):
|
def request_header(self, interface, height):
|
||||||
interface.print_error("requesting header %d" % height)
|
#interface.print_error("requesting header %d" % height)
|
||||||
self.queue_request('blockchain.block.get_header', [height], interface)
|
self.queue_request('blockchain.block.get_header', [height], interface)
|
||||||
interface.request = height
|
interface.request = height
|
||||||
interface.req_time = time.time()
|
interface.req_time = time.time()
|
||||||
|
@ -802,11 +818,10 @@ class Network(util.DaemonThread):
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
return
|
return
|
||||||
height = header.get('block_height')
|
height = header.get('block_height')
|
||||||
if int(interface.request) != height:
|
if interface.request != height:
|
||||||
interface.print_error("unsolicited header",interface.request, height)
|
interface.print_error("unsolicited header",interface.request, height)
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
return
|
return
|
||||||
interface.print_error("interface.mode %s" % interface.mode)
|
|
||||||
chain = blockchain.check_header(header)
|
chain = blockchain.check_header(header)
|
||||||
if interface.mode == 'backward':
|
if interface.mode == 'backward':
|
||||||
can_connect = blockchain.can_connect(header)
|
can_connect = blockchain.can_connect(header)
|
||||||
|
@ -822,6 +837,7 @@ class Network(util.DaemonThread):
|
||||||
interface.blockchain = chain
|
interface.blockchain = chain
|
||||||
interface.good = height
|
interface.good = height
|
||||||
next_height = (interface.bad + interface.good) // 2
|
next_height = (interface.bad + interface.good) // 2
|
||||||
|
assert next_height >= self.max_checkpoint(), (interface.bad, interface.good)
|
||||||
else:
|
else:
|
||||||
if height == 0:
|
if height == 0:
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
|
@ -830,7 +846,8 @@ class Network(util.DaemonThread):
|
||||||
interface.bad = height
|
interface.bad = height
|
||||||
interface.bad_header = header
|
interface.bad_header = header
|
||||||
delta = interface.tip - height
|
delta = interface.tip - height
|
||||||
next_height = max(0, interface.tip - 2 * delta)
|
next_height = max(self.max_checkpoint(), interface.tip - 2 * delta)
|
||||||
|
|
||||||
elif interface.mode == 'binary':
|
elif interface.mode == 'binary':
|
||||||
if chain:
|
if chain:
|
||||||
interface.good = height
|
interface.good = height
|
||||||
|
@ -840,6 +857,7 @@ class Network(util.DaemonThread):
|
||||||
interface.bad_header = header
|
interface.bad_header = header
|
||||||
if interface.bad != interface.good + 1:
|
if interface.bad != interface.good + 1:
|
||||||
next_height = (interface.bad + interface.good) // 2
|
next_height = (interface.bad + interface.good) // 2
|
||||||
|
assert next_height >= self.max_checkpoint()
|
||||||
elif not interface.blockchain.can_connect(interface.bad_header, check_height=False):
|
elif not interface.blockchain.can_connect(interface.bad_header, check_height=False):
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
next_height = None
|
next_height = None
|
||||||
|
@ -908,7 +926,7 @@ class Network(util.DaemonThread):
|
||||||
# If not finished, get the next header
|
# If not finished, get the next header
|
||||||
if next_height:
|
if next_height:
|
||||||
if interface.mode == 'catch_up' and interface.tip > next_height + 50:
|
if interface.mode == 'catch_up' and interface.tip > next_height + 50:
|
||||||
self.request_chunk(interface, next_height // NetworkConstants.CHUNK_SIZE)
|
self.request_chunk(interface, next_height // constants.net.CHUNK_SIZE)
|
||||||
else:
|
else:
|
||||||
self.request_header(interface, next_height)
|
self.request_header(interface, next_height)
|
||||||
else:
|
else:
|
||||||
|
@ -949,36 +967,18 @@ class Network(util.DaemonThread):
|
||||||
|
|
||||||
def init_headers_file(self):
|
def init_headers_file(self):
|
||||||
b = self.blockchains[0]
|
b = self.blockchains[0]
|
||||||
print(b.get_hash(0), NetworkConstants.GENESIS)
|
|
||||||
if b.get_hash(0) == NetworkConstants.GENESIS:
|
|
||||||
self.downloading_headers = False
|
|
||||||
return
|
|
||||||
filename = b.path()
|
filename = b.path()
|
||||||
def download_thread():
|
length = bitcoin.HEADER_SIZE * len(constants.net.CHECKPOINTS) * constants.net.CHUNK_SIZE
|
||||||
try:
|
if not os.path.exists(filename) or os.path.getsize(filename) < length:
|
||||||
import urllib, socket
|
with open(filename, 'wb') as f:
|
||||||
socket.setdefaulttimeout(30)
|
if length>0:
|
||||||
self.print_error("downloading ", NetworkConstants.HEADERS_URL)
|
f.seek(length-1)
|
||||||
urllib.request.urlretrieve(NetworkConstants.HEADERS_URL, filename)
|
f.write(b'\x00')
|
||||||
self.print_error("done.")
|
with b.lock:
|
||||||
except Exception:
|
b.update_size()
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
self.print_error("download failed. creating file", filename)
|
|
||||||
open(filename, 'wb+').close()
|
|
||||||
b = self.blockchains[0]
|
|
||||||
with b.lock: b.update_size()
|
|
||||||
self.downloading_headers = False
|
|
||||||
|
|
||||||
self.downloading_headers = True
|
|
||||||
t = threading.Thread(target = download_thread)
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.init_headers_file()
|
self.init_headers_file()
|
||||||
while self.is_running() and self.downloading_headers:
|
|
||||||
time.sleep(1)
|
|
||||||
while self.is_running():
|
while self.is_running():
|
||||||
self.maintain_sockets()
|
self.maintain_sockets()
|
||||||
self.wait_on_sockets()
|
self.wait_on_sockets()
|
||||||
|
@ -990,10 +990,11 @@ class Network(util.DaemonThread):
|
||||||
|
|
||||||
def on_notify_header(self, interface, header):
|
def on_notify_header(self, interface, header):
|
||||||
height = header.get('block_height')
|
height = header.get('block_height')
|
||||||
|
|
||||||
if not height:
|
if not height:
|
||||||
return
|
return
|
||||||
|
if height < self.max_checkpoint():
|
||||||
|
self.connection_down(interface.server)
|
||||||
|
return
|
||||||
interface.tip_header = header
|
interface.tip_header = header
|
||||||
interface.tip = height
|
interface.tip = height
|
||||||
|
|
||||||
|
@ -1078,7 +1079,7 @@ class Network(util.DaemonThread):
|
||||||
try:
|
try:
|
||||||
r = q.get(True, timeout)
|
r = q.get(True, timeout)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
raise BaseException('Server did not answer')
|
raise util.TimeoutException(_('Server did not answer'))
|
||||||
if r.get('error'):
|
if r.get('error'):
|
||||||
raise BaseException(r.get('error'))
|
raise BaseException(r.get('error'))
|
||||||
return r.get('result')
|
return r.get('result')
|
||||||
|
@ -1092,3 +1093,12 @@ class Network(util.DaemonThread):
|
||||||
if out != tx_hash:
|
if out != tx_hash:
|
||||||
return False, "error: " + out
|
return False, "error: " + out
|
||||||
return True, out
|
return True, out
|
||||||
|
|
||||||
|
def export_checkpoints(self, path):
|
||||||
|
# run manually from the console to generate checkpoints
|
||||||
|
cp = self.blockchain().get_checkpoints()
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(json.dumps(cp, indent=4))
|
||||||
|
|
||||||
|
def max_checkpoint(self):
|
||||||
|
return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1)
|
||||||
|
|
|
@ -39,7 +39,8 @@ except ImportError:
|
||||||
|
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from . import util
|
from . import util
|
||||||
from .util import print_error, bh2u, bfh, get_cert_path
|
from .util import print_error, bh2u, bfh
|
||||||
|
from .util import export_meta, import_meta
|
||||||
from . import transaction
|
from . import transaction
|
||||||
from . import x509
|
from . import x509
|
||||||
from . import rsakey
|
from . import rsakey
|
||||||
|
@ -49,7 +50,7 @@ from .bitcoin import TYPE_ADDRESS
|
||||||
REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
|
REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
|
||||||
ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
|
ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
|
||||||
|
|
||||||
ca_path = get_cert_path()
|
ca_path = requests.certs.where()
|
||||||
ca_list = None
|
ca_list = None
|
||||||
ca_keyID = None
|
ca_keyID = None
|
||||||
|
|
||||||
|
@ -467,24 +468,29 @@ class InvoiceStore(object):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def import_file(self, path):
|
def import_file(self, path):
|
||||||
try:
|
def validate(data):
|
||||||
with open(path, 'r') as f:
|
return data # TODO
|
||||||
d = json.loads(f.read())
|
import_meta(path, validate, self.on_import)
|
||||||
self.load(d)
|
|
||||||
except:
|
def on_import(self, data):
|
||||||
traceback.print_exc(file=sys.stderr)
|
self.load(data)
|
||||||
return
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(self):
|
def export_file(self, filename):
|
||||||
l = {}
|
export_meta(self.dump(), filename)
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
d = {}
|
||||||
for k, pr in self.invoices.items():
|
for k, pr in self.invoices.items():
|
||||||
l[k] = {
|
d[k] = {
|
||||||
'hex': bh2u(pr.raw),
|
'hex': bh2u(pr.raw),
|
||||||
'requestor': pr.requestor,
|
'requestor': pr.requestor,
|
||||||
'txid': pr.tx
|
'txid': pr.tx
|
||||||
}
|
}
|
||||||
self.storage.put('invoices', l)
|
return d
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.storage.put('invoices', self.dump())
|
||||||
|
|
||||||
def get_status(self, key):
|
def get_status(self, key):
|
||||||
pr = self.get(key)
|
pr = self.get(key)
|
||||||
|
|
31
lib/plot.py
31
lib/plot.py
|
@ -14,17 +14,23 @@ from matplotlib.patches import Ellipse
|
||||||
from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker
|
from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker
|
||||||
|
|
||||||
|
|
||||||
def plot_history(wallet, history):
|
class NothingToPlotException(Exception):
|
||||||
|
def __str__(self):
|
||||||
|
return _("Nothing to plot.")
|
||||||
|
|
||||||
|
|
||||||
|
def plot_history(history):
|
||||||
|
if len(history) == 0:
|
||||||
|
raise NothingToPlotException()
|
||||||
hist_in = defaultdict(int)
|
hist_in = defaultdict(int)
|
||||||
hist_out = defaultdict(int)
|
hist_out = defaultdict(int)
|
||||||
for item in history:
|
for item in history:
|
||||||
tx_hash, height, confirmations, timestamp, value, balance = item
|
if not item['confirmations']:
|
||||||
if not confirmations:
|
|
||||||
continue
|
continue
|
||||||
if timestamp is None:
|
if item['timestamp'] is None:
|
||||||
continue
|
continue
|
||||||
value = value*1./COIN
|
value = item['value'].value/COIN
|
||||||
date = datetime.datetime.fromtimestamp(timestamp)
|
date = item['date']
|
||||||
datenum = int(md.date2num(datetime.date(date.year, date.month, 1)))
|
datenum = int(md.date2num(datetime.date(date.year, date.month, 1)))
|
||||||
if value > 0:
|
if value > 0:
|
||||||
hist_in[datenum] += value
|
hist_in[datenum] += value
|
||||||
|
@ -43,12 +49,19 @@ def plot_history(wallet, history):
|
||||||
xfmt = md.DateFormatter('%Y-%m')
|
xfmt = md.DateFormatter('%Y-%m')
|
||||||
ax.xaxis.set_major_formatter(xfmt)
|
ax.xaxis.set_major_formatter(xfmt)
|
||||||
width = 20
|
width = 20
|
||||||
dates, values = zip(*sorted(hist_in.items()))
|
|
||||||
r1 = axarr[0].bar(dates, values, width, label='incoming')
|
r1 = None
|
||||||
axarr[0].legend(loc='upper left')
|
r2 = None
|
||||||
|
dates_values = list(zip(*sorted(hist_in.items())))
|
||||||
|
if dates_values and len(dates_values) == 2:
|
||||||
|
dates, values = dates_values
|
||||||
|
r1 = axarr[0].bar(dates, values, width, label='incoming')
|
||||||
|
axarr[0].legend(loc='upper left')
|
||||||
dates_values = list(zip(*sorted(hist_out.items())))
|
dates_values = list(zip(*sorted(hist_out.items())))
|
||||||
if dates_values and len(dates_values) == 2:
|
if dates_values and len(dates_values) == 2:
|
||||||
dates, values = dates_values
|
dates, values = dates_values
|
||||||
r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing')
|
r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing')
|
||||||
axarr[1].legend(loc='upper left')
|
axarr[1].legend(loc='upper left')
|
||||||
|
if r1 is None and r2 is None:
|
||||||
|
raise NothingToPlotException()
|
||||||
return plt
|
return plt
|
||||||
|
|
|
@ -312,6 +312,8 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
# What we recognise. Each entry is a (vendor_id, product_id)
|
# What we recognise. Each entry is a (vendor_id, product_id)
|
||||||
# pair.
|
# pair.
|
||||||
self.recognised_hardware = set()
|
self.recognised_hardware = set()
|
||||||
|
# Custom enumerate functions for devices we don't know about.
|
||||||
|
self.enumerate_func = set()
|
||||||
# For synchronization
|
# For synchronization
|
||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
self.hid_lock = threading.RLock()
|
self.hid_lock = threading.RLock()
|
||||||
|
@ -334,6 +336,9 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
for pair in device_pairs:
|
for pair in device_pairs:
|
||||||
self.recognised_hardware.add(pair)
|
self.recognised_hardware.add(pair)
|
||||||
|
|
||||||
|
def register_enumerate_func(self, func):
|
||||||
|
self.enumerate_func.add(func)
|
||||||
|
|
||||||
def create_client(self, device, handler, plugin):
|
def create_client(self, device, handler, plugin):
|
||||||
# Get from cache first
|
# Get from cache first
|
||||||
client = self.client_lookup(device.id_)
|
client = self.client_lookup(device.id_)
|
||||||
|
@ -362,15 +367,20 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
if not xpub in self.xpub_ids:
|
if not xpub in self.xpub_ids:
|
||||||
return
|
return
|
||||||
_id = self.xpub_ids.pop(xpub)
|
_id = self.xpub_ids.pop(xpub)
|
||||||
client = self.client_lookup(_id)
|
self._close_client(_id)
|
||||||
self.clients.pop(client, None)
|
|
||||||
if client:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
def unpair_id(self, id_):
|
def unpair_id(self, id_):
|
||||||
xpub = self.xpub_by_id(id_)
|
xpub = self.xpub_by_id(id_)
|
||||||
if xpub:
|
if xpub:
|
||||||
self.unpair_xpub(xpub)
|
self.unpair_xpub(xpub)
|
||||||
|
else:
|
||||||
|
self._close_client(id_)
|
||||||
|
|
||||||
|
def _close_client(self, id_):
|
||||||
|
client = self.client_lookup(id_)
|
||||||
|
self.clients.pop(client, None)
|
||||||
|
if client:
|
||||||
|
client.close()
|
||||||
|
|
||||||
def pair_xpub(self, xpub, id_):
|
def pair_xpub(self, xpub, id_):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -442,11 +452,11 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
# The user input has wrong PIN or passphrase, or cancelled input,
|
# The user input has wrong PIN or passphrase, or cancelled input,
|
||||||
# or it is not pairable
|
# or it is not pairable
|
||||||
raise DeviceUnpairableError(
|
raise DeviceUnpairableError(
|
||||||
_('Electrum cannot pair with your %s.\n\n'
|
_('Electrum cannot pair with your {}.\n\n'
|
||||||
'Before you request bitcoins to be sent to addresses in this '
|
'Before you request bitcoins to be sent to addresses in this '
|
||||||
'wallet, ensure you can pair with your device, or that you have '
|
'wallet, ensure you can pair with your device, or that you have '
|
||||||
'its seed (and passphrase, if any). Otherwise all bitcoins you '
|
'its seed (and passphrase, if any). Otherwise all bitcoins you '
|
||||||
'receive will be unspendable.') % plugin.device)
|
'receive will be unspendable.').format(plugin.device))
|
||||||
|
|
||||||
def unpaired_device_infos(self, handler, plugin, devices=None):
|
def unpaired_device_infos(self, handler, plugin, devices=None):
|
||||||
'''Returns a list of DeviceInfo objects: one for each connected,
|
'''Returns a list of DeviceInfo objects: one for each connected,
|
||||||
|
@ -472,9 +482,9 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
infos = self.unpaired_device_infos(handler, plugin, devices)
|
infos = self.unpaired_device_infos(handler, plugin, devices)
|
||||||
if infos:
|
if infos:
|
||||||
break
|
break
|
||||||
msg = _('Please insert your %s. Verify the cable is '
|
msg = _('Please insert your {}. Verify the cable is '
|
||||||
'connected and that no other application is using it.\n\n'
|
'connected and that no other application is using it.\n\n'
|
||||||
'Try to connect again?') % plugin.device
|
'Try to connect again?').format(plugin.device)
|
||||||
if not handler.yes_no_question(msg):
|
if not handler.yes_no_question(msg):
|
||||||
raise UserCancelled()
|
raise UserCancelled()
|
||||||
devices = None
|
devices = None
|
||||||
|
@ -484,7 +494,7 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
for info in infos:
|
for info in infos:
|
||||||
if info.label == keystore.label:
|
if info.label == keystore.label:
|
||||||
return info
|
return info
|
||||||
msg = _("Please select which %s device to use:") % plugin.device
|
msg = _("Please select which {} device to use:").format(plugin.device)
|
||||||
descriptions = [info.label + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos]
|
descriptions = [info.label + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos]
|
||||||
c = handler.query_choice(msg, descriptions)
|
c = handler.query_choice(msg, descriptions)
|
||||||
if c is None:
|
if c is None:
|
||||||
|
@ -492,7 +502,8 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
info = infos[c]
|
info = infos[c]
|
||||||
# save new label
|
# save new label
|
||||||
keystore.set_label(info.label)
|
keystore.set_label(info.label)
|
||||||
handler.win.wallet.save_keystore()
|
if handler.win.wallet is not None:
|
||||||
|
handler.win.wallet.save_keystore()
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def scan_devices(self):
|
def scan_devices(self):
|
||||||
|
@ -504,6 +515,7 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
self.print_error("scanning devices...")
|
self.print_error("scanning devices...")
|
||||||
with self.hid_lock:
|
with self.hid_lock:
|
||||||
hid_list = hid.enumerate(0, 0)
|
hid_list = hid.enumerate(0, 0)
|
||||||
|
|
||||||
# First see what's connected that we know about
|
# First see what's connected that we know about
|
||||||
devices = []
|
devices = []
|
||||||
for d in hid_list:
|
for d in hid_list:
|
||||||
|
@ -519,6 +531,10 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
devices.append(Device(d['path'], interface_number,
|
devices.append(Device(d['path'], interface_number,
|
||||||
id_, product_key, usage_page))
|
id_, product_key, usage_page))
|
||||||
|
|
||||||
|
# Let plugin handlers enumerate devices we don't know about
|
||||||
|
for f in self.enumerate_func:
|
||||||
|
devices.extend(f())
|
||||||
|
|
||||||
# Now find out what was disconnected
|
# Now find out what was disconnected
|
||||||
pairs = [(dev.path, dev.id_) for dev in devices]
|
pairs = [(dev.path, dev.id_) for dev in devices]
|
||||||
disconnected_ids = []
|
disconnected_ids = []
|
||||||
|
|
|
@ -29,8 +29,8 @@ import ctypes
|
||||||
|
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
name = 'libzbar.dylib'
|
name = 'libzbar.dylib'
|
||||||
elif sys.platform == 'windows':
|
elif sys.platform in ('windows', 'win32'):
|
||||||
name = 'libzbar.dll'
|
name = 'libzbar-0.dll'
|
||||||
else:
|
else:
|
||||||
name = 'libzbar.so.0'
|
name = 'libzbar.so.0'
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ except OSError:
|
||||||
libzbar = None
|
libzbar = None
|
||||||
|
|
||||||
|
|
||||||
def scan_barcode(device='', timeout=-1, display=True, threaded=False):
|
def scan_barcode(device='', timeout=-1, display=True, threaded=False, try_again=True):
|
||||||
if libzbar is None:
|
if libzbar is None:
|
||||||
raise RuntimeError("Cannot start QR scanner; zbar not available.")
|
raise RuntimeError("Cannot start QR scanner; zbar not available.")
|
||||||
libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p
|
libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p
|
||||||
|
@ -50,6 +50,10 @@ def scan_barcode(device='', timeout=-1, display=True, threaded=False):
|
||||||
proc = libzbar.zbar_processor_create(threaded)
|
proc = libzbar.zbar_processor_create(threaded)
|
||||||
libzbar.zbar_processor_request_size(proc, 640, 480)
|
libzbar.zbar_processor_request_size(proc, 640, 480)
|
||||||
if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0:
|
if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0:
|
||||||
|
if try_again:
|
||||||
|
# workaround for a bug in "ZBar for Windows"
|
||||||
|
# libzbar.zbar_processor_init always seem to fail the first time around
|
||||||
|
return scan_barcode(device, timeout, display, threaded, try_again=False)
|
||||||
raise RuntimeError("Can not start QR scanner; initialization failed.")
|
raise RuntimeError("Can not start QR scanner; initialization failed.")
|
||||||
libzbar.zbar_processor_set_visible(proc)
|
libzbar.zbar_processor_set_visible(proc)
|
||||||
if libzbar.zbar_process_one(proc, timeout):
|
if libzbar.zbar_process_one(proc, timeout):
|
||||||
|
|
|
@ -1,231 +0,0 @@
|
||||||
{
|
|
||||||
"E-X.not.fyi": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"ELECTRUMX.not.fyi": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"ELEX01.blackpole.online": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"VPS.hsmiths.com": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"bitcoin.freedomnode.com": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"btc.smsys.me": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "995",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"currentlane.lovebitco.in": {
|
|
||||||
"pruning": "-",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"daedalus.bauerj.eu": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"de01.hamster.science": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"ecdsa.net": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "110",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"elec.luggs.co": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "443",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.akinbo.org": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.antumbra.se": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.be": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.coinucopia.io": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.cutie.ga": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.festivaldelhumor.org": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.hsmiths.com": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.qtornado.com": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum.vom-stausee.de": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrum3.hachre.de": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrumx.bot.nu": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"electrumx.westeurope.cloudapp.azure.com": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"elx01.knas.systems": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"ex-btc.server-on.net": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"helicarrier.bauerj.eu": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"mooo.not.fyi": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50012",
|
|
||||||
"t": "50011",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"ndnd.selfhost.eu": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"node.arihanc.com": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"node.xbt.eu": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"node1.volatilevictory.com": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"noserver4u.de": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"qmebr.spdns.org": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"raspi.hsmiths.com": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "51002",
|
|
||||||
"t": "51001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"s2.noip.pl": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50102",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"s5.noip.pl": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50105",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"songbird.bauerj.eu": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"us.electrum.be": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
},
|
|
||||||
"us01.hamster.science": {
|
|
||||||
"pruning": "-",
|
|
||||||
"s": "50002",
|
|
||||||
"t": "50001",
|
|
||||||
"version": "1.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"electrum.zclassic.community": {"s":"50002"},
|
"electrum.zclassic.community": {"s":"50002", "pruning": "-", "version": "1.1"},
|
||||||
"zclele.duckdns.org": {"s":"50002"}
|
"zclele.duckdns.org": {"s":"50002", "pruning": "-", "version": "1.1"}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"localhost": {"t":"51001", "s":"51002"}
|
"localhost": {"t":"51001", "s":"51002", "pruning": "-", "version": "1.1"}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,21 @@ import os
|
||||||
import stat
|
import stat
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from .util import user_dir, print_error, print_stderr, PrintError
|
|
||||||
|
|
||||||
from .bitcoin import DEFAULT_FEE_RATE, MAX_FEE_RATE, FEE_TARGETS
|
from .util import (user_dir, print_error, PrintError,
|
||||||
|
NoDynamicFeeEstimates, format_satoshis)
|
||||||
|
from .i18n import _
|
||||||
|
|
||||||
|
FEE_ETA_TARGETS = [25, 10, 5, 2]
|
||||||
|
FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000]
|
||||||
|
|
||||||
|
# satoshi per kbyte
|
||||||
|
FEERATE_MAX_DYNAMIC = 1500000
|
||||||
|
FEERATE_WARNING_HIGH_FEE = 600000
|
||||||
|
FEERATE_FALLBACK_STATIC_FEE = 150000
|
||||||
|
FEERATE_DEFAULT_RELAY = 1000
|
||||||
|
FEERATE_STATIC_VALUES = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
|
||||||
|
|
||||||
SYSTEM_CONFIG_PATH = "/etc/electrum.conf"
|
|
||||||
|
|
||||||
config = None
|
config = None
|
||||||
|
|
||||||
|
@ -24,35 +34,37 @@ def set_config(c):
|
||||||
config = c
|
config = c
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_CONFIG_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
class SimpleConfig(PrintError):
|
class SimpleConfig(PrintError):
|
||||||
"""
|
"""
|
||||||
The SimpleConfig class is responsible for handling operations involving
|
The SimpleConfig class is responsible for handling operations involving
|
||||||
configuration files.
|
configuration files.
|
||||||
|
|
||||||
There are 3 different sources of possible configuration values:
|
There are two different sources of possible configuration values:
|
||||||
1. Command line options.
|
1. Command line options.
|
||||||
2. User configuration (in the user's config directory)
|
2. User configuration (in the user's config directory)
|
||||||
3. System configuration (in /etc/)
|
They are taken in order (1. overrides config options set in 2.)
|
||||||
They are taken in order (1. overrides config options set in 2., that
|
|
||||||
override config set in 3.)
|
|
||||||
"""
|
"""
|
||||||
fee_rates = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
|
|
||||||
|
|
||||||
def __init__(self, options={}, read_system_config_function=None,
|
def __init__(self, options=None, read_user_config_function=None,
|
||||||
read_user_config_function=None, read_user_dir_function=None):
|
read_user_dir_function=None):
|
||||||
|
|
||||||
|
if options is None:
|
||||||
|
options = {}
|
||||||
|
|
||||||
# This lock needs to be acquired for updating and reading the config in
|
# This lock needs to be acquired for updating and reading the config in
|
||||||
# a thread-safe way.
|
# a thread-safe way.
|
||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
|
|
||||||
|
self.mempool_fees = {}
|
||||||
self.fee_estimates = {}
|
self.fee_estimates = {}
|
||||||
self.fee_estimates_last_updated = {}
|
self.fee_estimates_last_updated = {}
|
||||||
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
|
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
|
||||||
|
|
||||||
# The following two functions are there for dependency injection when
|
# The following two functions are there for dependency injection when
|
||||||
# testing.
|
# testing.
|
||||||
if read_system_config_function is None:
|
|
||||||
read_system_config_function = read_system_config
|
|
||||||
if read_user_config_function is None:
|
if read_user_config_function is None:
|
||||||
read_user_config_function = read_user_config
|
read_user_config_function = read_user_config
|
||||||
if read_user_dir_function is None:
|
if read_user_dir_function is None:
|
||||||
|
@ -62,24 +74,30 @@ class SimpleConfig(PrintError):
|
||||||
|
|
||||||
# The command line options
|
# The command line options
|
||||||
self.cmdline_options = deepcopy(options)
|
self.cmdline_options = deepcopy(options)
|
||||||
|
# don't allow to be set on CLI:
|
||||||
# Portable wallets don't use a system config
|
self.cmdline_options.pop('config_version', None)
|
||||||
if self.cmdline_options.get('portable', False):
|
|
||||||
self.system_config = {}
|
|
||||||
else:
|
|
||||||
self.system_config = read_system_config_function()
|
|
||||||
|
|
||||||
# Set self.path and read the user config
|
# Set self.path and read the user config
|
||||||
self.user_config = {} # for self.get in electrum_path()
|
self.user_config = {} # for self.get in electrum_path()
|
||||||
self.path = self.electrum_path()
|
self.path = self.electrum_path()
|
||||||
self.user_config = read_user_config_function(self.path)
|
self.user_config = read_user_config_function(self.path)
|
||||||
# Upgrade obsolete keys
|
if not self.user_config:
|
||||||
self.fixup_keys({'auto_cycle': 'auto_connect'})
|
# avoid new config getting upgraded
|
||||||
|
self.user_config = {'config_version': FINAL_CONFIG_VERSION}
|
||||||
|
|
||||||
|
# config "upgrade" - CLI options
|
||||||
|
self.rename_config_keys(
|
||||||
|
self.cmdline_options, {'auto_cycle': 'auto_connect'}, True)
|
||||||
|
|
||||||
|
# config upgrade - user config
|
||||||
|
if self.requires_upgrade():
|
||||||
|
self.upgrade()
|
||||||
|
|
||||||
# Make a singleton instance of 'self'
|
# Make a singleton instance of 'self'
|
||||||
set_config(self)
|
set_config(self)
|
||||||
|
|
||||||
def electrum_path(self):
|
def electrum_path(self):
|
||||||
# Read electrum_path from command line / system configuration
|
# Read electrum_path from command line
|
||||||
# Otherwise use the user's default data directory.
|
# Otherwise use the user's default data directory.
|
||||||
path = self.get('electrum_path')
|
path = self.get('electrum_path')
|
||||||
if path is None:
|
if path is None:
|
||||||
|
@ -101,45 +119,92 @@ class SimpleConfig(PrintError):
|
||||||
self.print_error("electrum directory", path)
|
self.print_error("electrum directory", path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def fixup_config_keys(self, config, keypairs):
|
def rename_config_keys(self, config, keypairs, deprecation_warning=False):
|
||||||
|
"""Migrate old key names to new ones"""
|
||||||
updated = False
|
updated = False
|
||||||
for old_key, new_key in keypairs.items():
|
for old_key, new_key in keypairs.items():
|
||||||
if old_key in config:
|
if old_key in config:
|
||||||
if not new_key in config:
|
if new_key not in config:
|
||||||
config[new_key] = config[old_key]
|
config[new_key] = config[old_key]
|
||||||
|
if deprecation_warning:
|
||||||
|
self.print_stderr('Note that the {} variable has been deprecated. '
|
||||||
|
'You should use {} instead.'.format(old_key, new_key))
|
||||||
del config[old_key]
|
del config[old_key]
|
||||||
updated = True
|
updated = True
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
def fixup_keys(self, keypairs):
|
def set_key(self, key, value, save=True):
|
||||||
'''Migrate old key names to new ones'''
|
|
||||||
self.fixup_config_keys(self.cmdline_options, keypairs)
|
|
||||||
self.fixup_config_keys(self.system_config, keypairs)
|
|
||||||
if self.fixup_config_keys(self.user_config, keypairs):
|
|
||||||
self.save_user_config()
|
|
||||||
|
|
||||||
def set_key(self, key, value, save = True):
|
|
||||||
if not self.is_modifiable(key):
|
if not self.is_modifiable(key):
|
||||||
print_stderr("Warning: not changing config key '%s' set on the command line" % key)
|
self.print_stderr("Warning: not changing config key '%s' set on the command line" % key)
|
||||||
return
|
return
|
||||||
|
self._set_key_in_user_config(key, value, save)
|
||||||
|
|
||||||
|
def _set_key_in_user_config(self, key, value, save=True):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.user_config[key] = value
|
if value is not None:
|
||||||
|
self.user_config[key] = value
|
||||||
|
else:
|
||||||
|
self.user_config.pop(key, None)
|
||||||
if save:
|
if save:
|
||||||
self.save_user_config()
|
self.save_user_config()
|
||||||
return
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
out = self.cmdline_options.get(key)
|
out = self.cmdline_options.get(key)
|
||||||
if out is None:
|
if out is None:
|
||||||
out = self.user_config.get(key)
|
out = self.user_config.get(key, default)
|
||||||
if out is None:
|
|
||||||
out = self.system_config.get(key, default)
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def requires_upgrade(self):
|
||||||
|
return self.get_config_version() < FINAL_CONFIG_VERSION
|
||||||
|
|
||||||
|
def upgrade(self):
|
||||||
|
with self.lock:
|
||||||
|
self.print_error('upgrading config')
|
||||||
|
|
||||||
|
self.convert_version_2()
|
||||||
|
|
||||||
|
self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)
|
||||||
|
|
||||||
|
def convert_version_2(self):
|
||||||
|
if not self._is_upgrade_method_needed(1, 1):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# change server string FROM host:port:proto TO host:port:s
|
||||||
|
server_str = self.user_config.get('server')
|
||||||
|
host, port, protocol = str(server_str).rsplit(':', 2)
|
||||||
|
assert protocol in ('s', 't')
|
||||||
|
int(port) # Throw if cannot be converted to int
|
||||||
|
server_str = '{}:{}:s'.format(host, port)
|
||||||
|
self._set_key_in_user_config('server', server_str)
|
||||||
|
except BaseException:
|
||||||
|
self._set_key_in_user_config('server', None)
|
||||||
|
|
||||||
|
self.set_key('config_version', 2)
|
||||||
|
|
||||||
|
def _is_upgrade_method_needed(self, min_version, max_version):
|
||||||
|
cur_version = self.get_config_version()
|
||||||
|
if cur_version > max_version:
|
||||||
|
return False
|
||||||
|
elif cur_version < min_version:
|
||||||
|
raise BaseException(
|
||||||
|
('config upgrade: unexpected version %d (should be %d-%d)'
|
||||||
|
% (cur_version, min_version, max_version)))
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_config_version(self):
|
||||||
|
config_version = self.get('config_version', 1)
|
||||||
|
if config_version > FINAL_CONFIG_VERSION:
|
||||||
|
self.print_stderr('WARNING: config version ({}) is higher than ours ({})'
|
||||||
|
.format(config_version, FINAL_CONFIG_VERSION))
|
||||||
|
return config_version
|
||||||
|
|
||||||
def is_modifiable(self, key):
|
def is_modifiable(self, key):
|
||||||
return not key in self.cmdline_options
|
return key not in self.cmdline_options
|
||||||
|
|
||||||
def save_user_config(self):
|
def save_user_config(self):
|
||||||
if not self.path:
|
if not self.path:
|
||||||
|
@ -203,68 +268,197 @@ class SimpleConfig(PrintError):
|
||||||
path = wallet.storage.path
|
path = wallet.storage.path
|
||||||
self.set_key('gui_last_wallet', path)
|
self.set_key('gui_last_wallet', path)
|
||||||
|
|
||||||
def default_fee_rate(self):
|
def impose_hard_limits_on_fee(func):
|
||||||
f = self.get('default_fee_rate', DEFAULT_FEE_RATE)
|
def get_fee_within_limits(self, *args, **kwargs):
|
||||||
if f==0:
|
fee = func(self, *args, **kwargs)
|
||||||
f = DEFAULT_FEE_RATE
|
if fee is None:
|
||||||
return f
|
return fee
|
||||||
|
fee = min(FEERATE_MAX_DYNAMIC, fee)
|
||||||
|
fee = max(FEERATE_DEFAULT_RELAY, fee)
|
||||||
|
return fee
|
||||||
|
return get_fee_within_limits
|
||||||
|
|
||||||
def max_fee_rate(self):
|
@impose_hard_limits_on_fee
|
||||||
f = self.get('max_fee_rate', MAX_FEE_RATE)
|
def eta_to_fee(self, i):
|
||||||
if f==0:
|
"""Returns fee in sat/kbyte."""
|
||||||
f = MAX_FEE_RATE
|
|
||||||
return f
|
|
||||||
|
|
||||||
def dynfee(self, i):
|
|
||||||
if i < 4:
|
if i < 4:
|
||||||
j = FEE_TARGETS[i]
|
j = FEE_ETA_TARGETS[i]
|
||||||
fee = self.fee_estimates.get(j)
|
fee = self.fee_estimates.get(j)
|
||||||
else:
|
else:
|
||||||
assert i == 4
|
assert i == 4
|
||||||
fee = self.fee_estimates.get(2)
|
fee = self.fee_estimates.get(2)
|
||||||
if fee is not None:
|
if fee is not None:
|
||||||
fee += fee/2
|
fee += fee/2
|
||||||
if fee is not None:
|
|
||||||
fee = min(5*MAX_FEE_RATE, fee)
|
|
||||||
return fee
|
return fee
|
||||||
|
|
||||||
def reverse_dynfee(self, fee_per_kb):
|
def fee_to_depth(self, target_fee):
|
||||||
|
depth = 0
|
||||||
|
for fee, s in self.mempool_fees:
|
||||||
|
depth += s
|
||||||
|
if fee <= target_fee:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
return depth
|
||||||
|
|
||||||
|
@impose_hard_limits_on_fee
|
||||||
|
def depth_to_fee(self, i):
|
||||||
|
"""Returns fee in zat/kbyte."""
|
||||||
|
target = self.depth_target(i)
|
||||||
|
depth = 0
|
||||||
|
for fee, s in self.mempool_fees:
|
||||||
|
depth += s
|
||||||
|
if depth > target:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
return fee * 1000
|
||||||
|
|
||||||
|
def depth_target(self, i):
|
||||||
|
return FEE_DEPTH_TARGETS[i]
|
||||||
|
|
||||||
|
def eta_target(self, i):
|
||||||
|
if i == len(FEE_ETA_TARGETS):
|
||||||
|
return 1
|
||||||
|
return FEE_ETA_TARGETS[i]
|
||||||
|
|
||||||
|
def fee_to_eta(self, fee_per_kb):
|
||||||
import operator
|
import operator
|
||||||
l = list(self.fee_estimates.items()) + [(1, self.dynfee(4))]
|
l = list(self.fee_estimates.items()) + [(1, self.eta_to_fee(4))]
|
||||||
dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l)
|
dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l)
|
||||||
min_target, min_value = min(dist, key=operator.itemgetter(1))
|
min_target, min_value = min(dist, key=operator.itemgetter(1))
|
||||||
if fee_per_kb < self.fee_estimates.get(25)/2:
|
if fee_per_kb < self.fee_estimates.get(25)/2:
|
||||||
min_target = -1
|
min_target = -1
|
||||||
return min_target
|
return min_target
|
||||||
|
|
||||||
|
def depth_tooltip(self, depth):
|
||||||
|
return "%.1f MB from tip"%(depth/1000000)
|
||||||
|
|
||||||
|
def eta_tooltip(self, x):
|
||||||
|
if x < 0:
|
||||||
|
return _('Low fee')
|
||||||
|
elif x == 1:
|
||||||
|
return _('In the next block')
|
||||||
|
else:
|
||||||
|
return _('Within {} blocks').format(x)
|
||||||
|
|
||||||
|
def get_fee_status(self):
|
||||||
|
dyn = self.is_dynfee()
|
||||||
|
mempool = self.use_mempool_fees()
|
||||||
|
pos = self.get_depth_level() if mempool else self.get_fee_level()
|
||||||
|
fee_rate = self.fee_per_kb()
|
||||||
|
target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate)
|
||||||
|
return tooltip + ' [%s]'%target if dyn else target + ' [Static]'
|
||||||
|
|
||||||
|
def get_fee_text(self, pos, dyn, mempool, fee_rate):
|
||||||
|
"""Returns (text, tooltip) where
|
||||||
|
text is what we target: static fee / num blocks to confirm in / mempool depth
|
||||||
|
tooltip is the corresponding estimate (e.g. num blocks for a static fee)
|
||||||
|
"""
|
||||||
|
rate_str = (format_satoshis(fee_rate/1000, False, 0, 0, False) + ' sat/byte') if fee_rate is not None else 'unknown'
|
||||||
|
if dyn:
|
||||||
|
if mempool:
|
||||||
|
depth = self.depth_target(pos)
|
||||||
|
text = self.depth_tooltip(depth)
|
||||||
|
else:
|
||||||
|
eta = self.eta_target(pos)
|
||||||
|
text = self.eta_tooltip(eta)
|
||||||
|
tooltip = rate_str
|
||||||
|
else:
|
||||||
|
text = rate_str
|
||||||
|
if mempool and self.has_fee_mempool():
|
||||||
|
depth = self.fee_to_depth(fee_rate)
|
||||||
|
tooltip = self.depth_tooltip(depth)
|
||||||
|
elif not mempool and self.has_fee_etas():
|
||||||
|
eta = self.fee_to_eta(fee_rate)
|
||||||
|
tooltip = self.eta_tooltip(eta)
|
||||||
|
else:
|
||||||
|
tooltip = ''
|
||||||
|
return text, tooltip
|
||||||
|
|
||||||
|
def get_depth_level(self):
|
||||||
|
maxp = len(FEE_DEPTH_TARGETS) - 1
|
||||||
|
return min(maxp, self.get('depth_level', 2))
|
||||||
|
|
||||||
|
def get_fee_level(self):
|
||||||
|
maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
|
||||||
|
return min(maxp, self.get('fee_level', 2))
|
||||||
|
|
||||||
|
def get_fee_slider(self, dyn, mempool):
|
||||||
|
if dyn:
|
||||||
|
if mempool:
|
||||||
|
pos = self.get_depth_level()
|
||||||
|
maxp = len(FEE_DEPTH_TARGETS) - 1
|
||||||
|
fee_rate = self.depth_to_fee(pos)
|
||||||
|
else:
|
||||||
|
pos = self.get_fee_level()
|
||||||
|
maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
|
||||||
|
fee_rate = self.eta_to_fee(pos)
|
||||||
|
else:
|
||||||
|
fee_rate = self.fee_per_kb()
|
||||||
|
pos = self.static_fee_index(fee_rate)
|
||||||
|
maxp = 9
|
||||||
|
return maxp, pos, fee_rate
|
||||||
|
|
||||||
def static_fee(self, i):
|
def static_fee(self, i):
|
||||||
return self.fee_rates[i]
|
return FEERATE_STATIC_VALUES[i]
|
||||||
|
|
||||||
def static_fee_index(self, value):
|
def static_fee_index(self, value):
|
||||||
dist = list(map(lambda x: abs(x - value), self.fee_rates))
|
dist = list(map(lambda x: abs(x - value), FEERATE_STATIC_VALUES))
|
||||||
return min(range(len(dist)), key=dist.__getitem__)
|
return min(range(len(dist)), key=dist.__getitem__)
|
||||||
|
|
||||||
def has_fee_estimates(self):
|
def has_fee_etas(self):
|
||||||
return len(self.fee_estimates)==4
|
return len(self.fee_estimates) == 4
|
||||||
|
|
||||||
|
def has_fee_mempool(self):
|
||||||
|
return bool(self.mempool_fees)
|
||||||
|
|
||||||
|
def has_dynamic_fees_ready(self):
|
||||||
|
if self.use_mempool_fees():
|
||||||
|
return self.has_fee_mempool()
|
||||||
|
else:
|
||||||
|
return self.has_fee_etas()
|
||||||
|
|
||||||
# 'dynamic' fees are disabled - we use 'static' (but adjustable) fees
|
|
||||||
def is_dynfee(self):
|
def is_dynfee(self):
|
||||||
return self.get('dynamic_fees', False)
|
return bool(self.get('dynamic_fees', True))
|
||||||
|
|
||||||
|
def use_mempool_fees(self):
|
||||||
|
return bool(self.get('mempool_fees', False))
|
||||||
|
|
||||||
def fee_per_kb(self):
|
def fee_per_kb(self):
|
||||||
dyn = self.is_dynfee()
|
"""Returns sat/kvB fee to pay for a txn.
|
||||||
if dyn:
|
Note: might return None.
|
||||||
fee_rate = self.dynfee(self.get('fee_level', 2))
|
"""
|
||||||
|
if self.is_dynfee():
|
||||||
|
if self.use_mempool_fees():
|
||||||
|
fee_rate = self.depth_to_fee(self.get_depth_level())
|
||||||
|
else:
|
||||||
|
fee_rate = self.eta_to_fee(self.get_fee_level())
|
||||||
else:
|
else:
|
||||||
fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
|
fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE)
|
||||||
return fee_rate
|
return fee_rate
|
||||||
|
|
||||||
|
def fee_per_byte(self):
|
||||||
|
"""Returns sat/vB fee to pay for a txn.
|
||||||
|
Note: might return None.
|
||||||
|
"""
|
||||||
|
fee_per_kb = self.fee_per_kb()
|
||||||
|
return fee_per_kb / 1000 if fee_per_kb is not None else None
|
||||||
|
|
||||||
def estimate_fee(self, size):
|
def estimate_fee(self, size):
|
||||||
return self.estimate_fee_for_feerate(self.fee_per_kb(), size)
|
fee_per_kb = self.fee_per_kb()
|
||||||
|
if fee_per_kb is None:
|
||||||
|
raise NoDynamicFeeEstimates()
|
||||||
|
return self.estimate_fee_for_feerate(fee_per_kb, size)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def estimate_fee_for_feerate(cls, fee_per_kb, size):
|
def estimate_fee_for_feerate(cls, fee_per_kb, size):
|
||||||
return int(fee_per_kb * size / 1000.)
|
# note: We only allow integer sat/byte values atm.
|
||||||
|
# The GUI for simplicity reasons only displays integer sat/byte,
|
||||||
|
# and for the sake of consistency, we thus only use integer sat/byte in
|
||||||
|
# the backend too.
|
||||||
|
fee_per_byte = int(fee_per_kb / 1000)
|
||||||
|
return int(fee_per_byte * size)
|
||||||
|
|
||||||
def update_fee_estimates(self, key, value):
|
def update_fee_estimates(self, key, value):
|
||||||
self.fee_estimates[key] = value
|
self.fee_estimates[key] = value
|
||||||
|
@ -275,11 +469,7 @@ class SimpleConfig(PrintError):
|
||||||
Returns True if an update should be requested.
|
Returns True if an update should be requested.
|
||||||
"""
|
"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
prev_updates = self.fee_estimates_last_updated.values()
|
return now - self.last_time_fee_estimates_requested > 60
|
||||||
oldest_fee_time = min(prev_updates) if prev_updates else 0
|
|
||||||
stale_fees = now - oldest_fee_time > 7200
|
|
||||||
old_request = now - self.last_time_fee_estimates_requested > 60
|
|
||||||
return stale_fees and old_request
|
|
||||||
|
|
||||||
def requested_fee_estimates(self):
|
def requested_fee_estimates(self):
|
||||||
self.last_time_fee_estimates_requested = time.time()
|
self.last_time_fee_estimates_requested = time.time()
|
||||||
|
@ -291,21 +481,6 @@ class SimpleConfig(PrintError):
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
def read_system_config(path=SYSTEM_CONFIG_PATH):
|
|
||||||
"""Parse and return the system config settings in /etc/electrum.conf."""
|
|
||||||
result = {}
|
|
||||||
if os.path.exists(path):
|
|
||||||
import configparser
|
|
||||||
p = configparser.ConfigParser()
|
|
||||||
try:
|
|
||||||
p.read(path)
|
|
||||||
for k, v in p.items('client'):
|
|
||||||
result[k] = v
|
|
||||||
except (configparser.NoSectionError, configparser.MissingSectionHeaderError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def read_user_config(path):
|
def read_user_config(path):
|
||||||
"""Parse and store the user config settings in electrum.conf into user_config[]."""
|
"""Parse and store the user config settings in electrum.conf into user_config[]."""
|
||||||
if not path:
|
if not path:
|
||||||
|
|
101
lib/storage.py
101
lib/storage.py
|
@ -33,7 +33,7 @@ import pbkdf2, hmac, hashlib
|
||||||
import base64
|
import base64
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from .util import PrintError, profiler
|
from .util import PrintError, profiler, InvalidPassword
|
||||||
from .plugins import run_hook, plugin_loaders
|
from .plugins import run_hook, plugin_loaders
|
||||||
from .keystore import bip44_derivation
|
from .keystore import bip44_derivation
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
|
@ -56,6 +56,13 @@ def multisig_type(wallet_type):
|
||||||
match = [int(x) for x in match.group(1, 2)]
|
match = [int(x) for x in match.group(1, 2)]
|
||||||
return match
|
return match
|
||||||
|
|
||||||
|
def get_derivation_used_for_hw_device_encryption():
|
||||||
|
return ("m"
|
||||||
|
"/4541509'" # ascii 'ELE' as decimal ("BIP43 purpose")
|
||||||
|
"/1112098098'") # ascii 'BIE2' as decimal
|
||||||
|
|
||||||
|
# storage encryption version
|
||||||
|
STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW = range(0, 3)
|
||||||
|
|
||||||
class WalletStorage(PrintError):
|
class WalletStorage(PrintError):
|
||||||
|
|
||||||
|
@ -70,9 +77,11 @@ class WalletStorage(PrintError):
|
||||||
if self.file_exists():
|
if self.file_exists():
|
||||||
with open(self.path, "r") as f:
|
with open(self.path, "r") as f:
|
||||||
self.raw = f.read()
|
self.raw = f.read()
|
||||||
|
self._encryption_version = self._init_encryption_version()
|
||||||
if not self.is_encrypted():
|
if not self.is_encrypted():
|
||||||
self.load_data(self.raw)
|
self.load_data(self.raw)
|
||||||
else:
|
else:
|
||||||
|
self._encryption_version = STO_EV_PLAINTEXT
|
||||||
# avoid new wallets getting 'upgraded'
|
# avoid new wallets getting 'upgraded'
|
||||||
self.put('seed_version', FINAL_SEED_VERSION)
|
self.put('seed_version', FINAL_SEED_VERSION)
|
||||||
|
|
||||||
|
@ -106,11 +115,47 @@ class WalletStorage(PrintError):
|
||||||
if self.requires_upgrade():
|
if self.requires_upgrade():
|
||||||
self.upgrade()
|
self.upgrade()
|
||||||
|
|
||||||
|
def is_past_initial_decryption(self):
|
||||||
|
"""Return if storage is in a usable state for normal operations.
|
||||||
|
|
||||||
|
The value is True exactly
|
||||||
|
if encryption is disabled completely (self.is_encrypted() == False),
|
||||||
|
or if encryption is enabled but the contents have already been decrypted.
|
||||||
|
"""
|
||||||
|
return bool(self.data)
|
||||||
|
|
||||||
def is_encrypted(self):
|
def is_encrypted(self):
|
||||||
|
"""Return if storage encryption is currently enabled."""
|
||||||
|
return self.get_encryption_version() != STO_EV_PLAINTEXT
|
||||||
|
|
||||||
|
def is_encrypted_with_user_pw(self):
|
||||||
|
return self.get_encryption_version() == STO_EV_USER_PW
|
||||||
|
|
||||||
|
def is_encrypted_with_hw_device(self):
|
||||||
|
return self.get_encryption_version() == STO_EV_XPUB_PW
|
||||||
|
|
||||||
|
def get_encryption_version(self):
|
||||||
|
"""Return the version of encryption used for this storage.
|
||||||
|
|
||||||
|
0: plaintext / no encryption
|
||||||
|
|
||||||
|
ECIES, private key derived from a password,
|
||||||
|
1: password is provided by user
|
||||||
|
2: password is derived from an xpub; used with hw wallets
|
||||||
|
"""
|
||||||
|
return self._encryption_version
|
||||||
|
|
||||||
|
def _init_encryption_version(self):
|
||||||
try:
|
try:
|
||||||
return base64.b64decode(self.raw)[0:4] == b'BIE1'
|
magic = base64.b64decode(self.raw)[0:4]
|
||||||
|
if magic == b'BIE1':
|
||||||
|
return STO_EV_USER_PW
|
||||||
|
elif magic == b'BIE2':
|
||||||
|
return STO_EV_XPUB_PW
|
||||||
|
else:
|
||||||
|
return STO_EV_PLAINTEXT
|
||||||
except:
|
except:
|
||||||
return False
|
return STO_EV_PLAINTEXT
|
||||||
|
|
||||||
def file_exists(self):
|
def file_exists(self):
|
||||||
return self.path and os.path.exists(self.path)
|
return self.path and os.path.exists(self.path)
|
||||||
|
@ -120,20 +165,50 @@ class WalletStorage(PrintError):
|
||||||
ec_key = bitcoin.EC_KEY(secret)
|
ec_key = bitcoin.EC_KEY(secret)
|
||||||
return ec_key
|
return ec_key
|
||||||
|
|
||||||
|
def _get_encryption_magic(self):
|
||||||
|
v = self._encryption_version
|
||||||
|
if v == STO_EV_USER_PW:
|
||||||
|
return b'BIE1'
|
||||||
|
elif v == STO_EV_XPUB_PW:
|
||||||
|
return b'BIE2'
|
||||||
|
else:
|
||||||
|
raise Exception('no encryption magic for version: %s' % v)
|
||||||
|
|
||||||
def decrypt(self, password):
|
def decrypt(self, password):
|
||||||
ec_key = self.get_key(password)
|
ec_key = self.get_key(password)
|
||||||
s = zlib.decompress(ec_key.decrypt_message(self.raw)) if self.raw else None
|
if self.raw:
|
||||||
|
enc_magic = self._get_encryption_magic()
|
||||||
|
s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic))
|
||||||
|
else:
|
||||||
|
s = None
|
||||||
self.pubkey = ec_key.get_public_key()
|
self.pubkey = ec_key.get_public_key()
|
||||||
s = s.decode('utf8')
|
s = s.decode('utf8')
|
||||||
self.load_data(s)
|
self.load_data(s)
|
||||||
|
|
||||||
def set_password(self, password, encrypt):
|
def check_password(self, password):
|
||||||
self.put('use_encryption', bool(password))
|
"""Raises an InvalidPassword exception on invalid password"""
|
||||||
if encrypt and password:
|
if not self.is_encrypted():
|
||||||
|
return
|
||||||
|
if self.pubkey and self.pubkey != self.get_key(password).get_public_key():
|
||||||
|
raise InvalidPassword()
|
||||||
|
|
||||||
|
def set_keystore_encryption(self, enable):
|
||||||
|
self.put('use_encryption', enable)
|
||||||
|
|
||||||
|
def set_password(self, password, enc_version=None):
|
||||||
|
"""Set a password to be used for encrypting this storage."""
|
||||||
|
if enc_version is None:
|
||||||
|
enc_version = self._encryption_version
|
||||||
|
if password and enc_version != STO_EV_PLAINTEXT:
|
||||||
ec_key = self.get_key(password)
|
ec_key = self.get_key(password)
|
||||||
self.pubkey = ec_key.get_public_key()
|
self.pubkey = ec_key.get_public_key()
|
||||||
|
self._encryption_version = enc_version
|
||||||
else:
|
else:
|
||||||
self.pubkey = None
|
self.pubkey = None
|
||||||
|
self._encryption_version = STO_EV_PLAINTEXT
|
||||||
|
# make sure next storage.write() saves changes
|
||||||
|
with self.lock:
|
||||||
|
self.modified = True
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -175,7 +250,8 @@ class WalletStorage(PrintError):
|
||||||
if self.pubkey:
|
if self.pubkey:
|
||||||
s = bytes(s, 'utf8')
|
s = bytes(s, 'utf8')
|
||||||
c = zlib.compress(s)
|
c = zlib.compress(s)
|
||||||
s = bitcoin.encrypt_message(c, self.pubkey)
|
enc_magic = self._get_encryption_magic()
|
||||||
|
s = bitcoin.encrypt_message(c, self.pubkey, enc_magic)
|
||||||
s = s.decode('utf8')
|
s = s.decode('utf8')
|
||||||
|
|
||||||
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
||||||
|
@ -263,6 +339,9 @@ class WalletStorage(PrintError):
|
||||||
self.write()
|
self.write()
|
||||||
|
|
||||||
def convert_wallet_type(self):
|
def convert_wallet_type(self):
|
||||||
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
|
return
|
||||||
|
|
||||||
wallet_type = self.get('wallet_type')
|
wallet_type = self.get('wallet_type')
|
||||||
if wallet_type == 'btchip': wallet_type = 'ledger'
|
if wallet_type == 'btchip': wallet_type = 'ledger'
|
||||||
if self.get('keystore') or self.get('x1/') or wallet_type=='imported':
|
if self.get('keystore') or self.get('x1/') or wallet_type=='imported':
|
||||||
|
@ -446,6 +525,9 @@ class WalletStorage(PrintError):
|
||||||
self.put('seed_version', 16)
|
self.put('seed_version', 16)
|
||||||
|
|
||||||
def convert_imported(self):
|
def convert_imported(self):
|
||||||
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
|
return
|
||||||
|
|
||||||
# '/x' is the internal ID for imported accounts
|
# '/x' is the internal ID for imported accounts
|
||||||
d = self.get('accounts', {}).get('/x', {}).get('imported',{})
|
d = self.get('accounts', {}).get('/x', {}).get('imported',{})
|
||||||
if not d:
|
if not d:
|
||||||
|
@ -472,6 +554,9 @@ class WalletStorage(PrintError):
|
||||||
raise BaseException('no addresses or privkeys')
|
raise BaseException('no addresses or privkeys')
|
||||||
|
|
||||||
def convert_account(self):
|
def convert_account(self):
|
||||||
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
|
return
|
||||||
|
|
||||||
self.put('accounts', None)
|
self.put('accounts', None)
|
||||||
|
|
||||||
def _is_upgrade_method_needed(self, min_version, max_version):
|
def _is_upgrade_method_needed(self, min_version, max_version):
|
||||||
|
|
|
@ -84,11 +84,13 @@ class Synchronizer(ThreadJob):
|
||||||
return bh2u(hashlib.sha256(status.encode('ascii')).digest())
|
return bh2u(hashlib.sha256(status.encode('ascii')).digest())
|
||||||
|
|
||||||
def on_address_status(self, response):
|
def on_address_status(self, response):
|
||||||
|
if self.wallet.synchronizer is None:
|
||||||
|
return # we have been killed, this was just an orphan callback
|
||||||
params, result = self.parse_response(response)
|
params, result = self.parse_response(response)
|
||||||
if not params:
|
if not params:
|
||||||
return
|
return
|
||||||
addr = params[0]
|
addr = params[0]
|
||||||
history = self.wallet.get_address_history(addr)
|
history = self.wallet.history.get(addr, [])
|
||||||
if self.get_status(history) != result:
|
if self.get_status(history) != result:
|
||||||
if self.requested_histories.get(addr) is None:
|
if self.requested_histories.get(addr) is None:
|
||||||
self.requested_histories[addr] = result
|
self.requested_histories[addr] = result
|
||||||
|
@ -98,6 +100,8 @@ class Synchronizer(ThreadJob):
|
||||||
self.requested_addrs.remove(addr)
|
self.requested_addrs.remove(addr)
|
||||||
|
|
||||||
def on_address_history(self, response):
|
def on_address_history(self, response):
|
||||||
|
if self.wallet.synchronizer is None:
|
||||||
|
return # we have been killed, this was just an orphan callback
|
||||||
params, result = self.parse_response(response)
|
params, result = self.parse_response(response)
|
||||||
if not params:
|
if not params:
|
||||||
return
|
return
|
||||||
|
@ -127,6 +131,8 @@ class Synchronizer(ThreadJob):
|
||||||
self.requested_histories.pop(addr)
|
self.requested_histories.pop(addr)
|
||||||
|
|
||||||
def tx_response(self, response):
|
def tx_response(self, response):
|
||||||
|
if self.wallet.synchronizer is None:
|
||||||
|
return # we have been killed, this was just an orphan callback
|
||||||
params, result = self.parse_response(response)
|
params, result = self.parse_response(response)
|
||||||
if not params:
|
if not params:
|
||||||
return
|
return
|
||||||
|
|
|
@ -11,8 +11,9 @@ from lib.bitcoin import (
|
||||||
var_int, op_push, address_to_script, regenerate_key,
|
var_int, op_push, address_to_script, regenerate_key,
|
||||||
verify_message, deserialize_privkey, serialize_privkey, is_segwit_address,
|
verify_message, deserialize_privkey, serialize_privkey, is_segwit_address,
|
||||||
is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub,
|
is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub,
|
||||||
xpub_type, is_xprv, is_bip32_derivation, seed_type, NetworkConstants)
|
xpub_type, is_xprv, is_bip32_derivation, seed_type)
|
||||||
from lib.util import bfh
|
from lib.util import bfh
|
||||||
|
from lib import constants
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ecdsa
|
import ecdsa
|
||||||
|
@ -168,12 +169,12 @@ class Test_bitcoin_testnet(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
NetworkConstants.set_testnet()
|
constants.set_testnet()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
NetworkConstants.set_mainnet()
|
constants.set_mainnet()
|
||||||
|
|
||||||
def test_address_to_script(self):
|
def test_address_to_script(self):
|
||||||
# bech32 native segwit
|
# bech32 native segwit
|
||||||
|
@ -271,6 +272,7 @@ class Test_keyImport(unittest.TestCase):
|
||||||
|
|
||||||
priv_pub_addr = (
|
priv_pub_addr = (
|
||||||
{'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
|
{'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
|
||||||
|
'exported_privkey': 'p2pkh:KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
|
||||||
'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997',
|
'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997',
|
||||||
'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR',
|
'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR',
|
||||||
'minikey' : False,
|
'minikey' : False,
|
||||||
|
@ -278,7 +280,17 @@ class Test_keyImport(unittest.TestCase):
|
||||||
'compressed': True,
|
'compressed': True,
|
||||||
'addr_encoding': 'base58',
|
'addr_encoding': 'base58',
|
||||||
'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'},
|
'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'},
|
||||||
|
{'priv': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',
|
||||||
|
'exported_privkey': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',
|
||||||
|
'pub': '0352d78b4b37e0f6d4e164423436f2925fa57817467178eca550a88f2821973c41',
|
||||||
|
'address': '1GXgZ5Qi6gmXTHVSpUPZLy4Ci2nbfb3ZNb',
|
||||||
|
'minikey': False,
|
||||||
|
'txin_type': 'p2pkh',
|
||||||
|
'compressed': True,
|
||||||
|
'addr_encoding': 'base58',
|
||||||
|
'scripthash': 'a9b2a76fc196c553b352186dfcca81fcf323a721cd8431328f8e9d54216818c1'},
|
||||||
{'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
|
{'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
|
||||||
|
'exported_privkey': 'p2pkh:5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
|
||||||
'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f',
|
'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f',
|
||||||
'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6',
|
'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6',
|
||||||
'minikey': False,
|
'minikey': False,
|
||||||
|
@ -286,7 +298,17 @@ class Test_keyImport(unittest.TestCase):
|
||||||
'compressed': False,
|
'compressed': False,
|
||||||
'addr_encoding': 'base58',
|
'addr_encoding': 'base58',
|
||||||
'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'},
|
'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'},
|
||||||
|
{'priv': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',
|
||||||
|
'exported_privkey': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',
|
||||||
|
'pub': '048f0431b0776e8210376c81280011c2b68be43194cb00bd47b7e9aa66284b713ce09556cde3fee606051a07613f3c159ef3953b8927c96ae3dae94a6ba4182e0e',
|
||||||
|
'address': '147kiRHHm9fqeMQSgqf4k35XzuWLP9fmmS',
|
||||||
|
'minikey': False,
|
||||||
|
'txin_type': 'p2pkh',
|
||||||
|
'compressed': False,
|
||||||
|
'addr_encoding': 'base58',
|
||||||
|
'scripthash': '6dd2e07ad2de9ba8eec4bbe8467eb53f8845acff0d9e6f5627391acc22ff62df'},
|
||||||
{'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz',
|
{'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz',
|
||||||
|
'exported_privkey': 'p2wpkh-p2sh:Kz9XebiCXL2BZzhYJViiHDzn5iup1povWV8aqstzWU4sz1K5nVva',
|
||||||
'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81',
|
'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81',
|
||||||
'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7',
|
'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7',
|
||||||
'minikey': False,
|
'minikey': False,
|
||||||
|
@ -294,7 +316,17 @@ class Test_keyImport(unittest.TestCase):
|
||||||
'compressed': True,
|
'compressed': True,
|
||||||
'addr_encoding': 'base58',
|
'addr_encoding': 'base58',
|
||||||
'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'},
|
'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'},
|
||||||
|
{'priv': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW',
|
||||||
|
'exported_privkey': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW',
|
||||||
|
'pub': '0229da20a15b3363b2c28e3c5093c180b56c439df0b968a970366bb1f38435361e',
|
||||||
|
'address': '3C79goMwT7zSTjXnPoCg6VFGAnUpZAkyus',
|
||||||
|
'minikey': False,
|
||||||
|
'txin_type': 'p2wpkh-p2sh',
|
||||||
|
'compressed': True,
|
||||||
|
'addr_encoding': 'base58',
|
||||||
|
'scripthash': '714bf6bfe1083e69539f40d4c7a7dca85d187471b35642e55f20d7e866494cf7'},
|
||||||
{'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj',
|
{'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj',
|
||||||
|
'exported_privkey': 'p2wpkh:Kz6SuyPM5VktY5dr2d2YqdVgBA6LCWkiHqXJaC3BzxnMPSUuYzmF',
|
||||||
'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b',
|
'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b',
|
||||||
'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue',
|
'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue',
|
||||||
'minikey': False,
|
'minikey': False,
|
||||||
|
@ -302,8 +334,18 @@ class Test_keyImport(unittest.TestCase):
|
||||||
'compressed': True,
|
'compressed': True,
|
||||||
'addr_encoding': 'bech32',
|
'addr_encoding': 'bech32',
|
||||||
'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'},
|
'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'},
|
||||||
|
{'priv': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo',
|
||||||
|
'exported_privkey': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo',
|
||||||
|
'pub': '038c57657171c1f73e34d5b3971d05867d50221ad94980f7e87cbc2344425e6a1e',
|
||||||
|
'address': 'bc1qpakeeg4d9ydyjxd8paqrw4xy9htsg532xzxn50',
|
||||||
|
'minikey': False,
|
||||||
|
'txin_type': 'p2wpkh',
|
||||||
|
'compressed': True,
|
||||||
|
'addr_encoding': 'bech32',
|
||||||
|
'scripthash': '242f02adde84ebb2a7dd778b2f3a81b3826f111da4d8960d826d7a4b816cb261'},
|
||||||
# from http://bitscan.com/articles/security/spotlight-on-mini-private-keys
|
# from http://bitscan.com/articles/security/spotlight-on-mini-private-keys
|
||||||
{'priv': 'SzavMBLoXU6kDrqtUVmffv',
|
{'priv': 'SzavMBLoXU6kDrqtUVmffv',
|
||||||
|
'exported_privkey': 'p2pkh:L53fCHmQhbNp1B4JipfBtfeHZH7cAibzG9oK19XfiFzxHgAkz6JK',
|
||||||
'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9',
|
'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9',
|
||||||
'address': '19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR',
|
'address': '19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR',
|
||||||
'minikey': True,
|
'minikey': True,
|
||||||
|
@ -344,6 +386,7 @@ class Test_keyImport(unittest.TestCase):
|
||||||
def test_is_private_key(self):
|
def test_is_private_key(self):
|
||||||
for priv_details in self.priv_pub_addr:
|
for priv_details in self.priv_pub_addr:
|
||||||
self.assertTrue(is_private_key(priv_details['priv']))
|
self.assertTrue(is_private_key(priv_details['priv']))
|
||||||
|
self.assertTrue(is_private_key(priv_details['exported_privkey']))
|
||||||
self.assertFalse(is_private_key(priv_details['pub']))
|
self.assertFalse(is_private_key(priv_details['pub']))
|
||||||
self.assertFalse(is_private_key(priv_details['address']))
|
self.assertFalse(is_private_key(priv_details['address']))
|
||||||
self.assertFalse(is_private_key("not a privkey"))
|
self.assertFalse(is_private_key("not a privkey"))
|
||||||
|
@ -352,8 +395,7 @@ class Test_keyImport(unittest.TestCase):
|
||||||
for priv_details in self.priv_pub_addr:
|
for priv_details in self.priv_pub_addr:
|
||||||
txin_type, privkey, compressed = deserialize_privkey(priv_details['priv'])
|
txin_type, privkey, compressed = deserialize_privkey(priv_details['priv'])
|
||||||
priv2 = serialize_privkey(privkey, compressed, txin_type)
|
priv2 = serialize_privkey(privkey, compressed, txin_type)
|
||||||
if not priv_details['minikey']:
|
self.assertEqual(priv_details['exported_privkey'], priv2)
|
||||||
self.assertEqual(priv_details['priv'], priv2)
|
|
||||||
|
|
||||||
def test_address_to_scripthash(self):
|
def test_address_to_scripthash(self):
|
||||||
for priv_details in self.priv_pub_addr:
|
for priv_details in self.priv_pub_addr:
|
||||||
|
|
|
@ -6,8 +6,7 @@ import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from lib.simple_config import (SimpleConfig, read_system_config,
|
from lib.simple_config import (SimpleConfig, read_user_config)
|
||||||
read_user_config)
|
|
||||||
|
|
||||||
|
|
||||||
class Test_SimpleConfig(unittest.TestCase):
|
class Test_SimpleConfig(unittest.TestCase):
|
||||||
|
@ -37,18 +36,15 @@ class Test_SimpleConfig(unittest.TestCase):
|
||||||
|
|
||||||
def test_simple_config_key_rename(self):
|
def test_simple_config_key_rename(self):
|
||||||
"""auto_cycle was renamed auto_connect"""
|
"""auto_cycle was renamed auto_connect"""
|
||||||
fake_read_system = lambda : {}
|
|
||||||
fake_read_user = lambda _: {"auto_cycle": True}
|
fake_read_user = lambda _: {"auto_cycle": True}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
self.assertEqual(config.get("auto_connect"), True)
|
self.assertEqual(config.get("auto_connect"), True)
|
||||||
self.assertEqual(config.get("auto_cycle"), None)
|
self.assertEqual(config.get("auto_cycle"), None)
|
||||||
fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True}
|
fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True}
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
self.assertEqual(config.get("auto_connect"), False)
|
self.assertEqual(config.get("auto_connect"), False)
|
||||||
|
@ -57,110 +53,51 @@ class Test_SimpleConfig(unittest.TestCase):
|
||||||
def test_simple_config_command_line_overrides_everything(self):
|
def test_simple_config_command_line_overrides_everything(self):
|
||||||
"""Options passed by command line override all other configuration
|
"""Options passed by command line override all other configuration
|
||||||
sources"""
|
sources"""
|
||||||
fake_read_system = lambda : {"electrum_path": "a"}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": "b"}
|
fake_read_user = lambda _: {"electrum_path": "b"}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
self.assertEqual(self.options.get("electrum_path"),
|
self.assertEqual(self.options.get("electrum_path"),
|
||||||
config.get("electrum_path"))
|
config.get("electrum_path"))
|
||||||
|
|
||||||
def test_simple_config_user_config_overrides_system_config(self):
|
|
||||||
"""Options passed in user config override system config."""
|
|
||||||
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": "b"}
|
|
||||||
read_user_dir = lambda : self.user_dir
|
|
||||||
config = SimpleConfig(options={},
|
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
|
||||||
read_user_dir_function=read_user_dir)
|
|
||||||
self.assertEqual("b", config.get("electrum_path"))
|
|
||||||
|
|
||||||
def test_simple_config_system_config_ignored_if_portable(self):
|
|
||||||
"""If electrum is started with the "portable" flag, system
|
|
||||||
configuration is completely ignored."""
|
|
||||||
fake_read_system = lambda : {"some_key": "some_value"}
|
|
||||||
fake_read_user = lambda _: {}
|
|
||||||
read_user_dir = lambda : self.user_dir
|
|
||||||
config = SimpleConfig(options={"portable": True},
|
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
|
||||||
read_user_dir_function=read_user_dir)
|
|
||||||
self.assertEqual(config.get("some_key"), None)
|
|
||||||
|
|
||||||
def test_simple_config_user_config_is_used_if_others_arent_specified(self):
|
def test_simple_config_user_config_is_used_if_others_arent_specified(self):
|
||||||
"""If no system-wide configuration and no command-line options are
|
"""If no system-wide configuration and no command-line options are
|
||||||
specified, the user configuration is used instead."""
|
specified, the user configuration is used instead."""
|
||||||
fake_read_system = lambda : {}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
|
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options={},
|
config = SimpleConfig(options={},
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
self.assertEqual(self.options.get("electrum_path"),
|
self.assertEqual(self.options.get("electrum_path"),
|
||||||
config.get("electrum_path"))
|
config.get("electrum_path"))
|
||||||
|
|
||||||
def test_cannot_set_options_passed_by_command_line(self):
|
def test_cannot_set_options_passed_by_command_line(self):
|
||||||
fake_read_system = lambda : {}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": "b"}
|
fake_read_user = lambda _: {"electrum_path": "b"}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
config.set_key("electrum_path", "c")
|
config.set_key("electrum_path", "c")
|
||||||
self.assertEqual(self.options.get("electrum_path"),
|
self.assertEqual(self.options.get("electrum_path"),
|
||||||
config.get("electrum_path"))
|
config.get("electrum_path"))
|
||||||
|
|
||||||
def test_can_set_options_from_system_config(self):
|
|
||||||
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
|
|
||||||
fake_read_user = lambda _: {}
|
|
||||||
read_user_dir = lambda : self.user_dir
|
|
||||||
config = SimpleConfig(options={},
|
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
|
||||||
read_user_dir_function=read_user_dir)
|
|
||||||
config.set_key("electrum_path", "c")
|
|
||||||
self.assertEqual("c", config.get("electrum_path"))
|
|
||||||
|
|
||||||
def test_can_set_options_set_in_user_config(self):
|
def test_can_set_options_set_in_user_config(self):
|
||||||
another_path = tempfile.mkdtemp()
|
another_path = tempfile.mkdtemp()
|
||||||
fake_read_system = lambda : {}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
|
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options={},
|
config = SimpleConfig(options={},
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
|
||||||
read_user_dir_function=read_user_dir)
|
|
||||||
config.set_key("electrum_path", another_path)
|
|
||||||
self.assertEqual(another_path, config.get("electrum_path"))
|
|
||||||
|
|
||||||
def test_can_set_options_from_system_config_if_portable(self):
|
|
||||||
"""If the "portable" flag is set, the user can overwrite system
|
|
||||||
configuration options."""
|
|
||||||
another_path = tempfile.mkdtemp()
|
|
||||||
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
|
|
||||||
fake_read_user = lambda _: {}
|
|
||||||
read_user_dir = lambda : self.user_dir
|
|
||||||
config = SimpleConfig(options={"portable": True},
|
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
config.set_key("electrum_path", another_path)
|
config.set_key("electrum_path", another_path)
|
||||||
self.assertEqual(another_path, config.get("electrum_path"))
|
self.assertEqual(another_path, config.get("electrum_path"))
|
||||||
|
|
||||||
def test_user_config_is_not_written_with_read_only_config(self):
|
def test_user_config_is_not_written_with_read_only_config(self):
|
||||||
"""The user config does not contain command-line options or system
|
"""The user config does not contain command-line options when saved."""
|
||||||
options when saved."""
|
|
||||||
fake_read_system = lambda : {"something": "b"}
|
|
||||||
fake_read_user = lambda _: {"something": "a"}
|
fake_read_user = lambda _: {"something": "a"}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
self.options.update({"something": "c"})
|
self.options.update({"something": "c"})
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
config.save_user_config()
|
config.save_user_config()
|
||||||
|
@ -168,48 +105,10 @@ class Test_SimpleConfig(unittest.TestCase):
|
||||||
with open(os.path.join(self.electrum_dir, "config"), "r") as f:
|
with open(os.path.join(self.electrum_dir, "config"), "r") as f:
|
||||||
contents = f.read()
|
contents = f.read()
|
||||||
result = ast.literal_eval(contents)
|
result = ast.literal_eval(contents)
|
||||||
|
result.pop('config_version', None)
|
||||||
self.assertEqual({"something": "a"}, result)
|
self.assertEqual({"something": "a"}, result)
|
||||||
|
|
||||||
|
|
||||||
class TestSystemConfig(unittest.TestCase):
|
|
||||||
|
|
||||||
sample_conf = """
|
|
||||||
[client]
|
|
||||||
gap_limit = 5
|
|
||||||
|
|
||||||
[something_else]
|
|
||||||
everything = 42
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestSystemConfig, self).setUp()
|
|
||||||
self.thefile = tempfile.mkstemp(suffix=".electrum.test.conf")[1]
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super(TestSystemConfig, self).tearDown()
|
|
||||||
os.remove(self.thefile)
|
|
||||||
|
|
||||||
def test_read_system_config_file_does_not_exist(self):
|
|
||||||
somefile = "/foo/I/do/not/exist/electrum.conf"
|
|
||||||
result = read_system_config(somefile)
|
|
||||||
self.assertEqual({}, result)
|
|
||||||
|
|
||||||
def test_read_system_config_file_returns_file_options(self):
|
|
||||||
with open(self.thefile, "w") as f:
|
|
||||||
f.write(self.sample_conf)
|
|
||||||
|
|
||||||
result = read_system_config(self.thefile)
|
|
||||||
self.assertEqual({"gap_limit": "5"}, result)
|
|
||||||
|
|
||||||
def test_read_system_config_file_no_sections(self):
|
|
||||||
|
|
||||||
with open(self.thefile, "w") as f:
|
|
||||||
f.write("gap_limit = 5") # The file has no sections at all
|
|
||||||
|
|
||||||
result = read_system_config(self.thefile)
|
|
||||||
self.assertEqual({}, result)
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserConfig(unittest.TestCase):
|
class TestUserConfig(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -231,6 +231,389 @@ class TestTransaction(unittest.TestCase):
|
||||||
tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000')
|
tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000')
|
||||||
self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid())
|
self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_input_p2wsh_p2sh_not_multisig(self):
|
||||||
|
tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000')
|
||||||
|
self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid())
|
||||||
|
|
||||||
|
# input: p2sh, not multisig
|
||||||
|
def test_txid_regression_issue_3899(self):
|
||||||
|
tx = transaction.Transaction('0100000004328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c010000000b0009630330472d5fae685bffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c020000000b0009630359646d5fae6858ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c030000000b000963034bd4715fae6854ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c040000000b000963036de8705fae6860ffffffff0130750000000000001976a914b5abca61d20f9062fb1fdbb880d9d93bac36675188ac00000000')
|
||||||
|
self.assertEqual('f570d5d1e965ee61bcc7005f8fefb1d3abbed9d7ddbe035e2a68fa07e5fc4a0d', tx.txid())
|
||||||
|
|
||||||
|
|
||||||
|
# these transactions are from Bitcoin Core unit tests --->
|
||||||
|
# https://github.com/bitcoin/bitcoin/blob/11376b5583a283772c82f6d32d0007cdbf5b8ef0/src/test/data/tx_valid.json
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0001(self):
|
||||||
|
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
|
||||||
|
self.assertEqual('23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0002(self):
|
||||||
|
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
|
||||||
|
self.assertEqual('fcabc409d8e685da28536e1e5ccc91264d755cd4c57ed4cae3dbaa4d3b93e8ed', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0003(self):
|
||||||
|
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
|
||||||
|
self.assertEqual('c9aa95f2c48175fdb70b34c23f1c3fc44f869b073a6f79b1343fbce30c3cb575', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0004(self):
|
||||||
|
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
|
||||||
|
self.assertEqual('da94fda32b55deb40c3ed92e135d69df7efc4ee6665e0beb07ef500f407c9fd2', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0005(self):
|
||||||
|
tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000')
|
||||||
|
self.assertEqual('f76f897b206e4f78d60fe40f2ccb542184cfadc34354d3bb9bdc30cc2f432b86', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0006(self):
|
||||||
|
tx = transaction.Transaction('01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d00000000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac00000000')
|
||||||
|
self.assertEqual('c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0007(self):
|
||||||
|
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000')
|
||||||
|
self.assertEqual('e41ffe19dff3cbedb413a2ca3fbbcd05cb7fd7397ffa65052f8928aa9c700092', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0008(self):
|
||||||
|
tx = transaction.Transaction('01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000')
|
||||||
|
self.assertEqual('f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0009(self):
|
||||||
|
tx = transaction.Transaction('01000000020002000000000000000000000000000000000000000000000000000000000000000000000151ffffffff0001000000000000000000000000000000000000000000000000000000000000000000006b483045022100c9cdd08798a28af9d1baf44a6c77bcc7e279f47dc487c8c899911bc48feaffcc0220503c5c50ae3998a733263c5c0f7061b483e2b56c4c41b456e7d2f5a78a74c077032102d5c25adb51b61339d2b05315791e21bbe80ea470a49db0135720983c905aace0ffffffff010000000000000000015100000000')
|
||||||
|
self.assertEqual('b56471690c3ff4f7946174e51df68b47455a0d29344c351377d712e6d00eabe5', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0010(self):
|
||||||
|
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000')
|
||||||
|
self.assertEqual('99517e5b47533453cc7daa332180f578be68b80370ecfe84dbfff7f19d791da4', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0011(self):
|
||||||
|
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100c66c9cdf4c43609586d15424c54707156e316d88b0a1534c9e6b0d4f311406310221009c0fe51dbc9c4ab7cc25d3fdbeccf6679fe6827f08edf2b4a9f16ee3eb0e438a0123210338e8034509af564c62644c07691942e0c056752008a173c89f60ab2a88ac2ebfacffffffff010000000000000000015100000000')
|
||||||
|
self.assertEqual('ab097537b528871b9b64cb79a769ae13c3c3cd477cc9dddeebe657eabd7fdcea', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0012(self):
|
||||||
|
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010040075af0750700015100000000')
|
||||||
|
self.assertEqual('4d163e00f1966e9a1eab8f9374c3e37f4deb4857c247270e25f7d79a999d2dc9', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0013(self):
|
||||||
|
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510000000000000000015100000000')
|
||||||
|
self.assertEqual('9fe2ef9dde70e15d78894a4800b7df3bbfb1addb9a6f7d7c204492fdb6ee6cc4', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0014(self):
|
||||||
|
tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025151ffffffff010000000000000000015100000000')
|
||||||
|
self.assertEqual('99d3825137602e577aeaf6a2e3c9620fd0e605323dc5265da4a570593be791d4', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0015(self):
|
||||||
|
tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6451515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151ffffffff010000000000000000015100000000')
|
||||||
|
self.assertEqual('c0d67409923040cc766bbea12e4c9154393abef706db065ac2e07d91a9ba4f84', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0016(self):
|
||||||
|
tx = transaction.Transaction('010000000200010000000000000000000000000000000000000000000000000000000000000000000049483045022100d180fd2eb9140aeb4210c9204d3f358766eb53842b2a9473db687fa24b12a3cc022079781799cd4f038b85135bbe49ec2b57f306b2bb17101b17f71f000fcab2b6fb01ffffffff0002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000')
|
||||||
|
self.assertEqual('c610d85d3d5fdf5046be7f123db8a0890cee846ee58de8a44667cfd1ab6b8666', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0017(self):
|
||||||
|
tx = transaction.Transaction('01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df101010000000002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000')
|
||||||
|
self.assertEqual('a647a7b3328d2c698bfa1ee2dd4e5e05a6cea972e764ccb9bd29ea43817ca64f', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0018(self):
|
||||||
|
tx = transaction.Transaction('010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c59da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbea097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf950650802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e18217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a3788dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9cfdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa34b26f62db4a88ac00000000')
|
||||||
|
self.assertEqual('afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0019(self):
|
||||||
|
tx = transaction.Transaction('01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe0000483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa0148304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f4014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000')
|
||||||
|
self.assertEqual('f4b05f978689c89000f729cae187dcfbe64c9819af67a4f05c0b4d59e717d64d', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0020(self):
|
||||||
|
tx = transaction.Transaction('0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000')
|
||||||
|
self.assertEqual('cc60b1f899ec0a69b7c3f25ddf32c4524096a9c5b01cbd84c6d0312a0c478984', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0021(self):
|
||||||
|
tx = transaction.Transaction('01000000012c651178faca83be0b81c8c1375c4b0ad38d53c8fe1b1c4255f5e795c25792220000000049483045022100d6044562284ac76c985018fc4a90127847708c9edb280996c507b28babdc4b2a02203d74eca3f1a4d1eea7ff77b528fde6d5dc324ec2dbfdb964ba885f643b9704cd01ffffffff010100000000000000232102c2410f8891ae918cab4ffc4bb4a3b0881be67c7a1e7faa8b5acf9ab8932ec30cac00000000')
|
||||||
|
self.assertEqual('1edc7f214659d52c731e2016d258701911bd62a0422f72f6c87a1bc8dd3f8667', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0022(self):
|
||||||
|
tx = transaction.Transaction('0100000001f725ea148d92096a79b1709611e06e94c63c4ef61cbae2d9b906388efd3ca99c000000000100ffffffff0101000000000000002321028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02ac00000000')
|
||||||
|
self.assertEqual('018adb7133fde63add9149a2161802a1bcf4bdf12c39334e880c073480eda2ff', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0023(self):
|
||||||
|
tx = transaction.Transaction('0100000001be599efaa4148474053c2fa031c7262398913f1dc1d9ec201fd44078ed004e44000000004900473044022022b29706cb2ed9ef0cb3c97b72677ca2dfd7b4160f7b4beb3ba806aa856c401502202d1e52582412eba2ed474f1f437a427640306fd3838725fab173ade7fe4eae4a01ffffffff010100000000000000232103ac4bba7e7ca3e873eea49e08132ad30c7f03640b6539e9b59903cf14fd016bbbac00000000')
|
||||||
|
self.assertEqual('1464caf48c708a6cc19a296944ded9bb7f719c9858986d2501cf35068b9ce5a2', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0024(self):
|
||||||
|
tx = transaction.Transaction('010000000112b66d5e8c7d224059e946749508efea9d66bf8d0c83630f080cf30be8bb6ae100000000490047304402206ffe3f14caf38ad5c1544428e99da76ffa5455675ec8d9780fac215ca17953520220779502985e194d84baa36b9bd40a0dbd981163fa191eb884ae83fc5bd1c86b1101ffffffff010100000000000000232103905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9fac00000000')
|
||||||
|
self.assertEqual('1fb73fbfc947d52f5d80ba23b67c06a232ad83fdd49d1c0a657602f03fbe8f7a', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0025(self):
|
||||||
|
tx = transaction.Transaction('0100000001b0ef70cc644e0d37407e387e73bfad598d852a5aa6d691d72b2913cebff4bceb000000004a00473044022068cd4851fc7f9a892ab910df7a24e616f293bcb5c5fbdfbc304a194b26b60fba022078e6da13d8cb881a22939b952c24f88b97afd06b4c47a47d7f804c9a352a6d6d0100ffffffff0101000000000000002321033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194ac00000000')
|
||||||
|
self.assertEqual('24cecfce0fa880b09c9b4a66c5134499d1b09c01cc5728cd182638bea070e6ab', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0026(self):
|
||||||
|
tx = transaction.Transaction('0100000001c188aa82f268fcf08ba18950f263654a3ea6931dabc8bf3ed1d4d42aaed74cba000000004b0000483045022100940378576e069aca261a6b26fb38344e4497ca6751bb10905c76bb689f4222b002204833806b014c26fd801727b792b1260003c55710f87c5adbd7a9cb57446dbc9801ffffffff0101000000000000002321037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1ac00000000')
|
||||||
|
self.assertEqual('9eaa819e386d6a54256c9283da50c230f3d8cd5376d75c4dcc945afdeb157dd7', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0027(self):
|
||||||
|
tx = transaction.Transaction('01000000012432b60dc72cebc1a27ce0969c0989c895bdd9e62e8234839117f8fc32d17fbc000000004a493046022100a576b52051962c25e642c0fd3d77ee6c92487048e5d90818bcf5b51abaccd7900221008204f8fb121be4ec3b24483b1f92d89b1b0548513a134e345c5442e86e8617a501ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('46224764c7870f95b58f155bce1e38d4da8e99d42dbb632d0dd7c07e092ee5aa', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0028(self):
|
||||||
|
tx = transaction.Transaction('01000000014710b0e7cf9f8930de259bdc4b84aa5dfb9437b665a3e3a21ff26e0bf994e183000000004a493046022100a166121a61b4eeb19d8f922b978ff6ab58ead8a5a5552bf9be73dc9c156873ea02210092ad9bc43ee647da4f6652c320800debcf08ec20a094a0aaf085f63ecb37a17201ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('8d66836045db9f2d7b3a75212c5e6325f70603ee27c8333a3bce5bf670d9582e', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0029(self):
|
||||||
|
tx = transaction.Transaction('01000000015ebaa001d8e4ec7a88703a3bcf69d98c874bca6299cca0f191512bf2a7826832000000004948304502203bf754d1c6732fbf87c5dcd81258aefd30f2060d7bd8ac4a5696f7927091dad1022100f5bcb726c4cf5ed0ed34cc13dadeedf628ae1045b7cb34421bc60b89f4cecae701ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('aab7ef280abbb9cc6fbaf524d2645c3daf4fcca2b3f53370e618d9cedf65f1f8', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0030(self):
|
||||||
|
tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a900000000924830450221009c0a27f886a1d8cb87f6f595fbc3163d28f7a81ec3c4b252ee7f3ac77fd13ffa02203caa8dfa09713c8c4d7ef575c75ed97812072405d932bd11e6a1593a98b679370148304502201e3861ef39a526406bad1e20ecad06be7375ad40ddb582c9be42d26c3a0d7b240221009d0a3985e96522e59635d19cc4448547477396ce0ef17a58e7d74c3ef464292301ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('6327783a064d4e350c454ad5cd90201aedf65b1fc524e73709c52f0163739190', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0031(self):
|
||||||
|
tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0100ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('892464645599cc3c2d165adcc612e5f982a200dfaa3e11e9ce1d228027f46880', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0032(self):
|
||||||
|
tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510151ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('578db8c6c404fec22c4a8afeaf32df0e7b767c4dda3478e0471575846419e8fc', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0033(self):
|
||||||
|
tx = transaction.Transaction('0100000001e0be9e32f1f89c3d916c4f21e55cdcd096741b895cc76ac353e6023a05f4f7cc00000000d86149304602210086e5f736a2c3622ebb62bd9d93d8e5d76508b98be922b97160edc3dcca6d8c47022100b23c312ac232a4473f19d2aeb95ab7bdf2b65518911a0d72d50e38b5dd31dc820121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac4730440220508fa761865c8abd81244a168392876ee1d94e8ed83897066b5e2df2400dad24022043f5ee7538e87e9c6aef7ef55133d3e51da7cc522830a9c4d736977a76ef755c0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('974f5148a0946f9985e75a240bb24c573adbbdc25d61e7b016cdbb0a5355049f', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0034(self):
|
||||||
|
tx = transaction.Transaction('01000000013c6f30f99a5161e75a2ce4bca488300ca0c6112bde67f0807fe983feeff0c91001000000e608646561646265656675ab61493046022100ce18d384221a731c993939015e3d1bcebafb16e8c0b5b5d14097ec8177ae6f28022100bcab227af90bab33c3fe0a9abfee03ba976ee25dc6ce542526e9b2e56e14b7f10121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac493046022100c3b93edcc0fd6250eb32f2dd8a0bba1754b0f6c3be8ed4100ed582f3db73eba2022100bf75b5bd2eff4d6bf2bda2e34a40fcc07d4aa3cf862ceaa77b47b81eff829f9a01ab21038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('b0097ec81df231893a212657bf5fe5a13b2bff8b28c0042aca6fc4159f79661b', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0035(self):
|
||||||
|
tx = transaction.Transaction('01000000016f3dbe2ca96fa217e94b1017860be49f20820dea5c91bdcb103b0049d5eb566000000000fd1d0147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac47304402203757e937ba807e4a5da8534c17f9d121176056406a6465054bdd260457515c1a02200f02eccf1bec0f3a0d65df37889143c2e88ab7acec61a7b6f5aa264139141a2b0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('feeba255656c80c14db595736c1c7955c8c0a497622ec96e3f2238fbdd43a7c9', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0036(self):
|
||||||
|
tx = transaction.Transaction('01000000012139c555ccb81ee5b1e87477840991ef7b386bc3ab946b6b682a04a621006b5a01000000fdb40148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f2204148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390175ac4830450220646b72c35beeec51f4d5bc1cbae01863825750d7f490864af354e6ea4f625e9c022100f04b98432df3a9641719dbced53393022e7249fb59db993af1118539830aab870148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a580039017521038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000')
|
||||||
|
self.assertEqual('a0c984fc820e57ddba97f8098fa640c8a7eb3fe2f583923da886b7660f505e1e', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0037(self):
|
||||||
|
tx = transaction.Transaction('0100000002f9cbafc519425637ba4227f8d0a0b7160b4e65168193d5af39747891de98b5b5000000006b4830450221008dd619c563e527c47d9bd53534a770b102e40faa87f61433580e04e271ef2f960220029886434e18122b53d5decd25f1f4acb2480659fea20aabd856987ba3c3907e0121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffff42e7988254800876b69f24676b3e0205b77be476512ca4d970707dd5c60598ab00000000fd260100483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a53034930460221008431bdfa72bc67f9d41fe72e94c88fb8f359ffa30b33c72c121c5a877d922e1002210089ef5fc22dd8bfc6bf9ffdb01a9862d27687d424d1fefbab9e9c7176844a187a014c9052483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c7153aeffffffff01a08601000000000017a914d8dacdadb7462ae15cd906f1878706d0da8660e68700000000')
|
||||||
|
self.assertEqual('5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0038(self):
|
||||||
|
tx = transaction.Transaction('0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000')
|
||||||
|
self.assertEqual('ded7ff51d89a4e1ec48162aee5a96447214d93dfb3837946af2301a28f65dbea', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0039(self):
|
||||||
|
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000')
|
||||||
|
self.assertEqual('3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0040(self):
|
||||||
|
tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d')
|
||||||
|
self.assertEqual('abd62b4627d8d9b2d95fcfd8c87e37d2790637ce47d28018e3aece63c1d62649', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0041(self):
|
||||||
|
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d')
|
||||||
|
self.assertEqual('58b6de8413603b7f556270bf48caedcf17772e7105f5419f6a80be0df0b470da', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0042(self):
|
||||||
|
tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff')
|
||||||
|
self.assertEqual('5f99c0abf511294d76cbe144d86b77238a03e086974bc7a8ea0bdb2c681a0324', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0043(self):
|
||||||
|
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000')
|
||||||
|
self.assertEqual('25d35877eaba19497710666473c50d5527d38503e3521107a3fc532b74cd7453', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0044(self):
|
||||||
|
tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff')
|
||||||
|
self.assertEqual('1b9aef851895b93c62c29fbd6ca4d45803f4007eff266e2f96ff11e9b6ef197b', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0045(self):
|
||||||
|
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000')
|
||||||
|
self.assertEqual('3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0046(self):
|
||||||
|
tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000')
|
||||||
|
self.assertEqual('f53761038a728b1f17272539380d96e93f999218f8dcb04a8469b523445cd0fd', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0047(self):
|
||||||
|
tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000')
|
||||||
|
self.assertEqual('d193f0f32fceaf07bb25c897c8f99ca6f69a52f6274ca64efc2a2e180cb97fc1', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0048(self):
|
||||||
|
tx = transaction.Transaction('010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000')
|
||||||
|
self.assertEqual('50a1e0e6a134a564efa078e3bd088e7e8777c2c0aec10a752fd8706470103b89', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0049(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000')
|
||||||
|
self.assertEqual('e2207d1aaf6b74e5d98c2fa326d2dc803b56b30a3f90ce779fa5edb762f38755', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0050(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000')
|
||||||
|
self.assertEqual('f335864f7c12ec7946d2c123deb91eb978574b647af125a414262380c7fbd55c', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0051(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000')
|
||||||
|
self.assertEqual('d1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0052(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000')
|
||||||
|
self.assertEqual('3a13e1b6371c545147173cc4055f0ed73686a9f73f092352fb4b39ca27d360e6', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0053(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000')
|
||||||
|
self.assertEqual('bffda23e40766d292b0510a1b556453c558980c70c94ab158d8286b3413e220d', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0054(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000')
|
||||||
|
self.assertEqual('01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0055(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000')
|
||||||
|
self.assertEqual('f6d2359c5de2d904e10517d23e7c8210cca71076071bbf46de9fbd5f6233dbf1', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0056(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000')
|
||||||
|
self.assertEqual('19c2b7377229dae7aa3e50142a32fd37cef7171a01682f536e9ffa80c186f6c9', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0057(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000')
|
||||||
|
self.assertEqual('c9dda3a24cc8a5acb153d1085ecd2fecf6f87083122f8cdecc515b1148d4c40d', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0058(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000')
|
||||||
|
self.assertEqual('d1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0059(self):
|
||||||
|
tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000')
|
||||||
|
self.assertEqual('01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0060(self):
|
||||||
|
tx = transaction.Transaction('02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2010000000100000000000000000000000000')
|
||||||
|
self.assertEqual('4b5e0aae1251a9dc66b4d5f483f1879bf518ea5e1765abc5a9f2084b43ed1ea7', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0061(self):
|
||||||
|
tx = transaction.Transaction('0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000')
|
||||||
|
self.assertEqual('5f16eb3ca4581e2dfb46a28140a4ee15f85e4e1c032947da8b93549b53c105f5', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0062(self):
|
||||||
|
tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000')
|
||||||
|
self.assertEqual('b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0063(self):
|
||||||
|
tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000')
|
||||||
|
self.assertEqual('b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0064(self):
|
||||||
|
tx = transaction.Transaction('01000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000')
|
||||||
|
self.assertEqual('fee125c6cd142083fabd0187b1dd1f94c66c89ec6e6ef6da1374881c0c19aece', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0065(self):
|
||||||
|
tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000023220020ff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000')
|
||||||
|
self.assertEqual('5f32557914351fee5f89ddee6c8983d476491d29e601d854e3927299e50450da', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0066(self):
|
||||||
|
tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff05540b0000000000000151d0070000000000000151840300000000000001513c0f00000000000001512c010000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71000000000000')
|
||||||
|
self.assertEqual('07dfa2da3d67c8a2b9f7bd31862161f7b497829d5da90a88ba0f1a905e7a43f7', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0067(self):
|
||||||
|
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0068(self):
|
||||||
|
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff0484030000000000000151d0070000000000000151540b0000000000000151c800000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('f92bb6e4f3ff89172f23ef647f74c13951b665848009abb5862cdf7a0412415a', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0069(self):
|
||||||
|
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0070(self):
|
||||||
|
tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff04b60300000000000001519e070000000000000151860b00000000000001009600000000000000015100000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('e657e25fc9f2b33842681613402759222a58cf7dd504d6cdc0b69a0b8c2e7dcb', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0071(self):
|
||||||
|
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0072(self):
|
||||||
|
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('4ede5e22992d43d42ccdf6553fb46e448aa1065ba36423f979605c1e5ab496b8', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0073(self):
|
||||||
|
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0074(self):
|
||||||
|
tx = transaction.Transaction('01000000000103000100000000000000000000000000000000000000000000000000000000000000000000000200000000010000000000000000000000000000000000000000000000000000000000000100000000ffffffff000100000000000000000000000000000000000000000000000000000000000002000000000200000003e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('cfe9f4b19f52b8366860aec0d2b5815e329299b2e9890d477edd7f1182be7ac8', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0075(self):
|
||||||
|
tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('aee8f4865ca40fa77ff2040c0d7de683bea048b103d42ca406dc07dd29d539cb', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0076(self):
|
||||||
|
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0077(self):
|
||||||
|
tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0078(self):
|
||||||
|
tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd08020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000')
|
||||||
|
self.assertEqual('d93ab9e12d7c29d2adc13d5cdf619d53eec1f36eb6612f55af52be7ba0448e97', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0079(self):
|
||||||
|
tx = transaction.Transaction('0100000000010c00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff0001000000000000000000000000000000000000000000000000000000000000020000006a473044022026c2e65b33fcd03b2a3b0f25030f0244bd23cc45ae4dec0f48ae62255b1998a00220463aa3982b718d593a6b9e0044513fd67a5009c2fdccc59992cffc2b167889f4012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000030000006a4730440220008bd8382911218dcb4c9f2e75bf5c5c3635f2f2df49b36994fde85b0be21a1a02205a539ef10fb4c778b522c1be852352ea06c67ab74200977c722b0bc68972575a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000040000006b483045022100d9436c32ff065127d71e1a20e319e4fe0a103ba0272743dbd8580be4659ab5d302203fd62571ee1fe790b182d078ecfd092a509eac112bea558d122974ef9cc012c7012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000050000006a47304402200e2c149b114ec546015c13b2b464bbcb0cdc5872e6775787527af6cbc4830b6c02207e9396c6979fb15a9a2b96ca08a633866eaf20dc0ff3c03e512c1d5a1654f148012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000060000006b483045022100b20e70d897dc15420bccb5e0d3e208d27bdd676af109abbd3f88dbdb7721e6d6022005836e663173fbdfe069f54cde3c2decd3d0ea84378092a5d9d85ec8642e8a41012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff00010000000000000000000000000000000000000000000000000000000000000700000000ffffffff00010000000000000000000000000000000000000000000000000000000000000800000000ffffffff00010000000000000000000000000000000000000000000000000000000000000900000000ffffffff00010000000000000000000000000000000000000000000000000000000000000a00000000ffffffff00010000000000000000000000000000000000000000000000000000000000000b0000006a47304402206639c6e05e3b9d2675a7f3876286bdf7584fe2bbd15e0ce52dd4e02c0092cdc60220757d60b0a61fc95ada79d23746744c72bac1545a75ff6c2c7cdb6ae04e7e9592012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0ce8030000000000000151e9030000000000000151ea030000000000000151eb030000000000000151ec030000000000000151ed030000000000000151ee030000000000000151ef030000000000000151f0030000000000000151f1030000000000000151f2030000000000000151f30300000000000001510248304502210082219a54f61bf126bfc3fa068c6e33831222d1d7138c6faa9d33ca87fd4202d6022063f9902519624254d7c2c8ea7ba2d66ae975e4e229ae38043973ec707d5d4a83012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022017fb58502475848c1b09f162cb1688d0920ff7f142bed0ef904da2ccc88b168f02201798afa61850c65e77889cbcd648a5703b487895517c88f85cdd18b021ee246a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000000247304402202830b7926e488da75782c81a54cd281720890d1af064629ebf2e31bf9f5435f30220089afaa8b455bbeb7d9b9c3fe1ed37d07685ade8455c76472cda424d93e4074a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022026326fcdae9207b596c2b05921dbac11d81040c4d40378513670f19d9f4af893022034ecd7a282c0163b89aaa62c22ec202cef4736c58cd251649bad0d8139bcbf55012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71024730440220214978daeb2f38cd426ee6e2f44131a33d6b191af1c216247f1dd7d74c16d84a02205fdc05529b0bc0c430b4d5987264d9d075351c4f4484c16e91662e90a72aab24012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402204a6e9f199dc9672cf2ff8094aaa784363be1eb62b679f7ff2df361124f1dca3302205eeb11f70fab5355c9c8ad1a0700ea355d315e334822fa182227e9815308ee8f012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000')
|
||||||
|
self.assertEqual('b83579db5246aa34255642768167132a0c3d2932b186cd8fb9f5490460a0bf91', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0080(self):
|
||||||
|
tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000')
|
||||||
|
self.assertEqual('2b1e44fff489d09091e5e20f9a01bbc0e8d80f0662e629fd10709cdb4922a874', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0081(self):
|
||||||
|
tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff01d00700000000000001510003483045022100e078de4e96a0e05dcdc0a414124dd8475782b5f3f0ed3f607919e9a5eeeb22bf02201de309b3a3109adb3de8074b3610d4cf454c49b61247a2779a0bcbf31c889333032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc711976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac00000000')
|
||||||
|
self.assertEqual('60ebb1dd0b598e20dd0dd462ef6723dd49f8f803b6a2492926012360119cfdd7', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0082(self):
|
||||||
|
tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff02e8030000000000000151e90300000000000001510247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000')
|
||||||
|
self.assertEqual('ed0c7f4163e275f3f77064f471eac861d01fdf55d03aa6858ebd3781f70bf003', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0083(self):
|
||||||
|
tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff02e9030000000000000151e80300000000000001510248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000')
|
||||||
|
self.assertEqual('f531ddf5ce141e1c8a7fdfc85cc634e5ff686f446a5cf7483e9dbe076b844862', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0084(self):
|
||||||
|
tx = transaction.Transaction('01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000')
|
||||||
|
self.assertEqual('98229b70948f1c17851a541f1fe532bf02c408267fecf6d7e174c359ae870654', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0085(self):
|
||||||
|
tx = transaction.Transaction('01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000')
|
||||||
|
self.assertEqual('570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0086(self):
|
||||||
|
tx = transaction.Transaction('01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000')
|
||||||
|
self.assertEqual('e0b8142f587aaa322ca32abce469e90eda187f3851043cc4f2a0fff8c13fc84e', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0087(self):
|
||||||
|
tx = transaction.Transaction('0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000')
|
||||||
|
self.assertEqual('b9ecf72df06b8f98f8b63748d1aded5ffc1a1186f8a302e63cf94f6250e29f4d', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0088(self):
|
||||||
|
tx = transaction.Transaction('0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000')
|
||||||
|
self.assertEqual('27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0089(self):
|
||||||
|
tx = transaction.Transaction('010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000')
|
||||||
|
self.assertEqual('22d020638e3b7e1f2f9a63124ac76f5e333c74387862e3675f64b25e960d3641', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0090(self):
|
||||||
|
tx = transaction.Transaction('0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9781d66b61fb5a7ef00ac5ad5bc6ffc78be7b44a566e3c87870e1079368df4c4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000')
|
||||||
|
self.assertEqual('2862bc0c69d2af55da7284d1b16a7cddc03971b77e5a97939cca7631add83bf5', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0091(self):
|
||||||
|
tx = transaction.Transaction('01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601522102cd74a2809ffeeed0092bc124fd79836706e41f048db3f6ae9df8708cefb83a1c2102e615999372426e46fd107b76eaf007156a507584aa2cc21de9eee3bdbd26d36c4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000')
|
||||||
|
self.assertEqual('1aebf0c98f01381765a8c33d688f8903e4d01120589ac92b78f1185dc1f4119c', tx.txid())
|
||||||
|
|
||||||
|
def test_txid_bitcoin_core_0092(self):
|
||||||
|
tx = transaction.Transaction('010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960101022102966f109c54e85d3aee8321301136cedeb9fc710fdef58a9de8a73942f8e567c021034ffc99dd9a79dd3cb31e2ab3e0b09e0e67db41ac068c625cd1f491576016c84e9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000')
|
||||||
|
self.assertEqual('45d17fb7db86162b2b6ca29fa4e163acf0ef0b54110e49b819bda1f948d423a3', tx.txid())
|
||||||
|
|
||||||
|
# txns from Bitcoin Core ends <---
|
||||||
|
|
||||||
|
|
||||||
class NetworkMock(object):
|
class NetworkMock(object):
|
||||||
|
|
||||||
|
|
|
@ -128,7 +128,7 @@ class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
|
||||||
long_user_id, short_id = trustedcoin.get_user_id(
|
long_user_id, short_id = trustedcoin.get_user_id(
|
||||||
{'x1/': {'xpub': xpub1},
|
{'x1/': {'xpub': xpub1},
|
||||||
'x2/': {'xpub': xpub2}})
|
'x2/': {'xpub': xpub2}})
|
||||||
xpub3 = trustedcoin.make_xpub(trustedcoin.signing_xpub, long_user_id)
|
xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(), long_user_id)
|
||||||
ks3 = keystore.from_xpub(xpub3)
|
ks3 = keystore.from_xpub(xpub3)
|
||||||
self._check_xpub_keystore_sanity(ks3)
|
self._check_xpub_keystore_sanity(ks3)
|
||||||
self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))
|
self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))
|
||||||
|
|
|
@ -32,6 +32,8 @@ from .util import print_error, profiler
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from .bitcoin import *
|
from .bitcoin import *
|
||||||
import struct
|
import struct
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
#
|
#
|
||||||
# Workalike python implementation of Bitcoin's CDataStream class.
|
# Workalike python implementation of Bitcoin's CDataStream class.
|
||||||
|
@ -45,6 +47,14 @@ class SerializationError(Exception):
|
||||||
""" Thrown when there's a problem deserializing or serializing """
|
""" Thrown when there's a problem deserializing or serializing """
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownTxinType(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotRecognizedRedeemScript(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BCDataStream(object):
|
class BCDataStream(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.input = None
|
self.input = None
|
||||||
|
@ -295,17 +305,30 @@ def parse_scriptSig(d, _bytes):
|
||||||
decoded = [ x for x in script_GetOp(_bytes) ]
|
decoded = [ x for x in script_GetOp(_bytes) ]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# coinbase transactions raise an exception
|
# coinbase transactions raise an exception
|
||||||
print_error("cannot find address in input script", bh2u(_bytes))
|
print_error("parse_scriptSig: cannot find address in input script (coinbase?)",
|
||||||
|
bh2u(_bytes))
|
||||||
return
|
return
|
||||||
|
|
||||||
match = [ opcodes.OP_PUSHDATA4 ]
|
match = [ opcodes.OP_PUSHDATA4 ]
|
||||||
if match_decoded(decoded, match):
|
if match_decoded(decoded, match):
|
||||||
item = decoded[0][1]
|
item = decoded[0][1]
|
||||||
if item[0] == 0:
|
if item[0] == 0:
|
||||||
|
# segwit embedded into p2sh
|
||||||
|
# witness version 0
|
||||||
d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item))
|
d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item))
|
||||||
d['type'] = 'p2wpkh-p2sh' if len(item) == 22 else 'p2wsh-p2sh'
|
if len(item) == 22:
|
||||||
|
d['type'] = 'p2wpkh-p2sh'
|
||||||
|
elif len(item) == 34:
|
||||||
|
d['type'] = 'p2wsh-p2sh'
|
||||||
|
else:
|
||||||
|
print_error("unrecognized txin type", bh2u(item))
|
||||||
|
elif opcodes.OP_1 <= item[0] <= opcodes.OP_16:
|
||||||
|
# segwit embedded into p2sh
|
||||||
|
# witness version 1-16
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
# payto_pubkey
|
# assert item[0] == 0x30
|
||||||
|
# pay-to-pubkey
|
||||||
d['type'] = 'p2pk'
|
d['type'] = 'p2pk'
|
||||||
d['address'] = "(pubkey)"
|
d['address'] = "(pubkey)"
|
||||||
d['signatures'] = [bh2u(item)]
|
d['signatures'] = [bh2u(item)]
|
||||||
|
@ -314,9 +337,9 @@ def parse_scriptSig(d, _bytes):
|
||||||
d['pubkeys'] = ["(pubkey)"]
|
d['pubkeys'] = ["(pubkey)"]
|
||||||
return
|
return
|
||||||
|
|
||||||
# non-generated TxIn transactions push a signature
|
# p2pkh TxIn transactions push a signature
|
||||||
# (seventy-something bytes) and then their public key
|
# (71-73 bytes) and then their public key
|
||||||
# (65 bytes) onto the stack:
|
# (33 or 65 bytes) onto the stack:
|
||||||
match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ]
|
match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ]
|
||||||
if match_decoded(decoded, match):
|
if match_decoded(decoded, match):
|
||||||
sig = bh2u(decoded[0][1])
|
sig = bh2u(decoded[0][1])
|
||||||
|
@ -325,7 +348,8 @@ def parse_scriptSig(d, _bytes):
|
||||||
signatures = parse_sig([sig])
|
signatures = parse_sig([sig])
|
||||||
pubkey, address = xpubkey_to_address(x_pubkey)
|
pubkey, address = xpubkey_to_address(x_pubkey)
|
||||||
except:
|
except:
|
||||||
print_error("cannot find address in input script", bh2u(_bytes))
|
print_error("parse_scriptSig: cannot find address in input script (p2pkh?)",
|
||||||
|
bh2u(_bytes))
|
||||||
return
|
return
|
||||||
d['type'] = 'p2pkh'
|
d['type'] = 'p2pkh'
|
||||||
d['signatures'] = signatures
|
d['signatures'] = signatures
|
||||||
|
@ -337,31 +361,42 @@ def parse_scriptSig(d, _bytes):
|
||||||
|
|
||||||
# p2sh transaction, m of n
|
# p2sh transaction, m of n
|
||||||
match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1)
|
match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1)
|
||||||
if not match_decoded(decoded, match):
|
if match_decoded(decoded, match):
|
||||||
print_error("cannot find address in input script", bh2u(_bytes))
|
x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
|
||||||
|
try:
|
||||||
|
m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1])
|
||||||
|
except NotRecognizedRedeemScript:
|
||||||
|
print_error("parse_scriptSig: cannot find address in input script (p2sh?)",
|
||||||
|
bh2u(_bytes))
|
||||||
|
# we could still guess:
|
||||||
|
# d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1]))
|
||||||
|
return
|
||||||
|
# write result in d
|
||||||
|
d['type'] = 'p2sh'
|
||||||
|
d['num_sig'] = m
|
||||||
|
d['signatures'] = parse_sig(x_sig)
|
||||||
|
d['x_pubkeys'] = x_pubkeys
|
||||||
|
d['pubkeys'] = pubkeys
|
||||||
|
d['redeemScript'] = redeemScript
|
||||||
|
d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript)))
|
||||||
return
|
return
|
||||||
x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
|
|
||||||
m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1])
|
print_error("parse_scriptSig: cannot find address in input script (unknown)",
|
||||||
# write result in d
|
bh2u(_bytes))
|
||||||
d['type'] = 'p2sh'
|
|
||||||
d['num_sig'] = m
|
|
||||||
d['signatures'] = parse_sig(x_sig)
|
|
||||||
d['x_pubkeys'] = x_pubkeys
|
|
||||||
d['pubkeys'] = pubkeys
|
|
||||||
d['redeemScript'] = redeemScript
|
|
||||||
d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript)))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_redeemScript(s):
|
def parse_redeemScript(s):
|
||||||
dec2 = [ x for x in script_GetOp(s) ]
|
dec2 = [ x for x in script_GetOp(s) ]
|
||||||
m = dec2[0][0] - opcodes.OP_1 + 1
|
try:
|
||||||
n = dec2[-2][0] - opcodes.OP_1 + 1
|
m = dec2[0][0] - opcodes.OP_1 + 1
|
||||||
|
n = dec2[-2][0] - opcodes.OP_1 + 1
|
||||||
|
except IndexError:
|
||||||
|
raise NotRecognizedRedeemScript()
|
||||||
op_m = opcodes.OP_1 + m - 1
|
op_m = opcodes.OP_1 + m - 1
|
||||||
op_n = opcodes.OP_1 + n - 1
|
op_n = opcodes.OP_1 + n - 1
|
||||||
match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ]
|
match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ]
|
||||||
if not match_decoded(dec2, match_multisig):
|
if not match_decoded(dec2, match_multisig):
|
||||||
print_error("cannot find address in input script", bh2u(s))
|
raise NotRecognizedRedeemScript()
|
||||||
return
|
|
||||||
x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
|
x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
|
||||||
pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
|
pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
|
||||||
redeemScript = multisig_script(pubkeys, m)
|
redeemScript = multisig_script(pubkeys, m)
|
||||||
|
@ -416,7 +451,11 @@ def parse_input(vds):
|
||||||
d['num_sig'] = 0
|
d['num_sig'] = 0
|
||||||
if scriptSig:
|
if scriptSig:
|
||||||
d['scriptSig'] = bh2u(scriptSig)
|
d['scriptSig'] = bh2u(scriptSig)
|
||||||
parse_scriptSig(d, scriptSig)
|
try:
|
||||||
|
parse_scriptSig(d, scriptSig)
|
||||||
|
except BaseException:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
print_error('failed to parse scriptSig', bh2u(scriptSig))
|
||||||
else:
|
else:
|
||||||
d['scriptSig'] = ''
|
d['scriptSig'] = ''
|
||||||
|
|
||||||
|
@ -430,21 +469,55 @@ def parse_witness(vds, txin):
|
||||||
if n == 0xffffffff:
|
if n == 0xffffffff:
|
||||||
txin['value'] = vds.read_uint64()
|
txin['value'] = vds.read_uint64()
|
||||||
n = vds.read_compact_size()
|
n = vds.read_compact_size()
|
||||||
|
# now 'n' is the number of items in the witness
|
||||||
w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n))
|
w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n))
|
||||||
if txin['type'] == 'coinbase':
|
|
||||||
pass
|
add_w = lambda x: var_int(len(x) // 2) + x
|
||||||
elif n > 2:
|
txin['witness'] = var_int(n) + ''.join(add_w(i) for i in w)
|
||||||
txin['signatures'] = parse_sig(w[1:-1])
|
|
||||||
m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1]))
|
# FIXME: witness version > 0 will probably fail here.
|
||||||
txin['num_sig'] = m
|
# For native segwit, we would need the scriptPubKey of the parent txn
|
||||||
txin['x_pubkeys'] = x_pubkeys
|
# to determine witness program version, and properly parse the witness.
|
||||||
txin['pubkeys'] = pubkeys
|
# In case of p2sh-segwit, we can tell based on the scriptSig in this txn.
|
||||||
txin['witnessScript'] = witnessScript
|
# The code below assumes witness version 0.
|
||||||
else:
|
# p2sh-segwit should work in that case; for native segwit we need to tell
|
||||||
txin['num_sig'] = 1
|
# between p2wpkh and p2wsh; we do this based on number of witness items,
|
||||||
txin['x_pubkeys'] = [w[1]]
|
# hence (FIXME) p2wsh with n==2 (maybe n==1 ?) will probably fail.
|
||||||
txin['pubkeys'] = [safe_parse_pubkey(w[1])]
|
# If v==0 and n==2, we need parent scriptPubKey to distinguish between p2wpkh and p2wsh.
|
||||||
txin['signatures'] = parse_sig([w[0]])
|
try:
|
||||||
|
if txin['type'] == 'coinbase':
|
||||||
|
pass
|
||||||
|
elif txin['type'] == 'p2wsh-p2sh' or n > 2:
|
||||||
|
try:
|
||||||
|
m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1]))
|
||||||
|
except NotRecognizedRedeemScript:
|
||||||
|
raise UnknownTxinType()
|
||||||
|
txin['signatures'] = parse_sig(w[1:-1])
|
||||||
|
txin['num_sig'] = m
|
||||||
|
txin['x_pubkeys'] = x_pubkeys
|
||||||
|
txin['pubkeys'] = pubkeys
|
||||||
|
txin['witnessScript'] = witnessScript
|
||||||
|
if not txin.get('scriptSig'): # native segwit script
|
||||||
|
txin['type'] = 'p2wsh'
|
||||||
|
txin['address'] = bitcoin.script_to_p2wsh(txin['witnessScript'])
|
||||||
|
elif txin['type'] == 'p2wpkh-p2sh' or n == 2:
|
||||||
|
txin['num_sig'] = 1
|
||||||
|
txin['x_pubkeys'] = [w[1]]
|
||||||
|
txin['pubkeys'] = [safe_parse_pubkey(w[1])]
|
||||||
|
txin['signatures'] = parse_sig([w[0]])
|
||||||
|
if not txin.get('scriptSig'): # native segwit script
|
||||||
|
txin['type'] = 'p2wpkh'
|
||||||
|
txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0]))
|
||||||
|
else:
|
||||||
|
raise UnknownTxinType()
|
||||||
|
except UnknownTxinType:
|
||||||
|
txin['type'] = 'unknown'
|
||||||
|
# FIXME: GUI might show 'unknown' address (e.g. for a non-multisig p2wsh)
|
||||||
|
except BaseException:
|
||||||
|
txin['type'] = 'unknown'
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
print_error('failed to parse witness', txin.get('witness'))
|
||||||
|
|
||||||
|
|
||||||
def parse_output(vds, i):
|
def parse_output(vds, i):
|
||||||
d = {}
|
d = {}
|
||||||
|
@ -463,9 +536,18 @@ def deserialize(raw):
|
||||||
start = vds.read_cursor
|
start = vds.read_cursor
|
||||||
d['version'] = vds.read_int32()
|
d['version'] = vds.read_int32()
|
||||||
n_vin = vds.read_compact_size()
|
n_vin = vds.read_compact_size()
|
||||||
|
is_segwit = (n_vin == 0)
|
||||||
|
if is_segwit:
|
||||||
|
marker = vds.read_bytes(1)
|
||||||
|
assert marker == b'\x01'
|
||||||
|
n_vin = vds.read_compact_size()
|
||||||
d['inputs'] = [parse_input(vds) for i in range(n_vin)]
|
d['inputs'] = [parse_input(vds) for i in range(n_vin)]
|
||||||
n_vout = vds.read_compact_size()
|
n_vout = vds.read_compact_size()
|
||||||
d['outputs'] = [parse_output(vds, i) for i in range(n_vout)]
|
d['outputs'] = [parse_output(vds, i) for i in range(n_vout)]
|
||||||
|
if is_segwit:
|
||||||
|
for i in range(n_vin):
|
||||||
|
txin = d['inputs'][i]
|
||||||
|
parse_witness(vds, txin)
|
||||||
d['lockTime'] = vds.read_uint32()
|
d['lockTime'] = vds.read_uint32()
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@ -657,7 +739,9 @@ class Transaction:
|
||||||
witness_script = multisig_script(pubkeys, txin['num_sig'])
|
witness_script = multisig_script(pubkeys, txin['num_sig'])
|
||||||
witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script)
|
witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script)
|
||||||
else:
|
else:
|
||||||
raise BaseException('wrong txin type')
|
witness = txin.get('witness', None)
|
||||||
|
if not witness:
|
||||||
|
raise BaseException('wrong txin type:', txin['type'])
|
||||||
if self.is_txin_complete(txin) or estimate_size:
|
if self.is_txin_complete(txin) or estimate_size:
|
||||||
value_field = ''
|
value_field = ''
|
||||||
else:
|
else:
|
||||||
|
@ -666,7 +750,8 @@ class Transaction:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_segwit_input(cls, txin):
|
def is_segwit_input(cls, txin):
|
||||||
return cls.is_segwit_inputtype(txin['type'])
|
has_nonzero_witness = txin.get('witness', '00') != '00'
|
||||||
|
return cls.is_segwit_inputtype(txin['type']) or has_nonzero_witness
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_segwit_inputtype(cls, txin_type):
|
def is_segwit_inputtype(cls, txin_type):
|
||||||
|
@ -733,6 +818,14 @@ class Transaction:
|
||||||
def serialize_outpoint(self, txin):
|
def serialize_outpoint(self, txin):
|
||||||
return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4)
|
return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_outpoint_from_txin(cls, txin):
|
||||||
|
if txin['type'] == 'coinbase':
|
||||||
|
return None
|
||||||
|
prevout_hash = txin['prevout_hash']
|
||||||
|
prevout_n = txin['prevout_n']
|
||||||
|
return prevout_hash + ':%d' % prevout_n
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serialize_input(self, txin, script):
|
def serialize_input(self, txin, script):
|
||||||
# Prev hash and index
|
# Prev hash and index
|
||||||
|
|
143
lib/util.py
143
lib/util.py
|
@ -29,7 +29,6 @@ import traceback
|
||||||
import urllib
|
import urllib
|
||||||
import threading
|
import threading
|
||||||
import hmac
|
import hmac
|
||||||
import requests
|
|
||||||
|
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
|
||||||
|
@ -41,11 +40,7 @@ def inv_dict(d):
|
||||||
return {v: k for k, v in d.items()}
|
return {v: k for k, v in d.items()}
|
||||||
|
|
||||||
|
|
||||||
is_bundle = getattr(sys, 'frozen', False)
|
|
||||||
is_macOS = sys.platform == 'darwin'
|
|
||||||
|
|
||||||
base_units = {'ZCL':8, 'mZCL':5, 'uZCL':2}
|
base_units = {'ZCL':8, 'mZCL':5, 'uZCL':2}
|
||||||
fee_levels = [_('Within 25 blocks'), _('Within 10 blocks'), _('Within 5 blocks'), _('Within 2 blocks'), _('In the next block')]
|
|
||||||
|
|
||||||
def normalize_version(v):
|
def normalize_version(v):
|
||||||
return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
|
return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
|
||||||
|
@ -62,17 +57,80 @@ class InvalidPassword(Exception):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("Incorrect password")
|
return _("Incorrect password")
|
||||||
|
|
||||||
|
|
||||||
|
class FileImportFailed(Exception):
|
||||||
|
def __init__(self, message=''):
|
||||||
|
self.message = str(message)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("Failed to import from file.") + "\n" + self.message
|
||||||
|
|
||||||
|
|
||||||
|
class FileExportFailed(Exception):
|
||||||
|
def __init__(self, message=''):
|
||||||
|
self.message = str(message)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("Failed to export to file.") + "\n" + self.message
|
||||||
|
|
||||||
|
|
||||||
|
class TimeoutException(Exception):
|
||||||
|
def __init__(self, message=''):
|
||||||
|
self.message = str(message)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if not self.message:
|
||||||
|
return _("Operation timed out.")
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
# Throw this exception to unwind the stack like when an error occurs.
|
# Throw this exception to unwind the stack like when an error occurs.
|
||||||
# However unlike other exceptions the user won't be informed.
|
# However unlike other exceptions the user won't be informed.
|
||||||
class UserCancelled(Exception):
|
class UserCancelled(Exception):
|
||||||
'''An exception that is suppressed from the user'''
|
'''An exception that is suppressed from the user'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class Satoshis(object):
|
||||||
|
def __new__(cls, value):
|
||||||
|
self = super(Satoshis, cls).__new__(cls)
|
||||||
|
self.value = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Satoshis(%d)'%self.value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return format_satoshis(self.value) + " BTC"
|
||||||
|
|
||||||
|
class Fiat(object):
|
||||||
|
def __new__(cls, value, ccy):
|
||||||
|
self = super(Fiat, cls).__new__(cls)
|
||||||
|
self.ccy = ccy
|
||||||
|
self.value = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Fiat(%s)'% self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.value.is_nan():
|
||||||
|
return _('No Data')
|
||||||
|
else:
|
||||||
|
return "{:.2f}".format(self.value) + ' ' + self.ccy
|
||||||
|
|
||||||
class MyEncoder(json.JSONEncoder):
|
class MyEncoder(json.JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
from .transaction import Transaction
|
from .transaction import Transaction
|
||||||
if isinstance(obj, Transaction):
|
if isinstance(obj, Transaction):
|
||||||
return obj.as_dict()
|
return obj.as_dict()
|
||||||
|
if isinstance(obj, Satoshis):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, Fiat):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, Decimal):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, datetime):
|
||||||
|
return obj.isoformat(' ')[:-3]
|
||||||
return super(MyEncoder, self).default(obj)
|
return super(MyEncoder, self).default(obj)
|
||||||
|
|
||||||
class PrintError(object):
|
class PrintError(object):
|
||||||
|
@ -81,8 +139,12 @@ class PrintError(object):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def print_error(self, *msg):
|
def print_error(self, *msg):
|
||||||
|
# only prints with --verbose flag
|
||||||
print_error("[%s]" % self.diagnostic_name(), *msg)
|
print_error("[%s]" % self.diagnostic_name(), *msg)
|
||||||
|
|
||||||
|
def print_stderr(self, *msg):
|
||||||
|
print_stderr("[%s]" % self.diagnostic_name(), *msg)
|
||||||
|
|
||||||
def print_msg(self, *msg):
|
def print_msg(self, *msg):
|
||||||
print_msg("[%s]" % self.diagnostic_name(), *msg)
|
print_msg("[%s]" % self.diagnostic_name(), *msg)
|
||||||
|
|
||||||
|
@ -347,7 +409,7 @@ def format_satoshis_plain(x, decimal_point = 8):
|
||||||
def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespaces=False):
|
def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespaces=False):
|
||||||
from locale import localeconv
|
from locale import localeconv
|
||||||
if x is None:
|
if x is None:
|
||||||
return 'Unknown'
|
return 'unknown'
|
||||||
x = int(x) # Some callers pass Decimal
|
x = int(x) # Some callers pass Decimal
|
||||||
scale_factor = pow (10, decimal_point)
|
scale_factor = pow (10, decimal_point)
|
||||||
integer_part = "{:n}".format(int(abs(x) / scale_factor))
|
integer_part = "{:n}".format(int(abs(x) / scale_factor))
|
||||||
|
@ -367,10 +429,9 @@ def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespa
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def timestamp_to_datetime(timestamp):
|
def timestamp_to_datetime(timestamp):
|
||||||
try:
|
if timestamp is None:
|
||||||
return datetime.fromtimestamp(timestamp)
|
|
||||||
except:
|
|
||||||
return None
|
return None
|
||||||
|
return datetime.fromtimestamp(timestamp)
|
||||||
|
|
||||||
def format_time(timestamp):
|
def format_time(timestamp):
|
||||||
date = timestamp_to_datetime(timestamp)
|
date = timestamp_to_datetime(timestamp)
|
||||||
|
@ -453,8 +514,8 @@ testnet_block_explorers = {
|
||||||
}
|
}
|
||||||
|
|
||||||
def block_explorer_info():
|
def block_explorer_info():
|
||||||
from . import bitcoin
|
from . import constants
|
||||||
return testnet_block_explorers if bitcoin.NetworkConstants.TESTNET else mainnet_block_explorers
|
return testnet_block_explorers if constants.net.TESTNET else mainnet_block_explorers
|
||||||
|
|
||||||
def block_explorer(config):
|
def block_explorer(config):
|
||||||
return config.get('block_explorer', 'zclassic-ce.io')
|
return config.get('block_explorer', 'zclassic-ce.io')
|
||||||
|
@ -470,7 +531,7 @@ def block_explorer_URL(config, kind, item):
|
||||||
if not kind_str:
|
if not kind_str:
|
||||||
return
|
return
|
||||||
url_parts = [be_tuple[0], kind_str, item]
|
url_parts = [be_tuple[0], kind_str, item]
|
||||||
return "/".join(url_parts)
|
return ''.join(url_parts)
|
||||||
|
|
||||||
# URL decode
|
# URL decode
|
||||||
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
|
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
|
||||||
|
@ -693,8 +754,56 @@ class QueuePipe:
|
||||||
self.send(request)
|
self.send(request)
|
||||||
|
|
||||||
|
|
||||||
def get_cert_path():
|
|
||||||
if is_bundle and is_macOS:
|
|
||||||
# set in ./electrum
|
def setup_thread_excepthook():
|
||||||
return requests.utils.DEFAULT_CA_BUNDLE_PATH
|
"""
|
||||||
return requests.certs.where()
|
Workaround for `sys.excepthook` thread bug from:
|
||||||
|
http://bugs.python.org/issue1230540
|
||||||
|
|
||||||
|
Call once from the main thread before creating any threads.
|
||||||
|
"""
|
||||||
|
|
||||||
|
init_original = threading.Thread.__init__
|
||||||
|
|
||||||
|
def init(self, *args, **kwargs):
|
||||||
|
|
||||||
|
init_original(self, *args, **kwargs)
|
||||||
|
run_original = self.run
|
||||||
|
|
||||||
|
def run_with_except_hook(*args2, **kwargs2):
|
||||||
|
try:
|
||||||
|
run_original(*args2, **kwargs2)
|
||||||
|
except Exception:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
self.run = run_with_except_hook
|
||||||
|
|
||||||
|
threading.Thread.__init__ = init
|
||||||
|
|
||||||
|
|
||||||
|
def versiontuple(v):
|
||||||
|
return tuple(map(int, (v.split("."))))
|
||||||
|
|
||||||
|
|
||||||
|
def import_meta(path, validater, load_meta):
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
d = validater(json.loads(f.read()))
|
||||||
|
load_meta(d)
|
||||||
|
#backwards compatibility for JSONDecodeError
|
||||||
|
except ValueError:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
raise FileImportFailed(_("Invalid JSON code."))
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
raise FileImportFailed(e)
|
||||||
|
|
||||||
|
|
||||||
|
def export_meta(meta, fileName):
|
||||||
|
try:
|
||||||
|
with open(fileName, 'w+') as f:
|
||||||
|
json.dump(meta, f, indent=4, sort_keys=True)
|
||||||
|
except (IOError, os.error) as e:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
raise FileExportFailed(e)
|
||||||
|
|
|
@ -36,15 +36,22 @@ class SPV(ThreadJob):
|
||||||
self.merkle_roots = {}
|
self.merkle_roots = {}
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
interface = self.network.interface
|
||||||
|
if not interface:
|
||||||
|
return
|
||||||
|
blockchain = interface.blockchain
|
||||||
|
if not blockchain:
|
||||||
|
return
|
||||||
lh = self.network.get_local_height()
|
lh = self.network.get_local_height()
|
||||||
unverified = self.wallet.get_unverified_txs()
|
unverified = self.wallet.get_unverified_txs()
|
||||||
for tx_hash, tx_height in unverified.items():
|
for tx_hash, tx_height in unverified.items():
|
||||||
# do not request merkle branch before headers are available
|
# do not request merkle branch before headers are available
|
||||||
if (tx_height > 0) and (tx_height <= lh):
|
if (tx_height > 0) and (tx_height <= lh):
|
||||||
header = self.network.blockchain().read_header(tx_height)
|
header = blockchain.read_header(tx_height)
|
||||||
if header is None and self.network.interface:
|
if header is None:
|
||||||
index = tx_height // NetworkConstants.CHUNK_SIZE
|
index = tx_height // constants.net.CHUNK_SIZE
|
||||||
self.network.request_chunk(self.network.interface, index)
|
if index < len(blockchain.checkpoints):
|
||||||
|
self.network.request_chunk(interface, index)
|
||||||
else:
|
else:
|
||||||
if tx_hash not in self.merkle_roots:
|
if tx_hash not in self.merkle_roots:
|
||||||
request = ('blockchain.transaction.get_merkle',
|
request = ('blockchain.transaction.get_merkle',
|
||||||
|
@ -58,6 +65,8 @@ class SPV(ThreadJob):
|
||||||
self.undo_verifications()
|
self.undo_verifications()
|
||||||
|
|
||||||
def verify_merkle(self, r):
|
def verify_merkle(self, r):
|
||||||
|
if self.wallet.verifier is None:
|
||||||
|
return # we have been killed, this was just an orphan callback
|
||||||
if r.get('error'):
|
if r.get('error'):
|
||||||
self.print_error('received an error:', r)
|
self.print_error('received an error:', r)
|
||||||
return
|
return
|
||||||
|
@ -70,17 +79,26 @@ class SPV(ThreadJob):
|
||||||
pos = merkle.get('pos')
|
pos = merkle.get('pos')
|
||||||
merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos)
|
merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos)
|
||||||
header = self.network.blockchain().read_header(tx_height)
|
header = self.network.blockchain().read_header(tx_height)
|
||||||
if not header or header.get('merkle_root') != merkle_root:
|
# FIXME: if verification fails below,
|
||||||
# FIXME: we should make a fresh connection to a server to
|
# we should make a fresh connection to a server to
|
||||||
# recover from this, as this TX will now never verify
|
# recover from this, as this TX will now never verify
|
||||||
self.print_error("merkle verification failed for", tx_hash)
|
if not header:
|
||||||
|
self.print_error(
|
||||||
|
"merkle verification failed for {} (missing header {})"
|
||||||
|
.format(tx_hash, tx_height))
|
||||||
|
return
|
||||||
|
if header.get('merkle_root') != merkle_root:
|
||||||
|
self.print_error(
|
||||||
|
"merkle verification failed for {} (merkle root mismatch {} != {})"
|
||||||
|
.format(tx_hash, header.get('merkle_root'), merkle_root))
|
||||||
return
|
return
|
||||||
# we passed all the tests
|
# we passed all the tests
|
||||||
self.merkle_roots[tx_hash] = merkle_root
|
self.merkle_roots[tx_hash] = merkle_root
|
||||||
self.print_error("verified %s" % tx_hash)
|
self.print_error("verified %s" % tx_hash)
|
||||||
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
|
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
|
||||||
|
|
||||||
def hash_merkle_root(self, merkle_s, target_hash, pos):
|
@classmethod
|
||||||
|
def hash_merkle_root(cls, merkle_s, target_hash, pos):
|
||||||
h = hash_decode(target_hash)
|
h = hash_decode(target_hash)
|
||||||
for i in range(len(merkle_s)):
|
for i in range(len(merkle_s)):
|
||||||
item = merkle_s[i]
|
item = merkle_s[i]
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# version of the client package
|
ELECTRUM_VERSION = '1.0.7' # '3.1' = version of the client package
|
||||||
ELECTRUM_VERSION = '1.0.6'
|
PROTOCOL_VERSION = '1.1' # protocol version requested
|
||||||
# protocol version requested
|
|
||||||
PROTOCOL_VERSION = '1.1'
|
|
||||||
|
|
||||||
# The hash of the mnemonic seed must begin with this
|
# The hash of the mnemonic seed must begin with this
|
||||||
SEED_PREFIX = '01' # Standard wallet
|
SEED_PREFIX = '01' # Standard wallet
|
||||||
|
|
782
lib/wallet.py
782
lib/wallet.py
File diff suppressed because it is too large
Load Diff
|
@ -84,7 +84,8 @@ class WsClientThread(util.DaemonThread):
|
||||||
l = self.subscriptions.get(addr, [])
|
l = self.subscriptions.get(addr, [])
|
||||||
l.append((ws, amount))
|
l.append((ws, amount))
|
||||||
self.subscriptions[addr] = l
|
self.subscriptions[addr] = l
|
||||||
self.network.send([('blockchain.address.subscribe', [addr])], self.response_queue.put)
|
h = self.network.addr_to_scripthash(addr)
|
||||||
|
self.network.send([('blockchain.scripthash.subscribe', [h])], self.response_queue.put)
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
@ -100,10 +101,13 @@ class WsClientThread(util.DaemonThread):
|
||||||
result = r.get('result')
|
result = r.get('result')
|
||||||
if result is None:
|
if result is None:
|
||||||
continue
|
continue
|
||||||
if method == 'blockchain.address.subscribe':
|
if method == 'blockchain.scripthash.subscribe':
|
||||||
self.network.send([('blockchain.address.get_balance', params)], self.response_queue.put)
|
self.network.send([('blockchain.scripthash.get_balance', params)], self.response_queue.put)
|
||||||
elif method == 'blockchain.address.get_balance':
|
elif method == 'blockchain.scripthash.get_balance':
|
||||||
addr = params[0]
|
h = params[0]
|
||||||
|
addr = self.network.h2addr.get(h, None)
|
||||||
|
if addr is None:
|
||||||
|
util.print_error("can't find address for scripthash: %s" % h)
|
||||||
l = self.subscriptions.get(addr, [])
|
l = self.subscriptions.get(addr, [])
|
||||||
for ws, amount in l:
|
for ws, amount in l:
|
||||||
if not ws.closed:
|
if not ws.closed:
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
from . import util
|
from . import util
|
||||||
from .util import profiler, bh2u, get_cert_path
|
from .util import profiler, bh2u
|
||||||
import ecdsa
|
import ecdsa
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
|
|
@ -194,7 +194,7 @@ class Plugin(BasePlugin):
|
||||||
return
|
return
|
||||||
|
|
||||||
wallet = window.wallet
|
wallet = window.wallet
|
||||||
if wallet.has_password():
|
if wallet.has_keystore_encryption():
|
||||||
password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.')
|
password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.')
|
||||||
if not password:
|
if not password:
|
||||||
return
|
return
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue