diff --git a/contrib/build-osx/README.md b/contrib/build-osx/README.md new file mode 100644 index 000000000..c1e96d90b --- /dev/null +++ b/contrib/build-osx/README.md @@ -0,0 +1,36 @@ +Building Mac OS binaries +======================== + +This guide explains how to build Electrum binaries for macOS systems. + +The build process consists of two steps: + +## 1. Building the binary + +This needs to be done on a system running macOS or OS X. We use El Capitan (10.11.6) as building it on High Sierra +makes the binaries incompatible with older versions. + +Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`). + + + cd electrum + ./contrib/build-osx/make_osx + +This creates a folder named Electrum.app. + +## 2. Building the image +The usual way to distribute macOS applications is to use image files containing the +application. Although these images can be created on a Mac with the built-in `hdiutil`, +they are not deterministic. + +Instead, we use the toolchain that Bitcoin uses: genisoimage and libdmg-hfsplus. +These tools do not work on macOS, so you need a separate Linux machine (or VM). + +Copy the Electrum.app directory over and install the dependencies, e.g.: + + apt install libcap-dev cmake make gcc faketime + +Then you can just invoke `package.sh` with the path to the app: + + cd electrum + ./contrib/build-osx/package.sh ~/Electrum.app/ \ No newline at end of file diff --git a/contrib/build-osx/base.sh b/contrib/build-osx/base.sh new file mode 100644 index 000000000..c5a5c0d69 --- /dev/null +++ b/contrib/build-osx/base.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env 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 +} diff --git a/contrib/build-osx/cdrkit-deterministic.patch b/contrib/build-osx/cdrkit-deterministic.patch new file mode 100644 index 000000000..d01e5b75e --- /dev/null +++ b/contrib/build-osx/cdrkit-deterministic.patch @@ -0,0 +1,86 @@ +--- cdrkit-1.1.11.old/genisoimage/tree.c 2008-10-21 19:57:47.000000000 -0400 ++++ cdrkit-1.1.11/genisoimage/tree.c 2013-12-06 00:23:18.489622668 -0500 +@@ -1139,8 +1139,9 @@ + scan_directory_tree(struct directory *this_dir, char *path, + struct directory_entry *de) + { +- DIR *current_dir; ++ int current_file; + char whole_path[PATH_MAX]; ++ struct dirent **d_list; + struct dirent *d_entry; + struct directory *parent; + int dflag; +@@ -1164,7 +1165,8 @@ + this_dir->dir_flags |= DIR_WAS_SCANNED; + + errno = 0; /* Paranoia */ +- current_dir = opendir(path); ++ //current_dir = opendir(path); ++ current_file = scandir(path, &d_list, NULL, alphasort); + d_entry = NULL; + + /* +@@ -1173,12 +1175,12 @@ + */ + old_path = path; + +- if (current_dir) { ++ if (current_file >= 0) { + errno = 0; +- d_entry = readdir(current_dir); ++ d_entry = d_list[0]; + } + +- if (!current_dir || !d_entry) { ++ if (current_file < 0 || !d_entry) { + int ret = 1; + + #ifdef USE_LIBSCHILY +@@ -1191,8 +1193,8 @@ + de->isorec.flags[0] &= ~ISO_DIRECTORY; + ret = 0; + } +- if (current_dir) +- closedir(current_dir); ++ if(d_list) ++ free(d_list); + return (ret); + } + #ifdef ABORT_DEEP_ISO_ONLY +@@ -1208,7 +1210,7 @@ + errmsgno(EX_BAD, "use Rock Ridge extensions via -R or -r,\n"); + errmsgno(EX_BAD, "or allow deep ISO9660 directory nesting via -D.\n"); + } +- closedir(current_dir); ++ free(d_list); + return (1); + } + #endif +@@ -1250,13 +1252,13 @@ + * The first time through, skip this, since we already asked + * for the first entry when we opened the directory. + */ +- if (dflag) +- d_entry = readdir(current_dir); ++ if (dflag && current_file >= 0) ++ d_entry = d_list[current_file]; + dflag++; + +- if (!d_entry) ++ if (current_file < 0) + break; +- ++ current_file--; + /* OK, got a valid entry */ + + /* If we do not want all files, then pitch the backups. */ +@@ -1348,7 +1350,7 @@ + insert_file_entry(this_dir, whole_path, d_entry->d_name); + #endif /* APPLE_HYB */ + } +- closedir(current_dir); ++ free(d_list); + + #ifdef APPLE_HYB + /* \ No newline at end of file diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx new file mode 100644 index 000000000..599480e23 --- /dev/null +++ b/contrib/build-osx/make_osx @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Parameterize +PYTHON_VERSION=3.6.4 +BUILDDIR=/tmp/electrum-build +PACKAGE=Electrum +GIT_REPO=https://github.com/spesmilo/electrum +LIBSECP_VERSION=452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4 + +. $(dirname "$0")/base.sh + +src_dir=$(dirname "$0") +cd $src_dir/../.. + +export PYTHONHASHSEED=22 +VERSION=`git describe --tags --dirty --always` + +which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue" + +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 -I --user pyinstaller==3.4 || fail "Could not install pyinstaller" + +info "Using these versions for building $PACKAGE:" +sw_vers +python3 --version +echo -n "Pyinstaller " +pyinstaller --version + +rm -rf ./dist + +git submodule init +git submodule update + +rm -rf $BUILDDIR > /dev/null 2>&1 +mkdir $BUILDDIR + +cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./electrum/locale/ +cp ./contrib/deterministic-build/electrum-icons/icons_rc.py ./electrum/gui/qt/ + + +info "Downloading libusb..." +curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \ +tar xz --directory $BUILDDIR +cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/build-osx + +info "Building libsecp256k1" +brew install autoconf automake libtool +git clone https://github.com/bitcoin-core/secp256k1 $BUILDDIR/secp256k1 +pushd $BUILDDIR/secp256k1 +git reset --hard $LIBSECP_VERSION +git clean -f -x -q +./autogen.sh +./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --disable-jni +make +popd +cp $BUILDDIR/secp256k1/.libs/libsecp256k1.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 "Faking timestamps..." +for d in ~/Library/Python/ ~/.pyenv .; do + pushd $d + find . -exec touch -t '200101220000' {} + + popd +done + +info "Building binary" +pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary" + +info "Adding bitcoin URI types to Info.plist" +plutil -insert 'CFBundleURLTypes' \ + -xml ' CFBundleURLName bitcoin CFBundleURLSchemes bitcoin ' \ + -- dist/$PACKAGE.app/Contents/Info.plist \ + || fail "Could not add keys to Info.plist. Make sure the program 'plutil' exists and is installed." + +info "Creating .DMG" +hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG" diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec new file mode 100644 index 000000000..501df88ec --- /dev/null +++ b/contrib/build-osx/osx.spec @@ -0,0 +1,104 @@ +# -*- mode: python -*- + +from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs + +import sys +import os + +PACKAGE='Electrum' +PYPKG='electrum' +MAIN_SCRIPT='run_electrum' +ICONS_FILE='electrum.icns' + +for i, x in enumerate(sys.argv): + if x == '--name': + VERSION = sys.argv[i+1] + break +else: + raise Exception('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('safetlib') +hiddenimports += collect_submodules('btchip') +hiddenimports += collect_submodules('keepkeylib') +hiddenimports += collect_submodules('websocket') +hiddenimports += collect_submodules('ckcc') + +datas = [ + (electrum + PYPKG + '/*.json', PYPKG), + (electrum + PYPKG + '/wordlist/english.txt', PYPKG + '/wordlist'), + (electrum + PYPKG + '/locale', PYPKG + '/locale'), + (electrum + PYPKG + '/plugins', PYPKG + '/plugins'), +] +datas += collect_data_files('trezorlib') +datas += collect_data_files('safetlib') +datas += collect_data_files('btchip') +datas += collect_data_files('keepkeylib') +datas += collect_data_files('ckcc') + +# Add libusb so Trezor and Safe-T mini will work +binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")] +binaries += [(electrum + "contrib/build-osx/libsecp256k1.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+'electrum/gui/qt/main_window.py', + electrum+'electrum/gui/text.py', + electrum+'electrum/util.py', + electrum+'electrum/wallet.py', + electrum+'electrum/simple_config.py', + electrum+'electrum/bitcoin.py', + electrum+'electrum/dnssec.py', + electrum+'electrum/commands.py', + electrum+'electrum/plugins/cosigner_pool/qt.py', + electrum+'electrum/plugins/email_requests/qt.py', + electrum+'electrum/plugins/trezor/client.py', + electrum+'electrum/plugins/trezor/qt.py', + electrum+'electrum/plugins/safe_t/client.py', + electrum+'electrum/plugins/safe_t/qt.py', + electrum+'electrum/plugins/keepkey/qt.py', + electrum+'electrum/plugins/ledger/qt.py', + electrum+'electrum/plugins/coldcard/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', + 'NSSupportsAutomaticGraphicsSwitching': 'True' + } +) diff --git a/contrib/build-osx/package.sh b/contrib/build-osx/package.sh new file mode 100644 index 000000000..dcbc29388 --- /dev/null +++ b/contrib/build-osx/package.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +cdrkit_version=1.1.11 +cdrkit_download_path=http://distro.ibiblio.org/fatdog/source/600/c +cdrkit_file_name=cdrkit-${cdrkit_version}.tar.bz2 +cdrkit_sha256_hash=b50d64c214a65b1a79afe3a964c691931a4233e2ba605d793eb85d0ac3652564 +cdrkit_patches=cdrkit-deterministic.patch +genisoimage=genisoimage-$cdrkit_version + +libdmg_url=https://github.com/theuni/libdmg-hfsplus + + +export LD_PRELOAD=$(locate libfaketime.so.1) +export FAKETIME="2000-01-22 00:00:00" +export PATH=$PATH:~/bin + +. $(dirname "$0")/base.sh + +if [ -z "$1" ]; then + echo "Usage: $0 Electrum.app" + exit -127 +fi + +mkdir -p ~/bin + +if ! which ${genisoimage} > /dev/null 2>&1; then + mkdir -p /tmp/electrum-macos + cd /tmp/electrum-macos + info "Downloading cdrkit $cdrkit_version" + wget -nc ${cdrkit_download_path}/${cdrkit_file_name} + tar xvf ${cdrkit_file_name} + + info "Patching genisoimage" + cd cdrkit-${cdrkit_version} + patch -p1 < ../cdrkit-deterministic.patch + + info "Building genisoimage" + cmake . -Wno-dev + make genisoimage + cp genisoimage/genisoimage ~/bin/${genisoimage} +fi + +if ! which dmg > /dev/null 2>&1; then + mkdir -p /tmp/electrum-macos + cd /tmp/electrum-macos + info "Downloading libdmg" + LD_PRELOAD= git clone ${libdmg_url} + cd libdmg-hfsplus + info "Building libdmg" + cmake . + make + cp dmg/dmg ~/bin +fi + +${genisoimage} -version || fail "Unable to install genisoimage" +dmg -|| fail "Unable to install libdmg" + +plist=$1/Contents/Info.plist +test -f "$plist" || fail "Info.plist not found" +VERSION=$(grep -1 ShortVersionString $plist |tail -1|gawk 'match($0, /(.*)<\/string>/, a) {print a[1]}') +echo $VERSION + +rm -rf /tmp/electrum-macos/image > /dev/null 2>&1 +mkdir /tmp/electrum-macos/image/ +cp -r $1 /tmp/electrum-macos/image/ + +build_dir=$(dirname "$1") +test -n "$build_dir" -a -d "$build_dir" || exit +cd $build_dir + +${genisoimage} \ + -no-cache-inodes \ + -D \ + -l \ + -probe \ + -V "Electrum" \ + -no-pad \ + -r \ + -dir-mode 0755 \ + -apple \ + -o Electrum_uncompressed.dmg \ + /tmp/electrum-macos/image || fail "Unable to create uncompressed dmg" + +dmg dmg Electrum_uncompressed.dmg electrum-$VERSION.dmg || fail "Unable to create compressed dmg" +rm Electrum_uncompressed.dmg + +echo "Done." +md5sum electrum-$VERSION.dmg diff --git a/contrib/build-wine/build-secp256k1.sh b/contrib/build-wine/build-secp256k1.sh new file mode 100644 index 000000000..30d4a598b --- /dev/null +++ b/contrib/build-wine/build-secp256k1.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# heavily based on https://github.com/ofek/coincurve/blob/417e726f553460f88d7edfa5dc67bfda397c4e4a/.travis/build_windows_wheels.sh + +set -e + +build_dll() { + #sudo apt-get install -y mingw-w64 + export SOURCE_DATE_EPOCH=1530212462 + ./autogen.sh + echo "LDFLAGS = -no-undefined" >> Makefile.am + LDFLAGS="-Wl,--no-insert-timestamp" ./configure \ + --host=$1 \ + --enable-module-recovery \ + --enable-experimental \ + --enable-module-ecdh \ + --disable-jni + make + ${1}-strip .libs/libsecp256k1-0.dll +} + + +cd /tmp/electrum-build + +if [ ! -d secp256k1 ]; then + git clone https://github.com/bitcoin-core/secp256k1.git + cd secp256k1; +else + cd secp256k1 + git pull +fi + +git reset --hard 452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4 +git clean -f -x -q + +build_dll i686-w64-mingw32 # 64-bit would be: x86_64-w64-mingw32 +mv .libs/libsecp256k1-0.dll libsecp256k1.dll + +find -exec touch -d '2000-11-11T11:11:11+00:00' {} + + +echo "building libsecp256k1 finished" diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index 79d86067d..a5c225678 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -118,7 +118,7 @@ exe_standalone = EXE( a.scripts, a.binaries, a.datas, - name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + ".exe"), + name=os.path.join('build\\pyi.win32\\electrum-lbry', cmdline_name + ".exe"), debug=False, strip=None, upx=False, @@ -131,7 +131,7 @@ exe_portable = EXE( a.scripts, a.binaries, 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-lbry', cmdline_name + "-portable.exe"), debug=False, strip=None, upx=False, @@ -145,7 +145,7 @@ exe_dependent = EXE( pyz, a.scripts, exclude_binaries=True, - name=os.path.join('build\\pyi.win32\\electrum', cmdline_name), + name=os.path.join('build\\pyi.win32\\electrum-lbry', cmdline_name), debug=False, strip=None, upx=False, diff --git a/contrib/build-wine/docker/Dockerfile b/contrib/build-wine/docker/Dockerfile new file mode 100644 index 000000000..13d335228 --- /dev/null +++ b/contrib/build-wine/docker/Dockerfile @@ -0,0 +1,34 @@ +FROM ubuntu:18.04@sha256:5f4bdc3467537cbbe563e80db2c3ec95d548a9145d64453b06939c4592d67b6d + +ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 + +RUN dpkg --add-architecture i386 && \ + apt-get update -q && \ + apt-get install -qy \ + wget=1.19.4-1ubuntu2.1 \ + gnupg2=2.2.4-1ubuntu1.1 \ + dirmngr=2.2.4-1ubuntu1.1 \ + python3-software-properties=0.96.24.32.1 \ + software-properties-common=0.96.24.32.1 \ + && \ + wget -nc https://dl.winehq.org/wine-builds/Release.key && \ + apt-key add Release.key && \ + apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/ && \ + apt-get update -q && \ + apt-get install -qy \ + wine-stable-amd64:amd64=3.0.1~bionic \ + wine-stable-i386:i386=3.0.1~bionic \ + wine-stable:amd64=3.0.1~bionic \ + winehq-stable:amd64=3.0.1~bionic \ + git \ + p7zip-full=16.02+dfsg-6 \ + make=4.1-9.1ubuntu1 \ + mingw-w64=5.0.3-1 \ + autotools-dev=20180224.1 \ + autoconf=2.69-11 \ + libtool=2.4.6-2 \ + gettext=0.19.8.1-6 \ + && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get autoremove -y && \ + apt-get clean diff --git a/contrib/build-wine/docker/README.md b/contrib/build-wine/docker/README.md new file mode 100644 index 000000000..aba87018f --- /dev/null +++ b/contrib/build-wine/docker/README.md @@ -0,0 +1,103 @@ +Deterministic Windows binaries with Docker +========================================== + +Produced binaries are deterministic, so you should be able to generate +binaries that match the official releases. + +This assumes an Ubuntu host, but it should not be too hard to adapt to another +similar system. The docker commands should be executed in the project's root +folder. + +1. Install Docker + + ``` + $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + $ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + $ sudo apt-get update + $ sudo apt-get install -y docker-ce + ``` + +2. Build image + + ``` + $ sudo docker build --no-cache -t electrum-wine-builder-img contrib/build-wine/docker + ``` + + Note: see [this](https://stackoverflow.com/a/40516974/7499128) if having dns problems + +3. Build Windows binaries + + It's recommended to build from a fresh clone + (but you can skip this if reproducibility is not necessary). + + ``` + $ FRESH_CLONE=contrib/build-wine/fresh_clone && \ + rm -rf $FRESH_CLONE && \ + mkdir -p $FRESH_CLONE && \ + cd $FRESH_CLONE && \ + git clone https://github.com/spesmilo/electrum.git && \ + cd electrum + ``` + + And then build from this directory: + ``` + $ git checkout $REV + $ sudo docker run \ + --name electrum-wine-builder-cont \ + -v $PWD:/opt/wine64/drive_c/electrum \ + --rm \ + --workdir /opt/wine64/drive_c/electrum/contrib/build-wine \ + electrum-wine-builder-img \ + ./build.sh + ``` +4. The generated binaries are in `./contrib/build-wine/dist`. + + + +Note: the `setup` binary (NSIS installer) is not deterministic yet. + + +Code Signing +============ + +Electrum Windows builds are signed with a Microsoft Authenticode™ code signing +certificate in addition to the GPG-based signatures. + +The advantage of using Authenticode is that Electrum users won't receive a +Windows SmartScreen warning when starting it. + +The release signing procedure involves a signer (the holder of the +certificate/key) and one or multiple trusted verifiers: + + +| Signer | Verifier | +|-----------------------------------------------------------|-----------------------------------| +| Build .exe files using `build.sh` | | +| Sign .exe with `./sign.sh` | | +| Upload signed files to download server | | +| | Build .exe files using `build.sh` | +| | Compare files using `unsign.sh` | +| | Sign .exe file using `gpg -b` | + +| Signer and verifiers: | +|-----------------------------------------------------------------------------------------------| +| Upload signatures to 'electrum-signatures' repo, as `$version/$filename.$builder.asc` | + + + +Verify Integrity of signed binary +================================= + +Every user can verify that the official binary was created from the source code in this +repository. To do so, the Authenticode signature needs to be stripped since the signature +is not reproducible. + +This procedure removes the differences between the signed and unsigned binary: + +1. Remove the signature from the signed binary using osslsigncode or signtool. +2. Set the COFF image checksum for the signed binary to 0x0. This is necessary + because pyinstaller doesn't generate a checksum. +3. Append null bytes to the _unsigned_ binary until the byte count is a multiple + of 8. + +The script `unsign.sh` performs these steps. diff --git a/contrib/build-wine/electrum.nsi b/contrib/build-wine/electrum.nsi index 1946c7749..f5ea8eaf6 100644 --- a/contrib/build-wine/electrum.nsi +++ b/contrib/build-wine/electrum.nsi @@ -2,11 +2,11 @@ ;Include Modern UI !include "TextFunc.nsh" ;Needed for the $GetSize function. I know, doesn't sound logical, it isn't. !include "MUI2.nsh" - + ;-------------------------------- ;Variables - !define PRODUCT_NAME "Electrum" + !define PRODUCT_NAME "LBRY_Wallet" !define PRODUCT_WEB_SITE "https://github.com/spesmilo/electrum" !define PRODUCT_PUBLISHER "Electrum Technologies GmbH" !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" @@ -16,7 +16,7 @@ ;Name and file Name "${PRODUCT_NAME}" - OutFile "dist/electrum-setup.exe" + OutFile "dist/electrum-lbry-setup.exe" ;Default installation folder InstallDir "$PROGRAMFILES\${PRODUCT_NAME}" @@ -29,31 +29,31 @@ ;Specifies whether or not the installer will perform a CRC on itself before allowing an install CRCCheck on - + ;Sets whether or not the details of the install are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them. ShowInstDetails show - + ;Sets whether or not the details of the uninstall are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them. ShowUninstDetails show - + ;Sets the colors to use for the install info screen (the default is 00FF00 000000. Use the form RRGGBB (in hexadecimal, as in HTML, only minus the leading '#', since # can be used for comments). Note that if "/windows" is specified as the only parameter, the default windows colors will be used. InstallColors /windows - + ;This command sets the compression algorithm used to compress files/data in the installer. (http://nsis.sourceforge.net/Reference/SetCompressor) SetCompressor /SOLID lzma - + ;Sets the dictionary size in megabytes (MB) used by the LZMA compressor (default is 8 MB). SetCompressorDictSize 64 - + ;Sets the text that is shown (by default it is 'Nullsoft Install System vX.XX') in the bottom of the install window. Setting this to an empty string ("") uses the default; to set the string to blank, use " " (a space). - BrandingText "${PRODUCT_NAME} Installer v${PRODUCT_VERSION}" - + BrandingText "${PRODUCT_NAME} Installer v${PRODUCT_VERSION}" + ;Sets what the titlebars of the installer will display. By default, it is 'Name Setup', where Name is specified with the Name command. You can, however, override it with 'MyApp Installer' or whatever. If you specify an empty string (""), the default will be used (you can however specify " " to achieve a blank string) Caption "${PRODUCT_NAME}" ;Adds the Product Version on top of the Version Tab in the Properties of the file. VIProductVersion 1.0.0.0 - + ;VIAddVersionKey - Adds a field in the Version Tab of the File Properties. This can either be a field provided by the system or a user defined field. VIAddVersionKey ProductName "${PRODUCT_NAME} Installer" VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}" @@ -63,7 +63,7 @@ VIAddVersionKey FileVersion ${PRODUCT_VERSION} VIAddVersionKey ProductVersion ${PRODUCT_VERSION} VIAddVersionKey InternalName "${PRODUCT_NAME} Installer" - VIAddVersionKey LegalTrademarks "${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}" + VIAddVersionKey LegalTrademarks "${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}" VIAddVersionKey OriginalFilename "${PRODUCT_NAME}.exe" ;-------------------------------- @@ -71,9 +71,9 @@ !define MUI_ABORTWARNING !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?" - + !define MUI_ICON "c:\electrum\electrum\gui\icons\electrum.ico" - + ;-------------------------------- ;Pages @@ -108,7 +108,7 @@ Section RMDir /r "$INSTDIR\*.*" Delete "$DESKTOP\${PRODUCT_NAME}.lnk" Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*" - + ;Files to pack into the installer File /r "dist\electrum\*.*" File "c:\electrum\electrum\gui\icons\electrum.ico" @@ -122,21 +122,23 @@ Section ;Create desktop shortcut DetailPrint "Creating desktop shortcut..." - CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "" + CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-lbry-${PRODUCT_VERSION}.exe" "" ;Create start-menu items DetailPrint "Creating start-menu items..." CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0 - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0 + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "" "$INSTDIR\electrum-lbry-${PRODUCT_VERSION}.exe" 0 + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-lbry-${PRODUCT_VERSION}.exe" 0 + + ;Links bitcoin: URI's to Electrum WriteRegStr HKCU "Software\Classes\bitcoin" "" "URL:bitcoin Protocol" WriteRegStr HKCU "Software\Classes\bitcoin" "URL Protocol" "" WriteRegStr HKCU "Software\Classes\bitcoin" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\"" - WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\"" + WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\electrum-lbry-${PRODUCT_VERSION}.exe$\" $\"%1$\"" ;Adds an uninstaller possibility to Windows Uninstall or change a program section WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" @@ -166,8 +168,8 @@ Section "Uninstall" Delete "$DESKTOP\${PRODUCT_NAME}.lnk" Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*" RMDir "$SMPROGRAMS\${PRODUCT_NAME}" - - DeleteRegKey HKCU "Software\Classes\bitcoin" + + DeleteRegKey HKCU "Software\Classes\lbc" DeleteRegKey HKCU "Software\${PRODUCT_NAME}" DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}" SectionEnd diff --git a/contrib/make_locale b/contrib/make_locale new file mode 100644 index 000000000..3c28d5702 --- /dev/null +++ b/contrib/make_locale @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +import os +import subprocess +import io +import zipfile +import requests + +os.chdir(os.path.dirname(os.path.realpath(__file__))) +os.chdir('..') + +cmd = "find electrum -type f -name '*.py' -o -name '*.kv'" + +files = subprocess.check_output(cmd, shell=True) + +with open("app.fil", "wb") as f: + f.write(files) + +print("Found {} files to translate".format(len(files.splitlines()))) + +# Generate fresh translation template +if not os.path.exists('electrum/locale'): + os.mkdir('electrum/locale') +cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=electrum/locale/messages.pot' +print('Generate template') +os.system(cmd) + +os.chdir('electrum') + +crowdin_identifier = 'electrum' +crowdin_file_name = 'files[electrum-client/messages.pot]' +locale_file_name = 'locale/messages.pot' +crowdin_api_key = None + +filename = os.path.expanduser('~/.crowdin_api_key') +if os.path.exists(filename): + with open(filename) as f: + crowdin_api_key = f.read().strip() + +if "crowdin_api_key" in os.environ: + crowdin_api_key = os.environ["crowdin_api_key"] + +if crowdin_api_key: + # Push to Crowdin + print('Push to Crowdin') + url = ('https://api.crowdin.com/api/project/' + crowdin_identifier + '/update-file?key=' + crowdin_api_key) + with open(locale_file_name, 'rb') as f: + files = {crowdin_file_name: f} + response = requests.request('POST', url, files=files) + print("", "update-file:", "-"*20, response.text, "-"*20, sep="\n") + # Build translations + print('Build translations') + response = requests.request('GET', 'https://api.crowdin.com/api/project/' + crowdin_identifier + '/export?key=' + crowdin_api_key) + print("", "export:", "-" * 20, response.text, "-" * 20, sep="\n") + +# Download & unzip +print('Download translations') +s = requests.request('GET', 'https://crowdin.com/backend/download/project/' + crowdin_identifier + '.zip').content +zfobj = zipfile.ZipFile(io.BytesIO(s)) + +print('Unzip translations') +for name in zfobj.namelist(): + if not name.startswith('electrum-client/locale'): + continue + if name.endswith('/'): + if not os.path.exists(name[16:]): + os.mkdir(name[16:]) + else: + with open(name[16:], 'wb') as output: + output.write(zfobj.read(name)) + +# Convert .po to .mo +print('Installing') +for lang in os.listdir('locale'): + if lang.startswith('messages'): + continue + # Check LC_MESSAGES folder + mo_dir = 'locale/%s/LC_MESSAGES' % lang + if not os.path.exists(mo_dir): + os.mkdir(mo_dir) + cmd = 'msgfmt --output-file="%s/electrum.mo" "locale/%s/electrum.po"' % (mo_dir,lang) + print('Installing', lang) + os.system(cmd) diff --git a/electrum.icns b/electrum.icns new file mode 100644 index 000000000..977b124d0 Binary files /dev/null and b/electrum.icns differ diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 7f1f5e1a5..93de6f91f 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -139,7 +139,7 @@ class BaseWizard(Logger): ('standard', _("Standard wallet")), ('2fa', _("Wallet with two-factor authentication")), ('multisig', _("Multi-signature wallet")), - ('imported', _("Import Bitcoin addresses or private keys")), + ('imported', _("Import LBRY Credits addresses or private keys")), ] choices = [pair for pair in wallet_kinds if pair[0] in wallet_types] self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type) @@ -211,8 +211,8 @@ class BaseWizard(Logger): def import_addresses_or_keys(self): v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x, raise_on_error=True) - title = _("Import Bitcoin Addresses") - message = _("Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.") + title = _("Import LBRY Credits Addresses") + message = _("Enter a list of LBRY Credits addresses (this will create a watching-only wallet), or a list of private keys.") self.add_xpub_dialog(title=title, message=message, run_next=self.on_import, is_valid=v, allow_multi=True, show_wif_help=True) @@ -393,14 +393,13 @@ class BaseWizard(Logger): # There is no general standard for HD multisig. # For legacy, this is partially compatible with BIP45; assumes index=0 # For segwit, a custom path is used, as there is no standard at all. - default_choice_idx = 2 + default_choice_idx = 0 choices = [ ('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")), - ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), - ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), + ] else: - default_choice_idx = 2 + default_choice_idx = 0 choices = [ ('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), ('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), @@ -625,11 +624,11 @@ class BaseWizard(Logger): _("The type of addresses used by your wallet will depend on your seed."), _("Segwit wallets use bech32 addresses, defined in BIP173."), _("Please note that websites and other wallets may not support these addresses yet."), - _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.") + _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive LBRY Credits during the transition period.") ]) if choices is None: choices = [ - ('create_segwit_seed', _('Segwit')), + ('create_standard_seed', _('Legacy')), ] self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 259941bcd..96e21b0db 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -24,6 +24,9 @@ import os import threading from typing import Optional, Dict, Mapping, Sequence +import hashlib +import hmac + from . import util from .bitcoin import hash_encode, int_to_hex, rev_hex from .crypto import sha256d @@ -35,9 +38,10 @@ from .logging import get_logger, Logger _logger = get_logger(__name__) -HEADER_SIZE = 80 # bytes -MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 - +HEADER_SIZE = 112 # bytes +MAX_TARGET = 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +GENESIS_BITS = 0x1f00ffff +N_TARGET_TIMESPAN = 150 class MissingHeader(Exception): pass @@ -49,6 +53,7 @@ def serialize_header(header_dict: dict) -> str: s = int_to_hex(header_dict['version'], 4) \ + rev_hex(header_dict['prev_block_hash']) \ + rev_hex(header_dict['merkle_root']) \ + + rev_hex(header_dict['claim_trie_root']) \ + int_to_hex(int(header_dict['timestamp']), 4) \ + int_to_hex(int(header_dict['bits']), 4) \ + int_to_hex(int(header_dict['nonce']), 4) @@ -64,9 +69,10 @@ def deserialize_header(s: bytes, height: int) -> dict: h['version'] = hex_to_int(s[0:4]) h['prev_block_hash'] = hash_encode(s[4:36]) h['merkle_root'] = hash_encode(s[36:68]) - h['timestamp'] = hex_to_int(s[68:72]) - h['bits'] = hex_to_int(s[72:76]) - h['nonce'] = hex_to_int(s[76:80]) + h['claim_trie_root'] = hash_encode(s[68:100]) + h['timestamp'] = hex_to_int(s[100:104]) + h['bits'] = hex_to_int(s[104:108]) + h['nonce'] = hex_to_int(s[108:112]) h['block_height'] = height return h @@ -77,10 +83,35 @@ def hash_header(header: dict) -> str: header['prev_block_hash'] = '00'*32 return hash_raw_header(serialize_header(header)) +def pow_hash_header(header: dict) -> str: + if header is None: + return '0' * 64 + return hash_encode(PoWHash(bfh(serialize_header(header)))) + +def sha256(x): + return hashlib.sha256(x).digest() + +def sha512(x): + return hashlib.sha512(x).digest() + +def ripemd160(x): + h = hashlib.new('ripemd160') + h.update(x) + return h.digest() + +def Hash(x): + return sha256(sha256(x)) def hash_raw_header(header: str) -> str: return hash_encode(sha256d(bfh(header))) +def PoWHash(x): + + r = sha512(Hash(x)) + r1 = ripemd160(r[:len(r) // 2]) + r2 = ripemd160(r[len(r) // 2:]) + r3 = Hash(r1 + r2) + return r3 # key: blockhash hex at forkpoint # the chain at some key is the best chain that includes the given hash @@ -282,35 +313,37 @@ class Blockchain(Logger): self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0 @classmethod - def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None: - _hash = hash_header(header) - if expected_header_hash and expected_header_hash != _hash: - raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash)) + def verify_header(self, header: dict, prev_hash: str, target: int, bits: int, expected_header_hash: str=None) -> None: + _hash = pow_hash_header(header) + if expected_header_hash: + _hash2 = hash_header(header) + if expected_header_hash != _hash2: + raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash2)) if prev_hash != header.get('prev_block_hash'): raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) if constants.net.TESTNET: return - bits = cls.target_to_bits(target) - if bits != header.get('bits'): - raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits'))) - block_hash_as_num = int.from_bytes(bfh(_hash), byteorder='big') - if block_hash_as_num > target: - raise Exception(f"insufficient proof of work: {block_hash_as_num} vs target {target}") + + #if bits != header.get('bits'): + # raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits'))) + #if int('0x' + _hash, 16) > target: + # raise Exception("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target)) def verify_chunk(self, index: int, data: bytes) -> None: num = len(data) // HEADER_SIZE start_height = index * 2016 prev_hash = self.get_hash(start_height - 1) - target = self.get_target(index-1) for i in range(num): height = start_height + i + header = self.read_header(height - 1) + #bits, target = self.get_target2(height - 1, header) try: expected_header_hash = self.get_hash(height) except MissingHeader: expected_header_hash = None raw_header = data[i*HEADER_SIZE : (i+1)*HEADER_SIZE] header = deserialize_header(raw_header, index*2016 + i) - self.verify_header(header, prev_hash, target, expected_header_hash) + self.verify_header(header, prev_hash, 0, 0, expected_header_hash) prev_hash = hash_header(header) @with_lock @@ -507,18 +540,73 @@ class Blockchain(Logger): 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) - # not any target can be represented in 32 bits: - new_target = self.bits_to_target(self.target_to_bits(new_target)) - return new_target + nTargetTimespan = 150 + nModulatedTimespan = nTargetTimespan - (nActualTimespan - nTargetTimespan) / 8 + nMinTimespan = nTargetTimespan - (nTargetTimespan / 8) + nMaxTimespan = nTargetTimespan + (nTargetTimespan / 2) + if nModulatedTimespan < nMinTimespan: + nModulatedTimespan = nMinTimespan + elif nModulatedTimespan > nMaxTimespan: + nModulatedTimespan = nMaxTimespan + + bnOld = ArithUint256.SetCompact(bits) + bnNew = bnOld * nModulatedTimespan + # this doesn't work if it is nTargetTimespan even though that + # is what it looks like it should be based on reading the code + # in lbry.cpp + bnNew /= nModulatedTimespan + if bnNew > MAX_TARGET: + bnNew = ArithUint256(MAX_TARGET) + return bnNew.compact(), bnNew._value + + def get_target2(self, index, last, chain='main'): + + if index == -1: + return GENESIS_BITS, MAX_TARGET + if index == 0: + return GENESIS_BITS, MAX_TARGET + first = self.read_header(index-1) + assert last is not None, "Last shouldn't be none" + # bits to target + bits = last.get('bits') + # print_error("Last bits: ", bits) + self.check_bits(bits) + + # new target + nActualTimespan = last.get('timestamp') - first.get('timestamp') + nTargetTimespan = N_TARGET_TIMESPAN + nModulatedTimespan = nTargetTimespan - (nActualTimespan - nTargetTimespan) / 8 + nMinTimespan = nTargetTimespan - (nTargetTimespan / 8) + nMaxTimespan = nTargetTimespan + (nTargetTimespan / 2) + if nModulatedTimespan < nMinTimespan: + nModulatedTimespan = nMinTimespan + elif nModulatedTimespan > nMaxTimespan: + nModulatedTimespan = nMaxTimespan + + bnOld = ArithUint256.SetCompact(bits) + bnNew = bnOld * nModulatedTimespan + # this doesn't work if it is nTargetTimespan even though that + # is what it looks like it should be based on reading the code + # in lbry.cpp + bnNew /= nModulatedTimespan + if bnNew > MAX_TARGET: + bnNew = ArithUint256(MAX_TARGET) + return bnNew.compact, bnNew._value + + def check_bits(self, bits): + bitsN = (bits >> 24) & 0xff + assert 0x03 <= bitsN <= 0x1f, \ + "First part of bits should be in [0x03, 0x1d], but it was {}".format(hex(bitsN)) + bitsBase = bits & 0xffffff + assert 0x8000 <= bitsBase <= 0x7fffff, \ + "Second part of bits should be in [0x8000, 0x7fffff] but it was {}".format(bitsBase) + + @classmethod def bits_to_target(cls, bits: int) -> int: bitsN = (bits >> 24) & 0xff - if not (0x03 <= bitsN <= 0x1d): + if not (0x03 <= bitsN <= 0x1f): raise Exception("First part of bits should be in [0x03, 0x1d]") bitsBase = bits & 0xffffff if not (0x8000 <= bitsBase <= 0x7fffff): @@ -573,25 +661,33 @@ class Blockchain(Logger): def can_connect(self, header: dict, check_height: bool=True) -> bool: if header is None: return False + height = header['block_height'] if check_height and self.height() != height - 1: + print("cannot connect at height", height) return False if height == 0: return hash_header(header) == constants.net.GENESIS + try: prev_hash = self.get_hash(height - 1) except: return False + if prev_hash != header.get('prev_block_hash'): return False + try: - target = self.get_target(height // 2016 - 1) + bits, target = self.get_target2(height, header) except MissingHeader: return False + try: - self.verify_header(header, prev_hash, target) + self.verify_header(header, prev_hash, target, bits) except BaseException as e: + print(e) return False + return True def connect_chunk(self, idx: int, hexdata: str) -> bool: @@ -632,3 +728,83 @@ def can_connect(header: dict) -> Optional[Blockchain]: if b.can_connect(header): return b return None + +class ArithUint256: + # https://github.com/bitcoin/bitcoin/blob/master/src/arith_uint256.cpp + + __slots__ = '_value', '_compact' + + def __init__(self, value: int) -> None: + self._value = value + self._compact: Optional[int] = None + + @classmethod + def SetCompact(cls, nCompact): + return (ArithUint256.from_compact(nCompact)) + + @classmethod + def from_compact(cls, compact) -> 'ArithUint256': + size = compact >> 24 + word = compact & 0x007fffff + if size <= 3: + return cls(word >> 8 * (3 - size)) + else: + return cls(word << 8 * (size - 3)) + + @property + def value(self) -> int: + return self._value + + @property + def compact(self) -> int: + if self._compact is None: + self._compact = self._calculate_compact() + return self._compact + + @property + def negative(self) -> int: + return self._calculate_compact(negative=True) + + @property + def bits(self) -> int: + """ Returns the position of the highest bit set plus one. """ + bits = bin(self._value)[2:] + for i, d in enumerate(bits): + if d: + return (len(bits) - i) + 1 + return 0 + + @property + def low64(self) -> int: + return self._value & 0xffffffffffffffff + + def _calculate_compact(self, negative=False) -> int: + size = (self.bits + 7) // 8 + if size <= 3: + compact = self.low64 << 8 * (3 - size) + else: + compact = ArithUint256(self._value >> 8 * (size - 3)).low64 + # The 0x00800000 bit denotes the sign. + # Thus, if it is already set, divide the mantissa by 256 and increase the exponent. + if compact & 0x00800000: + compact >>= 8 + size += 1 + assert (compact & ~0x007fffff) == 0 + assert size < 256 + compact |= size << 24 + if negative and compact & 0x007fffff: + compact |= 0x00800000 + return compact + + def __mul__(self, x): + # Take the mod because we are limited to an unsigned 256 bit number + return ArithUint256((self._value * x) % 2 ** 256) + + def __truediv__(self, x): + return ArithUint256(int(self._value / x)) + + def __gt__(self, other): + return self._value > other + + def __lt__(self, other): + return self._value < other diff --git a/electrum/commands.py b/electrum/commands.py index 95c065e4f..396099788 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -239,7 +239,7 @@ class Commands: @command('') async def restore(self, text, passphrase=None, password=None, encrypt_file=True, wallet_path=None): """Restore a wallet from text. Text can be a seed phrase, a master - public key, a master private key, a list of bitcoin addresses + public key, a master private key, a list of LBRY Credits addresses or bitcoin private keys. If you want to be prompted for an argument, type '?' or ':' (concealed) """ @@ -667,7 +667,7 @@ class Commands: @command('w') async def setlabel(self, key, label, wallet: Abstract_Wallet = None): - """Assign a label to an item. Item may be a bitcoin address or a + """Assign a label to an item. Item may be a LBRY Credits address or a transaction ID""" wallet.set_label(key, label) @@ -1012,8 +1012,8 @@ def eval_bool(x: str) -> bool: param_descriptions = { 'privkey': 'Private key. Type \'?\' to get a prompt.', - 'destination': 'Bitcoin address, contact or alias', - 'address': 'Bitcoin address', + 'destination': 'LBRY Credits address, contact or alias', + 'address': 'LBRY Credits address', 'seed': 'Seed phrase', 'txid': 'Transaction ID', 'pos': 'Position', @@ -1023,8 +1023,8 @@ param_descriptions = { 'pubkey': 'Public key', 'message': 'Clear text message. Use quotes if it contains spaces.', 'encrypted': 'Encrypted message', - 'amount': 'Amount to be sent (in BTC). Type \'!\' to send the maximum available.', - 'requested_amount': 'Requested amount (in BTC).', + 'amount': 'Amount to be sent (in LBC). Type \'!\' to send the maximum available.', + 'requested_amount': 'Requested amount (in LBC).', 'outputs': 'list of ["address", amount]', 'redeem_script': 'redeem script (hexadecimal)', } @@ -1042,7 +1042,7 @@ command_options = { 'labels': ("-l", "Show the labels of listed addresses"), 'nocheck': (None, "Do not verify aliases"), 'imax': (None, "Maximum number of inputs"), - 'fee': ("-f", "Transaction fee (absolute, in BTC)"), + 'fee': ("-f", "Transaction fee (absolute, in LBC)"), 'feerate': (None, "Transaction fee rate (in sat/byte)"), 'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."), 'from_coins': (None, "Source coins (must be in wallet; use sweep to spend from non-wallet address)."), @@ -1062,7 +1062,7 @@ command_options = { 'timeout': (None, "Timeout in seconds"), 'force': (None, "Create new address beyond gap limit, if no more addresses are available."), 'pending': (None, "Show only pending requests."), - 'push_amount': (None, 'Push initial amount (in BTC)'), + 'push_amount': (None, 'Push initial amount (in LBC)'), 'expired': (None, "Show only expired requests."), 'paid': (None, "Show only paid requests."), 'show_addresses': (None, "Show input and output addresses"), @@ -1106,10 +1106,10 @@ config_variables = { 'addrequest': { 'ssl_privkey': 'Path to your SSL private key, needed to sign the request.', 'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end', - 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', + 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of lbry credits: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', }, 'listrequests':{ - 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', + 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of lbry credits: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', } } @@ -1194,7 +1194,7 @@ def get_parser(): subparsers = parser.add_subparsers(dest='cmd', metavar='') # gui parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)") - parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)") + parser_gui.add_argument("url", nargs='?', default=None, help="lbry credits URI (or bip70 file)") parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio']) parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup") parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI") diff --git a/electrum/constants.py b/electrum/constants.py index f049e4f76..77bada886 100644 --- a/electrum/constants.py +++ b/electrum/constants.py @@ -26,6 +26,7 @@ import os import json +BLOCKS_PER_CHUNK = 96 from .util import inv_dict from . import bitcoin @@ -60,14 +61,14 @@ class AbstractNet: class BitcoinMainnet(AbstractNet): TESTNET = False - WIF_PREFIX = 0x80 - ADDRTYPE_P2PKH = 0 - ADDRTYPE_P2SH = 5 - SEGWIT_HRP = "bc" - GENESIS = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + WIF_PREFIX = 0x1c + ADDRTYPE_P2PKH = 0x55 + ADDRTYPE_P2SH = 0x7A + SEGWIT_HRP = "lbc" + GENESIS = "9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463" DEFAULT_PORTS = {'t': '50001', 's': '50002'} DEFAULT_SERVERS = read_json('servers.json', {}) - CHECKPOINTS = read_json('checkpoints.json', []) + CHECKPOINTS = read_json('bullshit.json', []) BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS = 497000 XPRV_HEADERS = { @@ -86,7 +87,7 @@ class BitcoinMainnet(AbstractNet): 'p2wsh': 0x02aa7ed3, # Zpub } XPUB_HEADERS_INV = inv_dict(XPUB_HEADERS) - BIP44_COIN_TYPE = 0 + BIP44_COIN_TYPE = 140 LN_REALM_BYTE = 0 LN_DNS_SEEDS = [ 'nodes.lightning.directory.', @@ -100,7 +101,7 @@ class BitcoinTestnet(AbstractNet): WIF_PREFIX = 0xef ADDRTYPE_P2PKH = 111 ADDRTYPE_P2SH = 196 - SEGWIT_HRP = "tb" + SEGWIT_HRP = "tlbc" GENESIS = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943" DEFAULT_PORTS = {'t': '51001', 's': '51002'} DEFAULT_SERVERS = read_json('servers_testnet.json', {}) @@ -132,7 +133,7 @@ class BitcoinTestnet(AbstractNet): class BitcoinRegtest(BitcoinTestnet): - SEGWIT_HRP = "bcrt" + SEGWIT_HRP = "blbc" GENESIS = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" DEFAULT_SERVERS = read_json('servers_regtest.json', {}) CHECKPOINTS = [] @@ -144,7 +145,7 @@ class BitcoinSimnet(BitcoinTestnet): WIF_PREFIX = 0x64 ADDRTYPE_P2PKH = 0x3f ADDRTYPE_P2SH = 0x7b - SEGWIT_HRP = "sb" + SEGWIT_HRP = "slbc" GENESIS = "683e86bd5c6d110d91b94b97137ba6bfe02dbbdb8e3dff722a669b5d69d77af6" DEFAULT_SERVERS = read_json('servers_regtest.json', {}) CHECKPOINTS = [] diff --git a/electrum/contacts.py b/electrum/contacts.py index a4eaadbed..705af3dbd 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -93,7 +93,7 @@ class Contacts(dict, Logger): 'type': 'openalias', 'validated': validated } - raise Exception("Invalid Bitcoin address or alias", k) + raise Exception("Invalid LBRY Credits address or alias", k) def resolve_openalias(self, url): # support email-style addresses, per the OA standard @@ -103,7 +103,7 @@ class Contacts(dict, Logger): except DNSException as e: self.logger.info(f'Error resolving openalias: {repr(e)}') return None - prefix = 'btc' + prefix = 'lbc' for record in records: string = to_string(record.strings[0], 'utf8') if string.startswith('oa1:' + prefix): @@ -121,7 +121,7 @@ class Contacts(dict, Logger): return regex.search(haystack).groups()[0] except AttributeError: return None - + def _validate(self, data): for k, v in list(data.items()): if k == 'contacts': @@ -133,4 +133,3 @@ class Contacts(dict, Logger): if _type != 'address': data.pop(k) return data - diff --git a/electrum/currencies.json b/electrum/currencies.json index ee04e65b7..854b19c8c 100644 --- a/electrum/currencies.json +++ b/electrum/currencies.json @@ -28,6 +28,7 @@ "BTC", "BTN", "BWP", + "BYN", "BZD", "CAD", "CDF", @@ -46,6 +47,7 @@ "DZD", "EGP", "ETB", + "ETH", "EUR", "FJD", "FKP", @@ -160,6 +162,7 @@ "XCD", "XOF", "XPF", + "XRP", "YER", "ZAR", "ZMW", @@ -172,175 +175,6 @@ "Bitbank": [ "JPY" ], - "BitcoinAverage": [ - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTN", - "BWP", - "BYN", - "BZD", - "CAD", - "CDF", - "CHF", - "CLF", - "CLP", - "CNH", - "CNY", - "COP", - "CRC", - "CUC", - "CUP", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ERN", - "ETB", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GGP", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "IMP", - "INR", - "IQD", - "IRR", - "ISK", - "JEP", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KPW", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SDG", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "SSP", - "STD", - "SVC", - "SYP", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VES", - "VND", - "VUV", - "WST", - "XAF", - "XAG", - "XAU", - "XCD", - "XDR", - "XOF", - "XPD", - "XPF", - "XPT", - "YER", - "ZAR", - "ZMW", - "ZWL" - ], "BitcoinVenezuela": [ "ARS", "ETH", @@ -350,9 +184,6 @@ "VEF", "XMR" ], - "Bitcointoyou": [ - "BRL" - ], "Bitso": [ "MXN" ], @@ -604,6 +435,7 @@ "THB", "TRY", "TWD", + "UAH", "USD", "VEF", "VND", @@ -657,12 +489,14 @@ "CUC", "CVE", "CZK", + "DAI", "DJF", "DKK", "DOP", "DZD", "EEK", "EGP", + "EOS", "ERN", "ETB", "ETC", @@ -733,6 +567,7 @@ "NPR", "NZD", "OMR", + "OXT", "PAB", "PEN", "PGK", @@ -741,10 +576,12 @@ "PLN", "PYG", "QAR", + "REP", "RON", "RSD", "RUB", "RWF", + "SAI", "SAR", "SBD", "SCR", @@ -773,6 +610,7 @@ "UYU", "UZS", "VEF", + "VES", "VND", "VUV", "WST", @@ -781,10 +619,13 @@ "XAU", "XCD", "XDR", + "XLM", "XOF", "XPD", "XPF", "XPT", + "XRP", + "XTZ", "YER", "ZAR", "ZEC", @@ -802,14 +643,14 @@ ], "LocalBitcoins": [ "AED", + "AMD", + "AOA", "ARS", "AUD", - "BAM", "BDT", "BGN", "BOB", "BRL", - "BWP", "BYN", "CAD", "CHF", @@ -828,7 +669,6 @@ "GHS", "GTQ", "HKD", - "HNL", "HRK", "HUF", "IDR", @@ -836,25 +676,26 @@ "INR", "IRR", "JOD", - "JPY", "KES", "KRW", + "KWD", "KZT", - "LKR", "LTC", "MAD", + "MDL", + "MUR", "MXN", "MYR", "NGN", + "NIO", "NOK", "NZD", - "OMR", "PAB", "PEN", "PHP", "PKR", "PLN", - "QAR", + "PYG", "RON", "RSD", "RUB", @@ -865,7 +706,6 @@ "SZL", "THB", "TRY", - "TTD", "TWD", "TZS", "UAH", @@ -875,21 +715,15 @@ "VES", "VND", "XAF", - "XMR", + "XOF", "XRP", - "ZAR" - ], - "MercadoBitcoin": [ - "BRL" - ], - "NegocieCoins": [ - "BRL" + "ZAR", + "ZMW" ], "TheRockTrading": [ "EUR" ], "Zaif": [ "JPY" - ], - "itBit": [] -} + ] +} \ No newline at end of file diff --git a/electrum/ecc.py b/electrum/ecc.py index 4dabe5629..28e66db97 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -352,7 +352,7 @@ POINT_AT_INFINITY = ECPubkey(None) def msg_magic(message: bytes) -> bytes: from .bitcoin import var_int length = bfh(var_int(len(message))) - return b"\x18Bitcoin Signed Message:\n" + length + message + return b"\x18LBRYcrd Signed Message:\n" + length + message def verify_signature(pubkey: bytes, sig: bytes, h: bytes) -> bool: diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index b7b3b57af..90a962ffd 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -184,7 +184,7 @@ class ElectrumWindow(App): def on_new_intent(self, intent): data = intent.getDataString() - if intent.getScheme() == 'bitcoin': + if intent.getScheme() == 'lbrycredits': self.set_URI(data) elif intent.getScheme() == 'lightning': self.set_ln_invoice(data) diff --git a/electrum/gui/kivy/uix/context_menu.py b/electrum/gui/kivy/uix/context_menu.py new file mode 100644 index 000000000..84d5ba647 --- /dev/null +++ b/electrum/gui/kivy/uix/context_menu.py @@ -0,0 +1,56 @@ +#!python +#!/usr/bin/env python +from kivy.app import App +from kivy.uix.bubble import Bubble +from kivy.animation import Animation +from kivy.uix.floatlayout import FloatLayout +from kivy.lang import Builder +from kivy.factory import Factory +from kivy.clock import Clock + +from electrum.gui.kivy.i18n import _ + +Builder.load_string(''' + + background_normal: '' + background_color: (0.192, .498, 0.745, 1) + height: '48dp' + size_hint: 1, None + + + size_hint: 1, None + height: '48dp' + pos: (0, 0) + show_arrow: False + arrow_pos: 'top_mid' + padding: 0 + orientation: 'horizontal' + BoxLayout: + size_hint: 1, 1 + height: '48dp' + padding: '12dp', '0dp' + spacing: '3dp' + orientation: 'horizontal' + id: buttons +''') + + +class MenuItem(Factory.Button): + pass + +class ContextMenu(Bubble): + + def __init__(self, obj, action_list): + Bubble.__init__(self) + self.obj = obj + for k, v in action_list: + l = MenuItem() + l.text = _(k) + def func(f=v): + Clock.schedule_once(lambda dt: f(obj), 0.15) + l.on_release = func + self.ids.buttons.add_widget(l) + + def hide(self): + if self.parent: + self.parent.hide_menu() diff --git a/electrum/gui/kivy/uix/dialogs/invoices.py b/electrum/gui/kivy/uix/dialogs/invoices.py new file mode 100644 index 000000000..4fb986df1 --- /dev/null +++ b/electrum/gui/kivy/uix/dialogs/invoices.py @@ -0,0 +1,169 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder +from decimal import Decimal + +Builder.load_string(''' + + #color: .305, .309, .309, 1 + text_size: self.width, None + halign: 'left' + valign: 'top' + + + requestor: '' + memo: '' + amount: '' + status: '' + date: '' + icon: 'atlas://electrum/gui/kivy/theming/light/important' + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + InvoicesLabel: + text: root.requestor + shorten: True + Widget + InvoicesLabel: + text: root.memo + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + Widget + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + InvoicesLabel: + text: root.amount + font_size: '15sp' + halign: 'right' + width: '110sp' + Widget + InvoicesLabel: + text: root.status + font_size: '13sp' + halign: 'right' + color: .699, .699, .699, 1 + Widget + + + + id: popup + title: _('Invoices') + BoxLayout: + id: box + orientation: 'vertical' + spacing: '1dp' + ScrollView: + GridLayout: + cols: 1 + id: invoices_container + size_hint: 1, None + height: self.minimum_height + spacing: '2dp' + padding: '12dp' +''') + +from kivy.properties import BooleanProperty +from electrum.gui.kivy.i18n import _ +from electrum.util import format_time +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED +from electrum.gui.kivy.uix.context_menu import ContextMenu + +invoice_text = { + PR_UNPAID:_('Pending'), + PR_UNKNOWN:_('Unknown'), + PR_PAID:_('Paid'), + PR_EXPIRED:_('Expired') +} +pr_icon = { + PR_UNPAID: 'atlas://electrum/gui/kivy/theming/light/important', + PR_UNKNOWN: 'atlas://electrum/gui/kivy/theming/light/important', + PR_PAID: 'atlas://electrum/gui/kivy/theming/light/confirmed', + PR_EXPIRED: 'atlas://electrum/gui/kivy/theming/light/close' +} + + +class InvoicesDialog(Factory.Popup): + + def __init__(self, app, screen, callback): + Factory.Popup.__init__(self) + self.app = app + self.screen = screen + self.callback = callback + self.cards = {} + self.context_menu = None + + def get_card(self, pr): + key = pr.get_id() + ci = self.cards.get(key) + if ci is None: + ci = Factory.InvoiceItem() + ci.key = key + ci.screen = self + self.cards[key] = ci + ci.requestor = pr.get_requestor() + ci.memo = pr.get_memo() + amount = pr.get_amount() + if amount: + ci.amount = self.app.format_amount_and_units(amount) + status = self.app.wallet.invoices.get_status(ci.key) + ci.status = invoice_text[status] + ci.icon = pr_icon[status] + else: + ci.amount = _('No Amount') + ci.status = '' + exp = pr.get_expiration_date() + ci.date = format_time(exp) if exp else _('Never') + return ci + + def update(self): + self.menu_actions = [('Pay', self.do_pay), ('Details', self.do_view), ('Delete', self.do_delete)] + invoices_list = self.ids.invoices_container + invoices_list.clear_widgets() + _list = self.app.wallet.invoices.sorted_list() + for pr in _list: + ci = self.get_card(pr) + invoices_list.add_widget(ci) + + def do_pay(self, obj): + self.hide_menu() + self.dismiss() + pr = self.app.wallet.invoices.get(obj.key) + self.app.on_pr(pr) + + def do_view(self, obj): + pr = self.app.wallet.invoices.get(obj.key) + pr.verify(self.app.wallet.contacts) + self.app.show_pr_details(pr.get_dict(), obj.status, True) + + def do_delete(self, obj): + from .question import Question + def cb(result): + if result: + self.app.wallet.invoices.remove(obj.key) + self.hide_menu() + self.update() + d = Question(_('Delete invoice?'), cb) + d.open() + + def show_menu(self, obj): + self.hide_menu() + self.context_menu = ContextMenu(obj, self.menu_actions) + self.ids.box.add_widget(self.context_menu) + + def hide_menu(self): + if self.context_menu is not None: + self.ids.box.remove_widget(self.context_menu) + self.context_menu = None diff --git a/electrum/gui/kivy/uix/dialogs/requests.py b/electrum/gui/kivy/uix/dialogs/requests.py new file mode 100644 index 000000000..5aadae0d0 --- /dev/null +++ b/electrum/gui/kivy/uix/dialogs/requests.py @@ -0,0 +1,157 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder +from decimal import Decimal + +Builder.load_string(''' + + #color: .305, .309, .309, 1 + text_size: self.width, None + halign: 'left' + valign: 'top' + + + address: '' + memo: '' + amount: '' + status: '' + date: '' + icon: 'atlas://electrum/gui/kivy/theming/light/important' + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + RequestLabel: + text: root.address + shorten: True + Widget + RequestLabel: + text: root.memo + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + Widget + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + RequestLabel: + text: root.amount + halign: 'right' + font_size: '15sp' + Widget + RequestLabel: + text: root.status + halign: 'right' + font_size: '13sp' + color: .699, .699, .699, 1 + Widget + + + id: popup + title: _('Requests') + BoxLayout: + id:box + orientation: 'vertical' + spacing: '1dp' + ScrollView: + GridLayout: + cols: 1 + id: requests_container + size_hint: 1, None + height: self.minimum_height + spacing: '2dp' + padding: '12dp' +''') + +from kivy.properties import BooleanProperty +from electrum.gui.kivy.i18n import _ +from electrum.util import format_time +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED +from electrum.gui.kivy.uix.context_menu import ContextMenu + +pr_icon = { + PR_UNPAID: 'atlas://electrum/gui/kivy/theming/light/important', + PR_UNKNOWN: 'atlas://electrum/gui/kivy/theming/light/important', + PR_PAID: 'atlas://electrum/gui/kivy/theming/light/confirmed', + PR_EXPIRED: 'atlas://electrum/gui/kivy/theming/light/close' +} +request_text = { + PR_UNPAID: _('Pending'), + PR_UNKNOWN: _('Unknown'), + PR_PAID: _('Received'), + PR_EXPIRED: _('Expired') +} + + +class RequestsDialog(Factory.Popup): + + def __init__(self, app, screen, callback): + Factory.Popup.__init__(self) + self.app = app + self.screen = screen + self.callback = callback + self.cards = {} + self.context_menu = None + + def get_card(self, req): + address = req['address'] + ci = self.cards.get(address) + if ci is None: + ci = Factory.RequestItem() + ci.address = address + ci.screen = self + self.cards[address] = ci + + amount = req.get('amount') + ci.amount = self.app.format_amount_and_units(amount) if amount else '' + ci.memo = req.get('memo', '') + status, conf = self.app.wallet.get_request_status(address) + ci.status = request_text[status] + ci.icon = pr_icon[status] + #exp = pr.get_expiration_date() + #ci.date = format_time(exp) if exp else _('Never') + return ci + + def update(self): + self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)] + requests_list = self.ids.requests_container + requests_list.clear_widgets() + _list = self.app.wallet.get_sorted_requests(self.app.electrum_config) + for pr in _list: + ci = self.get_card(pr) + requests_list.add_widget(ci) + + def do_show(self, obj): + self.hide_menu() + self.dismiss() + self.app.show_request(obj.address) + + def do_delete(self, req): + from .question import Question + def cb(result): + if result: + self.app.wallet.remove_payment_request(req.address, self.app.electrum_config) + self.hide_menu() + self.update() + d = Question(_('Delete request'), cb) + d.open() + + def show_menu(self, obj): + self.hide_menu() + self.context_menu = ContextMenu(obj, self.menu_actions) + self.ids.box.add_widget(self.context_menu) + + def hide_menu(self): + if self.context_menu is not None: + self.ids.box.remove_widget(self.context_menu) + self.context_menu = None diff --git a/electrum/gui/kivy/uix/ui_screens/invoice.kv b/electrum/gui/kivy/uix/ui_screens/invoice.kv new file mode 100644 index 000000000..d3d4a179a --- /dev/null +++ b/electrum/gui/kivy/uix/ui_screens/invoice.kv @@ -0,0 +1,89 @@ +#:import Decimal decimal.Decimal + + + +Popup: + id: popup + is_invoice: True + amount: 0 + requestor: '' + exp: '' + description: '' + status: '' + signature: '' + isaddr: '' + fund: 0 + pk: '' + title: _('Invoice') if popup.is_invoice else _('Request') + tx_hash: '' + BoxLayout: + orientation: 'vertical' + ScrollView: + GridLayout: + cols: 1 + height: self.minimum_height + size_hint_y: None + padding: '10dp' + spacing: '10dp' + GridLayout: + cols: 1 + size_hint_y: None + height: self.minimum_height + spacing: '10dp' + BoxLabel: + text: (_('Status') if popup.amount or popup.is_invoice or popup.isaddr == 'y' else _('Amount received')) if root.status else '' + value: root.status + BoxLabel: + text: _('Request amount') if root.amount else '' + value: app.format_amount_and_units(root.amount) if root.amount else '' + BoxLabel: + text: _('Requestor') if popup.is_invoice else _('Address') + value: root.requestor + BoxLabel: + text: _('Signature') if root.signature else '' + value: root.signature + BoxLabel: + text: _('Expiration') if root.exp else '' + value: root.exp + BoxLabel: + text: _('Description') if root.description else '' + value: root.description + BoxLabel: + text: _('Balance') if popup.fund else '' + value: app.format_amount_and_units(root.fund) if root.fund else '' + TopLabel: + text: _('Private Key') + RefLabel: + id: pk_label + touched: True if not self.touched else True + data: root.pk + + TopLabel: + text: _('Outputs') if popup.is_invoice else '' + OutputList: + id: output_list + TopLabel: + text: _('Transaction ID') if popup.tx_hash else '' + TxHashLabel: + data: popup.tx_hash + name: _('Transaction ID') + Widget: + size_hint: 1, 0.1 + + BoxLayout: + size_hint: 1, None + height: '48dp' + Widget: + size_hint: 0.5, None + height: '48dp' + Button: + size_hint: 2, None + height: '48dp' + text: _('Close') + on_release: popup.dismiss() + Button: + size_hint: 2, None + height: '48dp' + text: _('Hide private key') if pk_label.data else _('Export private key') + on_release: + setattr(pk_label, 'data', '') if pk_label.data else popup.export(pk_label, popup.requestor) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 65d8644f9..ef0a42534 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -82,7 +82,7 @@ from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdi from .qrcodewidget import QRCodeWidget, QRDialog from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit from .transaction_dialog import show_transaction -from .fee_slider import FeeSlider +#from .fee_slider import FeeSlider from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog, WindowModalDialog, ChoicesLayout, HelpLabel, Buttons, OkButton, InfoButton, WWLabel, TaskThread, CancelButton, @@ -991,8 +991,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): msg = ' '.join([ _('Expiration date of your request.'), _('This information is seen by the recipient if you send them a signed payment request.'), - _('Expired requests have to be deleted manually from your list, in order to free the corresponding Bitcoin addresses.'), - _('The bitcoin address never expires and will always be part of this electrum wallet.'), + _('Expired requests have to be deleted manually from your list, in order to free the corresponding LBRY Credits addresses.'), + _('The LBRY Credits address never expires and will always be part of this electrum wallet.'), ]) grid.addWidget(HelpLabel(_('Request expires'), msg), 2, 0) grid.addWidget(self.expires_combo, 2, 1) @@ -1033,7 +1033,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): addr = str(self.receive_address_e.text()) self.receive_address_widgets.setVisible(bool(addr)) - msg = _('Bitcoin address where the payment should be received. Note that each payment request uses a different Bitcoin address.') + msg = _('LBRY Credits address where the payment should be received. Note that each payment request uses a different LBRY Credits address.') receive_address_label = HelpLabel(_('Receiving address'), msg) self.receive_address_e = ButtonsTextEdit() @@ -1246,7 +1246,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.amount_e = BTCAmountEdit(self.get_decimal_point) self.payto_e = PayToEdit(self) msg = _('Recipient of the funds.') + '\n\n'\ - + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)') + + _('You may enter a LBRY Credits address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a LBRY Credits address)') payto_label = HelpLabel(_('Pay to'), msg) grid.addWidget(payto_label, 1, 0) grid.addWidget(self.payto_e, 1, 1, 1, -1) @@ -2199,7 +2199,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def show_mpk(index): mpk_text.setText(mpk_list[index]) mpk_text.repaint() # macOS hack for #4777 - + # only show the combobox in case multiple accounts are available if len(mpk_list) > 1: # only show the combobox if multiple master keys are defined diff --git a/electrum/interface.py b/electrum/interface.py index 9faaa7078..f70836887 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -445,8 +445,8 @@ class Interface(Logger): self.logger.info(f'requesting block header {height} in mode {assert_mode}') # use lower timeout as we usually have network.bhi_lock here timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent) - res = await self.session.send_request('blockchain.block.header', [height], timeout=timeout) - return blockchain.deserialize_header(bytes.fromhex(res), height) + res = await self.session.send_request('blockchain.block.headers', [height,1], timeout=timeout) + return blockchain.deserialize_header(bytes.fromhex(res['hex']), height) async def request_chunk(self, height: int, tip=None, *, can_return_early=False): index = height // 2016 @@ -518,10 +518,12 @@ class Interface(Logger): async def run_fetch_blocks(self): header_queue = asyncio.Queue() - await self.session.subscribe('blockchain.headers.subscribe', [], header_queue) + await self.session.subscribe('blockchain.headers.subscribe', [True], header_queue) while True: item = await header_queue.get() - raw_header = item[0] + print(item) + raw_header = item[1] + print(raw_header) height = raw_header['height'] header = blockchain.deserialize_header(bfh(raw_header['hex']), height) self.tip_header = header diff --git a/electrum/jsonrpc.py b/electrum/jsonrpc.py new file mode 100644 index 000000000..1640f5293 --- /dev/null +++ b/electrum/jsonrpc.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2018 Thomas Voegtlin +# +# 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. + +from base64 import b64decode +import time + +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler + +from . import util + + +class RPCAuthCredentialsInvalid(Exception): + def __str__(self): + return 'Authentication failed (bad credentials)' + + +class RPCAuthCredentialsMissing(Exception): + def __str__(self): + return 'Authentication failed (missing credentials)' + + +class RPCAuthUnsupportedType(Exception): + def __str__(self): + return 'Authentication failed (only basic auth is supported)' + + +# based on http://acooke.org/cute/BasicHTTPA0.html by andrew cooke +class VerifyingJSONRPCServer(SimpleJSONRPCServer): + + def __init__(self, *args, rpc_user, rpc_password, **kargs): + + self.rpc_user = rpc_user + self.rpc_password = rpc_password + + class VerifyingRequestHandler(SimpleJSONRPCRequestHandler): + def parse_request(myself): + # first, call the original implementation which returns + # True if all OK so far + if SimpleJSONRPCRequestHandler.parse_request(myself): + # Do not authenticate OPTIONS-requests + if myself.command.strip() == 'OPTIONS': + return True + try: + self.authenticate(myself.headers) + return True + except (RPCAuthCredentialsInvalid, RPCAuthCredentialsMissing, + RPCAuthUnsupportedType) as e: + myself.send_error(401, str(e)) + except BaseException as e: + import traceback, sys + traceback.print_exc(file=sys.stderr) + myself.send_error(500, str(e)) + return False + + SimpleJSONRPCServer.__init__( + self, requestHandler=VerifyingRequestHandler, *args, **kargs) + + def authenticate(self, headers): + if self.rpc_password == '': + # RPC authentication is disabled + return + + auth_string = headers.get('Authorization', None) + if auth_string is None: + raise RPCAuthCredentialsMissing() + + (basic, _, encoded) = auth_string.partition(' ') + if basic != 'Basic': + raise RPCAuthUnsupportedType() + + encoded = util.to_bytes(encoded, 'utf8') + credentials = util.to_string(b64decode(encoded), 'utf8') + (username, _, password) = credentials.partition(':') + if not (util.constant_time_compare(username, self.rpc_user) + and util.constant_time_compare(password, self.rpc_password)): + time.sleep(0.050) + raise RPCAuthCredentialsInvalid() diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py index ea37d0a2c..5dbc74510 100644 --- a/electrum/mnemonic.py +++ b/electrum/mnemonic.py @@ -140,7 +140,7 @@ class Mnemonic(Logger): mnemonic = normalize_text(mnemonic) passphrase = passphrase or '' passphrase = normalize_text(passphrase) - return hashlib.pbkdf2_hmac('sha512', mnemonic.encode('utf-8'), b'electrum' + passphrase.encode('utf-8'), iterations = PBKDF2_ROUNDS) + return hashlib.pbkdf2_hmac('sha512', mnemonic.encode('utf-8'), b'lbryum' + passphrase.encode('utf-8'), iterations = PBKDF2_ROUNDS) def mnemonic_encode(self, i): n = len(self.wordlist) diff --git a/electrum/msqr.py b/electrum/msqr.py new file mode 100644 index 000000000..76629fc68 --- /dev/null +++ b/electrum/msqr.py @@ -0,0 +1,94 @@ +# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/ + +def modular_sqrt(a, p): + """ Find a quadratic residue (mod p) of 'a'. p + must be an odd prime. + + Solve the congruence of the form: + x^2 = a (mod p) + And returns x. Note that p - x is also a root. + + 0 is returned is no square root exists for + these a and p. + + The Tonelli-Shanks algorithm is used (except + for some simple cases in which the solution + is known from an identity). This algorithm + runs in polynomial time (unless the + generalized Riemann hypothesis is false). + """ + # Simple cases + # + if legendre_symbol(a, p) != 1: + return 0 + elif a == 0: + return 0 + elif p == 2: + return p + elif p % 4 == 3: + return pow(a, (p + 1) // 4, p) + + # Partition p-1 to s * 2^e for an odd s (i.e. + # reduce all the powers of 2 from p-1) + # + s = p - 1 + e = 0 + while s % 2 == 0: + s //= 2 + e += 1 + + # Find some 'n' with a legendre symbol n|p = -1. + # Shouldn't take long. + # + n = 2 + while legendre_symbol(n, p) != -1: + n += 1 + + # Here be dragons! + # Read the paper "Square roots from 1; 24, 51, + # 10 to Dan Shanks" by Ezra Brown for more + # information + # + + # x is a guess of the square root that gets better + # with each iteration. + # b is the "fudge factor" - by how much we're off + # with the guess. The invariant x^2 = ab (mod p) + # is maintained throughout the loop. + # g is used for successive powers of n to update + # both a and b + # r is the exponent - decreases with each update + # + x = pow(a, (s + 1) // 2, p) + b = pow(a, s, p) + g = pow(n, s, p) + r = e + + while True: + t = b + m = 0 + for m in range(r): + if t == 1: + break + t = pow(t, 2, p) + + if m == 0: + return x + + gs = pow(g, 2 ** (r - m - 1), p) + g = (gs * gs) % p + x = (x * gs) % p + b = (b * g) % p + r = m + +def legendre_symbol(a, p): + """ Compute the Legendre symbol a|p using + Euler's criterion. p is a prime, a is + relatively prime to p (if p divides + a, then a|p = 0) + + Returns 1 if a has a square root modulo + p, -1 otherwise. + """ + ls = pow(a, (p - 1) // 2, p) + return -1 if ls == p - 1 else ls diff --git a/electrum/network.py b/electrum/network.py index 35a620c52..8b4d495eb 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -117,7 +117,7 @@ def filter_noonion(servers): return {k: v for k, v in servers.items() if not k.endswith('.onion')} -def filter_protocol(hostmap, protocol='s'): +def filter_protocol(hostmap, protocol='t'): '''Filters the hostmap for those implementing protocol. The result is a list in serialized form.''' eligible = [] @@ -128,7 +128,7 @@ def filter_protocol(hostmap, protocol='s'): return eligible -def pick_random_server(hostmap=None, protocol='s', exclude_set=None): +def pick_random_server(hostmap=None, protocol='t', exclude_set=None): if hostmap is None: hostmap = constants.net.DEFAULT_SERVERS if exclude_set is None: diff --git a/electrum/plugins/greenaddress_instant/__init__.py b/electrum/plugins/greenaddress_instant/__init__.py new file mode 100644 index 000000000..d2b317854 --- /dev/null +++ b/electrum/plugins/greenaddress_instant/__init__.py @@ -0,0 +1,5 @@ +from electrum.i18n import _ + +fullname = 'GreenAddress instant' +description = _("Allows validating if your transactions have instant confirmations by GreenAddress") +available_for = ['qt'] diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py new file mode 100644 index 000000000..85977b861 --- /dev/null +++ b/electrum/plugins/greenaddress_instant/qt.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2014 Thomas Voegtlin +# +# 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 base64 +import urllib.parse +import sys +import requests + +from PyQt5.QtWidgets import QApplication, QPushButton + +from electrum.plugin import BasePlugin, hook +from electrum.i18n import _ + + +class Plugin(BasePlugin): + + button_label = _("Verify GA instant") + + @hook + def transaction_dialog(self, d): + d.verify_button = QPushButton(self.button_label) + d.verify_button.clicked.connect(lambda: self.do_verify(d)) + d.buttons.insert(0, d.verify_button) + self.transaction_dialog_update(d) + + def get_my_addr(self, d): + """Returns the address for given tx which can be used to request + instant confirmation verification from GreenAddress""" + for o in d.tx.outputs(): + if d.wallet.is_mine(o.address): + return o.address + return None + + @hook + def transaction_dialog_update(self, d): + if d.tx.is_complete() and self.get_my_addr(d): + d.verify_button.show() + else: + d.verify_button.hide() + + def do_verify(self, d): + tx = d.tx + wallet = d.wallet + window = d.main_window + + if wallet.is_watching_only(): + d.show_critical(_('This feature is not available for watch-only wallets.')) + return + + # 1. get the password and sign the verification request + password = None + if wallet.has_keystore_encryption(): + msg = _('GreenAddress requires your signature \n' + 'to verify that transaction is instant.\n' + 'Please enter your password to sign a\n' + 'verification request.') + password = window.password_dialog(msg, parent=d) + if not password: + return + try: + d.verify_button.setText(_('Verifying...')) + QApplication.processEvents() # update the button label + + addr = self.get_my_addr(d) + message = "Please verify if %s is GreenAddress instant confirmed" % tx.txid() + sig = wallet.sign_message(addr, message, password) + sig = base64.b64encode(sig).decode('ascii') + + # 2. send the request + response = requests.request("GET", ("https://greenaddress.it/verify/?signature=%s&txhash=%s" % (urllib.parse.quote(sig), tx.txid())), + headers = {'User-Agent': 'Electrum'}) + response = response.json() + + # 3. display the result + if response.get('verified'): + d.show_message(_('{} is covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification successful!')) + else: + d.show_critical(_('{} is not covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification failed!')) + except BaseException as e: + import traceback + traceback.print_exc(file=sys.stdout) + d.show_error(str(e)) + finally: + d.verify_button.setText(self.button_label) diff --git a/electrum/plugins/trezor/client.py b/electrum/plugins/trezor/client.py new file mode 100644 index 000000000..89b5c2927 --- /dev/null +++ b/electrum/plugins/trezor/client.py @@ -0,0 +1,11 @@ +from trezorlib.client import proto, BaseClient, ProtocolMixin +from .clientbase import TrezorClientBase + +class TrezorClient(TrezorClientBase, ProtocolMixin, BaseClient): + def __init__(self, transport, handler, plugin): + BaseClient.__init__(self, transport=transport) + ProtocolMixin.__init__(self, transport=transport) + TrezorClientBase.__init__(self, handler, plugin, proto) + + +TrezorClientBase.wrap_methods(TrezorClient) diff --git a/electrum/plugins/trezor/transport.py b/electrum/plugins/trezor/transport.py new file mode 100644 index 000000000..78c1dd205 --- /dev/null +++ b/electrum/plugins/trezor/transport.py @@ -0,0 +1,95 @@ +from electrum.util import PrintError + + +class TrezorTransport(PrintError): + + @staticmethod + def all_transports(): + """Reimplemented trezorlib.transport.all_transports so that we can + enable/disable specific transports. + """ + try: + # only to detect trezorlib version + from trezorlib.transport import all_transports + except ImportError: + # old trezorlib. compat for trezorlib < 0.9.2 + transports = [] + try: + from trezorlib.transport_bridge import BridgeTransport + transports.append(BridgeTransport) + except BaseException: + pass + try: + from trezorlib.transport_hid import HidTransport + transports.append(HidTransport) + except BaseException: + pass + try: + from trezorlib.transport_udp import UdpTransport + transports.append(UdpTransport) + except BaseException: + pass + try: + from trezorlib.transport_webusb import WebUsbTransport + transports.append(WebUsbTransport) + except BaseException: + pass + else: + # new trezorlib. + transports = [] + try: + from trezorlib.transport.bridge import BridgeTransport + transports.append(BridgeTransport) + except BaseException: + pass + try: + from trezorlib.transport.hid import HidTransport + transports.append(HidTransport) + except BaseException: + pass + try: + from trezorlib.transport.udp import UdpTransport + transports.append(UdpTransport) + except BaseException: + pass + try: + from trezorlib.transport.webusb import WebUsbTransport + transports.append(WebUsbTransport) + except BaseException: + pass + return transports + return transports + + def enumerate_devices(self): + """Just like trezorlib.transport.enumerate_devices, + but with exception catching, so that transports can fail separately. + """ + devices = [] + for transport in self.all_transports(): + try: + new_devices = transport.enumerate() + except BaseException as e: + self.print_error('enumerate failed for {}. error {}' + .format(transport.__name__, str(e))) + else: + devices.extend(new_devices) + return devices + + def get_transport(self, path=None): + """Reimplemented trezorlib.transport.get_transport, + (1) for old trezorlib + (2) to be able to disable specific transports + (3) to call our own enumerate_devices that catches exceptions + """ + if path is None: + try: + return self.enumerate_devices()[0] + except IndexError: + raise Exception("No TREZOR device found") from None + + def match_prefix(a, b): + return a.startswith(b) or b.startswith(a) + transports = [t for t in self.all_transports() if match_prefix(path, t.PATH_PREFIX)] + if transports: + return transports[0].find_by_path(path) + raise Exception("Unknown path prefix '%s'" % path) diff --git a/electrum/scripts/util.py b/electrum/scripts/util.py new file mode 100644 index 000000000..0fe8663db --- /dev/null +++ b/electrum/scripts/util.py @@ -0,0 +1,47 @@ +import asyncio +from typing import List, Sequence + +from aiorpcx import TaskGroup + +from electrum.network import parse_servers, Network +from electrum.interface import Interface + + +#electrum.util.set_verbosity(True) + +async def get_peers(network: Network): + while not network.is_connected(): + await asyncio.sleep(1) + print("waiting for network to get connected...") + interface = network.interface + session = interface.session + print(f"asking server {interface.server} for its peers") + peers = parse_servers(await session.send_request('server.peers.subscribe')) + print(f"got {len(peers)} servers") + return peers + + +async def send_request(network: Network, servers: List[str], method: str, params: Sequence): + print(f"contacting {len(servers)} servers") + num_connecting = len(network.connecting) + for server in servers: + network._start_interface(server) + # sleep a bit + for _ in range(10): + if len(network.connecting) < num_connecting: + break + await asyncio.sleep(1) + print(f"connected to {len(network.interfaces)} servers. sending request to all.") + responses = dict() + async def get_response(iface: Interface): + try: + res = await iface.session.send_request(method, params, timeout=10) + except Exception as e: + print(f"server {iface.server} errored or timed out: ({repr(e)})") + res = e + responses[iface.server] = res + async with TaskGroup() as group: + for interface in network.interfaces.values(): + await group.spawn(get_response(interface)) + print("%d answers" % len(responses)) + return responses diff --git a/electrum/servers.json b/electrum/servers.json index 281973fc6..94ebbe420 100644 --- a/electrum/servers.json +++ b/electrum/servers.json @@ -1,405 +1,14 @@ { - "3smoooajg7qqac2y.onion": { + "spv1.lbry.com": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.4" + "version": "1.2" }, - "81-7-10-251.blue.kundencontroller.de": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "E-X.not.fyi": { + "lbryumx2.lbry.io": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.4" - }, - "VPS.hsmiths.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "b.ooze.cc": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "bauerjda5hnedjam.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "bauerjhejlv6di7s.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "bitcoin.corgi.party": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "bitcoin3nqy3db7c.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "bitcoins.sk": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "btc.cihar.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "btc.xskyx.net": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "currentlane.lovebitco.in": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "daedalus.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.jochen-hoenicke.de": { - "pruning": "-", - "s": "50005", - "t": "50003", - "version": "1.4" - }, - "dragon085.startdedicated.de": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "e-1.claudioboxx.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "e.keff.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum-server.ninja": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum-unlimited.criptolayer.net": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum.eff.ro": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.festivaldelhumor.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.hsmiths.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.leblancnet.us": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.mindspot.org": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum.qtornado.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.taborsky.cz": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum.villocq.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum2.eff.ro": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum2.villocq.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrumx.bot.nu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrumx.ddns.net": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrumx.ftp.sh": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrumx.soon.it": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrumxhqdsmlu.onion": { - "pruning": "-", - "t": "50001", - "version": "1.4" - }, - "elx01.knas.systems": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "enode.duckdns.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "fedaykin.goip.de": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "fn.48.org": { - "pruning": "-", - "s": "50002", - "t": "50003", - "version": "1.4" - }, - "helicarrier.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "hsmiths4fyqlw5xw.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "hsmiths5mjk6uijs.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.emzy.de": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "ndnd.selfhost.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "ndndword5lpb7eex.onion": { - "pruning": "-", - "t": "50001", - "version": "1.4" - }, - "orannis.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "ozahtqwp25chjdjd.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "qtornadoklbgdyww.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "rbx.curalle.ovh": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "s7clinmo4cazmhul.onion": { - "pruning": "-", - "t": "50001", - "version": "1.4" - }, - "tardis.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "technetium.network": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "tomscryptos.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "ulrichard.ch": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "vmd27610.contaboserver.net": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "vmd30612.contaboserver.net": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "wsw6tua3xl24gsmi264zaep6seppjyrkyucpsmuxnjzyt3f3j6swshad.onion": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "xray587.startdedicated.de": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "yuio.top": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "bitcoin.dragon.zone": { - "pruning": "-", - "s": "50004", - "t": "50003", - "version": "1.4" - }, - "ecdsa.net" : { - "pruning": "-", - "s": "110", - "t": "50001", - "version": "1.4" - }, - "btc.usebsv.com": { - "pruning": "-", - "s": "50006", - "version": "1.4" - }, - "e2.keff.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.hodlister.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum3.hodlister.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum5.hodlister.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrumx.electricnewyear.net": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "fortress.qtornado.com": { - "pruning": "-", - "s": "443", - "t": "50001", - "version": "1.4" - }, - "green-gold.westeurope.cloudapp.azure.com": { - "pruning": "-", - "s": "56002", - "t": "56001", - "version": "1.4" - }, - "electrumx.erbium.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" + "version": "1.2" } } diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 4831c49fd..4ee4db3f3 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -23,13 +23,12 @@ FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000 FEE_LN_ETA_TARGET = 2 # note: make sure the network is asking for estimates for this target # satoshi per kbyte -FEERATE_MAX_DYNAMIC = 1500000 +FEERATE_MAX_DYNAMIC = 50000 FEERATE_WARNING_HIGH_FEE = 600000 -FEERATE_FALLBACK_STATIC_FEE = 150000 -FEERATE_DEFAULT_RELAY = 1000 -FEERATE_MAX_RELAY = 50000 -FEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000, - 50000, 70000, 100000, 150000, 200000, 300000] +FEERATE_FALLBACK_STATIC_FEE = 50000 +FEERATE_DEFAULT_RELAY = 50000 +FEERATE_STATIC_VALUES = [50000,100000] + FEERATE_REGTEST_HARDCODED = 180000 # for eclair compat @@ -197,7 +196,7 @@ class SimpleConfig(Logger): base_unit = self.user_config.get('base_unit') if isinstance(base_unit, str): self._set_key_in_user_config('base_unit', None) - map_ = {'btc':8, 'mbtc':5, 'ubtc':2, 'bits':2, 'sat':0} + map_ = {'lbc':8, 'mlbc':5, 'ulbc':2, 'lbcs':2, 'dewey':0} decimal_point = map_.get(base_unit.lower()) self._set_key_in_user_config('decimal_point', decimal_point) diff --git a/electrum/util.py b/electrum/util.py index 9555be489..4e3ecde1f 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -68,11 +68,11 @@ def inv_dict(d): ca_path = certifi.where() -base_units = {'BTC':8, 'mBTC':5, 'bits':2, 'sat':0} +base_units = {'LBC':8, 'mLBC':5, 'deweys':2, 'dewey':0} base_units_inverse = inv_dict(base_units) -base_units_list = ['BTC', 'mBTC', 'bits', 'sat'] # list(dict) does not guarantee order +base_units_list = ['LBC', 'mLBC', 'deweys', 'dewey'] # list(dict) does not guarantee order -DECIMAL_POINT_DEFAULT = 5 # mBTC +DECIMAL_POINT_DEFAULT = 8 # mBTC # types of payment requests PR_TYPE_ONCHAIN = 0 @@ -554,9 +554,9 @@ def user_dir(): elif os.name == 'posix': return os.path.join(os.environ["HOME"], ".electrum") elif "APPDATA" in os.environ: - return os.path.join(os.environ["APPDATA"], "Electrum") + return os.path.join(os.environ["APPDATA"], "Electrum-lbry") elif "LOCALAPPDATA" in os.environ: - return os.path.join(os.environ["LOCALAPPDATA"], "Electrum") + return os.path.join(os.environ["LOCALAPPDATA"], "Electrum-lbry") else: #raise Exception("No home directory found in environment variables.") return @@ -721,39 +721,7 @@ def time_difference(distance_in_time, include_seconds): return "over %d years" % (round(distance_in_minutes / 525600)) mainnet_block_explorers = { - 'Bitupper Explorer': ('https://bitupper.com/en/explorer/bitcoin/', - {'tx': 'transactions/', 'addr': 'addresses/'}), - 'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/', - {'tx': 'Transaction/', 'addr': 'Address/'}), - 'Blockchain.info': ('https://blockchain.com/btc/', - {'tx': 'tx/', 'addr': 'address/'}), - 'blockchainbdgpzk.onion': ('https://blockchainbdgpzk.onion/', - {'tx': 'tx/', 'addr': 'address/'}), - 'Blockstream.info': ('https://blockstream.info/', - {'tx': 'tx/', 'addr': 'address/'}), - 'Bitaps.com': ('https://btc.bitaps.com/', - {'tx': '', 'addr': ''}), - 'BTC.com': ('https://btc.com/', - {'tx': '', 'addr': ''}), - 'Chain.so': ('https://www.chain.so/', - {'tx': 'tx/BTC/', 'addr': 'address/BTC/'}), - 'Insight.is': ('https://insight.bitpay.com/', - {'tx': 'tx/', 'addr': 'address/'}), - 'TradeBlock.com': ('https://tradeblock.com/blockchain/', - {'tx': 'tx/', 'addr': 'address/'}), - 'BlockCypher.com': ('https://live.blockcypher.com/btc/', - {'tx': 'tx/', 'addr': 'address/'}), - 'Blockchair.com': ('https://blockchair.com/bitcoin/', - {'tx': 'transaction/', 'addr': 'address/'}), - 'blockonomics.co': ('https://www.blockonomics.co/', - {'tx': 'api/tx?txid=', 'addr': '#/search?q='}), - 'OXT.me': ('https://oxt.me/', - {'tx': 'transaction/', 'addr': 'address/'}), - 'smartbit.com.au': ('https://www.smartbit.com.au/', - {'tx': 'tx/', 'addr': 'address/'}), - 'mynode.local': ('http://mynode.local:3002/', - {'tx': 'tx/', 'addr': 'address/'}), - 'system default': ('blockchain:/', + 'LBRY Explorer': ('https://explorer.lbry.io/', {'tx': 'tx/', 'addr': 'address/'}), } @@ -778,7 +746,7 @@ def block_explorer_info(): def block_explorer(config: 'SimpleConfig') -> str: from . import constants - default_ = 'Blockstream.info' + default_ = 'LBRY Explorer' be_key = config.get('block_explorer', default_) be = block_explorer_info().get(be_key) return be_key if be is not None else default_ diff --git a/electrum/version.py b/electrum/version.py index 1d2c0bb56..b74e589f6 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,7 +1,7 @@ ELECTRUM_VERSION = '4.0.0a0' # version of the client package APK_VERSION = '4.0.0.0' # read by buildozer.spec -PROTOCOL_VERSION = '1.4' # protocol version requested +PROTOCOL_VERSION = '1.2' # protocol version requested # The hash of the mnemonic seed must begin with this SEED_PREFIX = '01' # Standard wallet diff --git a/electrum/websockets.py b/electrum/websockets.py new file mode 100644 index 000000000..19dd8eca2 --- /dev/null +++ b/electrum/websockets.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2015 Thomas Voegtlin +# +# 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 threading +import os +import json +from collections import defaultdict +import asyncio +from typing import Dict, List, Tuple, TYPE_CHECKING +import traceback +import sys + +try: + from SimpleWebSocketServer import WebSocket, SimpleSSLWebSocketServer +except ImportError: + sys.exit("install SimpleWebSocketServer") + +from .util import PrintError +from . import bitcoin +from .synchronizer import SynchronizerBase + +if TYPE_CHECKING: + from .network import Network + from .simple_config import SimpleConfig + + +request_queue = asyncio.Queue() + + +class ElectrumWebSocket(WebSocket, PrintError): + + def handleMessage(self): + assert self.data[0:3] == 'id:' + self.print_error("message received", self.data) + request_id = self.data[3:] + asyncio.run_coroutine_threadsafe( + request_queue.put((self, request_id)), asyncio.get_event_loop()) + + def handleConnected(self): + self.print_error("connected", self.address) + + def handleClose(self): + self.print_error("closed", self.address) + + +class BalanceMonitor(SynchronizerBase): + + def __init__(self, config: 'SimpleConfig', network: 'Network'): + SynchronizerBase.__init__(self, network) + self.config = config + self.expected_payments = defaultdict(list) # type: Dict[str, List[Tuple[WebSocket, int]]] + + def make_request(self, request_id): + # read json file + rdir = self.config.get('requests_dir') + n = os.path.join(rdir, 'req', request_id[0], request_id[1], request_id, request_id + '.json') + with open(n, encoding='utf-8') as f: + s = f.read() + d = json.loads(s) + addr = d.get('address') + amount = d.get('amount') + return addr, amount + + async def main(self): + # resend existing subscriptions if we were restarted + for addr in self.expected_payments: + await self._add_address(addr) + # main loop + while True: + ws, request_id = await request_queue.get() + try: + addr, amount = self.make_request(request_id) + except Exception: + traceback.print_exc(file=sys.stderr) + continue + self.expected_payments[addr].append((ws, amount)) + await self._add_address(addr) + + async def _on_address_status(self, addr, status): + self.print_error('new status for addr {}'.format(addr)) + sh = bitcoin.address_to_scripthash(addr) + balance = await self.network.get_balance_for_scripthash(sh) + for ws, amount in self.expected_payments[addr]: + if not ws.closed: + if sum(balance.values()) >= amount: + ws.sendMessage('paid') + + +class WebSocketServer(threading.Thread): + + def __init__(self, config: 'SimpleConfig', network: 'Network'): + threading.Thread.__init__(self) + self.config = config + self.network = network + asyncio.set_event_loop(network.asyncio_loop) + self.daemon = True + self.balance_monitor = BalanceMonitor(self.config, self.network) + self.start() + + def run(self): + asyncio.set_event_loop(self.network.asyncio_loop) + host = self.config.get('websocket_server') + port = self.config.get('websocket_port', 9999) + certfile = self.config.get('ssl_chain') + keyfile = self.config.get('ssl_privkey') + self.server = SimpleSSLWebSocketServer(host, port, ElectrumWebSocket, certfile, keyfile) + self.server.serveforever() diff --git a/icons.qrc b/icons.qrc new file mode 100644 index 000000000..19c298adc --- /dev/null +++ b/icons.qrc @@ -0,0 +1,67 @@ + + + icons/electrum.png + icons/clock1.png + icons/clock2.png + icons/clock3.png + icons/clock4.png + icons/clock5.png + icons/confirmed.png + icons/copy.png + icons/digitalbitbox.png + icons/digitalbitbox_unpaired.png + icons/expired.png + icons/electrum_light_icon.png + icons/electrum_dark_icon.png + icons/electrumb.png + icons/eye1.png + icons/file.png + icons/info.png + icons/keepkey.png + icons/keepkey_unpaired.png + icons/key.png + icons/ledger.png + icons/ledger_unpaired.png + icons/lock.png + icons/microphone.png + icons/network.png + icons/offline_tx.png + icons/revealer.png + icons/revealer_c.png + icons/qrcode.png + icons/qrcode_white.png + icons/preferences.png + icons/safe-t_unpaired.png + icons/safe-t.png + icons/seed.png + icons/status_connected.png + icons/status_connected_fork.png + icons/status_connected_proxy.png + icons/status_connected_proxy_fork.png + icons/status_disconnected.png + icons/status_waiting.png + icons/status_lagging.png + icons/status_lagging_fork.png + icons/seal.png + icons/tab_addresses.png + icons/tab_coins.png + icons/tab_console.png + icons/tab_contacts.png + icons/tab_history.png + icons/tab_receive.png + icons/tab_send.png + icons/tor_logo.png + icons/speaker.png + icons/trezor_unpaired.png + icons/trezor.png + icons/coldcard.png + icons/coldcard_unpaired.png + icons/trustedcoin-status.png + icons/trustedcoin-wizard.png + icons/unconfirmed.png + icons/unpaid.png + icons/unlock.png + icons/warning.png + icons/zoom.png + + diff --git a/icons/clock1.png b/icons/clock1.png new file mode 100644 index 000000000..4850431b8 Binary files /dev/null and b/icons/clock1.png differ diff --git a/icons/clock2.png b/icons/clock2.png new file mode 100644 index 000000000..4bbad0549 Binary files /dev/null and b/icons/clock2.png differ diff --git a/icons/clock3.png b/icons/clock3.png new file mode 100644 index 000000000..bb74995ef Binary files /dev/null and b/icons/clock3.png differ diff --git a/icons/clock4.png b/icons/clock4.png new file mode 100644 index 000000000..e640dfd3d Binary files /dev/null and b/icons/clock4.png differ diff --git a/icons/clock5.png b/icons/clock5.png new file mode 100644 index 000000000..327eb4831 Binary files /dev/null and b/icons/clock5.png differ diff --git a/icons/coldcard.png b/icons/coldcard.png new file mode 100644 index 000000000..74104d76e Binary files /dev/null and b/icons/coldcard.png differ diff --git a/icons/coldcard_unpaired.png b/icons/coldcard_unpaired.png new file mode 100644 index 000000000..2560407e8 Binary files /dev/null and b/icons/coldcard_unpaired.png differ diff --git a/icons/confirmed.png b/icons/confirmed.png new file mode 100644 index 000000000..2023abd15 Binary files /dev/null and b/icons/confirmed.png differ diff --git a/icons/confirmed.svg b/icons/confirmed.svg new file mode 100644 index 000000000..710b3f8c3 --- /dev/null +++ b/icons/confirmed.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/copy.png b/icons/copy.png new file mode 100644 index 000000000..b82da41bf Binary files /dev/null and b/icons/copy.png differ diff --git a/icons/digitalbitbox.png b/icons/digitalbitbox.png new file mode 100644 index 000000000..ed6d72e6d Binary files /dev/null and b/icons/digitalbitbox.png differ diff --git a/icons/digitalbitbox_unpaired.png b/icons/digitalbitbox_unpaired.png new file mode 100644 index 000000000..2e88ed488 Binary files /dev/null and b/icons/digitalbitbox_unpaired.png differ diff --git a/icons/electrum.ico b/icons/electrum.ico new file mode 100644 index 000000000..618bc99e2 Binary files /dev/null and b/icons/electrum.ico differ diff --git a/icons/electrum.png b/icons/electrum.png new file mode 100644 index 000000000..e07a7dc07 Binary files /dev/null and b/icons/electrum.png differ diff --git a/icons/electrum_dark_icon.png b/icons/electrum_dark_icon.png new file mode 100644 index 000000000..6fee7de9f Binary files /dev/null and b/icons/electrum_dark_icon.png differ diff --git a/icons/electrum_launcher.png b/icons/electrum_launcher.png new file mode 100644 index 000000000..37af81ca9 Binary files /dev/null and b/icons/electrum_launcher.png differ diff --git a/icons/electrum_light_icon.png b/icons/electrum_light_icon.png new file mode 100644 index 000000000..da52cb3c1 Binary files /dev/null and b/icons/electrum_light_icon.png differ diff --git a/icons/electrum_presplash.png b/icons/electrum_presplash.png new file mode 100644 index 000000000..8d7b414db Binary files /dev/null and b/icons/electrum_presplash.png differ diff --git a/icons/electrumb.png b/icons/electrumb.png new file mode 100644 index 000000000..efa26486e Binary files /dev/null and b/icons/electrumb.png differ diff --git a/icons/expired.png b/icons/expired.png new file mode 100644 index 000000000..6400b8ba6 Binary files /dev/null and b/icons/expired.png differ diff --git a/icons/eye1.png b/icons/eye1.png new file mode 100644 index 000000000..0bded339f Binary files /dev/null and b/icons/eye1.png differ diff --git a/icons/file.png b/icons/file.png new file mode 100644 index 000000000..ae84e2599 Binary files /dev/null and b/icons/file.png differ diff --git a/icons/info.png b/icons/info.png new file mode 100644 index 000000000..f11f99694 Binary files /dev/null and b/icons/info.png differ diff --git a/icons/keepkey.png b/icons/keepkey.png new file mode 100644 index 000000000..dd5c244fc Binary files /dev/null and b/icons/keepkey.png differ diff --git a/icons/keepkey_unpaired.png b/icons/keepkey_unpaired.png new file mode 100644 index 000000000..ac15d2e85 Binary files /dev/null and b/icons/keepkey_unpaired.png differ diff --git a/icons/key.png b/icons/key.png new file mode 100644 index 000000000..4470abfe8 Binary files /dev/null and b/icons/key.png differ diff --git a/icons/ledger.png b/icons/ledger.png new file mode 100644 index 000000000..bab69ec52 Binary files /dev/null and b/icons/ledger.png differ diff --git a/icons/ledger_unpaired.png b/icons/ledger_unpaired.png new file mode 100644 index 000000000..65db1ea87 Binary files /dev/null and b/icons/ledger_unpaired.png differ diff --git a/icons/lock.png b/icons/lock.png new file mode 100644 index 000000000..a190964ed Binary files /dev/null and b/icons/lock.png differ diff --git a/icons/lock.svg b/icons/lock.svg new file mode 100644 index 000000000..9024d2a37 --- /dev/null +++ b/icons/lock.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/microphone.png b/icons/microphone.png new file mode 100644 index 000000000..f9a271c55 Binary files /dev/null and b/icons/microphone.png differ diff --git a/icons/network.png b/icons/network.png new file mode 100644 index 000000000..6a5bcba97 Binary files /dev/null and b/icons/network.png differ diff --git a/icons/offline_tx.png b/icons/offline_tx.png new file mode 100644 index 000000000..32fee54dd Binary files /dev/null and b/icons/offline_tx.png differ diff --git a/icons/preferences.png b/icons/preferences.png new file mode 100644 index 000000000..b10ba6ea0 Binary files /dev/null and b/icons/preferences.png differ diff --git a/icons/preferences.svg b/icons/preferences.svg new file mode 100644 index 000000000..39f7bd143 --- /dev/null +++ b/icons/preferences.svg @@ -0,0 +1,686 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + System Preferences + + + Andreas Nilsson + + + + + category + system + preferences + settings + control center + + + + + Jakub Steiner +Ulisse Perusin + + + + + + + + + + + + + + image/svg+xml + + Preferences + + + Andreas Nilsson + + + + + Lapo Calamandrei, Ulisse Perusin, Jakub Steiner + + + + + + category + system + preferences + settings + control center + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/qrcode.png b/icons/qrcode.png new file mode 100644 index 000000000..41a84aa1d Binary files /dev/null and b/icons/qrcode.png differ diff --git a/icons/qrcode_white.png b/icons/qrcode_white.png new file mode 100644 index 000000000..23ea91655 Binary files /dev/null and b/icons/qrcode_white.png differ diff --git a/icons/revealer.png b/icons/revealer.png new file mode 100644 index 000000000..9caef09dd Binary files /dev/null and b/icons/revealer.png differ diff --git a/icons/revealer_c.png b/icons/revealer_c.png new file mode 100644 index 000000000..993d8fc90 Binary files /dev/null and b/icons/revealer_c.png differ diff --git a/icons/safe-t.png b/icons/safe-t.png new file mode 100644 index 000000000..7a574dec8 Binary files /dev/null and b/icons/safe-t.png differ diff --git a/icons/safe-t_unpaired.png b/icons/safe-t_unpaired.png new file mode 100644 index 000000000..c67a344f1 Binary files /dev/null and b/icons/safe-t_unpaired.png differ diff --git a/icons/seal.png b/icons/seal.png new file mode 100644 index 000000000..f6d51baa3 Binary files /dev/null and b/icons/seal.png differ diff --git a/icons/seed.png b/icons/seed.png new file mode 100644 index 000000000..54c38b6ab Binary files /dev/null and b/icons/seed.png differ diff --git a/icons/speaker.png b/icons/speaker.png new file mode 100644 index 000000000..963db53f9 Binary files /dev/null and b/icons/speaker.png differ diff --git a/icons/status_connected.png b/icons/status_connected.png new file mode 100644 index 000000000..1fe3dace9 Binary files /dev/null and b/icons/status_connected.png differ diff --git a/icons/status_connected.svg b/icons/status_connected.svg new file mode 100644 index 000000000..e0779998c --- /dev/null +++ b/icons/status_connected.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + + + + record + media + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + diff --git a/icons/status_connected_fork.png b/icons/status_connected_fork.png new file mode 100644 index 000000000..a65c2a883 Binary files /dev/null and b/icons/status_connected_fork.png differ diff --git a/icons/status_connected_proxy.png b/icons/status_connected_proxy.png new file mode 100644 index 000000000..ff553d9f1 Binary files /dev/null and b/icons/status_connected_proxy.png differ diff --git a/icons/status_connected_proxy.svg b/icons/status_connected_proxy.svg new file mode 100644 index 000000000..5e44b5e51 --- /dev/null +++ b/icons/status_connected_proxy.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + + + + record + media + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + diff --git a/icons/status_connected_proxy_fork.png b/icons/status_connected_proxy_fork.png new file mode 100644 index 000000000..f6b4541e1 Binary files /dev/null and b/icons/status_connected_proxy_fork.png differ diff --git a/icons/status_disconnected.png b/icons/status_disconnected.png new file mode 100644 index 000000000..cb5ac1b9f Binary files /dev/null and b/icons/status_disconnected.png differ diff --git a/icons/status_disconnected.svg b/icons/status_disconnected.svg new file mode 100644 index 000000000..46d1a1d7f --- /dev/null +++ b/icons/status_disconnected.svg @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + Record + + + record + media + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/status_lagging.png b/icons/status_lagging.png new file mode 100644 index 000000000..b558791f5 Binary files /dev/null and b/icons/status_lagging.png differ diff --git a/icons/status_lagging.svg b/icons/status_lagging.svg new file mode 100644 index 000000000..1fd487964 --- /dev/null +++ b/icons/status_lagging.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + + + + record + media + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + diff --git a/icons/status_lagging_fork.png b/icons/status_lagging_fork.png new file mode 100644 index 000000000..82826721b Binary files /dev/null and b/icons/status_lagging_fork.png differ diff --git a/icons/status_waiting.png b/icons/status_waiting.png new file mode 100644 index 000000000..d5513838f Binary files /dev/null and b/icons/status_waiting.png differ diff --git a/icons/status_waiting.svg b/icons/status_waiting.svg new file mode 100644 index 000000000..069a4d3dd --- /dev/null +++ b/icons/status_waiting.svg @@ -0,0 +1,398 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + View Refresh + + + reload + refresh + view + + + + + Ricardo 'Rick' González + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/tab_addresses.png b/icons/tab_addresses.png new file mode 100644 index 000000000..449b23ba1 Binary files /dev/null and b/icons/tab_addresses.png differ diff --git a/icons/tab_coins.png b/icons/tab_coins.png new file mode 100644 index 000000000..a6c8265da Binary files /dev/null and b/icons/tab_coins.png differ diff --git a/icons/tab_console.png b/icons/tab_console.png new file mode 100644 index 000000000..4f70e6f60 Binary files /dev/null and b/icons/tab_console.png differ diff --git a/icons/tab_contacts.png b/icons/tab_contacts.png new file mode 100644 index 000000000..d21ae9ed6 Binary files /dev/null and b/icons/tab_contacts.png differ diff --git a/icons/tab_history.png b/icons/tab_history.png new file mode 100644 index 000000000..ecdc46819 Binary files /dev/null and b/icons/tab_history.png differ diff --git a/icons/tab_receive.png b/icons/tab_receive.png new file mode 100644 index 000000000..ba486694c Binary files /dev/null and b/icons/tab_receive.png differ diff --git a/icons/tab_send.png b/icons/tab_send.png new file mode 100644 index 000000000..7ef90a422 Binary files /dev/null and b/icons/tab_send.png differ diff --git a/icons/tor_logo.png b/icons/tor_logo.png new file mode 100644 index 000000000..a4afd8492 Binary files /dev/null and b/icons/tor_logo.png differ diff --git a/icons/trezor.png b/icons/trezor.png new file mode 100644 index 000000000..3edee94f8 Binary files /dev/null and b/icons/trezor.png differ diff --git a/icons/trezor_unpaired.png b/icons/trezor_unpaired.png new file mode 100644 index 000000000..c9854be1a Binary files /dev/null and b/icons/trezor_unpaired.png differ diff --git a/icons/trustedcoin-status.png b/icons/trustedcoin-status.png new file mode 100644 index 000000000..ee920c13e Binary files /dev/null and b/icons/trustedcoin-status.png differ diff --git a/icons/trustedcoin-wizard.png b/icons/trustedcoin-wizard.png new file mode 100644 index 000000000..19f4f6e72 Binary files /dev/null and b/icons/trustedcoin-wizard.png differ diff --git a/icons/unconfirmed.png b/icons/unconfirmed.png new file mode 100644 index 000000000..6ebfe290e Binary files /dev/null and b/icons/unconfirmed.png differ diff --git a/icons/unlock.png b/icons/unlock.png new file mode 100644 index 000000000..869e4de32 Binary files /dev/null and b/icons/unlock.png differ diff --git a/icons/unlock.svg b/icons/unlock.svg new file mode 100644 index 000000000..b22e40207 --- /dev/null +++ b/icons/unlock.svg @@ -0,0 +1,509 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/unpaid.png b/icons/unpaid.png new file mode 100644 index 000000000..579ec4eb5 Binary files /dev/null and b/icons/unpaid.png differ diff --git a/icons/warning.png b/icons/warning.png new file mode 100644 index 000000000..ee3eacea9 Binary files /dev/null and b/icons/warning.png differ diff --git a/icons/zoom.png b/icons/zoom.png new file mode 100644 index 000000000..c5b58a39b Binary files /dev/null and b/icons/zoom.png differ diff --git a/secp256k1 b/secp256k1 new file mode 160000 index 000000000..856a01d6a --- /dev/null +++ b/secp256k1 @@ -0,0 +1 @@ +Subproject commit 856a01d6ad60c70fd92bdd44fa8584493b87594d diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 000000000..20f2b63de --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,23 @@ +name: electrum +version: master +summary: Bitcoin thin client +description: | + Lightweight Bitcoin client + +grade: devel # must be 'stable' to release into candidate/stable channels +confinement: strict + +apps: + electrum: + command: desktop-launch electrum + plugs: [network, network-bind, x11, unity7] + +parts: + electrum: + source: . + plugin: python + python-version: python3 + stage-packages: [python3-pyqt5] + build-packages: [pyqt5-dev-tools] + install: pyrcc5 icons.qrc -o $SNAPCRAFT_PART_INSTALL/lib/python3.5/site-packages/electrum/gui/qt/icons_rc.py + after: [desktop-qt5]