diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..ef05ebc2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,2 @@ + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..1d61cb934 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +####-*.patch +*.pyc +*.swp +build/ +dist/ +*.egg/ +contrib/pyinstaller/ +Electrum.egg-info/ +contrib/build-wine/LICENSE +#electrum/gui/qt/icons_rc.py +locale/ +.devlocaltmp/ +*_trial_temp +packages +env/ +.tox/ +.buildozer/ +bin/ +/app.fil + +# tox files +.cache/ +.coverage diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..5a0f914f1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "contrib/deterministic-build/electrum-icons"] + path = contrib/deterministic-build/electrum-icons + url = https://github.com/spesmilo/electrum-icons +[submodule "contrib/deterministic-build/electrum-locale"] + path = contrib/deterministic-build/electrum-locale + url = https://github.com/spesmilo/electrum-locale diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..6f28247b7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,58 @@ +sudo: true +dist: xenial +language: python +python: + - 3.5 + - 3.6 + - 3.7 +addons: + apt: + sources: + - sourceline: 'ppa:tah83/secp256k1' + packages: + - libsecp256k1-0 +install: + - pip install -r contrib/requirements/requirements-travis.txt +cache: + - pip: true + - directories: + - /tmp/electrum-build +script: + - tox +after_success: + - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi + - coveralls +jobs: + include: + - stage: binary builds + sudo: true + language: c + python: false + env: + - TARGET_OS=Windows + services: + - docker + install: + - sudo docker build --no-cache -t electrum-wine-builder-img ./contrib/build-wine/docker/ + script: + - 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 + after_success: true + - os: osx + language: c + env: + - TARGET_OS=macOS + python: false + install: + - git fetch --all --tags + - git fetch origin --unshallow + script: ./contrib/build-osx/make_osx + after_script: ls -lah dist && md5 dist/* + after_success: true + - stage: release check + install: + - git fetch --all --tags + - git fetch origin --unshallow + script: + - ./contrib/deterministic-build/check_submodules.sh + after_success: true + if: tag IS present diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 000000000..9cff06784 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,12 @@ +ThomasV - Creator and maintainer. +Animazing / Tachikoma - Styled the new GUI. Mac version. +Azelphur - GUI stuff. +Coblee - Alternate coin support and py2app support. +Deafboy - Ubuntu packages. +EagleTM - Bugfixes. +ErebusBat - Mac distribution. +Genjix - Porting pro-mode functionality to lite-gui and worked on server +Slush - Work on the server. Designed the original Stratum spec. +Julian Toash (Tuxavant) - Various fixes to the client. +rdymac - Website and translations. +kyuupichan - Miscellaneous. \ No newline at end of file diff --git a/Info.plist b/Info.plist new file mode 100644 index 000000000..e914dea11 --- /dev/null +++ b/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleURLTypes + + + CFBundleURLName + bitcore + CFBundleURLSchemes + + bitcore + + + + LSArchitecturePriority + + x86_64 + i386 + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..b8bb97185 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..a46190b80 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,18 @@ +include LICENSE RELEASE-NOTES AUTHORS +include README.rst +include electrum.conf.sample +include electrum.desktop +include *.py +include run_electrum +include contrib/requirements/requirements.txt +include contrib/requirements/requirements-hw.txt +recursive-include packages *.py +recursive-include packages cacert.pem +include icons.qrc +graft icons + +graft electrum +prune electrum/tests + +global-exclude __pycache__ +global-exclude *.py[co] diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..553f94325 --- /dev/null +++ b/README.rst @@ -0,0 +1,115 @@ +Electrum - Lightweight Bitcore client +===================================== + +:: + + Licence: MIT Licence + Author: Thomas Voegtlin + Language: Python + Homepage: https://electrum.org/ + + +.. image:: https://travis-ci.org/spesmilo/electrum.svg?branch=master + :target: https://travis-ci.org/spesmilo/electrum + :alt: Build Status +.. image:: https://coveralls.io/repos/github/spesmilo/electrum/badge.svg?branch=master + :target: https://coveralls.io/github/spesmilo/electrum?branch=master + :alt: Test coverage statistics +.. image:: https://d322cqt584bo4o.cloudfront.net/electrum/localized.svg + :target: https://crowdin.com/project/electrum + :alt: Help translate Electrum online + + + + + +Getting started +=============== + +Electrum is a pure python application. If you want to use the +Qt interface, install the Qt dependencies:: + + sudo apt-get install python3-pyqt5 + +If you downloaded the official package (tar.gz), you can run +Electrum from its root directory, without installing it on your +system; all the python dependencies are included in the 'packages' +directory. To run Electrum from its root directory, just do:: + + ./run_electrum + +You can also install Electrum on your system, by running this command:: + + sudo apt-get install python3-setuptools + pip3 install .[fast] + +This will download and install the Python dependencies used by +Electrum, instead of using the 'packages' directory. +The 'fast' extra contains some optional dependencies that we think +are often useful but they are not strictly needed. + +If you cloned the git repository, you need to compile extra files +before you can run Electrum. Read the next section, "Development +Version". + + + +Development version +=================== + +Check out the code from GitHub:: + + git clone git://github.com/spesmilo/electrum.git + cd electrum + +Run install (this should install dependencies):: + + pip3 install .[fast] + +Render the SVG icons to PNGs (optional):: + + for i in lock unlock confirmed status_lagging status_disconnected status_connected_proxy status_connected status_waiting preferences; do convert -background none icons/$i.svg icons/$i.png; done + +Compile the icons file for Qt:: + + sudo apt-get install pyqt5-dev-tools + pyrcc5 icons.qrc -o electrum/gui/qt/icons_rc.py + +Compile the protobuf description file:: + + sudo apt-get install protobuf-compiler + protoc --proto_path=electrum --python_out=electrum electrum/paymentrequest.proto + +Create translations (optional):: + + sudo apt-get install python-requests gettext + ./contrib/make_locale + + + + +Creating Binaries +================= + + +To create binaries, create the 'packages' directory:: + + ./contrib/make_packages + +This directory contains the python dependencies used by Electrum. + +Mac OS X / macOS +-------- + +See `contrib/build-osx/`. + +Windows +------- + +See `contrib/build-wine/`. + + +Android +------- + +See `electrum/gui/kivy/Readme.md` file. diff --git a/RELEASE-NOTES b/RELEASE-NOTES new file mode 100644 index 000000000..74b687747 --- /dev/null +++ b/RELEASE-NOTES @@ -0,0 +1,1061 @@ +# Release 3.2.3 - (September 3, 2018) + + * hardware wallet: the Safe-T mini from Archos is now supported. + * hardware wallet: the Coldcard from Coinkite is now supported. + * BIP39 seeds: if a seed extension (aka passphrase) contained + multiple consecutive whitespaces or leading/trailing whitespaces + then the derived addresses were not following spec. This has been + fixed, and affected should move their coins. The wizard will show a + warning in this case. (#4566) + * Revealer: the PRNG used has been changed (#4649) + * fix Linux distributables: 'typing' was not bundled, needed for python 3.4 + * fix #4626: fix spending from segwit multisig wallets involving a Trezor + cosigner when using a custom derivation path + * fix #4491: on Android, if user had set "uBTC" as base unit, app crashed + * fix #4497: on Android, paying bip70 invoices from cold start did not work + * Several other minor bugfixes and usability improvements. + + +# Release 3.2.2 - (July 2nd, 2018) + + * Fix DNS resolution on Windows + * Fix websocket bug in daemon + + +# Release 3.2.1 - (July 1st, 2018) + + * fix Windows binaries: due to build process changes, the locale files + were not included; the language could not be changed from English + * fix Linux distributables: wordlists were not included (#4475) + + +# Release 3.2.0 - Satoshi's Vision (June 30, 2018) + + * If present, libsecp256k1 is used to speed up elliptic curve + operations. The library is bundled in the Windows, MacOS, and + Android binaries. On Linux, it needs to be installed separately. + * Two-factor authentication is available on Android. Note that this + will only provide additional security if one time passwords are + generated on a separate device. + * Semi-automated crash reporting is implemented for Android. + * Transactions that are dropped from the mempool are kept in the + wallet as 'local', and can be rebroadcast. Previously these + transactions were deleted from the wallet. + * The scriptSig and witness part of transaction inputs are no longer + parsed, unless actually needed. The wallet will no longer display + 'from' addresses corresponding to transaction inputs, except for + its own inputs. + * The partial transaction format has been incompatibly changed. This + was needed as for partial transactions the scriptSig/witness has to + be parsed, but for signed transactions we did not want to do the + parsing. Users should make sure that all instances of Electrum + they use to co-sign or offline sign, are updated together. + * Signing of partial transactions created with online imported + addresses wallets now supports significantly more + setups. Previously only online p2pkh address + offline WIF was + supported. Now the following setups are all supported: + - online {p2pkh, p2wpkh-p2sh, p2wpkh} address + offline WIF, + - online {p2pkh, p2wpkh-p2sh, p2wpkh} address + offline seed/xprv, + - online {p2sh, p2wsh-p2sh, p2wsh}-multisig address + offline seeds/xprvs + (potentially distributed among several different machines) + Note that for the online address + offline HD secret case, you need + the offline wallet to recognize the address (i.e. within gap + limit). Having an xpub on the online machine is still the + recommended setup, as this allows the online machine to generate + new addresses on demand. + * Segwit multisig for bip39 and hardware wallets is now enabled. + (both p2wsh-p2sh and native p2wsh) + * Ledger: offline signing for segwit inputs (#3302) This has already + worked for Trezor and Digital Bitbox. Offline segwit signing can be + combined with online imported addresses wallets. + * Added Revealer plugin. ( https://revealer.cc ) Revealer is a seed + phrase back-up solution. It allows you to create a cold, analog, + multi-factor backup of your wallet seeds, or of any arbitrary + secret. The Revealer utilizes a transparent plastic visual one time + pad. + * Fractional fee rates: the Qt GUI now displays fee rates with 0.1 + sat/byte precision, and also allows this same resolution in the + Send tab. + * Hardware wallets: a "show address" button is now displayed in the + Receive tab of the Qt GUI. (#4316) + * Trezor One: implemented advanced/matrix recovery (#4329) + * Qt/Kivy: added "sat" as optional base unit. + * Kivy GUI: significant performance improvements when displaying + history and address list of large wallets; and transaction dialog + of large transactions. + * Windows: use dnspython to resolve dns instead of socket.getaddrinfo + (#4422) + * Importing minikeys: use uncompressed pubkey instead of compressed + (#4384) + * SPV proofs: check inner nodes not to be valid transactions (#4436) + * Qt GUI: there is now an optional "dark" theme (#4461) + * Several other minor bugfixes and usability improvements. + + +# Release 3.1.3 - (April 16, 2018) + + * Qt GUI: seed word auto-complete during restore + * Android: fix some crashes + * performance improvements (wallet, and Qt GUI) + * hardware wallets: show debug message during device scan + * Digital Bitbox: enabled BIP84 (p2wpkh) wallet creation + * add regtest support (via --regtest flag) + * other minor bugfixes and usability improvements + +# Release 3.1.2 - (March 28, 2018) + + * Kivy/android: request PIN on startup + * Improve OSX build process + * Fix various bugs with hardware wallets + * Other minor bugfixes + +# Release 3.1.1 - (March 12, 2018) + + * fix #4031: Trezor T support + * partial fix #4060: proxy and hardware wallet can't be used together + * fix #4039: can't set address labels + * fix crash related to coinbase transactions + * MacOS: use internal graphics card + * fix openalias related crashes + * speed-up capital gains calculations + * hw wallet encryption: re-prompt for passphrase if incorrect + * other minor fixes. + + + +# Release 3.1.0 - (March 5, 2018) + + * Memory-pool based fee estimation. Dynamic fees can target a desired + depth in the memory pool. This feature is optional, and ETA-based + estimates from Bitcoin Core are still available. Note that miners + could exploit this feature, if they conspired and filled the memory + pool with expensive transactions that never get mined. However, + since the Electrum client already trusts an Electrum server with + fee estimates, activating this feature does not introduce any new + vulnerability. In addition, the client uses a hard threshold to + protect itself from servers sending excessive fee estimates. In + practice, ETA-based estimates have resulted in sticky fees, and + caused many users to overpay for transactions. Advanced users tend + to visit (and trust) websites that display memory-pool data in + order to set their fees. + * Capital gains: For each outgoing transaction, the difference + between the acquisition and liquidation prices of outgoing coins is + displayed in the wallet history. By default, historical exchange + rates are used to compute acquisition and liquidation prices. These + values can also be entered manually, in order to match the actual + price realized by the user. The order of liquidation of coins is + the natural order defined by the blockchain; this results in + capital gain values that are invariant to changes in the set of + addresses that are in the wallet. Any other ordering strategy (such + as FIFO, LIFO) would result in capital gain values that depend on + the presence of other addresses in the wallet. + * Local transactions: Transactions can be saved in the wallet without + being broadcast. The inputs of local transactions are considered as + spent, and their change outputs can be re-used in subsequent + transactions. This can be combined with cold storage, in order to + create several transactions before broadcasting them. Outgoing + transactions that have been removed from the memory pool are also + saved in the wallet, and can be broadcast again. + * Checkpoints: The initial download of a headers file was replaced + with hardcoded checkpoints. The wallet uses one checkpoint per + retargeting period. The headers for a retargeting period are + downloaded only if transactions need to be verified in this period. + * The 'privacy' and 'priority' coin selection policies have been + merged into one. Previously, the 'privacy' policy has been unusable + because it was was not prioritizing confirmed coins. The new policy + is similar to 'privacy', except that it de-prioritizes addresses + that have unconfirmed coins. + * The 'Send' tab of the Qt GUI displays how transaction fees are + computed from transaction size. + * The wallet history can be filtered by time interval. + * Replace-by-fee is enabled by default. Note that this might cause + some issues with wallets that do not display RBF transactions until + they are confirmed. + * Watching-only wallets and hardware wallets can be encrypted. + * Semi-automated crash reporting + * The SSL checkbox option was removed from the GUI. + * The Trezor T hardware wallet is now supported. + * BIP84: native segwit p2wpkh scripts for bip39 seeds and hardware + wallets can now be created when specifying a BIP84 derivation + path. This is usable with Trezor and Ledger. + * Windows: the binaries now include ZBar, and QR code scanning should work. + * The Wallet Import Format (WIF) for private keys that was extended in 3.0 + is changed. Keys in the previous format can be imported, compatibility + is maintained. Newly exported keys will be serialized as + "script_type:original_wif_format_key". + * BIP32 master keys for testnet once again have different version bytes than + on mainnet. For the mainnet prefixes {x,y,Y,z,Z}|{pub,prv}, the + corresponding testnet prefixes are {t,u,U,v,V}|{pub,prv}. + More details and exact version bytes are specified at: + https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst + Note that due to this change, testnet wallet files created with previous + versions of Electrum must be considered broken, and they need to be + recreated from seed words. + * A new version of the Electrum protocol is required by the client + (version 1.2). Servers using older versions of the protocol will + not be displayed in the GUI. + + +# Release 3.0.6 : + * Fix transaction parsing bug #3788 + +# Release 3.0.5 : (Security update) + +This is a follow-up to the 3.0.4 release, which did not completely fix +issue #3374. Users should upgrade to 3.0.5. + + * The JSONRPC interface is password protected + * JSONRPC commands are disabled if the GUI is running, except 'ping', + which is used to determine if a GUI is already running + + +# Release 3.0.4 : (Security update) + + * Fix a vulnerability caused by Cross-Origin Resource Sharing (CORS) + in the JSONRPC interface. Previous versions of Electrum are + vulnerable to port scanning and deanonimization attacks from + malicious websites. Wallets that are not password-protected are + vulnerable to theft. + * Bundle QR scanner with Android app + * Minor bug fixes + +# Release 3.0.3 + * Qt GUI: sweeping now uses the Send tab, allowing fees to be set + * Windows: if using the installer binary, there is now a separate shortcut + for "Electrum Testnet" + * Digital Bitbox: added suport for p2sh-segwit + * OS notifications for incoming transactions + * better transaction size estimation: + - fees for segwit txns were somewhat underestimated (#3347) + - some multisig txns were underestimated + - handle uncompressed pubkeys + * fix #3321: testnet for Windows binaries + * fix #3264: Ledger/dbb signing on some platforms + * fix #3407: KeepKey sending to p2sh output + * other minor fixes and usability improvements + +# Release 3.0.2 + * Android: replace requests tab with address tab, with access to + private keys + * sweeping minikeys: search for both compressed and uncompressed + pubkeys + * fix wizard crash when attempting to reset Google Authenticator + * fix #3248: fix Ledger+segwit signing + * fix #3262: fix SSL payment request signing + * other minor fixes. + +# Release 3.0.1 + * minor bug and usability fixes + +# Release 3.0 - Uncanny Valley (November 1st, 2017) + + * The project was migrated to Python3 and Qt5. Python2 is no longer + supported. If you cloned the source repository, you will need to + run "python3 setup.py install" in order to install the new + dependencies. + + * Segwit support: + + - Native segwit scripts are supported using a new type of + seed. The version number for segwit seeds is 0x100. The install + wizard will not create segwit seeds by default; users must + opt-in with the segwit option. + + - Native segwit scripts are represented using bech32 addresses, + following BIP173. Please note that BIP173 is still in draft + status, and that other wallets/websites may not support + it. Thus, you should keep a non-segwit wallet in order to be + able to receive bitcoins during the transition period. If BIP173 + ends up being rejected or substantially modified, your wallet + may have to be restored from seed. This will not affect funds + sent to bech32 addresses, and it will not affect the capacity of + Electrum to spend these funds. + + - Segwit scripts embedded in p2sh are supported with hardware + wallets or bip39 seeds. To create a segwit-in-p2sh wallet, + trezor/ledger users will need to enter a BIP49 derivation path. + + - The BIP32 master keys of segwit wallets are serialized using new + version numbers. The new version numbers encode the script type, + and they result in the following prefixes: + + * xpub/xprv : p2pkh or p2sh + * ypub/yprv : p2wpkh-in-p2sh + * Ypub/Yprv : p2wsh-in-p2sh + * zpub/zprv : p2wpkh + * Zpub/Zprv : p2wsh + + These values are identical for mainnet and testnet; tpub/tprv + prefixes are no longer used in testnet wallets. + + - The Wallet Import Format (WIF) is similarly extended for segwit + scripts. After a base58-encoded key is decoded to binary, its + first byte encodes the script type: + + * 128 + 0: p2pkh + * 128 + 1: p2wpkh + * 128 + 2: p2wpkh-in-p2sh + * 128 + 5: p2sh + * 128 + 6: p2wsh + * 128 + 7: p2wsh-in-p2sh + + The distinction between p2sh and p2pkh in private key means that + it is not possible to import a p2sh private key and associate it + to a p2pkh address. + + * A new version of the Electrum protocol is required by the client + (version 1.1). Servers using older versions of the protocol will + not be displayed in the GUI. + + * By default, transactions are time-locked to the height of the + current block. Other values of locktime may be passed using the + command line. + + +# Release 2.9.3 + * fix configuration file issue #2719 + * fix ledger signing of non-RBF transactions + * disable 'spend confirmed only' option by default + +# Release 2.9.2 + * force headers download if headers file is corrupted + * add websocket to windows builds + +# Release 2.9.1 + * fix initial headers download + * validate contacts on import + * command-line option for locktime + +# Release 2.9 - Independence (July 27th, 2017) + * Multiple Chain Validation: Electrum will download and validate + block headers sent by servers that may follow different branches + of a fork in the Bitcoin blockchain. Instead of a linear sequence, + block headers are organized in a tree structure. Branching points + are located efficiently using binary search. The purpose of MCV is + to detect and handle blockchain forks that are invisible to the + classical SPV model. + * The desired branch of a blockchain fork can be selected using the + network dialog. Branches are identified by the hash and height of + the diverging block. Coin splitting is possible using RBF + transaction (a tutorial will be added). + * Multibit support: If the user enters a BIP39 seed (or uses a + hardware wallet), the full derivation path is configurable in the + install wizard. + * Option to send only confirmed coins + * Qt GUI: + - Network dialog uses tabs and gets updated by network events. + - The gui tabs use icons + * Kivy GUI: + - separation between network dialog and wallet settings dialog. + - option for manual server entry + - proxy configuration + * Daemon: The wallet password can be passed as parameter to the + JSONRPC API. + * Various other bugfixes and improvements. + + +# Release 2.8.3 + * Fix crash on reading older wallet formats. + * TrustedCoin: remove pay-per-tx option + +# Release 2.8.2 + * show paid invoices in history tab + * improve CPFP dialog + * fixes for trezor, keepkey + * other minor bugfixes + +# Release 2.8.1 + * fix Digital Bitbox plugin + * fix daemon jsonrpc + * fix trustedcoin wallet creation + * other minor bugfixes + +# Release 2.8.0 (March 9, 2017) + * Wallet file encryption using ECIES: A keypair is derived from the + wallet password. Once the wallet is decrypted, only the public key + is retained in memory, in order to save the encrypted file. + * The daemon requires wallets to be explicitly loaded before + commands can use them. Wallets can be loaded using: 'electrum + daemon load_wallet [-w path]'. This command will require a + password if the wallet is encrypted. + * Invoices and contacts are stored in the wallet file and are no + longer shared between wallets. Previously created invoices and + contacts files may be imported from the menu. + * Fees improvements: + - Dynamic fees are enabled by default. + - Child Pays For Parent (CPFP) dialog in the GUI. + - RBF is automatically proposed for low fee transactions. + * Support for Segregated Witness (testnet only). + * Support for Digital Bitbox hardware wallet. + * The GUI shows a blue icon when connected using a proxy. + +# Release 2.7.18 + * enforce https on exchange rate APIs + * use hardcoded list of exchanges + * move 'Freeze' menu to Coins (utxo) tab + * various bugfixes + +# Release 2.7.17 + * fix a few minor regressions in the Qt GUI + +# Release 2.7.16 + * add Testnet support (fix #541) + * allow daemon to be launched in the foreground (fix #1873) + * Qt: use separate tabs for addresses and UTXOs + * Qt: update fee slider with a network callback + * Ledger: new ui and mobile 2fa validation (neocogent) + +# Release 2.7.15 + * Use fee slider for both static and dynamic fees. + * Add fee slider to RBF dialog (fix #2083). + * Simplify fee preferences. + * Critical: Fix password update issue (#2097). This bug prevents + password updates in multisig and 2FA wallets. It may also cause + wallet corruption if the wallet contains several master private + keys (such as 2FA wallets that have been restored from + seed). Affected wallets will need to be restored again. + +# Release 2.7.14 + * Merge exchange_rate plugin with main code + * Faster synchronization and transaction creation + * Fix bugs #2096, #2016 + +# Release 2.7.13 + * fix message signing with imported keys + * add size to transaction details window + * move plot plugin to main code + * minor bugfixes + +# Release 2.7.12 + various bugfixes + +# Release 2.7.11 + * fix offline signing (issue #195) + * fix android crashes caused by threads + +# Release 2.7.10 + * various fixes for hardware wallets + * improve fee bumping + * separate sign and broadcast buttons in Qt tx dialog + * allow spaces in private keys + +# Release 2.7.9 + * Fix a bug with the ordering of pubkeys in recent multisig wallets. + Affected wallets will regenerate their public keys when opened for + the first time. This bug does not affect address generation. + * Fix hardware wallet issues #1975, #1976 + +# Release 2.7.8 + * Fix a bug with fee bumping + * Fix crash when parsing request (issue #1969) + +# Release 2.7.7 + * Fix utf8 encoding bug with old wallet seeds (issue #1967) + * Fix delete request from menu (isue #1968) + +# Release 2.7.6 + * Fixes a critical bug with imported private keys (issue #1966). Keys + imported in Electrum 2.7.x were not encrypted, even if the wallet + had a password. If you imported private keys using Electrum 2.7.x, + you will need to import those keys again. If you imported keys in + 2.6 and converted with 2.7.x, you don't need to do anything, but + you still need to upgrade in order to be able to spend. + * Wizard: Hide seed options in a popup dialog. + +# Release 2.7.5 + * Add number of confirmations to request status. (issue #1757) + * In the GUI, refer to passphrase as 'seed extension'. + * Fix bug with utf8 encoded passphrases. + * Kivy wizard: add a dialog for seed options. + * Kivy wizard: add current word to suggestions, because some users + don't see the space key. + +# Release 2.7.4 + * Fix private key import in wizard + * Fix Ledger display (issue #1961) + * Fix old watching-only wallets (issue #1959) + * Fix Android compatibility (issue #1947) + +# Release 2.7.3 + * fix Trezor and Keepkey support in Windows builds + * fix sweep private key dialog + * minor fixes: #1958, #1959 + +# Release 2.7.2 + * fix bug in password update (issue #1954) + * fix fee slider (issue #1953) + +# Release 2.7.1 + * fix wizard crash with old seeds + * fix issue #1948: fee slider + +# Release 2.7.0 (Oct 2 2016) + + * The wallet file format has been upgraded. This upgrade is not + backward compatible, which means that a wallet upgraded to the 2.7 + format will not be readable by earlier versions of + Electrum. Multiple accounts inside the same wallet are not + supported in the new format; the Qt GUI will propose to split any + wallet that has several accounts. Make sure that you have saved + your seed phrase before you upgrade Electrum. + * This version introduces a separation between wallets types and + keystores types. 'Wallet type' defines the type of Bitcoin contract + used in the wallet, while 'keystore type' refers to the method used + to store private keys. Therefore, so-called 'hardware wallets' will + be referred to as 'hardware keystores'. + * Hardware keystores: + - The Ledger Nano S is supported. + - Hardware keystores can be used as cosigners in multi-signature + wallets. + - Multiple hardware cosigners can be used in the same multisig + wallet. One icon per keystore is displayed in the satus bar. Each + connected device will co-sign the transaction. + * Replace-By-Fee: RBF transactions are supported in both Qt and + Android. A warning is displayed in the history for transactions + that are replaceable, have unconfirmed parents, or that have very + low fees. + * Dynamic fees: Dynamic fees are enabled by default. A slider allows + the user to select the expected confirmation time of their + transaction. The expected confirmation times of incoming + transactions is also displayed in the history. + * The install wizards of Qt and Kivy have been unified. + * Qt GUI (Desktop): + - A fee slider is visible in the in send tab + - The Address tab is hidden by default, can be shown with Ctrl-A + - UTXOs are displayed in the Address tab + * Kivy GUI (Android): + - The GUI displays the complete transaction history. + - Multisig wallets are supported. + - Wallets can be created and deleted in the GUI. + * Seed phrases can be extended with a user-chosen passphrase. The + length of seed phrases is standardized to 12 words, using 132 bits + of entropy (including 2FA seeds). In the wizard, the type of the + seed is displayed in the seed input dialog. + * TrustedCoin users can request a reset of their Google Authenticator + account, if they still have their seed. + + +# Release 2.6.4 (bugfixes) + * fix coinchooser bug (#1703) + * fix daemon JSONRPC (#1731) + * fix command-line broadcast (#1728) + * QT: add colors to labels + +# Release 2.6.3 (bugfixes) + * fix command line parsing of transactions + * fix signtransaction --privkey (#1715) + +# Release 2.6.2 (bugfixes) + * fix Trustedcoin restore from seed (bug #1704) + * small improvements to kivy GUI + +# Release 2.6.1 (bugfixes) + * fix broadcast command (bug #1688) + * fix tx dialog (bug #1690) + * kivy: support old-type seed phrases in wizard + +# Release 2.6 + * The source code is relicensed under the MIT Licence + * First official release of the Kivy GUI, with android APK + * The old 'android' and 'gtk' GUIs are deprecated + * Separation between plugins and GUIs + * The command line uses jsonrpc to communicate with the daemon + * New command: 'notify
' + * Alternative coin selection policy, designed to help preserve user + privacy. Enable it by setting the Coin Selection preference to + Privacy. + * The install wizard has been rewritten and improved + * Support minikeys as used in Casascius coins for private key import + and sweeping + * Much improved support for TREZOR and KeepKey devices: + - full device information display + - initialize a new or wiped device in 4 ways: + 1) device generates a new wallet + 2) you enter a seed + 3) you enter a BIP39 mnemonic to generate the seed + 4) you enter a master private key + - KeepKey secure seed recovery (KeepKey only) + - change / set / disable PIN + - set homescreen (TREZOR only) + - set a session timeout. Once a session has timed out, further use + of the device requires your PIN and passhphrase to be re-entered + - enable / disable passphrases + - device wipe + - multiple device support + +# Release 2.5.4 + * increase MIN_RELAY_TX_FEE to avoid dust transactions + +# Release 2.5.3 (bugfixes) + * installwizard: do not allow direct copy-paste of the seed + * installwizard: fix bug #1531 (starting offline) + +# Release 2.5.2 (bugfixes) + * fix bug #1513 (client tries to broadcast transaction while not connected) + * fix synchronization bug (#1520) + * fix command line bug (#1494) + * fixes for exchange rate plugin + +# Release 2.5.1 (bugfixes) + * signatures in transactions were still using the old class + * make sure that setup.py uses python2 + * fix wizard crash with trustedcoin plugin + * fix socket infinite loop + * fix history bug #1479 + +# Release 2.5 + * Low-S values are used in signatures (BIP 62). + * The Kivy GUI has been merged into master. + * The Qt GUI supports multiple windows in the same process. When a + new Electrum instance is started, it checks for an already running + Electrum process, and connects to it. + * The network layer uses select(), so all server communication is + handled by a single thread. Moreover, the synchronizer, verifier, + and exchange rate plugin now run as separate jobs within the + networking thread instead of as their own threads. + * Plugins are revamped, particularly the exchange rate plugin. + +# Release 2.4.4 + * Fix bug with TrustedCoin plugin + +# Release 2.4.3 + * Support for KeepKey hardware wallet + * Simplified Chinese wordlist + * Minor bugfixes and GUI tweaks + +# Release 2.4.2 + * Command line can read arguments from stdin (pipe) + * Speedup fee computation for large transactions + * Various bugfixes + +# Release 2.4.1 + * Use ssl.PROTOCOL_TLSv1 + * Fix DNSSEC issues with ECDSA signatures + * Replace TLSLite dependency with minimal RSA implementation + * Dynamic Fees: using estimatefee value returned by server + * Various GUI improvements + +# Release 2.4 + * Payment to DNS names storing a Bitcoin addresses (OpenAlias) is + supported directly, without activating a plugin. The verification + uses DNSSEC. + * The DNSSEC verification code was rewritten. The previous code, + which was part of the OpenAlias plugin, is vulnerable and should + not be trusted (Electrum 2.0 to 2.3). + * Payment requests can be signed using Bitcoin addresses stored + in DNS (OpenAlias). The identity of the requestor is verified using + DNSSEC. + * Payment requests signed with OpenAlias keys can be shared as + bitcoin: URIs, if they are simple (a single address-type + output). The BIP21 URI scheme is extended with 'name', 'sig', + 'time', 'exp'. + * Arbitrary m-of-n multisig wallets are supported (n<=15). + * Multisig transactions can be signed with TREZOR. When you create + the multisig wallet, just enter the xpub of your existing TREZOR + wallet. + * Transaction fees set manually in the GUI are retained, including + when the user uses the '!' shortcut. + * New 'email' plugin, that enables sending and receiving payment + requests by email. + * The daemon supports Websocket notifications of payments. + +# Release 2.3.3 + * fix proxy settings (issue #1309) + * improvements to the transaction dialog: + - request password after showing transaction + - show change addresses in yellow color + +# Release 2.3.2 + * minor bugfixes + * updated ledger plugin + * sort inputs/outputs lexicographically (BIP-LI01) + +# Release 2.3.1 + * patch a bug with payment requests + +# Release 2.3 + * Improved logic for the network layer. + * More efficient coin selection. Spend oldest coins first, and + minimize the number of transaction inputs. + * Plugins are loaded independently of the GUI. As a result, Openalias, + TrustedCoin and TREZOR wallets can be used with the command + line. Example: 'electrum payto ' + * The command line has been refactored: + - Arguments are parsed with argparse. + - The inline help includes a description of options. + - Some commands have been renamed. Notably, 'mktx' and 'payto' have + been merged into a single command, with a --broadcast option. + Type 'electrum --help' for a complete overview. + * The command line accepts the '!' syntax to send the maximum + amount available. It can be combined with the '--from' option. + Example: 'payto ! --from ' + * The command line also accepts a '?' shortcut for private keys + arguments, that triggers a prompt. + * Payment requests can be managed with the command line, using the + following commands: 'addrequest', 'rmrequest', 'listrequests'. + Payment requests can be signed with a SSL certificate, and published + as bip70 files in a public web directory. To see the relevant + configuration variables, type 'electrum addrequest --help' + * Commands can be called with jsonrpc, using the 'jsonrpc' gui. The + jsonrpc interface may be called by php. + +# Release 2.2 + * Show amounts (thousands separators and decimal point) + according to locale in GUI + * Show unmatured coins in balance + * Fix exchange rates plugin + * Network layer: refactoring and fixes + +# Release 2.1.1 + * patch a bug that prevents new wallet creation. + * fix connection issue on osx binaries + +# Release 2.1 + * Faster startup, thanks to the following optimizations: + 1. Transaction input/outputs are cached in the wallet file + 2. Fast X509 certificate parser, not using pyasn1 anymore. + 3. The Label Sync plugin only requests modified labels. + * The 'Invoices' and 'Send' tabs have been merged. + * Contacts are stored in a separate file, shared between wallets. + * A Search Box is available in the GUI (Ctrl-S) + * Payment requests have an expiration date and can be exported to + BIP70 files. + * file: scheme support in BIP72 URIs: "bitcoin:?r=file:///..." + * Own addresses are shown in green in the Transaction dialog. + * Address History dialog. + * The OpenAlias plugin was improved. + * Various bug fixes and GUI improvements. + * A new LabelSync backend is being used an import of the old + database was made but since the release came later it's + recommended that you do a full push when you upgrade. + +# Release 2.0.4 - Minor GUI improvements + * The password dialog will ask for password again if the user enters + a wrong password + * The Master Public Key dialog displays which keys belong to the + wallet, and which are cosigners + * The transaction dialog will ask to save unsaved transaction + received from cosigner pool, when user clicks on 'Close' + * The multisig restore dialog accepts xprv keys. + * The network daemon must be started explicitly before using commands + that require a connection + Example: + electrum daemon start + electrum getaddressunspent + electrum daemon status + electrum daemon stop + If a daemon is running, the GUI will use it. + +# Release 2.0.3 - bugfixes and minor GUI improvements + * Do not use daemon threads (fix #960) + * Add a zoom button to receive tab + * Add exchange rate conversion to receive tab + * Use Tor's default port number in default proxy config + +# Release 2.0.2 - bugfixes + * Fix transaction sweep (#1066) + * Fix thread timing bug (#1054) + +# Release 2.0.1 - bugfixes + * Fix critical bug in TREZOR address derivation: passphrases were not + NFKD normalized. TREZOR users who created a wallet protected by a + passphrase containing utf-8 characters with diacritics are + affected. These users will have to open their wallet with version + 2.0 and to move their funds to a new wallet. + * Use a file socket for the daemon (fixes network dialog issues) + * Fix crash caused by QR scanner icon when zbar not installed. + * Fix CosignerPool plugin + * Label Sync plugin: Fix label sharing between multisig wallets + + +# Release 2.0 + + * Before you upgrade, make sure you have saved your wallet seed on + paper. + + * Documentation is now hosted on a wiki: http://electrum.orain.org + + * New seed derivation method (not compatible with BIP39). The seed + phrase includes a version number, that refers to the wallet + structure. The version number also serves as a checksum, and it + will prevent the import of seeds from incompatible wallets. Old + Electrum seeds are still supported. + + * New address derivation (BIP32). Standard wallets are single account + and use a gap limit of 20. + + * Support for Multisig wallets using parallel BIP32 derivations and + P2SH addresses ("2 of 2", "2 of 3"). + + * Compact serialization format for unsigned or partially signed + transactions, that includes the BIP32 master public key and + derivation needed to sign inputs. Serialized transactions can be + sent to cosigners or to cold storage using QR codes (using Andreas + Schildbach's base 43 idea). + + * Support for BIP70 payment requests: + - Verification of the chain of signatures uses tlslite. + - In the GUI, payment requests are shown in the 'Invoices' tab. + + * Support for hardware wallets: TREZOR (SatoshiLabs) and Btchip (Ledger). + + * Two-factor authentication service by TrustedCoin. This service uses + "2 of 3" multisig wallets and Google Authenticator. Note that + wallets protected by this service can be deterministically restored + from seed, without Trustedcoin's server. + + * Cosigner Pool plugin: encrypted communication channel for multisig + wallets, to send and receive partially signed transactions. + + * Audio Modem plugin: send and receive transactions by sound. + + * OpenAlias plugin: send bitcoins to aliases verified using DNSSEC. + + * New 'Receive' tab in the GUI: + - create and manage payment requests, with QR Codes + - the former 'Receive' tab was renamed to 'Addresses' + - the former Point of Sale plugin is replaced by a resizeable + window that pops up if you click on the QR code + + * The 'Send' tab in the Qt GUI supports transactions with multiple + outputs, and raw hexadecimal scripts. + + * The GUI can connect to the Electrum daemon: "electrum -d" will + start the daemon if it is not already running, and the GUI will + connect to it. The daemon can serve several clients. It times out + if no client uses if for more than 5 minutes. + + * The install wizard can be used to import addresses or private + keys. A watching-only wallet is created by entering a list of + addresses in the wizard dialog. + + * New file format: Wallets files are saved as JSON. Note that new + wallet files cannot be read by older versions of Electrum. Old + wallet files will be converted to the new format; this operation + may take some time, because public keys will be derived for each + address of your wallet. + + * The client accepts servers with a CA-signed SSL certificate. + + * ECIES encrypt/decrypt methods, availabe in the GUI and using + the command line: + encrypt + decrypt + + * The Android GUI has received various updates and it is much more + stable. Another script was added to Android, called Authenticator, + that works completely offline: it reads an unsigned transaction + shown as QR code, signs it and shows the result as a QR code. + + +# Release 1.9.8 + +* Electrum servers were upgraded to version 0.9. The new server stores + a Patrica tree of all UTXOs, an idea proposed by Alan Reiner in the + bitcointalk forum. This property allows the client to directly + request the balance of any address. The new commands are: + 1. getaddressbalance
+ 2. getaddressunspent
+ 3. getutxoaddress + +* Command-line commands that require a connection to the network spawn + a daemon, that remains connected and handles subsequent + commands. The daemon terminates itself if it remains unused for more + than one minute. The purpose of this is to make scripting more + efficient. For example, a bash script using many electrum commands + will open only one connection. + +# Release 1.9.7 +* Fix for offline signing +* Various bugfixes +* GUI usability improvements +* Coinbase Buyback plugin + +# Release 1.9.6 +* During wallet creation, do not write seed to disk until it is encrypted. +* Confirmation dialog if the transaction fee is higher than 1mBTC. +* bugfixes + +# Release 1.9.5 + +* Coin control: select addresses to send from +* Put addresses that have been used in a minimized section (Qt GUI) +* Allow non ascii chars in passwords + + +# Release 1.9.4 +bugfixes: offline transactions + +# Release 1.9.3 +bugfixes: connection problems, transactions staying unverified + +# Release 1.9.2 +* fix a syntax error + +# Release 1.9.1 +* fix regression with --offline mode +* fix regression with --portable mode: use a dedicated directory + +# Release 1.9 + +* The client connects to multiple servers in order to retrieve block headers and find the longest chain +* SSL certificate validation (to prevent MITM) +* Deterministic signatures (RFC 6979) +* Menu to create/restore/open wallets +* Create transactions with multiple outputs from CSV (comma separated values) +* New text gui: stdio +* Plugins are no longer tied to the qt GUI, they can reach all GUIs +* Proxy bugs have been fixed + + +# Release 1.8.1 + +* Notification option when receiving new tranactions +* Confirm dialogue before sending large amounts +* Alternative datafile location for non-windows systems +* Fix offline wallet creation +* Remove enforced tx fee +* Tray icon improvements +* Various bugfixes + + +# Release 1.8 + +* Menubar in classic gui +* Updated the QR Code plugin to enable offline/online wallets to transmit unsigned/signed transactions via QR code. +* Fixed bug where never-confirmed transactions prevented further spending + + +# Release 1.7.4 + +* Increase default fee +* fix create and restore in command line +* fix verify message in the gui + + +# Release 1.7.3: + +* Classic GUI can display amounts in mBTC +* Account selector in the classic GUI +* Changed the way the portable flag uses without supplying a -w argument +* Classic GUI asks users to enter their seed on wallet creation + + +# Release 1.7.2: + +* Transactions that are in the same block are displayed in chronological order in the history. +* The client computes transaction priority and rejects zero-fee transactions that need a fee. +* The default fee was lowered to 200 uBTC per kb. +* Due to an internal format change, your history may be pruned when + you open your wallet for the first time after upgrading to 1.7.2. If + this is the case, please visit a full server to restore your full + history. You will only need to do that once. + + +# Release 1.7.1: bugfixes. + + +# Release 1.7 + +* The Classic GUI can be extended with plugins. Developers who want to +add new features or third-party services to Electrum are invited to +write plugins. Some previously existing and non-essential features of +Electrum (point-of-sale mode, qrcode scanner) were removed from the +core and are now available as plugins. + +* The wallet waits for 2 confirmations before creating new +addresses. This makes recovery from seed more robust. Note that it +might create unwanted gaps if you use Electrum 1.7 together with older +versions of Electrum. + +* An interactive Python console replaces the 'Wall' tab. The provided +python environment gives users access to the wallet and gui. Most +electrum commands are available as python function in the +console. Custom scripts an be loaded with a "run(filename)" +command. Tab-completions are available. + +* The location of the Electrum folder in Windows changed from +LOCALAPPDATA to APPDATA. Discussion on this topic can be found here: +https://bitcointalk.org/index.php?topic=144575.0 + +* Private keys can be exported from within the classic GUI: + For a single address, use the address menu (right-click). + To export the keys of your entire wallet, use the settings dialog (import/export tab). + +* It is possible to create, sign and redeem multisig transaction using the +command line interface. This is made possible by the following new commands: + dumpprivkey, listunspent, createmultisig, createrawtransaction, decoderawtransaction, signrawtransaction +The syntax of these commands is similar to their bitcoind counterpart. +For an example, see Gavin's tutorial: https://gist.github.com/gavinandresen/3966071 + +* Offline wallets now work in a way similar to Armory: + 1. user creates an unsigned transaction using the online (watching-only) wallet. + 2. unsigned transaction is copied to the offline computer, and signed by the offline wallet. + 3. signed transaction is copied to the online computer, broadcasted by the online client. + 4. All these steps can be done via the command line interface or the classic GUI. + +* Many command line commands have been renamed in order to make the syntax consistent with bitcoind. + +# Release 1.6.2 + +== Classic GUI +* Added new version notification + +# Release 1.6.1 (11-01-2013) + +== Core +* It is now possible to restore a wallet from MPK (this will create a watching-only wallet) +* A switch button allows to easily switch between Lite and Classic GUI. + +== Classic GUI +* Seed and MPK help dialogs were rewritten +* Point of Sale: requested amounts can be expressed in other currencies and are converted to bitcoin. + +== Lite GUI +* The receiving button was removed in favor of a menu item to keep it consistent with the history toggle. + +# Release 1.6.0 (07-01-2013) + +== Core +* (Feature) Add support for importing, signing and verifiying compressed keys +* (Feature) Auto reconnect to random server on disconnect +* (Feature) Ultimate fallback to HTTP port 80 if TCP doesn't work on any server +* (Bug) Under rare circumstances changing password with incorrect password could damage wallet + +== Lite GUI +* (Chore) Use blockchain.info for exchange rate data +* (Feature) added currency conversion for BRL, CNY, RUB +* (Feature) Saraha theme +* (Feature) csv import/export for transactions including labels + +== Classic GUI +* (Chore) pruning servers now called "p", full servers "f" to avoid confusion with terms +* (Feature) Debits in history shown in red +* (Feature) csv import/export for transactions including labels + +# Release 1.5.8 (02-01-2013) + +== Core +* (Bug) Fix pending address balance on received coins for pruning servers +* (Bug) Fix history command line option to show output again (regression by SPV) +* (Chore) Add timeout to blockchain headers file download by HTTP +* (Feature) new option: -L, --language: default language used in GUI. + +== Lite GUI +* (Bug) Sending to auto-completed contacts works again +* (Chore) Added version number to title bar + +== Classic GUI +* (Feature) Language selector in options. + +# Release 1.5.7 (18-12-2012) + +== Core +* The blockchain headers file is no longer included in the packages, it is downloaded on startup. +* New command line option: -P or --portable, for portable wallets. With this flag, all preferences are saved to the wallet file, and the blockchain headers file is in the same directory as the wallet + +== Lite GUI +* (Feature) Added the ability to export your transactions to a CSV file. +* (Feature) Added a label dialog after sending a transaction. +* (Feature) Reworked receiving addresses; instead of a random selection from one of your receiving addresses a new widget will show listing unused addresses. +* (Chore) Removed server selection. With all the new server options a simple menu item does not suffice anymore. 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 100755 index 000000000..8697838e7 --- /dev/null +++ b/contrib/build-osx/make_osx @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# Parameterize +PYTHON_VERSION=3.6.4 +BUILDDIR=/tmp/electrum-build +PACKAGE=electrum-btx +GIT_REPO=https://github.com/LIMXTEC/electrum-btx +LIBSECP_VERSION=452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4 + +. $(dirname "$0")/base.sh + +src_dir=$(dirname "$0") +cd $src_dir/../.. + +export PYTHONHASHSEED=22 +VERSION=`git describe --tags --dirty` + +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 git+https://github.com/ecdsa/pyinstaller@fix_2952 -I --user || fail "Could not install pyinstaller" + +info "Using these versions for building $PACKAGE:" +sw_vers +python3 --version +echo -n "Pyinstaller " +pyinstaller --version + +rm -rf ./dist + +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 "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..d27967643 --- /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-btx' +PYPKG='electrum-btx' +MAIN_SCRIPT='run_electrum' +ICONS_FILE='icons/electrumBTX.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 100755 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/README.md b/contrib/build-wine/README.md new file mode 100644 index 000000000..c6348c975 --- /dev/null +++ b/contrib/build-wine/README.md @@ -0,0 +1,37 @@ +Windows Binary Builds +===================== + +These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine. + +For reproducible builds, see the `docker` folder. + + +Usage: + + +1. Install the following dependencies: + + - dirmngr + - gpg + - 7Zip + - Wine (>= v2) + - (and, for building libsecp256k1) + - mingw-w64 + - autotools-dev + - autoconf + - libtool + + +For example: + +``` +$ sudo apt-get install wine-development dirmngr gnupg2 p7zip-full +$ sudo apt-get install mingw-w64 autotools-dev autoconf libtool +``` + +The binaries are also built by Travis CI, so if you are having problems, +[that script](https://github.com/spesmilo/electrum/blob/master/.travis.yml) might help. + +2. Make sure `/opt` is writable by the current user. +3. Run `build.sh`. +4. The generated binaries are in `./dist`. diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh new file mode 100755 index 000000000..76797050d --- /dev/null +++ b/contrib/build-wine/build-electrum-git.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +NAME_ROOT=electrum +PYTHON_VERSION=3.6.6 + +# These settings probably don't need any change +export WINEPREFIX=/opt/wine64 +export PYTHONDONTWRITEBYTECODE=1 +export PYTHONHASHSEED=22 + +PYHOME=c:/python$PYTHON_VERSION +PYTHON="wine $PYHOME/python.exe -OO -B" + + +# Let's begin! +cd `dirname $0` +set -e + +pushd ../../electrum +if ! which msgfmt > /dev/null 2>&1; then + echo "Please install gettext" + exit 1 +fi +for i in ./locale/*; do + dir=$i/LC_MESSAGES + mkdir -p $dir + msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true +done +popd + +cp -f ../../LICENSE . + +# Install frozen dependencies +$PYTHON -m pip install -r ../deterministic-build/requirements.txt +$PYTHON -m pip install -r ../deterministic-build/requirements-hw.txt + +pushd $WINEPREFIX/drive_c/electrum +find -exec touch -d '2000-11-11T11:11:11+00:00' {} + +popd + +pushd $WINEPREFIX/drive_c/electrum +$PYTHON setup.py install +popd + +#rm -rf dist/ + +# build standalone and portable versions +wine "C:/python$PYTHON_VERSION/scripts/pyinstaller.exe" --noconfirm --ascii --clean --name electrum-btx-3.2.3 -w deterministic.spec + +# build NSIS installer +# $VERSION could be passed to the electrum.nsi script, but this would require some rewriting in the script itself. +wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" /DPRODUCT_VERSION=$VERSION electrum.nsi + +echo "Done." +md5sum dist/electrum*exe diff --git a/contrib/build-wine/build-secp256k1.sh b/contrib/build-wine/build-secp256k1.sh new file mode 100755 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/build.sh b/contrib/build-wine/build.sh new file mode 100755 index 000000000..01ca071fe --- /dev/null +++ b/contrib/build-wine/build.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Lucky number +export PYTHONHASHSEED=22 + +here=$(dirname "$0") +test -n "$here" -a -d "$here" || exit + +echo "Clearing $here/build and $here/dist..." +rm "$here"/build/* -rf +rm "$here"/dist/* -rf + +mkdir -p /tmp/electrum-build +mkdir -p /tmp/electrum-build/pip-cache +export PIP_CACHE_DIR="/tmp/electrum-build/pip-cache" + +$here/build-secp256k1.sh || exit 1 + +$here/prepare-wine.sh || exit 1 + +echo "Resetting modification time in C:\Python..." +# (Because of some bugs in pyinstaller) +pushd /opt/wine64/drive_c/python* +find -exec touch -d '2000-11-11T11:11:11+00:00' {} + +popd +ls -l /opt/wine64/drive_c/python* + +$here/build-electrum-git.sh && \ +echo "Done." diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec new file mode 100644 index 000000000..875a091ae --- /dev/null +++ b/contrib/build-wine/deterministic.spec @@ -0,0 +1,140 @@ +# -*- mode: python -*- + +from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs + +import sys +for i, x in enumerate(sys.argv): + if x == '--name': + cmdline_name = sys.argv[i+1] + break +else: + raise Exception('no name') + +PYTHON_VERSION = '3.6.6' +PYHOME = 'c:/python' + PYTHON_VERSION + +home = 'C:\\electrum\\' + +# 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') + +# Add libusb binary +binaries = [(PYHOME+"/libusb-1.0.dll", ".")] + +# Workaround for "Retro Look": +binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]] + +binaries += [('C:/tmp/libsecp256k1.dll', '.')] + +datas = [ + (home+'electrum/*.json', 'electrum'), + (home+'electrum/wordlist/english.txt', 'electrum/wordlist'), + (home+'electrum/locale', 'electrum/locale'), + (home+'electrum/plugins', 'electrum/plugins'), + ('C:\\Program Files (x86)\\ZBar\\bin\\', '.'), +] +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') + +# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports +a = Analysis([home+'run_electrum', + home+'electrum/gui/qt/main_window.py', + home+'electrum/gui/text.py', + home+'electrum/util.py', + home+'electrum/wallet.py', + home+'electrum/simple_config.py', + home+'electrum/bitcoin.py', + home+'electrum/dnssec.py', + home+'electrum/commands.py', + home+'electrum/plugins/cosigner_pool/qt.py', + home+'electrum/plugins/email_requests/qt.py', + home+'electrum/plugins/trezor/client.py', + home+'electrum/plugins/trezor/qt.py', + home+'electrum/plugins/safe_t/client.py', + home+'electrum/plugins/safe_t/qt.py', + home+'electrum/plugins/keepkey/qt.py', + home+'electrum/plugins/ledger/qt.py', + home+'electrum/plugins/coldcard/qt.py', + #home+'packages/requests/utils.py' + ], + binaries=binaries, + datas=datas, + #pathex=[home+'lib', home+'gui', home+'plugins'], + 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 + +# hotfix for #3171 (pre-Win10 binaries) +a.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\windows')] + +pyz = PYZ(a.pure) + + +##### +# "standalone" exe with all dependencies packed into it + +exe_standalone = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + ".exe"), + debug=False, + strip=None, + upx=False, + icon=home+'icons/electrum.ico', + console=False) + # console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used + +exe_portable = EXE( + pyz, + a.scripts, + a.binaries, + a.datas + [ ('is_portable', 'README.md', 'DATA' ) ], + name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + "-portable.exe"), + debug=False, + strip=None, + upx=False, + icon=home+'icons/electrum.ico', + console=False) + +##### +# exe and separate files that NSIS uses to build installer "setup" exe + +exe_dependent = EXE( + pyz, + a.scripts, + exclude_binaries=True, + name=os.path.join('build\\pyi.win32\\electrum', cmdline_name), + debug=False, + strip=None, + upx=False, + icon=home+'icons/electrum.ico', + console=False) + +coll = COLLECT( + exe_dependent, + a.binaries, + a.zipfiles, + a.datas, + strip=None, + upx=True, + debug=False, + icon=home+'icons/electrum.ico', + console=False, + name=os.path.join('dist', 'electrum')) diff --git a/contrib/build-wine/docker/Dockerfile b/contrib/build-wine/docker/Dockerfile new file mode 100644 index 000000000..f46e98128 --- /dev/null +++ b/contrib/build-wine/docker/Dockerfile @@ -0,0 +1,33 @@ +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 \ + software-properties-common=0.96.24.32.4 \ + && \ + 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=1:2.17.1-1ubuntu0.1 \ + 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..0df96ea5a --- /dev/null +++ b/contrib/build-wine/docker/README.md @@ -0,0 +1,90 @@ +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 + + ``` + $ 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 new file mode 100644 index 000000000..baf210fec --- /dev/null +++ b/contrib/build-wine/electrum.nsi @@ -0,0 +1,170 @@ +;-------------------------------- +;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-btx" + !define PRODUCT_VER "3.2.3" + !define PRODUCT_WEB_SITE "https://github.com/LIMXTEC/electrum-btx" + !define PRODUCT_PUBLISHER "bitcore.cc" + !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}-${PRODUCT_VER}" + !define HOME "C:\electrum" + !define LICENSE_TXT "LICENSE" + +;-------------------------------- +;General + + ;Name and file + Name "${PRODUCT_NAME}-${PRODUCT_VER}" + OutFile "dist/${PRODUCT_NAME}-${PRODUCT_VER}-setup.exe" + + ;Default installation folder + InstallDir "$PROGRAMFILES\${PRODUCT_NAME}-${PRODUCT_VER}" + + ;Get installation folder from registry if available + InstallDirRegKey HKCU "Software\${PRODUCT_NAME}-${PRODUCT_VER}" "" + + ;Request application privileges for Windows Vista + RequestExecutionLevel admin + + ;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_VER}" + + ;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}-${PRODUCT_VER}" + + ;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}" + VIAddVersionKey CompanyName "${PRODUCT_PUBLISHER}" + VIAddVersionKey LegalCopyright "2017-2018 ${PRODUCT_PUBLISHER}" + VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer" + VIAddVersionKey FileVersion ${PRODUCT_VER} + VIAddVersionKey ProductVersion ${PRODUCT_VER} + VIAddVersionKey InternalName "${PRODUCT_NAME} Installer" + VIAddVersionKey LegalTrademarks "${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}" + VIAddVersionKey OriginalFilename "${PRODUCT_NAME}-${PRODUCT_VER}-setup.exe" + +;-------------------------------- +;Interface Settings + + !define MUI_ABORTWARNING + !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?" + + !define MUI_ICON "${HOME}\icons\electrum.ico" + +;-------------------------------- +;Pages + + !insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}" + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_INSTFILES + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" + +;-------------------------------- +;Installer Sections + +;Check if we have Administrator rights +Function .onInit + UserInfo::GetAccountType + pop $0 + ${If} $0 != "admin" ;Require admin rights on NT4+ + MessageBox mb_iconstop "Administrator rights required!" + SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED + Quit + ${EndIf} +FunctionEnd + +Section + SetOutPath $INSTDIR + + ;Files to pack into the installer + File "dist\${PRODUCT_NAME}-${PRODUCT_VER}.exe" + File "${HOME}\icons\electrum.ico" + + ;Store installation folder + WriteRegStr HKCU "Software\${PRODUCT_NAME}-${PRODUCT_VER}" "" $INSTDIR + + ;Create uninstaller + DetailPrint "Creating uninstaller..." + WriteUninstaller "$INSTDIR\Uninstall.exe" + + ;Create desktop shortcut + DetailPrint "Creating desktop shortcut..." + CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}-${PRODUCT_VER}.exe" "" + + ;Create start-menu items + DetailPrint "Creating start-menu items..." + CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}-${PRODUCT_VER}" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}-${PRODUCT_VER}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}-${PRODUCT_VER}\${PRODUCT_NAME}-${PRODUCT_VER}.lnk" "$INSTDIR\${PRODUCT_NAME}-${PRODUCT_VER}.exe" "" "$INSTDIR\${PRODUCT_NAME}-${PRODUCT_VER}.exe" 0 + + ;Links bitcoin: URI's to Electrum + WriteRegStr HKCU "Software\Classes\${PRODUCT_NAME}-${PRODUCT_VER}" "" "URL:bitcore Protocol" + WriteRegStr HKCU "Software\Classes\${PRODUCT_NAME}-${PRODUCT_VER}" "URL Protocol" "" + WriteRegStr HKCU "Software\Classes\${PRODUCT_NAME}-${PRODUCT_VER}" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\"" + WriteRegStr HKCU "Software\Classes\${PRODUCT_NAME}-${PRODUCT_VER}\shell\open\command" "" "$\"$INSTDIR\${PRODUCT_NAME}-${PRODUCT_VER}.exe$\" $\"%1$\"" + + ;Adds an uninstaller possibility to Windows Uninstall or change a program section + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VER}" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\electrum.ico" + + ;Fixes Windows broken size estimates + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "EstimatedSize" "$0" +SectionEnd + +;-------------------------------- +;Descriptions + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + RMDir /r "$INSTDIR\*.*" + + RMDir "$INSTDIR" + + Delete "$DESKTOP\${PRODUCT_NAME}-${PRODUCT_VER}.lnk" + Delete "$SMPROGRAMS\${PRODUCT_NAME}-${PRODUCT_VER}\*.*" + RMDir "$SMPROGRAMS\${PRODUCT_NAME}-${PRODUCT_VER}" + + DeleteRegKey HKCU "Software\Classes\${PRODUCT_NAME}-${PRODUCT_VER}" + DeleteRegKey HKCU "Software\${PRODUCT_NAME}-${PRODUCT_VER}" + DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}" +SectionEnd diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh new file mode 100755 index 000000000..ffa31e620 --- /dev/null +++ b/contrib/build-wine/prepare-wine.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# Please update these carefully, some versions won't work under Wine +NSIS_FILENAME=nsis-3.03-setup.exe +NSIS_URL=https://prdownloads.sourceforge.net/nsis/$NSIS_FILENAME?download +NSIS_SHA256=bd3b15ab62ec6b0c7a00f46022d441af03277be893326f6fea8e212dc2d77743 + +ZBAR_FILENAME=zbarw-20121031-setup.exe +ZBAR_URL=https://sourceforge.net/projects/zbarw/files/$ZBAR_FILENAME/download +ZBAR_SHA256=177e32b272fa76528a3af486b74e9cb356707be1c5ace4ed3fcee9723e2c2c02 + +LIBUSB_FILENAME=libusb-1.0.22.7z +LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.22/$LIBUSB_FILENAME?download +LIBUSB_SHA256=671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b + +PYTHON_VERSION=3.6.6 + +## These settings probably don't need change +export WINEPREFIX=/opt/wine64 +#export WINEARCH='win32' + +PYHOME=c:/python$PYTHON_VERSION +PYTHON="wine $PYHOME/python.exe -OO -B" + + +# based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg +verify_signature() { + local file=$1 keyring=$2 out= + if out=$(gpg --no-default-keyring --keyring "$keyring" --status-fd 1 --verify "$file" 2>/dev/null) && + echo "$out" | grep -qs "^\[GNUPG:\] VALIDSIG "; then + return 0 + else + echo "$out" >&2 + exit 1 + fi +} + +verify_hash() { + local file=$1 expected_hash=$2 + actual_hash=$(sha256sum $file | awk '{print $1}') + if [ "$actual_hash" == "$expected_hash" ]; then + return 0 + else + echo "$file $actual_hash (unexpected hash)" >&2 + rm "$file" + exit 1 + fi +} + +download_if_not_exist() { + local file_name=$1 url=$2 + if [ ! -e $file_name ] ; then + wget -O $PWD/$file_name "$url" + fi +} + +# https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh +retry() { + local result=0 + local count=1 + while [ $count -le 3 ]; do + [ $result -ne 0 ] && { + echo -e "\nThe command \"$@\" failed. Retrying, $count of 3.\n" >&2 + } + ! { "$@"; result=$?; } + [ $result -eq 0 ] && break + count=$(($count + 1)) + sleep 1 + done + + [ $count -gt 3 ] && { + echo -e "\nThe command \"$@\" failed 3 times.\n" >&2 + } + + return $result +} + +# Let's begin! +here=$(dirname $(readlink -e $0)) +set -e + +wine 'wineboot' + +# HACK to work around https://bugs.winehq.org/show_bug.cgi?id=42474#c22 +# needed for python 3.6+ +rm -f /opt/wine-stable/lib/wine/fakedlls/api-ms-win-core-path-l1-1-0.dll +rm -f /opt/wine-stable/lib/wine/api-ms-win-core-path-l1-1-0.dll.so + +cd /tmp/electrum-build + +# Install Python +# note: you might need "sudo apt-get install dirmngr" for the following +# keys from https://www.python.org/downloads/#pubkeys +KEYLIST_PYTHON_DEV="531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5" +KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg" +for server in $(shuf -e ha.pool.sks-keyservers.net \ + hkp://p80.pool.sks-keyservers.net:80 \ + keyserver.ubuntu.com \ + hkp://keyserver.ubuntu.com:80) ; do + retry gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --keyserver "$server" --recv-keys $KEYLIST_PYTHON_DEV \ + && break || : ; +done +for msifile in core dev exe lib pip tools; do + echo "Installing $msifile..." + wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" + wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc" + verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV + wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION +done + +# upgrade pip +$PYTHON -m pip install pip --upgrade + +# Install pywin32-ctypes (needed by pyinstaller) +$PYTHON -m pip install pywin32-ctypes==0.1.2 + +# install PySocks +$PYTHON -m pip install win_inet_pton==1.0.1 + +$PYTHON -m pip install -r $here/../deterministic-build/requirements-binaries.txt + +# Install PyInstaller +$PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip + +# Install ZBar +download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL" +verify_hash $ZBAR_FILENAME "$ZBAR_SHA256" +wine "$PWD/$ZBAR_FILENAME" /S + +# Upgrade setuptools (so Electrum can be installed later) +$PYTHON -m pip install setuptools --upgrade + +# Install NSIS installer +download_if_not_exist $NSIS_FILENAME "$NSIS_URL" +verify_hash $NSIS_FILENAME "$NSIS_SHA256" +wine "$PWD/$NSIS_FILENAME" /S + +download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL" +verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256" +7z x -olibusb $LIBUSB_FILENAME -aoa + +cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/python$PYTHON_VERSION/ + +# add dlls needed for pyinstaller: +cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/ + +mkdir -p $WINEPREFIX/drive_c/tmp +cp secp256k1/libsecp256k1.dll $WINEPREFIX/drive_c/tmp/ + +echo "Wine is configured." diff --git a/contrib/build-wine/sign.sh b/contrib/build-wine/sign.sh new file mode 100755 index 000000000..724b13dd1 --- /dev/null +++ b/contrib/build-wine/sign.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +here=$(dirname "$0") +test -n "$here" -a -d "$here" || exit +cd $here + +CERT_FILE=${CERT_FILE:-~/codesigning/cert.pem} +KEY_FILE=${KEY_FILE:-~/codesigning/key.pem} +if [[ ! -f "$CERT_FILE" ]]; then + ls $CERT_FILE + echo "Make sure that $CERT_FILE and $KEY_FILE exist" +fi + +if ! which osslsigncode > /dev/null 2>&1; then + echo "Please install osslsigncode" +fi + +rm -rf signed +mkdir -p signed >/dev/null 2>&1 + +cd dist +echo "Found $(ls *.exe | wc -w) files to sign." +for f in $(ls *.exe); do + echo "Signing $f..." + osslsigncode sign \ + -certs "$CERT_FILE" \ + -key "$KEY_FILE" \ + -n "Electrum" \ + -i "https://electrum.org/" \ + -t "http://timestamp.digicert.com/" \ + -in "$f" \ + -out "../signed/$f" + ls ../signed/$f -lah +done diff --git a/contrib/build-wine/tmp/LICENSE b/contrib/build-wine/tmp/LICENSE new file mode 100644 index 000000000..b8bb97185 --- /dev/null +++ b/contrib/build-wine/tmp/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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. diff --git a/contrib/build-wine/unsign.sh b/contrib/build-wine/unsign.sh new file mode 100755 index 000000000..fd1e5da81 --- /dev/null +++ b/contrib/build-wine/unsign.sh @@ -0,0 +1,56 @@ +#!/bin/bash +here=$(dirname "$0") +test -n "$here" -a -d "$here" || exit +cd $here + +if ! which osslsigncode > /dev/null 2>&1; then + echo "Please install osslsigncode" + exit +fi + +# exit if command fails +set -e + +mkdir -p signed >/dev/null 2>&1 +mkdir -p signed/stripped >/dev/null 2>&1 + +version=`python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)"` + +echo "Found $(ls dist/*.exe | wc -w) files to verify." + +for mine in $(ls dist/*.exe); do + echo "---------------" + f=$(basename $mine) + echo "Downloading https://download.electrum.org/$version/$f" + wget -q https://download.electrum.org/$version/$f -O signed/$f + out="signed/stripped/$f" + size=$( wc -c < $mine ) + # Step 1: Remove PE signature from signed binary + osslsigncode remove-signature -in signed/$f -out $out > /dev/null 2>&1 + # Step 2: Remove checksum and padding from signed binary + python3 < 0: + if binary[-n:] != bytearray(n): + print('expecting failure for', str(pe_file)) + binary = binary[:size] +with open(pe_file, "wb") as f: + f.write(binary) +EOF + chmod +x $out + if cmp -s $out $mine; then + echo "Success: $f" + gpg --sign --armor --detach signed/$f + else + echo "Failure: $f" + fi +done diff --git a/contrib/deterministic-build/check_submodules.sh b/contrib/deterministic-build/check_submodules.sh new file mode 100755 index 000000000..d9c1b61d5 --- /dev/null +++ b/contrib/deterministic-build/check_submodules.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +here=$(dirname "$0") +test -n "$here" -a -d "$here" || exit + +cd ${here}/../.. + +git submodule init +git submodule update + +function get_git_mtime { + if [ $# -eq 1 ]; then + git log --pretty=%at -n1 -- $1 + else + git log --pretty=%ar -n1 -- $2 + fi +} + +fail=0 + +for f in icons/* "icons.qrc"; do + if (( $(get_git_mtime "$f") > $(get_git_mtime "contrib/deterministic-build/electrum-icons/") )); then + echo "Modification time of $f (" $(get_git_mtime --readable "$f") ") is newer than"\ + "last update of electrum-icons" + fail=1 + fi +done + +if [ $(date +%s -d "2 weeks ago") -gt $(get_git_mtime "contrib/deterministic-build/electrum-locale/") ]; then + echo "Last update from electrum-locale is older than 2 weeks."\ + "Please update it to incorporate the latest translations from crowdin." + fail=1 +fi + +exit ${fail} \ No newline at end of file diff --git a/contrib/deterministic-build/find_restricted_dependencies.py b/contrib/deterministic-build/find_restricted_dependencies.py new file mode 100755 index 000000000..1734d5750 --- /dev/null +++ b/contrib/deterministic-build/find_restricted_dependencies.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import sys + +import requests + + +def check_restriction(p, r): + # See: https://www.python.org/dev/peps/pep-0496/ + # Hopefully we don't need to parse the whole microlanguage + if "extra" in r and "[" not in p: + return False + for marker in ["os_name", "platform_release", "sys_platform", "platform_system"]: + if marker in r: + return True + + +for p in sys.stdin.read().split(): + p = p.strip() + if not p: + continue + assert "==" in p, "This script expects a list of packages with pinned version, e.g. package==1.2.3, not {}".format(p) + p, v = p.rsplit("==", 1) + try: + data = requests.get("https://pypi.org/pypi/{}/{}/json".format(p, v)).json()["info"] + except ValueError: + raise Exception("Package could not be found: {}=={}".format(p, v)) + try: + for r in data["requires_dist"]: + if ";" not in r: + continue + d, restricted = r.split(";", 1) + if check_restriction(d, restricted): + print(d, sep=" ") + print("Installing {} from {} although it is only needed for {}".format(d, p, restricted), file=sys.stderr) + except TypeError: + # Has no dependencies at all + continue + diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt new file mode 100644 index 000000000..02561924b --- /dev/null +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -0,0 +1,56 @@ +pip==10.0.1 \ + --hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \ + --hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68 +pycryptodomex==3.6.4 \ + --hash=sha256:0461e88a7199f9e88f9f90c2c1e109e9e1f7bbb94dc6192e5df52829d31510c1 \ + --hash=sha256:08d0aba5a72e8af5da118ac4b6a5d75befceca7dd92a031b040ed5ff4417cec2 \ + --hash=sha256:0e22d47935d5fa95f556d5f5857576bc6750233964de06a840d58459010c3889 \ + --hash=sha256:10ef21d1728ec0b8afc4f8e1d8d9ea66f317154ea18731a4a05bd996cdc33fdf \ + --hash=sha256:1962b81eef81bf5c42d625816904a22a0bd23d15ca5d49891a54e3c0d0189d84 \ + --hash=sha256:24aae88efe3cbcb4a9cf840b2c352e7de1d6c2c5b3df37ff99b5c7e271e8f3a8 \ + --hash=sha256:43ad6d1d7ca545d53360bf412ee70fcb9ede876b4376fc6db06fc7328f70588c \ + --hash=sha256:4daabe7c0404e673b9029aa43761c779b9b4df2cbe11ccd94daded6a0acd8808 \ + --hash=sha256:4e15af025e02b04b0d0728e8248e4384d3dc7a3a89a020f5bd4d04ef2c5d9d4c \ + --hash=sha256:5b4d3c4a069a05972e0ed7111071bbcb4727ac652b5d7e8f786e8ea2fe63306b \ + --hash=sha256:67ad8b2ad15a99ae70e287454a112f67d2abaf160ee9c97f9daebf2296066447 \ + --hash=sha256:6d7e6fb69d9fd2c57e177f8a9cdf6489a725da77568e3d0a226c7dd18504396a \ + --hash=sha256:7907d7a5adde7cd07d19f129a4afa892b68b0b52a07eaf989e48e2677040b4bf \ + --hash=sha256:88210edafd564c8ff4a68716aaf0627e3bc43e9c192a33d6f5616743f72c2d9b \ + --hash=sha256:8a6b14a90bdcbcdc268acae87126c33bf4250d3842803a93a548d7c10135893a \ + --hash=sha256:94a10446ad61965516aecd610a2dd28d79ab1dfd8723903e1bd19ffa985c208e \ + --hash=sha256:99bda900a0bf6f9e6c69bdeb6114f7f6730b9d36a47bc1fe144263ce85bfc403 \ + --hash=sha256:9dae2e738622bd35ba82fe0b06f773be137a14e6b28defb2e36efc2d809cd28a \ + --hash=sha256:a04cd6021ff2756c38135a95f81b980485507bccbff4d2b8f62e537552270471 \ + --hash=sha256:a3b61625b60dd5e72556520a77464e2ac568c20b8ad12ea1f4443bf5051dc624 \ + --hash=sha256:a9a91fd9e7967a5bad88d542c9fce09323e15d16cb6fa9b8978390e46e68cbdf \ + --hash=sha256:afc44f1b595bd736ec3762dd9a2d0ef276a6ac560c85f643acfc4c0bf0c73384 \ + --hash=sha256:b5f3c8912b36e6abb843a51eecb414a1161f80c0ca0b65066c23aa449b5f98db \ + --hash=sha256:cc07c8b7686dd7093f33067a02b92f4fed860d75ad2bcc4e60624f70fdb94576 \ + --hash=sha256:da646eddbe026306fd1cb2c392a9aee4ebea13f2a9add9af303bb3151786a5d8 \ + --hash=sha256:df93eaccd5c09e6380fab8f15c06a89944415e4bb9af64a94f467ce4c782ff8e \ + --hash=sha256:e667303019770834354c75022ab0324d5ae5bf7cd7015939678033a58f87ee70 \ + --hash=sha256:f921219040ce994c9118b7218b7f7b4e9394e507c97cfc869ce5358437fc26cd +PyQt5==5.10.1 \ + --hash=sha256:1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac \ + --hash=sha256:4db7113f464c733a99fcb66c4c093a47cf7204ad3f8b3bda502efcc0839ac14b \ + --hash=sha256:9c17ab3974c1fc7bbb04cc1c9dae780522c0ebc158613f3025fccae82227b5f7 \ + --hash=sha256:f6035baa009acf45e5f460cf88f73580ad5dc0e72330029acd99e477f20a5d61 +setuptools==40.0.0 \ + --hash=sha256:012adb8e25fbfd64c652e99e7bab58799a3aaf05d39ab38561f69190a909015f \ + --hash=sha256:d68abee4eed409fbe8c302ac4d8429a1ffef912cd047a903b5701c024048dd49 +SIP==4.19.8 \ + --hash=sha256:09f9a4e6c28afd0bafedb26ffba43375b97fe7207bd1a0d3513f79b7d168b331 \ + --hash=sha256:105edaaa1c8aa486662226360bd3999b4b89dd56de3e314d82b83ed0587d8783 \ + --hash=sha256:1bb10aac55bd5ab0e2ee74b3047aa2016cfa7932077c73f602a6f6541af8cd51 \ + --hash=sha256:265ddf69235dd70571b7d4da20849303b436192e875ce7226be7144ca702a45c \ + --hash=sha256:52074f7cb5488e8b75b52f34ec2230bc75d22986c7fe5cd3f2d266c23f3349a7 \ + --hash=sha256:5ff887a33839de8fc77d7f69aed0259b67a384dc91a1dc7588e328b0b980bde2 \ + --hash=sha256:74da4ddd20c5b35c19cda753ce1e8e1f71616931391caeac2de7a1715945c679 \ + --hash=sha256:7d69e9cf4f8253a3c0dfc5ba6bb9ac8087b8239851f22998e98cb35cfe497b68 \ + --hash=sha256:97bb93ee0ef01ba90f57be2b606e08002660affd5bc380776dd8b0fcaa9e093a \ + --hash=sha256:cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85 \ + --hash=sha256:d9023422127b94d11c1a84bfa94933e959c484f2c79553c1ef23c69fe00d25f8 \ + --hash=sha256:e72955e12f4fccf27aa421be383453d697b8a44bde2cc26b08d876fd492d0174 +wheel==0.31.1 \ + --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \ + --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt new file mode 100644 index 000000000..ea2d91190 --- /dev/null +++ b/contrib/deterministic-build/requirements-hw.txt @@ -0,0 +1,122 @@ +btchip-python==0.1.27 \ + --hash=sha256:e58a941abbb2d8901bf4858baa18012537c60812c7f895f9a039113ecce3032b +certifi==2018.4.16 \ + --hash=sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7 \ + --hash=sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0 +chardet==3.0.4 \ + --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ + --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 +click==6.7 \ + --hash=sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d \ + --hash=sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b +Cython==0.28.4 \ + --hash=sha256:01487236575df8f17b46982071438dce4f7eaf8acc8fb99fca3510d343cd7a28 \ + --hash=sha256:0671d17c7a27634d6819246e535241b951141ed0e3f6f2a6d618fd32344dae3e \ + --hash=sha256:0e6190d6971c46729f712dd7307a9c0a8c027bfa5b4d8f2edef106b01759926c \ + --hash=sha256:202587c754901d0678bd6ff89c707f099987928239049a528470c06c6c922cf8 \ + --hash=sha256:345197ba9278cf6a914cb7421dc665a0531a219b0072abf6b0cebfdf68e75725 \ + --hash=sha256:3a296b8d6b02f0e01ab04bedea658f43eef5ad2f8e586a820226ead1a677d9b1 \ + --hash=sha256:484572a2b22823a967be106137a93f7d634db116b3f7accb37dbd760eda2fa9f \ + --hash=sha256:4c67c9c803e50ceff32cc5e4769c50fc8ae8df9c4e5cc592ce8310b5a1076d23 \ + --hash=sha256:539038087c321911745fc2e77049209b1231300d481cb4d682b2f95c724814b3 \ + --hash=sha256:58113e0683c3688594c112103d7e9f2d0092fd2d8297a220240bea22e184dfdd \ + --hash=sha256:65cb25ca4284804293a2404d1be3b5a98818be21a72791649bacbcfa4e431d41 \ + --hash=sha256:699e765da2580e34b08473fc0acef3a2d7bcb7f13eb29401cd25236bcf000080 \ + --hash=sha256:6b54c3470810cea49a8be90814d05c5325ceb9c5bf429fd86c36fc1b32dfc157 \ + --hash=sha256:71ac1629e4eae2ed329be8caf45efea10bfe1af3d8767e12e64b83e4ea5a3250 \ + --hash=sha256:722c179d3df8677f3daf45b1a2764678ed4f0aaddbaa7211a8a08ebfd907c0db \ + --hash=sha256:76ac2b08d3d956d77b574bb43cbf1d37bd58b9d50c04ba281303e695854ebc46 \ + --hash=sha256:7eff1157be9e26bf7494288c89979ca69d593a009e2c7420a739e2cf1e0635f5 \ + --hash=sha256:99546c8696d27d0efa639c77b2f8af6e61dc3a5073caae4f27ffd991ca926f42 \ + --hash=sha256:a0c263b31d335f29c11f4a9e98fbcd908d0731d4ea99bfd27c1c47caaeb4ca2e \ + --hash=sha256:a29c66292605bff962adc26530c030607aa699206b12dfb84f131b0454e15df4 \ + --hash=sha256:a4d3724c5a1ddd86d7d830d8e02c40151839b833791dd4b6fe9e144380fa7d37 \ + --hash=sha256:aed9f33b19d542eea56c38ef3862ca56147f7903648156cd57eabb0fe47c35d6 \ + --hash=sha256:b57e733dd8871d2cc7358c2e0fe33027453afffbcd0ea6a537f54877cad5131c \ + --hash=sha256:d5bf4db62236e82955c40bafbaa18d54b20b5ceefa06fb57c7facc443929f4bd \ + --hash=sha256:d9272dd71ab78e87fa34a0a59bbd6acc9a9c0005c834a6fc8457ff9619dc6795 \ + --hash=sha256:e9d5671bcbb90a41b0832fcb3872fcbaca3d68ff11ea09724dd6cbdf31d947fb \ + --hash=sha256:ee54646afb2b73b293c94cf079682d18d404ebd6c01122dc3980f111aec2d8ae \ + --hash=sha256:f16a87197939977824609005b73f9ebb291b9653a14e5f27afc1c5d6f981ba39 +ecdsa==0.13 \ + --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ + --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa +hidapi==0.7.99.post21 \ + --hash=sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24 \ + --hash=sha256:8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946 \ + --hash=sha256:b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87 \ + --hash=sha256:bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660 \ + --hash=sha256:c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7 \ + --hash=sha256:d4ad1e46aef98783a9e6274d523b8b1e766acfc3d72828cd44a337564d984cfa \ + --hash=sha256:d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b \ + --hash=sha256:e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97 \ + --hash=sha256:edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922 +idna==2.7 \ + --hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \ + --hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16 +keepkey==4.0.2 \ + --hash=sha256:cddee60ae405841cdff789cbc54168ceaeb2282633420f2be155554c25c69138 +libusb1==1.6.4 \ + --hash=sha256:8c930d9c1d037d9c83924c82608aa6a1adcaa01ca0e4a23ee0e8e18d7eee670d +mnemonic==0.18 \ + --hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d +pbkdf2==1.3 \ + --hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979 +pip==10.0.1 \ + --hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \ + --hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68 +protobuf==3.6.0 \ + --hash=sha256:12985d9f40c104da2f44ec089449214876809b40fdc5d9e43b93b512b9e74056 \ + --hash=sha256:12c97fe27af12fc5d66b23f905ab09dd4fb0c68d5a74a419d914580e6d2e71e3 \ + --hash=sha256:327fb9d8a8247bc780b9ea7ed03c0643bc0d22c139b761c9ec1efc7cc3f0923e \ + --hash=sha256:3895319db04c0b3baed74fb66be7ba9f4cd8e88a432b8e71032cdf08b2dfee23 \ + --hash=sha256:695072063e256d32335d48b9484451f7c7948edc3dbd419469d6a778602682fc \ + --hash=sha256:7d786f3ef5b33a04e6538089674f244a3b0f588155016559d950989010af97d0 \ + --hash=sha256:8bf82bb7a466a54be7272dcb492f71d55a2453a58d862fb74c3f2083f2768543 \ + --hash=sha256:9bbc1ae1c33c1bd3a2fc05a3aec328544d2b039ff0ce6f000063628a32fad777 \ + --hash=sha256:9e992c68103ab5635728d29fcf132c669cb4e2db24d012685210276185009d17 \ + --hash=sha256:9f1087abb67b34e55108bc610936b34363a7aac692023bcbb17e065c253a1f80 \ + --hash=sha256:9fefcb92a3784b446abf3641d9a14dad815bee88e0edd10b9a9e0e144d01a991 \ + --hash=sha256:a37836aa47d1b81c2db1a6b7a5e79926062b5d76bd962115a0e615551be2b48d \ + --hash=sha256:cca22955443c55cf86f963a4ad7057bca95e4dcde84d6a493066d380cfab3bb0 \ + --hash=sha256:d7ac50bc06d31deb07ace6de85556c1d7330e5c0958f3b2af85037d6d1182abf \ + --hash=sha256:dfe6899304b898538f4dc94fa0b281b56b70e40f58afa4c6f807805261cbe2e8 +pyblake2==1.1.2 \ + --hash=sha256:3757f7ad709b0e1b2a6b3919fa79fe3261f166fc375cd521f2be480f8319dde9 \ + --hash=sha256:407e02c7f8f36fcec1b7aa114ddca0c1060c598142ea6f6759d03710b946a7e3 \ + --hash=sha256:4d47b4a2c1d292b1e460bde1dda4d13aa792ed2ed70fcc263b6bc24632c8e902 \ + --hash=sha256:5ccc7eb02edb82fafb8adbb90746af71460fbc29aa0f822526fc976dff83e93f \ + --hash=sha256:8043267fbc0b2f3748c6920591cd0b8b5609dcce60c504c32858aa36206386f2 \ + --hash=sha256:982295a87907d50f4723db6bc724660da76b6547826d52160171d54f95b919ac \ + --hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \ + --hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \ + --hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358 +requests==2.19.1 \ + --hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \ + --hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a +safet==0.1.3 \ + --hash=sha256:ba80fe9f6ba317ab9514a8726cd3792e68eb46dd419f380d48ae4a0ccae646dc \ + --hash=sha256:e5d8e6a87c8bdf1cefd07004181b93fd7631557fdab09d143ba8d1b29291d6dc +setuptools==40.0.0 \ + --hash=sha256:012adb8e25fbfd64c652e99e7bab58799a3aaf05d39ab38561f69190a909015f \ + --hash=sha256:d68abee4eed409fbe8c302ac4d8429a1ffef912cd047a903b5701c024048dd49 +six==1.11.0 \ + --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \ + --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb +trezor==0.10.2 \ + --hash=sha256:4dba4d5c53d3ca22884d79fb4aa68905fb8353a5da5f96c734645d8cf537138d \ + --hash=sha256:d2b32f25982ab403758d870df1d0de86d0751c106ef1cd1289f452880ce68b84 +urllib3==1.23 \ + --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \ + --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 +websocket-client==0.48.0 \ + --hash=sha256:18f1170e6a1b5463986739d9fd45c4308b0d025c1b2f9b88788d8f69e8a5eb4a \ + --hash=sha256:db70953ae4a064698b27ae56dcad84d0ee68b7b43cb40940f537738f38f510c1 +wheel==0.31.1 \ + --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \ + --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f +pyaes==1.6.1 \ + --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f +ckcc-protocol==0.7.2 \ + --hash=sha256:498db4ccdda018cd9f40210f5bd02ddcc98e7df583170b2eab4035c86c3cc03b \ + --hash=sha256:31ee5178cfba8895eb2a6b8d06dc7830b51461a0ff767a670a64707c63e6b264 diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt new file mode 100644 index 000000000..1b226f9dd --- /dev/null +++ b/contrib/deterministic-build/requirements.txt @@ -0,0 +1,69 @@ +certifi==2018.4.16 \ + --hash=sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7 \ + --hash=sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0 +chardet==3.0.4 \ + --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ + --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 +dnspython==1.15.0 \ + --hash=sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c \ + --hash=sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31 +ecdsa==0.13 \ + --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ + --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa +idna==2.7 \ + --hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \ + --hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16 +jsonrpclib-pelix==0.3.1 \ + --hash=sha256:5417b1508d5a50ec64f6e5b88907f111155d52607b218ff3ba9a777afb2e49e3 \ + --hash=sha256:bd89a6093bc4d47dc8a096197aacb827359944a4533be5193f3845f57b9f91b4 +pip==10.0.1 \ + --hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \ + --hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68 +protobuf==3.6.0 \ + --hash=sha256:12985d9f40c104da2f44ec089449214876809b40fdc5d9e43b93b512b9e74056 \ + --hash=sha256:12c97fe27af12fc5d66b23f905ab09dd4fb0c68d5a74a419d914580e6d2e71e3 \ + --hash=sha256:327fb9d8a8247bc780b9ea7ed03c0643bc0d22c139b761c9ec1efc7cc3f0923e \ + --hash=sha256:3895319db04c0b3baed74fb66be7ba9f4cd8e88a432b8e71032cdf08b2dfee23 \ + --hash=sha256:695072063e256d32335d48b9484451f7c7948edc3dbd419469d6a778602682fc \ + --hash=sha256:7d786f3ef5b33a04e6538089674f244a3b0f588155016559d950989010af97d0 \ + --hash=sha256:8bf82bb7a466a54be7272dcb492f71d55a2453a58d862fb74c3f2083f2768543 \ + --hash=sha256:9bbc1ae1c33c1bd3a2fc05a3aec328544d2b039ff0ce6f000063628a32fad777 \ + --hash=sha256:9e992c68103ab5635728d29fcf132c669cb4e2db24d012685210276185009d17 \ + --hash=sha256:9f1087abb67b34e55108bc610936b34363a7aac692023bcbb17e065c253a1f80 \ + --hash=sha256:9fefcb92a3784b446abf3641d9a14dad815bee88e0edd10b9a9e0e144d01a991 \ + --hash=sha256:a37836aa47d1b81c2db1a6b7a5e79926062b5d76bd962115a0e615551be2b48d \ + --hash=sha256:cca22955443c55cf86f963a4ad7057bca95e4dcde84d6a493066d380cfab3bb0 \ + --hash=sha256:d7ac50bc06d31deb07ace6de85556c1d7330e5c0958f3b2af85037d6d1182abf \ + --hash=sha256:dfe6899304b898538f4dc94fa0b281b56b70e40f58afa4c6f807805261cbe2e8 +pyaes==1.6.1 \ + --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f +PySocks==1.6.8 \ + --hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672 +QDarkStyle==2.5.4 \ + --hash=sha256:3eb60922b8c4d9cedecb6897ca4c9f8a259d81bdefe5791976ccdf12432de1f0 \ + --hash=sha256:51331fc6490b38c376e6ba8d8c814320c8d2d1c2663055bc396321a7c28fa8be +qrcode==6.0 \ + --hash=sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf \ + --hash=sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3 +requests==2.19.1 \ + --hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \ + --hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a +setuptools==40.0.0 \ + --hash=sha256:012adb8e25fbfd64c652e99e7bab58799a3aaf05d39ab38561f69190a909015f \ + --hash=sha256:d68abee4eed409fbe8c302ac4d8429a1ffef912cd047a903b5701c024048dd49 +six==1.11.0 \ + --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \ + --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb +typing==3.6.4 \ + --hash=sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf \ + --hash=sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8 \ + --hash=sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2 +urllib3==1.23 \ + --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \ + --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 +wheel==0.31.1 \ + --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \ + --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f +colorama==0.3.9 \ + --hash=sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda \ + --hash=sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1 diff --git a/contrib/freeze_packages.sh b/contrib/freeze_packages.sh new file mode 100755 index 000000000..19d6b5fcc --- /dev/null +++ b/contrib/freeze_packages.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Run this after a new release to update dependencies + +venv_dir=~/.electrum-venv +contrib=$(dirname "$0") + +which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; } +python3 -m hashin -h > /dev/null 2>&1 || { python3 -m pip install hashin; } +other_python=$(which python3) + +for i in '' '-hw' '-binaries'; do + rm -rf "$venv_dir" + virtualenv -p $(which python3) $venv_dir + + source $venv_dir/bin/activate + + echo "Installing $m dependencies" + + python -m pip install -r $contrib/requirements/requirements${i}.txt --upgrade + + echo "OK." + + requirements=$(pip freeze --all) + restricted=$(echo $requirements | $other_python $contrib/deterministic-build/find_restricted_dependencies.py) + requirements="$requirements $restricted" + + echo "Generating package hashes..." + rm $contrib/deterministic-build/requirements${i}.txt + touch $contrib/deterministic-build/requirements${i}.txt + + for requirement in $requirements; do + echo -e "\r Hashing $requirement..." + $other_python -m hashin -r $contrib/deterministic-build/requirements${i}.txt ${requirement} + done + + echo "OK." +done + +echo "Done. Updated requirements" diff --git a/contrib/make_apk b/contrib/make_apk new file mode 100755 index 000000000..773aeab54 --- /dev/null +++ b/contrib/make_apk @@ -0,0 +1,17 @@ +#!/bin/bash + +pushd ./electrum/gui/kivy/ + +if [[ -n "$1" && "$1" == "release" ]] ; then + echo -n Keystore Password: + read -s password + export P4A_RELEASE_KEYSTORE=~/.keystore + export P4A_RELEASE_KEYSTORE_PASSWD=$password + export P4A_RELEASE_KEYALIAS_PASSWD=$password + export P4A_RELEASE_KEYALIAS=electrum + make release +else + make apk +fi + +popd diff --git a/contrib/make_download b/contrib/make_download new file mode 100755 index 000000000..097fbf06d --- /dev/null +++ b/contrib/make_download @@ -0,0 +1,57 @@ +#!/usr/bin/python3 +import re +import os +import sys + +from electrum.version import ELECTRUM_VERSION, APK_VERSION +print("version", ELECTRUM_VERSION) + +dirname = sys.argv[1] +print("directory", dirname) + +download_page = os.path.join(dirname, "panel-download.html") +download_template = download_page + ".template" + +with open(download_template) as f: + string = f.read() + +version = version_win = version_mac = version_android = ELECTRUM_VERSION +string = string.replace("##VERSION##", version) +string = string.replace("##VERSION_WIN##", version_win) +string = string.replace("##VERSION_MAC##", version_mac) +string = string.replace("##VERSION_ANDROID##", version_android) +string = string.replace("##VERSION_APK##", APK_VERSION) + +files = { + 'tgz': "Electrum-%s.tar.gz" % version, + 'zip': "Electrum-%s.zip" % version, + 'mac': "electrum-%s.dmg" % version_mac, + 'win': "electrum-%s.exe" % version_win, + 'win_setup': "electrum-%s-setup.exe" % version_win, + 'win_portable': "electrum-%s-portable.exe" % version_win, +} + +for k, n in files.items(): + path = "dist/%s"%n + link = "https://download.electrum.org/%s/%s"%(version,n) + if not os.path.exists(path): + os.system("wget -q %s -O %s" % (link, path)) + if not os.path.getsize(path): + os.unlink(path) + string = re.sub("
(.*?)
"%k, '', string, flags=re.DOTALL + re.MULTILINE) + continue + sigpath = path + '.asc' + siglink = link + '.asc' + if not os.path.exists(sigpath): + os.system("wget -q %s -O %s" % (siglink, sigpath)) + if not os.path.getsize(sigpath): + os.unlink(sigpath) + string = re.sub("
(.*?)
"%k, '', string, flags=re.DOTALL + re.MULTILINE) + continue + if os.system("gpg --verify %s"%sigpath) != 0: + raise Exception(sigpath) + string = string.replace("##link_%s##"%k, link) + + +with open(download_page,'w') as f: + f.write(string) diff --git a/contrib/make_locale b/contrib/make_locale new file mode 100755 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/contrib/make_packages b/contrib/make_packages new file mode 100755 index 000000000..9cfd32bb2 --- /dev/null +++ b/contrib/make_packages @@ -0,0 +1,13 @@ +#!/bin/bash + +contrib=$(dirname "$0") +test -n "$contrib" -a -d "$contrib" || exit + +whereis pip3 +if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi + +rm "$contrib"/../packages/ -r + +#Install pure python modules in electrum directory +pip3 install -r $contrib/deterministic-build/requirements.txt -t $contrib/../packages + diff --git a/contrib/make_tgz b/contrib/make_tgz new file mode 100755 index 000000000..9e53dd288 --- /dev/null +++ b/contrib/make_tgz @@ -0,0 +1 @@ +python3 setup.py sdist --format=zip,gztar diff --git a/contrib/requirements/requirements-binaries.txt b/contrib/requirements/requirements-binaries.txt new file mode 100644 index 000000000..9faf682e9 --- /dev/null +++ b/contrib/requirements/requirements-binaries.txt @@ -0,0 +1,2 @@ +PyQt5<5.11 +pycryptodomex diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt new file mode 100644 index 000000000..a6ae0a3a5 --- /dev/null +++ b/contrib/requirements/requirements-hw.txt @@ -0,0 +1,8 @@ +Cython>=0.27 +trezor[hidapi]>=0.9.0 +safet[hidapi]>=0.1.0 +keepkey +btchip-python +ckcc-protocol>=0.7.2 +websocket-client +hidapi diff --git a/contrib/requirements/requirements-travis.txt b/contrib/requirements/requirements-travis.txt new file mode 100644 index 000000000..b0aaeff51 --- /dev/null +++ b/contrib/requirements/requirements-travis.txt @@ -0,0 +1,3 @@ +tox +python-coveralls +tox-travis diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt new file mode 100644 index 000000000..99b859c30 --- /dev/null +++ b/contrib/requirements/requirements.txt @@ -0,0 +1,10 @@ +pyaes>=0.1a1 +ecdsa>=0.9 +requests +qrcode +protobuf +dnspython +jsonrpclib-pelix +PySocks>=1.6.6 +qdarkstyle<3.0 +typing>=3.0.0 diff --git a/contrib/sign_packages b/contrib/sign_packages new file mode 100755 index 000000000..d11ef5fc3 --- /dev/null +++ b/contrib/sign_packages @@ -0,0 +1,18 @@ +#!/usr/bin/python2 + +import os +import getpass + +if __name__ == '__main__': + + os.chdir("dist") + password = getpass.getpass("Password:") + for f in os.listdir('.'): + if f.endswith('asc'): + continue + os.system( "gpg --sign --armor --detach --passphrase \"%s\" %s"%(password, f) ) + + os.chdir("..") + + + diff --git a/contrib/upload b/contrib/upload new file mode 100755 index 000000000..29c7e0276 --- /dev/null +++ b/contrib/upload @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +version=`git describe --tags` +echo $version + +here=$(dirname "$0") +cd $here/../dist + +sftp -oBatchMode=no -b - thomasv@download.electrum.org << ! + cd electrum-downloads + mkdir $version + cd $version + mput * + bye +! \ No newline at end of file diff --git a/electrum-env b/electrum-env new file mode 100755 index 000000000..71dfd5958 --- /dev/null +++ b/electrum-env @@ -0,0 +1,27 @@ +#!/bin/bash +# +# This script creates a virtualenv named 'env' and installs all +# python dependencies before activating the env and running Electrum. +# If 'env' already exists, it is activated and Electrum is started +# without any installations. Additionally, the PYTHONPATH environment +# variable is set properly before running Electrum. +# +# python-qt and its dependencies will still need to be installed with +# your package manager. + +PYTHON_VER="$(python3 -c 'import sys; print(sys.version[:3])')" + +cd $(dirname $0) +if [ -e ./env/bin/activate ]; then + source ./env/bin/activate +else + virtualenv env -p `which python3` + source ./env/bin/activate + python3 setup.py install +fi + +export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH" + +./run_electrum "$@" + +deactivate diff --git a/electrum.conf.sample b/electrum.conf.sample new file mode 100644 index 000000000..72a50b238 --- /dev/null +++ b/electrum.conf.sample @@ -0,0 +1,16 @@ +# Configuration file for the Electrum client +# Settings defined here are shared across wallets +# +# copy this file to /etc/electrum.conf if you want read-only settings + +[client] +server = electrum.novit.ro:50001:t +proxy = None +gap_limit = 5 +# booleans use python syntax +use_change = True +gui = qt +num_zeros = 2 +# default transaction fee is in Satoshis +fee = 10000 +winpos-qt = [799, 226, 877, 435] diff --git a/electrum.desktop b/electrum.desktop new file mode 100644 index 000000000..2eba0b6f7 --- /dev/null +++ b/electrum.desktop @@ -0,0 +1,21 @@ +# If you want Electrum to appear in a Linux app launcher ("start menu"), install this by doing: +# sudo desktop-file-install electrum.desktop + +[Desktop Entry] +Comment=Lightweight Bitcore Client +Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\" electrum %u" +GenericName[en_US]=Bitcore Wallet +GenericName=Bitcore Wallet +Icon=electrumBTX +Name[en_US]=Electrum Bitcore Wallet +Name=Electrum Bitcore Wallet +Categories=Finance;Network; +StartupNotify=false +Terminal=false +Type=Application +MimeType=x-scheme-handler/bitcore; +Actions=Testnet; + +[Desktop Action Testnet] +Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\" electrum-btx --testnet %u" +Name=Testnet mode 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/__init__.py b/electrum/__init__.py new file mode 100644 index 000000000..48a60c15c --- /dev/null +++ b/electrum/__init__.py @@ -0,0 +1,14 @@ +from .version import ELECTRUM_VERSION +from .util import format_satoshis, print_msg, print_error, set_verbosity +from .wallet import Wallet +from .storage import WalletStorage +from .coinchooser import COIN_CHOOSERS +from .network import Network, pick_random_server +from .interface import Connection, Interface +from .simple_config import SimpleConfig, get_config, set_config +from . import bitcoin +from . import transaction +from . import daemon +from .transaction import Transaction +from .plugin import BasePlugin +from .commands import Commands, known_commands diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py new file mode 100644 index 000000000..4aa6f2880 --- /dev/null +++ b/electrum/address_synchronizer.py @@ -0,0 +1,794 @@ +# Electrum - lightweight Bitcoin client +# Copyright (C) 2018 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import threading +import itertools +from collections import defaultdict + +from . import bitcoin +from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY +from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus +from .transaction import Transaction, TxOutput +from .synchronizer import Synchronizer +from .verifier import SPV +from .blockchain import hash_header +from .i18n import _ + +TX_HEIGHT_LOCAL = -2 +TX_HEIGHT_UNCONF_PARENT = -1 +TX_HEIGHT_UNCONFIRMED = 0 + +class AddTransactionException(Exception): + pass + + +class UnrelatedTransactionException(AddTransactionException): + def __str__(self): + return _("Transaction is unrelated to this wallet.") + + +class AddressSynchronizer(PrintError): + """ + inherited by wallet + """ + + def __init__(self, storage): + self.storage = storage + self.network = None + # verifier (SPV) and synchronizer are started in start_threads + self.synchronizer = None + self.verifier = None + # locks: if you need to take multiple ones, acquire them in the order they are defined here! + self.lock = threading.RLock() + self.transaction_lock = threading.RLock() + # address -> list(txid, height) + self.history = storage.get('addr_history',{}) + # Verified transactions. txid -> VerifiedTxInfo. Access with self.lock. + verified_tx = storage.get('verified_tx3', {}) + self.verified_tx = {} + for txid, (height, timestamp, txpos, header_hash) in verified_tx.items(): + self.verified_tx[txid] = VerifiedTxInfo(height, timestamp, txpos, header_hash) + # Transactions pending verification. txid -> tx_height. Access with self.lock. + self.unverified_tx = defaultdict(int) + # true when synchronized + self.up_to_date = False + # thread local storage for caching stuff + self.threadlocal_cache = threading.local() + + self.load_and_cleanup() + + def load_and_cleanup(self): + self.load_transactions() + self.load_local_history() + self.check_history() + self.load_unverified_transactions() + self.remove_local_transactions_we_dont_have() + + def is_mine(self, address): + return address in self.history + + def get_addresses(self): + return sorted(self.history.keys()) + + def get_address_history(self, addr): + h = [] + # we need self.transaction_lock but get_tx_height will take self.lock + # so we need to take that too here, to enforce order of locks + with self.lock, self.transaction_lock: + related_txns = self._history_local.get(addr, set()) + for tx_hash in related_txns: + tx_height = self.get_tx_height(tx_hash).height + h.append((tx_hash, tx_height)) + return h + + def get_address_history_len(self, addr: str) -> int: + """Return number of transactions where address is involved.""" + return len(self._history_local.get(addr, ())) + + def get_txin_address(self, txi): + addr = txi.get('address') + if addr and addr != "(pubkey)": + return addr + prevout_hash = txi.get('prevout_hash') + prevout_n = txi.get('prevout_n') + dd = self.txo.get(prevout_hash, {}) + for addr, l in dd.items(): + for n, v, is_cb in l: + if n == prevout_n: + return addr + return None + + def get_txout_address(self, txo: TxOutput): + if txo.type == TYPE_ADDRESS: + addr = txo.address + elif txo.type == TYPE_PUBKEY: + addr = bitcoin.public_key_to_p2pkh(bfh(txo.address)) + else: + addr = None + return addr + + def load_unverified_transactions(self): + # review transactions that are in the history + for addr, hist in self.history.items(): + for tx_hash, tx_height in hist: + # add it in case it was previously unconfirmed + self.add_unverified_tx(tx_hash, tx_height) + + def start_threads(self, network): + self.network = network + if self.network is not None: + self.verifier = SPV(self.network, self) + self.synchronizer = Synchronizer(self, network) + network.add_jobs([self.verifier, self.synchronizer]) + else: + self.verifier = None + self.synchronizer = None + + def stop_threads(self): + if self.network: + self.network.remove_jobs([self.synchronizer, self.verifier]) + self.synchronizer.release() + self.synchronizer = None + self.verifier = None + # Now no references to the synchronizer or verifier + # remain so they will be GC-ed + self.storage.put('stored_height', self.get_local_height()) + self.save_transactions() + self.save_verified_tx() + self.storage.write() + + def add_address(self, address): + if address not in self.history: + self.history[address] = [] + self.set_up_to_date(False) + if self.synchronizer: + self.synchronizer.add(address) + + def get_conflicting_transactions(self, tx): + """Returns a set of transaction hashes from the wallet history that are + directly conflicting with tx, i.e. they have common outpoints being + spent with tx. If the tx is already in wallet history, that will not be + reported as a conflict. + """ + conflicting_txns = set() + with self.transaction_lock: + for txin in tx.inputs(): + if txin['type'] == 'coinbase': + continue + prevout_hash = txin['prevout_hash'] + prevout_n = txin['prevout_n'] + spending_tx_hash = self.spent_outpoints[prevout_hash].get(prevout_n) + if spending_tx_hash is None: + continue + # this outpoint has already been spent, by spending_tx + assert spending_tx_hash in self.transactions + conflicting_txns |= {spending_tx_hash} + txid = tx.txid() + if txid in conflicting_txns: + # this tx is already in history, so it conflicts with itself + if len(conflicting_txns) > 1: + raise Exception('Found conflicting transactions already in wallet history.') + conflicting_txns -= {txid} + return conflicting_txns + + def add_transaction(self, tx_hash, tx, allow_unrelated=False): + assert tx_hash, tx_hash + assert tx, tx + assert tx.is_complete() + # we need self.transaction_lock but get_tx_height will take self.lock + # so we need to take that too here, to enforce order of locks + with self.lock, self.transaction_lock: + # NOTE: returning if tx in self.transactions might seem like a good idea + # BUT we track is_mine inputs in a txn, and during subsequent calls + # of add_transaction tx, we might learn of more-and-more inputs of + # being is_mine, as we roll the gap_limit forward + is_coinbase = tx.inputs()[0]['type'] == 'coinbase' + tx_height = self.get_tx_height(tx_hash).height + if not allow_unrelated: + # note that during sync, if the transactions are not properly sorted, + # it could happen that we think tx is unrelated but actually one of the inputs is is_mine. + # this is the main motivation for allow_unrelated + is_mine = any([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()]) + is_for_me = any([self.is_mine(self.get_txout_address(txo)) for txo in tx.outputs()]) + if not is_mine and not is_for_me: + raise UnrelatedTransactionException() + # Find all conflicting transactions. + # In case of a conflict, + # 1. confirmed > mempool > local + # 2. this new txn has priority over existing ones + # When this method exits, there must NOT be any conflict, so + # either keep this txn and remove all conflicting (along with dependencies) + # or drop this txn + conflicting_txns = self.get_conflicting_transactions(tx) + if conflicting_txns: + existing_mempool_txn = any( + self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) + for tx_hash2 in conflicting_txns) + existing_confirmed_txn = any( + self.get_tx_height(tx_hash2).height > 0 + for tx_hash2 in conflicting_txns) + if existing_confirmed_txn and tx_height <= 0: + # this is a non-confirmed tx that conflicts with confirmed txns; drop. + return False + if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL: + # this is a local tx that conflicts with non-local txns; drop. + return False + # keep this txn and remove all conflicting + to_remove = set() + to_remove |= conflicting_txns + for conflicting_tx_hash in conflicting_txns: + to_remove |= self.get_depending_transactions(conflicting_tx_hash) + for tx_hash2 in to_remove: + self.remove_transaction(tx_hash2) + # add inputs + def add_value_from_prev_output(): + dd = self.txo.get(prevout_hash, {}) + # note: this nested loop takes linear time in num is_mine outputs of prev_tx + for addr, outputs in dd.items(): + # note: instead of [(n, v, is_cb), ...]; we could store: {n -> (v, is_cb)} + for n, v, is_cb in outputs: + if n == prevout_n: + if addr and self.is_mine(addr): + if d.get(addr) is None: + d[addr] = set() + d[addr].add((ser, v)) + return + self.txi[tx_hash] = d = {} + for txi in tx.inputs(): + if txi['type'] == 'coinbase': + continue + prevout_hash = txi['prevout_hash'] + prevout_n = txi['prevout_n'] + ser = prevout_hash + ':%d' % prevout_n + self.spent_outpoints[prevout_hash][prevout_n] = tx_hash + add_value_from_prev_output() + # add outputs + self.txo[tx_hash] = d = {} + for n, txo in enumerate(tx.outputs()): + v = txo[2] + ser = tx_hash + ':%d'%n + addr = self.get_txout_address(txo) + if addr and self.is_mine(addr): + if d.get(addr) is None: + d[addr] = [] + d[addr].append((n, v, is_coinbase)) + # give v to txi that spends me + next_tx = self.spent_outpoints[tx_hash].get(n) + if next_tx is not None: + dd = self.txi.get(next_tx, {}) + if dd.get(addr) is None: + dd[addr] = set() + if (ser, v) not in dd[addr]: + dd[addr].add((ser, v)) + self._add_tx_to_local_history(next_tx) + # add to local history + self._add_tx_to_local_history(tx_hash) + # save + self.transactions[tx_hash] = tx + return True + + def remove_transaction(self, tx_hash): + def remove_from_spent_outpoints(): + # undo spends in spent_outpoints + if tx is not None: # if we have the tx, this branch is faster + for txin in tx.inputs(): + if txin['type'] == 'coinbase': + continue + prevout_hash = txin['prevout_hash'] + prevout_n = txin['prevout_n'] + self.spent_outpoints[prevout_hash].pop(prevout_n, None) + if not self.spent_outpoints[prevout_hash]: + self.spent_outpoints.pop(prevout_hash) + else: # expensive but always works + for prevout_hash, d in list(self.spent_outpoints.items()): + for prevout_n, spending_txid in d.items(): + if spending_txid == tx_hash: + self.spent_outpoints[prevout_hash].pop(prevout_n, None) + if not self.spent_outpoints[prevout_hash]: + self.spent_outpoints.pop(prevout_hash) + # Remove this tx itself; if nothing spends from it. + # It is not so clear what to do if other txns spend from it, but it will be + # removed when those other txns are removed. + if not self.spent_outpoints[tx_hash]: + self.spent_outpoints.pop(tx_hash) + + with self.transaction_lock: + self.print_error("removing tx from history", tx_hash) + tx = self.transactions.pop(tx_hash, None) + remove_from_spent_outpoints() + self._remove_tx_from_local_history(tx_hash) + self.txi.pop(tx_hash, None) + self.txo.pop(tx_hash, None) + + def get_depending_transactions(self, tx_hash): + """Returns all (grand-)children of tx_hash in this wallet.""" + children = set() + for other_hash in self.spent_outpoints[tx_hash].values(): + children.add(other_hash) + children |= self.get_depending_transactions(other_hash) + return children + + def receive_tx_callback(self, tx_hash, tx, tx_height): + self.add_unverified_tx(tx_hash, tx_height) + self.add_transaction(tx_hash, tx, allow_unrelated=True) + + def receive_history_callback(self, addr, hist, tx_fees): + with self.lock: + old_hist = self.get_address_history(addr) + for tx_hash, height in old_hist: + if (tx_hash, height) not in hist: + # make tx local + self.unverified_tx.pop(tx_hash, None) + self.verified_tx.pop(tx_hash, None) + if self.verifier: + self.verifier.remove_spv_proof_for_tx(tx_hash) + self.history[addr] = hist + + for tx_hash, tx_height in hist: + # add it in case it was previously unconfirmed + self.add_unverified_tx(tx_hash, tx_height) + # if addr is new, we have to recompute txi and txo + tx = self.transactions.get(tx_hash) + if tx is None: + continue + self.add_transaction(tx_hash, tx, allow_unrelated=True) + + # Store fees + self.tx_fees.update(tx_fees) + + @profiler + def load_transactions(self): + # load txi, txo, tx_fees + self.txi = self.storage.get('txi', {}) + for txid, d in list(self.txi.items()): + for addr, lst in d.items(): + self.txi[txid][addr] = set([tuple(x) for x in lst]) + self.txo = self.storage.get('txo', {}) + self.tx_fees = self.storage.get('tx_fees', {}) + tx_list = self.storage.get('transactions', {}) + # load transactions + self.transactions = {} + for tx_hash, raw in tx_list.items(): + tx = Transaction(raw) + self.transactions[tx_hash] = tx + if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None: + self.print_error("removing unreferenced tx", tx_hash) + self.transactions.pop(tx_hash) + # load spent_outpoints + _spent_outpoints = self.storage.get('spent_outpoints', {}) + self.spent_outpoints = defaultdict(dict) + for prevout_hash, d in _spent_outpoints.items(): + for prevout_n_str, spending_txid in d.items(): + prevout_n = int(prevout_n_str) + self.spent_outpoints[prevout_hash][prevout_n] = spending_txid + + @profiler + def load_local_history(self): + self._history_local = {} # address -> set(txid) + for txid in itertools.chain(self.txi, self.txo): + self._add_tx_to_local_history(txid) + + @profiler + def check_history(self): + save = False + hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.history.keys())) + hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.history.keys())) + for addr in hist_addrs_not_mine: + self.history.pop(addr) + save = True + for addr in hist_addrs_mine: + hist = self.history[addr] + for tx_hash, tx_height in hist: + if self.txi.get(tx_hash) or self.txo.get(tx_hash): + continue + tx = self.transactions.get(tx_hash) + if tx is not None: + self.add_transaction(tx_hash, tx, allow_unrelated=True) + save = True + if save: + self.save_transactions() + + def remove_local_transactions_we_dont_have(self): + txid_set = set(self.txi) | set(self.txo) + for txid in txid_set: + tx_height = self.get_tx_height(txid).height + if tx_height == TX_HEIGHT_LOCAL and txid not in self.transactions: + self.remove_transaction(txid) + + @profiler + def save_transactions(self, write=False): + with self.transaction_lock: + tx = {} + for k,v in self.transactions.items(): + tx[k] = str(v) + self.storage.put('transactions', tx) + self.storage.put('txi', self.txi) + self.storage.put('txo', self.txo) + self.storage.put('tx_fees', self.tx_fees) + self.storage.put('addr_history', self.history) + self.storage.put('spent_outpoints', self.spent_outpoints) + if write: + self.storage.write() + + def save_verified_tx(self, write=False): + with self.lock: + self.storage.put('verified_tx3', self.verified_tx) + if write: + self.storage.write() + + def clear_history(self): + with self.lock: + with self.transaction_lock: + self.txi = {} + self.txo = {} + self.tx_fees = {} + self.spent_outpoints = defaultdict(dict) + self.history = {} + self.verified_tx = {} + self.transactions = {} + self.save_transactions() + + def get_txpos(self, tx_hash): + """Returns (height, txpos) tuple, even if the tx is unverified.""" + with self.lock: + if tx_hash in self.verified_tx: + info = self.verified_tx[tx_hash] + return info.height, info.txpos + elif tx_hash in self.unverified_tx: + height = self.unverified_tx[tx_hash] + return (height, 0) if height > 0 else ((1e9 - height), 0) + else: + return (1e9+1, 0) + + def with_local_height_cached(func): + # get local height only once, as it's relatively expensive. + # take care that nested calls work as expected + def f(self, *args, **kwargs): + orig_val = getattr(self.threadlocal_cache, 'local_height', None) + self.threadlocal_cache.local_height = orig_val or self.get_local_height() + try: + return func(self, *args, **kwargs) + finally: + self.threadlocal_cache.local_height = orig_val + return f + + @with_local_height_cached + def get_history(self, domain=None): + # get domain + if domain is None: + domain = self.history.keys() + domain = set(domain) + # 1. Get the history of each address in the domain, maintain the + # delta of a tx as the sum of its deltas on domain addresses + tx_deltas = defaultdict(int) + for addr in domain: + h = self.get_address_history(addr) + for tx_hash, height in h: + delta = self.get_tx_delta(tx_hash, addr) + if delta is None or tx_deltas[tx_hash] is None: + tx_deltas[tx_hash] = None + else: + tx_deltas[tx_hash] += delta + # 2. create sorted history + history = [] + for tx_hash in tx_deltas: + delta = tx_deltas[tx_hash] + tx_mined_status = self.get_tx_height(tx_hash) + history.append((tx_hash, tx_mined_status, delta)) + history.sort(key = lambda x: self.get_txpos(x[0])) + history.reverse() + # 3. add balance + c, u, x = self.get_balance(domain) + balance = c + u + x + h2 = [] + for tx_hash, tx_mined_status, delta in history: + h2.append((tx_hash, tx_mined_status, delta, balance)) + if balance is None or delta is None: + balance = None + else: + balance -= delta + h2.reverse() + # fixme: this may happen if history is incomplete + if balance not in [None, 0]: + self.print_error("Error: history not synchronized") + return [] + + return h2 + + def _add_tx_to_local_history(self, txid): + with self.transaction_lock: + for addr in itertools.chain(self.txi.get(txid, []), self.txo.get(txid, [])): + cur_hist = self._history_local.get(addr, set()) + cur_hist.add(txid) + self._history_local[addr] = cur_hist + + def _remove_tx_from_local_history(self, txid): + with self.transaction_lock: + for addr in itertools.chain(self.txi.get(txid, []), self.txo.get(txid, [])): + cur_hist = self._history_local.get(addr, set()) + try: + cur_hist.remove(txid) + except KeyError: + pass + else: + self._history_local[addr] = cur_hist + + def add_unverified_tx(self, tx_hash, tx_height): + if tx_hash in self.verified_tx: + if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT): + with self.lock: + self.verified_tx.pop(tx_hash) + if self.verifier: + self.verifier.remove_spv_proof_for_tx(tx_hash) + else: + with self.lock: + # tx will be verified only if height > 0 + self.unverified_tx[tx_hash] = tx_height + # to remove pending proof requests: + if self.verifier: + self.verifier.remove_spv_proof_for_tx(tx_hash) + + def add_verified_tx(self, tx_hash: str, info: VerifiedTxInfo): + # Remove from the unverified map and add to the verified map + with self.lock: + self.unverified_tx.pop(tx_hash, None) + self.verified_tx[tx_hash] = info + tx_mined_status = self.get_tx_height(tx_hash) + self.network.trigger_callback('verified', tx_hash, tx_mined_status) + + def get_unverified_txs(self): + '''Returns a map from tx hash to transaction height''' + with self.lock: + return dict(self.unverified_tx) # copy + + def undo_verifications(self, blockchain, height): + '''Used by the verifier when a reorg has happened''' + txs = set() + with self.lock: + for tx_hash, info in list(self.verified_tx.items()): + tx_height = info.height + if tx_height >= height: + header = blockchain.read_header(tx_height) + if not header or hash_header(header) != info.header_hash: + self.verified_tx.pop(tx_hash, None) + # NOTE: we should add these txns to self.unverified_tx, + # but with what height? + # If on the new fork after the reorg, the txn is at the + # same height, we will not get a status update for the + # address. If the txn is not mined or at a diff height, + # we should get a status update. Unless we put tx into + # unverified_tx, it will turn into local. So we put it + # into unverified_tx with the old height, and if we get + # a status update, that will overwrite it. + self.unverified_tx[tx_hash] = tx_height + txs.add(tx_hash) + return txs + + def get_local_height(self): + """ return last known height if we are offline """ + cached_local_height = getattr(self.threadlocal_cache, 'local_height', None) + if cached_local_height is not None: + return cached_local_height + return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) + + def get_tx_height(self, tx_hash: str) -> TxMinedStatus: + """ Given a transaction, returns (height, conf, timestamp, header_hash) """ + with self.lock: + if tx_hash in self.verified_tx: + info = self.verified_tx[tx_hash] + conf = max(self.get_local_height() - info.height + 1, 0) + return TxMinedStatus(info.height, conf, info.timestamp, info.header_hash) + elif tx_hash in self.unverified_tx: + height = self.unverified_tx[tx_hash] + return TxMinedStatus(height, 0, None, None) + else: + # local transaction + return TxMinedStatus(TX_HEIGHT_LOCAL, 0, None, None) + + def set_up_to_date(self, up_to_date): + with self.lock: + self.up_to_date = up_to_date + if up_to_date: + self.save_transactions(write=True) + # if the verifier is also up to date, persist that too; + # otherwise it will persist its results when it finishes + if self.verifier and self.verifier.is_up_to_date(): + self.save_verified_tx(write=True) + + def is_up_to_date(self): + with self.lock: return self.up_to_date + + def get_tx_delta(self, tx_hash, address): + "effect of tx on address" + delta = 0 + # substract the value of coins sent from address + d = self.txi.get(tx_hash, {}).get(address, []) + for n, v in d: + delta -= v + # add the value of the coins received at address + d = self.txo.get(tx_hash, {}).get(address, []) + for n, v, cb in d: + delta += v + return delta + + def get_tx_value(self, txid): + " effect of tx on the entire domain" + delta = 0 + for addr, d in self.txi.get(txid, {}).items(): + for n, v in d: + delta -= v + for addr, d in self.txo.get(txid, {}).items(): + for n, v, cb in d: + delta += v + return delta + + def get_wallet_delta(self, tx): + """ effect of tx on wallet """ + is_relevant = False # "related to wallet?" + is_mine = False + is_pruned = False + is_partial = False + v_in = v_out = v_out_mine = 0 + for txin in tx.inputs(): + addr = self.get_txin_address(txin) + if self.is_mine(addr): + is_mine = True + is_relevant = True + d = self.txo.get(txin['prevout_hash'], {}).get(addr, []) + for n, v, cb in d: + if n == txin['prevout_n']: + value = v + break + else: + value = None + if value is None: + is_pruned = True + else: + v_in += value + else: + is_partial = True + if not is_mine: + is_partial = False + for addr, value in tx.get_outputs(): + v_out += value + if self.is_mine(addr): + v_out_mine += value + is_relevant = True + if is_pruned: + # some inputs are mine: + fee = None + if is_mine: + v = v_out_mine - v_out + else: + # no input is mine + v = v_out_mine + else: + v = v_out_mine - v_in + if is_partial: + # some inputs are mine, but not all + fee = None + else: + # all inputs are mine + fee = v_in - v_out + if not is_mine: + fee = None + return is_relevant, is_mine, v, fee + + def get_addr_io(self, address): + h = self.get_address_history(address) + received = {} + sent = {} + for tx_hash, height in h: + l = self.txo.get(tx_hash, {}).get(address, []) + for n, v, is_cb in l: + received[tx_hash + ':%d'%n] = (height, v, is_cb) + for tx_hash, height in h: + l = self.txi.get(tx_hash, {}).get(address, []) + for txi, v in l: + sent[txi] = height + return received, sent + + def get_addr_utxo(self, address): + coins, spent = self.get_addr_io(address) + for txi in spent: + coins.pop(txi) + out = {} + for txo, v in coins.items(): + tx_height, value, is_cb = v + prevout_hash, prevout_n = txo.split(':') + x = { + 'address':address, + 'value':value, + 'prevout_n':int(prevout_n), + 'prevout_hash':prevout_hash, + 'height':tx_height, + 'coinbase':is_cb + } + out[txo] = x + return out + + # return the total amount ever received by an address + def get_addr_received(self, address): + received, sent = self.get_addr_io(address) + return sum([v for height, v, is_cb in received.values()]) + + @with_local_height_cached + def get_addr_balance(self, address): + """Return the balance of a bitcore address: + confirmed and matured, unconfirmed, unmatured + """ + received, sent = self.get_addr_io(address) + c = u = x = 0 + local_height = self.get_local_height() + for txo, (tx_height, v, is_cb) in received.items(): + if is_cb and tx_height + COINBASE_MATURITY > local_height: + x += v + elif tx_height > 0: + c += v + else: + u += v + if txo in sent: + if sent[txo] > 0: + c -= v + else: + u -= v + return c, u, x + + @with_local_height_cached + def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False): + coins = [] + if domain is None: + domain = self.get_addresses() + domain = set(domain) + if excluded: + domain = set(domain) - excluded + for addr in domain: + utxos = self.get_addr_utxo(addr) + for x in utxos.values(): + if confirmed_only and x['height'] <= 0: + continue + if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height(): + continue + coins.append(x) + continue + return coins + + def get_balance(self, domain=None): + if domain is None: + domain = self.get_addresses() + domain = set(domain) + cc = uu = xx = 0 + for addr in domain: + c, u, x = self.get_addr_balance(addr) + cc += c + uu += u + xx += x + return cc, uu, xx + + def is_used(self, address): + h = self.history.get(address,[]) + return len(h) != 0 + + def is_empty(self, address): + c, u, x = self.get_addr_balance(address) + return c+u+x == 0 diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py new file mode 100644 index 000000000..a5702e2e6 --- /dev/null +++ b/electrum/base_crash_reporter.py @@ -0,0 +1,128 @@ +# Electrum - lightweight Bitcoin client +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import json +import locale +import traceback +import subprocess +import sys +import os + +import requests + +from .version import ELECTRUM_VERSION +from .import constants +from .i18n import _ + + +class BaseCrashReporter(object): + report_server = "https://crashhub.electrum.org" + config_key = "show_crash_reporter" + issue_template = """

Traceback

+
+{traceback}
+
+ +

Additional information

+
    +
  • Electrum version: {app_version}
  • +
  • Python version: {python_version}
  • +
  • Operating system: {os}
  • +
  • Wallet type: {wallet_type}
  • +
  • Locale: {locale}
  • +
+ """ + CRASH_MESSAGE = _('Something went wrong while executing Electrum.') + CRASH_TITLE = _('Sorry!') + REQUEST_HELP_MESSAGE = _('To help us diagnose and fix the problem, you can send us a bug report that contains ' + 'useful debug information:') + DESCRIBE_ERROR_MESSAGE = _("Please briefly describe what led to the error (optional):") + ASK_CONFIRM_SEND = _("Do you want to send this report?") + + def __init__(self, exctype, value, tb): + self.exc_args = (exctype, value, tb) + + def send_report(self, endpoint="/crash"): + if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server: + # Gah! Some kind of altcoin wants to send us crash reports. + raise Exception(_("Missing report URL.")) + report = self.get_traceback_info() + report.update(self.get_additional_info()) + report = json.dumps(report) + response = requests.post(BaseCrashReporter.report_server + endpoint, data=report) + return response + + def get_traceback_info(self): + exc_string = str(self.exc_args[1]) + stack = traceback.extract_tb(self.exc_args[2]) + readable_trace = "".join(traceback.format_list(stack)) + id = { + "file": stack[-1].filename, + "name": stack[-1].name, + "type": self.exc_args[0].__name__ + } + return { + "exc_string": exc_string, + "stack": readable_trace, + "id": id + } + + def get_additional_info(self): + args = { + "app_version": ELECTRUM_VERSION, + "python_version": sys.version, + "os": self.get_os_version(), + "wallet_type": "unknown", + "locale": locale.getdefaultlocale()[0] or "?", + "description": self.get_user_description() + } + try: + args["wallet_type"] = self.get_wallet_type() + except: + # Maybe the wallet isn't loaded yet + pass + try: + args["app_version"] = self.get_git_version() + except: + # This is probably not running from source + pass + return args + + @staticmethod + def get_git_version(): + dir = os.path.dirname(os.path.realpath(sys.argv[0])) + version = subprocess.check_output( + ['git', 'describe', '--always', '--dirty'], cwd=dir) + return str(version, "utf8").strip() + + def get_report_string(self): + info = self.get_additional_info() + info["traceback"] = "".join(traceback.format_exception(*self.exc_args)) + return self.issue_template.format(**info) + + def get_user_description(self): + raise NotImplementedError + + def get_wallet_type(self): + raise NotImplementedError + + def get_os_version(self): + raise NotImplementedError diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py new file mode 100644 index 000000000..50f5243d8 --- /dev/null +++ b/electrum/base_wizard.py @@ -0,0 +1,572 @@ +# -*- coding: utf-8 -*- +# +# Electrum - lightweight Bitcore client +# Copyright (C) 2016 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 os +import sys +import traceback +from functools import partial + +from . import bitcoin +from . import keystore +from .keystore import bip44_derivation, purpose48_derivation +from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet +from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption +from .i18n import _ +from .util import UserCancelled, InvalidPassword, WalletFileException + +# hardware device setup purpose +HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) + + +class ScriptTypeNotSupported(Exception): pass + + +class GoBack(Exception): pass + + +class BaseWizard(object): + + def __init__(self, config, plugins, storage): + super(BaseWizard, self).__init__() + self.config = config + self.plugins = plugins + self.storage = storage + self.wallet = None + self.stack = [] + self.plugin = None + self.keystores = [] + self.is_kivy = config.get('gui') == 'kivy' + self.seed_type = None + + def set_icon(self, icon): + pass + + def run(self, *args): + action = args[0] + args = args[1:] + self.stack.append((action, args)) + if not action: + return + if type(action) is tuple: + self.plugin, action = action + if self.plugin and hasattr(self.plugin, action): + f = getattr(self.plugin, action) + f(self, *args) + elif hasattr(self, action): + f = getattr(self, action) + f(*args) + else: + raise Exception("unknown action", action) + + def can_go_back(self): + return len(self.stack)>1 + + def go_back(self): + if not self.can_go_back(): + return + self.stack.pop() + action, args = self.stack.pop() + self.run(action, *args) + + def new(self): + name = os.path.basename(self.storage.path) + title = _("Create") + ' ' + name + message = '\n'.join([ + _("What kind of wallet do you want to create?") + ]) + wallet_kinds = [ + ('standard', _("Standard wallet")), + ('2fa', _("Wallet with two-factor authentication")), + ('multisig', _("Multi-signature wallet")), + ('imported', _("Import Bitcore 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) + + def upgrade_storage(self): + exc = None + def on_finished(): + if exc is None: + self.wallet = Wallet(self.storage) + self.terminate() + else: + raise exc + def do_upgrade(): + nonlocal exc + try: + self.storage.upgrade() + except Exception as e: + exc = e + self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished) + + def load_2fa(self): + self.storage.put('wallet_type', '2fa') + self.storage.put('use_trustedcoin', True) + self.plugin = self.plugins.load_plugin('trustedcoin') + + def on_wallet_type(self, choice): + self.wallet_type = choice + if choice == 'standard': + action = 'choose_keystore' + elif choice == 'multisig': + action = 'choose_multisig' + elif choice == '2fa': + self.load_2fa() + action = self.storage.get_action() + elif choice == 'imported': + action = 'import_addresses_or_keys' + self.run(action) + + def choose_multisig(self): + def on_multisig(m, n): + self.multisig_type = "%dof%d"%(m, n) + self.storage.put('wallet_type', self.multisig_type) + self.n = n + self.run('choose_keystore') + self.multisig_dialog(run_next=on_multisig) + + def choose_keystore(self): + assert self.wallet_type in ['standard', 'multisig'] + i = len(self.keystores) + title = _('Add cosigner') + ' (%d of %d)'%(i+1, self.n) if self.wallet_type=='multisig' else _('Keystore') + if self.wallet_type =='standard' or i==0: + message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') + choices = [ + ('choose_seed_type', _('Create a new seed')), + ('restore_from_seed', _('I already have a seed')), + ('restore_from_key', _('Use a master key')), + ] + if not self.is_kivy: + choices.append(('choose_hw_device', _('Use a hardware device'))) + else: + message = _('Add a cosigner to your multi-sig wallet') + choices = [ + ('restore_from_key', _('Enter cosigner key')), + ('restore_from_seed', _('Enter cosigner seed')), + ] + if not self.is_kivy: + choices.append(('choose_hw_device', _('Cosign with hardware device'))) + + self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) + + def import_addresses_or_keys(self): + v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x) + title = _("Import Bitcore Addresses") + message = _("Enter a list of Bitcore 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) + + def on_import(self, text): + # create a temporary wallet and exploit that modifications + # will be reflected on self.storage + if keystore.is_address_list(text): + w = Imported_Wallet(self.storage) + for x in text.split(): + w.import_address(x) + elif keystore.is_private_key_list(text): + k = keystore.Imported_KeyStore({}) + self.storage.put('keystore', k.dump()) + w = Imported_Wallet(self.storage) + for x in keystore.get_private_keys(text): + w.import_private_key(x, None) + self.keystores.append(w.keystore) + else: + return self.terminate() + return self.run('create_wallet') + + def restore_from_key(self): + if self.wallet_type == 'standard': + v = keystore.is_master_key + title = _("Create keystore from a master key") + message = ' '.join([ + _("To create a watching-only wallet, please enter your master public key (xpub/ypub/zpub)."), + _("To create a spending wallet, please enter a master private key (xprv/yprv/zprv).") + ]) + self.add_xpub_dialog(title=title, message=message, run_next=self.on_restore_from_key, is_valid=v) + else: + i = len(self.keystores) + 1 + self.add_cosigner_dialog(index=i, run_next=self.on_restore_from_key, is_valid=keystore.is_bip32_key) + + def on_restore_from_key(self, text): + k = keystore.from_master_key(text) + self.on_keystore(k) + + def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET): + title = _('Hardware Keystore') + # check available plugins + support = self.plugins.get_hardware_support() + if not support: + msg = '\n'.join([ + _('No hardware wallet support found on your system.'), + _('Please install the relevant libraries (eg python-trezor for Trezor).'), + ]) + self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose)) + return + # scan devices + devices = [] + devmgr = self.plugins.device_manager + try: + scanned_devices = devmgr.scan_devices() + except BaseException as e: + devmgr.print_error('error scanning devices: {}'.format(e)) + debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e) + else: + debug_msg = '' + for name, description, plugin in support: + try: + # FIXME: side-effect: unpaired_device_info sets client.handler + u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices) + except BaseException as e: + devmgr.print_error('error getting device infos for {}: {}'.format(name, e)) + indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) + debug_msg += ' {}:\n{}\n'.format(plugin.name, indented_error_msg) + continue + devices += list(map(lambda x: (name, x), u)) + if not debug_msg: + debug_msg = ' {}'.format(_('No exceptions encountered.')) + if not devices: + msg = ''.join([ + _('No hardware device detected.') + '\n', + _('To trigger a rescan, press \'Next\'.') + '\n\n', + _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ', + _('On Linux, you might have to add a new permission to your udev rules.') + '\n\n', + _('Debug message') + '\n', + debug_msg + ]) + self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose)) + return + # select device + self.devices = devices + choices = [] + for name, info in devices: + state = _("initialized") if info.initialized else _("wiped") + label = info.label or _("An unnamed {}").format(name) + descr = "%s [%s, %s]" % (label, name, state) + choices.append(((name, info), descr)) + msg = _('Select a device') + ':' + self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose)) + + def on_device(self, name, device_info, *, purpose): + self.plugin = self.plugins.get_plugin(name) + try: + self.plugin.setup_device(device_info, self, purpose) + except OSError as e: + self.show_error(_('We encountered an error while connecting to your device:') + + '\n' + str(e) + '\n' + + _('To try to fix this, we will now re-pair with your device.') + '\n' + + _('Please try again.')) + devmgr = self.plugins.device_manager + devmgr.unpair_id(device_info.device.id_) + self.choose_hw_device(purpose) + return + except (UserCancelled, GoBack): + self.choose_hw_device(purpose) + return + except BaseException as e: + traceback.print_exc(file=sys.stderr) + self.show_error(str(e)) + self.choose_hw_device(purpose) + return + if purpose == HWD_SETUP_NEW_WALLET: + def f(derivation, script_type): + self.run('on_hw_derivation', name, device_info, derivation, script_type) + self.derivation_and_script_type_dialog(f) + elif purpose == HWD_SETUP_DECRYPT_WALLET: + derivation = get_derivation_used_for_hw_device_encryption() + xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self) + password = keystore.Xpub.get_pubkey_from_xpub(xpub, ()) + try: + self.storage.decrypt(password) + except InvalidPassword: + # try to clear session so that user can type another passphrase + devmgr = self.plugins.device_manager + client = devmgr.client_by_id(device_info.device.id_) + if hasattr(client, 'clear_session'): # FIXME not all hw wallet plugins have this + client.clear_session() + raise + else: + raise Exception('unknown purpose: %s' % purpose) + + def derivation_and_script_type_dialog(self, f): + message1 = _('Choose the type of addresses in your wallet.') + message2 = '\n'.join([ + _('You can override the suggested derivation path.'), + _('If you are not sure what this is, leave this field unchanged.') + ]) + if self.wallet_type == 'multisig': + # 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. + choices = [ + ('standard', 'legacy multisig (p2sh)', "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: + choices = [ + ('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), + ('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), + ('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)), + ] + while True: + try: + self.choice_and_line_dialog( + run_next=f, title=_('Script type and Derivation path'), message1=message1, + message2=message2, choices=choices, test_text=bitcoin.is_bip32_derivation) + return + except ScriptTypeNotSupported as e: + self.show_error(e) + # let the user choose again + + def on_hw_derivation(self, name, device_info, derivation, xtype): + from .keystore import hardware_keystore + try: + xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) + except ScriptTypeNotSupported: + raise # this is handled in derivation_dialog + except BaseException as e: + traceback.print_exc(file=sys.stderr) + self.show_error(e) + return + d = { + 'type': 'hardware', + 'hw_type': name, + 'derivation': derivation, + 'xpub': xpub, + 'label': device_info.label, + } + k = hardware_keystore(d) + self.on_keystore(k) + + def passphrase_dialog(self, run_next, is_restoring=False): + title = _('Seed extension') + message = '\n'.join([ + _('You may extend your seed with custom words.'), + _('Your seed extension must be saved together with your seed.'), + ]) + warning = '\n'.join([ + _('Note that this is NOT your encryption password.'), + _('If you do not know what this is, leave this field empty.'), + ]) + warn_issue4566 = is_restoring and self.seed_type == 'bip39' + self.line_dialog(title=title, message=message, warning=warning, + default='', test=lambda x:True, run_next=run_next, + warn_issue4566=warn_issue4566) + + def restore_from_seed(self): + self.opt_bip39 = True + self.opt_ext = True + is_cosigning_seed = lambda x: bitcoin.seed_type(x) in ['standard', 'segwit'] + test = bitcoin.is_seed if self.wallet_type == 'standard' else is_cosigning_seed + self.restore_seed_dialog(run_next=self.on_restore_seed, test=test) + + def on_restore_seed(self, seed, is_bip39, is_ext): + self.seed_type = 'bip39' if is_bip39 else bitcoin.seed_type(seed) + if self.seed_type == 'bip39': + f = lambda passphrase: self.on_restore_bip39(seed, passphrase) + self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') + elif self.seed_type in ['standard', 'segwit']: + f = lambda passphrase: self.run('create_keystore', seed, passphrase) + self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') + elif self.seed_type == 'old': + self.run('create_keystore', seed, '') + elif self.seed_type == '2fa': + self.load_2fa() + self.run('on_restore_seed', seed, is_ext) + else: + raise Exception('Unknown seed type', self.seed_type) + + def on_restore_bip39(self, seed, passphrase): + def f(derivation, script_type): + self.run('on_bip43', seed, passphrase, derivation, script_type) + self.derivation_and_script_type_dialog(f) + + def create_keystore(self, seed, passphrase): + k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig') + self.on_keystore(k) + + def on_bip43(self, seed, passphrase, derivation, script_type): + k = keystore.from_bip39_seed(seed, passphrase, derivation, xtype=script_type) + self.on_keystore(k) + + def on_keystore(self, k): + has_xpub = isinstance(k, keystore.Xpub) + if has_xpub: + from .bitcoin import xpub_type + t1 = xpub_type(k.xpub) + if self.wallet_type == 'standard': + if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: + self.show_error(_('Wrong key type') + ' %s'%t1) + self.run('choose_keystore') + return + self.keystores.append(k) + self.run('create_wallet') + elif self.wallet_type == 'multisig': + assert has_xpub + if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: + self.show_error(_('Wrong key type') + ' %s'%t1) + self.run('choose_keystore') + return + if k.xpub in map(lambda x: x.xpub, self.keystores): + self.show_error(_('Error: duplicate master public key')) + self.run('choose_keystore') + return + if len(self.keystores)>0: + t2 = xpub_type(self.keystores[0].xpub) + if t1 != t2: + self.show_error(_('Cannot add this cosigner:') + '\n' + "Their key type is '%s', we are '%s'"%(t1, t2)) + self.run('choose_keystore') + return + self.keystores.append(k) + if len(self.keystores) == 1: + xpub = k.get_master_public_key() + self.stack = [] + self.run('show_xpub_and_add_cosigners', xpub) + elif len(self.keystores) < self.n: + self.run('choose_keystore') + else: + self.run('create_wallet') + + def create_wallet(self): + encrypt_keystore = any(k.may_have_password() for k in self.keystores) + # note: the following condition ("if") is duplicated logic from + # wallet.get_available_storage_encryption_version() + if self.wallet_type == 'standard' and isinstance(self.keystores[0], keystore.Hardware_KeyStore): + # offer encrypting with a pw derived from the hw device + k = self.keystores[0] + try: + k.handler = self.plugin.create_handler(self) + password = k.get_password_for_storage_encryption() + except UserCancelled: + devmgr = self.plugins.device_manager + devmgr.unpair_xpub(k.xpub) + self.choose_hw_device() + return + except BaseException as e: + traceback.print_exc(file=sys.stderr) + self.show_error(str(e)) + return + self.request_storage_encryption( + run_next=lambda encrypt_storage: self.on_password( + password, + encrypt_storage=encrypt_storage, + storage_enc_version=STO_EV_XPUB_PW, + encrypt_keystore=False)) + else: + # prompt the user to set an arbitrary password + self.request_password( + run_next=lambda password, encrypt_storage: self.on_password( + password, + encrypt_storage=encrypt_storage, + storage_enc_version=STO_EV_USER_PW, + encrypt_keystore=encrypt_keystore), + force_disable_encrypt_cb=not encrypt_keystore) + + def on_password(self, password, *, encrypt_storage, + storage_enc_version=STO_EV_USER_PW, encrypt_keystore): + self.storage.set_keystore_encryption(bool(password) and encrypt_keystore) + if encrypt_storage: + self.storage.set_password(password, enc_version=storage_enc_version) + for k in self.keystores: + if k.may_have_password(): + k.update_password(None, password) + if self.wallet_type == 'standard': + self.storage.put('seed_type', self.seed_type) + keys = self.keystores[0].dump() + self.storage.put('keystore', keys) + self.wallet = Standard_Wallet(self.storage) + self.run('create_addresses') + elif self.wallet_type == 'multisig': + for i, k in enumerate(self.keystores): + self.storage.put('x%d/'%(i+1), k.dump()) + self.storage.write() + self.wallet = Multisig_Wallet(self.storage) + self.run('create_addresses') + elif self.wallet_type == 'imported': + if len(self.keystores) > 0: + keys = self.keystores[0].dump() + self.storage.put('keystore', keys) + self.wallet = Imported_Wallet(self.storage) + self.wallet.storage.write() + self.terminate() + + def show_xpub_and_add_cosigners(self, xpub): + self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) + + def choose_seed_type(self): + title = _('Choose Seed type') + message = ' '.join([ + _("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.") + ]) + choices = [ + ('create_standard_seed', _('Standard')), + ('create_segwit_seed', _('Segwit')), + ] + self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) + + def create_segwit_seed(self): self.create_seed('segwit') + def create_standard_seed(self): self.create_seed('standard') + + def create_seed(self, seed_type): + from . import mnemonic + self.seed_type = seed_type + seed = mnemonic.Mnemonic('en').make_seed(self.seed_type) + self.opt_bip39 = False + f = lambda x: self.request_passphrase(seed, x) + self.show_seed_dialog(run_next=f, seed_text=seed) + + def request_passphrase(self, seed, opt_passphrase): + if opt_passphrase: + f = lambda x: self.confirm_seed(seed, x) + self.passphrase_dialog(run_next=f) + else: + self.run('confirm_seed', seed, '') + + def confirm_seed(self, seed, passphrase): + f = lambda x: self.confirm_passphrase(seed, passphrase) + self.confirm_seed_dialog(run_next=f, test=lambda x: x==seed) + + def confirm_passphrase(self, seed, passphrase): + f = lambda x: self.run('create_keystore', seed, x) + if passphrase: + title = _('Confirm Seed Extension') + message = '\n'.join([ + _('Your seed extension must be saved together with your seed.'), + _('Please type it here.'), + ]) + self.line_dialog(run_next=f, title=title, message=message, default='', test=lambda x: x==passphrase) + else: + f('') + + def create_addresses(self): + def task(): + self.wallet.synchronize() + self.wallet.storage.write() + self.terminate() + msg = _("Electrum is generating your addresses, please wait...") + self.waiting_dialog(task, msg) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py new file mode 100644 index 000000000..ccd57070f --- /dev/null +++ b/electrum/bitcoin.py @@ -0,0 +1,782 @@ +# -*- coding: utf-8 -*- +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2011 thomasv@gitorious +# +# 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 hashlib +from typing import List + +from .util import bfh, bh2u, BitcoinException, print_error, assert_bytes, to_bytes, inv_dict +from . import version +from . import segwit_addr +from . import constants +from . import ecc +from .crypto import Hash, sha256, hash_160, hmac_oneshot + + +################################## transactions + +COINBASE_MATURITY = 100 +COIN = 100000000 +TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000 + +# supported types of transaction outputs +TYPE_ADDRESS = 0 +TYPE_PUBKEY = 1 +TYPE_SCRIPT = 2 + + +def rev_hex(s): + return bh2u(bfh(s)[::-1]) + + +def int_to_hex(i: int, length: int=1) -> str: + """Converts int to little-endian hex string. + `length` is the number of bytes available + """ + if not isinstance(i, int): + raise TypeError('{} instead of int'.format(i)) + range_size = pow(256, length) + if i < -range_size/2 or i >= range_size: + raise OverflowError('cannot convert int {} to hex ({} bytes)'.format(i, length)) + if i < 0: + # two's complement + i = range_size + i + s = hex(i)[2:].rstrip('L') + s = "0"*(2*length - len(s)) + s + return rev_hex(s) + +def script_num_to_hex(i: int) -> str: + """See CScriptNum in Bitcoin Core. + Encodes an integer as hex, to be used in script. + + ported from https://github.com/bitcoin/bitcoin/blob/8cbc5c4be4be22aca228074f087a374a7ec38be8/src/script/script.h#L326 + """ + if i == 0: + return '' + + result = bytearray() + neg = i < 0 + absvalue = abs(i) + while absvalue > 0: + result.append(absvalue & 0xff) + absvalue >>= 8 + + if result[-1] & 0x80: + result.append(0x80 if neg else 0x00) + elif neg: + result[-1] |= 0x80 + + return bh2u(result) + + +def var_int(i: int) -> str: + # https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer + if i<0xfd: + return int_to_hex(i) + elif i<=0xffff: + return "fd"+int_to_hex(i,2) + elif i<=0xffffffff: + return "fe"+int_to_hex(i,4) + else: + return "ff"+int_to_hex(i,8) + + +def witness_push(item: str) -> str: + """Returns data in the form it should be present in the witness. + hex -> hex + """ + return var_int(len(item) // 2) + item + + +def op_push(i: int) -> str: + if i<0x4c: # OP_PUSHDATA1 + return int_to_hex(i) + elif i<=0xff: + return '4c' + int_to_hex(i) + elif i<=0xffff: + return '4d' + int_to_hex(i,2) + else: + return '4e' + int_to_hex(i,4) + + +def push_script(data: str) -> str: + """Returns pushed data to the script, automatically + choosing canonical opcodes depending on the length of the data. + hex -> hex + + ported from https://github.com/btcsuite/btcd/blob/fdc2bc867bda6b351191b5872d2da8270df00d13/txscript/scriptbuilder.go#L128 + """ + data = bfh(data) + from .transaction import opcodes + + data_len = len(data) + + # "small integer" opcodes + if data_len == 0 or data_len == 1 and data[0] == 0: + return bh2u(bytes([opcodes.OP_0])) + elif data_len == 1 and data[0] <= 16: + return bh2u(bytes([opcodes.OP_1 - 1 + data[0]])) + elif data_len == 1 and data[0] == 0x81: + return bh2u(bytes([opcodes.OP_1NEGATE])) + + return op_push(data_len) + bh2u(data) + + +def add_number_to_script(i: int) -> bytes: + return bfh(push_script(script_num_to_hex(i))) + + +hash_encode = lambda x: bh2u(x[::-1]) +hash_decode = lambda x: bfh(x)[::-1] +hmac_sha_512 = lambda x, y: hmac_oneshot(x, y, hashlib.sha512) + + +def is_new_seed(x, prefix=version.SEED_PREFIX): + from . import mnemonic + x = mnemonic.normalize_text(x) + s = bh2u(hmac_sha_512(b"Seed version", x.encode('utf8'))) + return s.startswith(prefix) + + +def is_old_seed(seed): + from . import old_mnemonic, mnemonic + seed = mnemonic.normalize_text(seed) + words = seed.split() + try: + # checks here are deliberately left weak for legacy reasons, see #3149 + old_mnemonic.mn_decode(words) + uses_electrum_words = True + except Exception: + uses_electrum_words = False + try: + seed = bfh(seed) + is_hex = (len(seed) == 16 or len(seed) == 32) + except Exception: + is_hex = False + return is_hex or (uses_electrum_words and (len(words) == 12 or len(words) == 24)) + + +def seed_type(x): + if is_old_seed(x): + return 'old' + elif is_new_seed(x): + return 'standard' + elif is_new_seed(x, version.SEED_PREFIX_SW): + return 'segwit' + elif is_new_seed(x, version.SEED_PREFIX_2FA): + return '2fa' + return '' + +is_seed = lambda x: bool(seed_type(x)) + + +############ functions from pywallet ##################### + +def hash160_to_b58_address(h160: bytes, addrtype): + s = bytes([addrtype]) + s += h160 + return base_encode(s+Hash(s)[0:4], base=58) + + +def b58_address_to_hash160(addr): + addr = to_bytes(addr, 'ascii') + _bytes = base_decode(addr, 25, base=58) + return _bytes[0], _bytes[1:21] + + +def hash160_to_p2pkh(h160, *, net=None): + if net is None: + net = constants.net + return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH) + +def hash160_to_p2sh(h160, *, net=None): + if net is None: + net = constants.net + return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH) + +def public_key_to_p2pkh(public_key: bytes) -> str: + return hash160_to_p2pkh(hash_160(public_key)) + +def hash_to_segwit_addr(h, witver, *, net=None): + if net is None: + net = constants.net + return segwit_addr.encode(net.SEGWIT_HRP, witver, h) + +def public_key_to_p2wpkh(public_key): + return hash_to_segwit_addr(hash_160(public_key), witver=0) + +def script_to_p2wsh(script): + return hash_to_segwit_addr(sha256(bfh(script)), witver=0) + +def p2wpkh_nested_script(pubkey): + pkh = bh2u(hash_160(bfh(pubkey))) + return '00' + push_script(pkh) + +def p2wsh_nested_script(witness_script): + wsh = bh2u(sha256(bfh(witness_script))) + return '00' + push_script(wsh) + +def pubkey_to_address(txin_type, pubkey): + if txin_type == 'p2pkh': + return public_key_to_p2pkh(bfh(pubkey)) + elif txin_type == 'p2wpkh': + return public_key_to_p2wpkh(bfh(pubkey)) + elif txin_type == 'p2wpkh-p2sh': + scriptSig = p2wpkh_nested_script(pubkey) + return hash160_to_p2sh(hash_160(bfh(scriptSig))) + else: + raise NotImplementedError(txin_type) + +def redeem_script_to_address(txin_type, redeem_script): + if txin_type == 'p2sh': + return hash160_to_p2sh(hash_160(bfh(redeem_script))) + elif txin_type == 'p2wsh': + return script_to_p2wsh(redeem_script) + elif txin_type == 'p2wsh-p2sh': + scriptSig = p2wsh_nested_script(redeem_script) + return hash160_to_p2sh(hash_160(bfh(scriptSig))) + else: + raise NotImplementedError(txin_type) + + +def script_to_address(script, *, net=None): + from .transaction import get_address_from_output_script + t, addr = get_address_from_output_script(bfh(script), net=net) + assert t == TYPE_ADDRESS + return addr + +def address_to_script(addr, *, net=None): + if net is None: + net = constants.net + witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr) + if witprog is not None: + if not (0 <= witver <= 16): + raise BitcoinException('impossible witness version: {}'.format(witver)) + OP_n = witver + 0x50 if witver > 0 else 0 + script = bh2u(bytes([OP_n])) + script += push_script(bh2u(bytes(witprog))) + return script + addrtype, hash_160 = b58_address_to_hash160(addr) + if addrtype == net.ADDRTYPE_P2PKH: + script = '76a9' # op_dup, op_hash_160 + script += push_script(bh2u(hash_160)) + script += '88ac' # op_equalverify, op_checksig + elif addrtype == net.ADDRTYPE_P2SH: + script = 'a9' # op_hash_160 + script += push_script(bh2u(hash_160)) + script += '87' # op_equal + else: + raise BitcoinException('unknown address type: {}'.format(addrtype)) + return script + +def address_to_scripthash(addr): + script = address_to_script(addr) + return script_to_scripthash(script) + +def script_to_scripthash(script): + h = sha256(bytes.fromhex(script))[0:32] + return bh2u(bytes(reversed(h))) + +def public_key_to_p2pk_script(pubkey): + script = push_script(pubkey) + script += 'ac' # op_checksig + return script + +__b58chars = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +assert len(__b58chars) == 58 + +__b43chars = b'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:' +assert len(__b43chars) == 43 + + +def base_encode(v: bytes, base: int) -> str: + """ encode v, which is a string of bytes, to base58.""" + assert_bytes(v) + if base not in (58, 43): + raise ValueError('not supported base: {}'.format(base)) + chars = __b58chars + if base == 43: + chars = __b43chars + long_value = 0 + for (i, c) in enumerate(v[::-1]): + long_value += (256**i) * c + result = bytearray() + while long_value >= base: + div, mod = divmod(long_value, base) + result.append(chars[mod]) + long_value = div + result.append(chars[long_value]) + # Bitcoin does a little leading-zero-compression: + # leading 0-bytes in the input become leading-1s + nPad = 0 + for c in v: + if c == 0x00: + nPad += 1 + else: + break + result.extend([chars[0]] * nPad) + result.reverse() + return result.decode('ascii') + + +def base_decode(v, length, base): + """ decode v into a string of len bytes.""" + # assert_bytes(v) + v = to_bytes(v, 'ascii') + if base not in (58, 43): + raise ValueError('not supported base: {}'.format(base)) + chars = __b58chars + if base == 43: + chars = __b43chars + long_value = 0 + for (i, c) in enumerate(v[::-1]): + digit = chars.find(bytes([c])) + if digit == -1: + raise ValueError('Forbidden character {} for base {}'.format(c, base)) + long_value += digit * (base**i) + result = bytearray() + while long_value >= 256: + div, mod = divmod(long_value, 256) + result.append(mod) + long_value = div + result.append(long_value) + nPad = 0 + for c in v: + if c == chars[0]: + nPad += 1 + else: + break + result.extend(b'\x00' * nPad) + if length is not None and len(result) != length: + return None + result.reverse() + return bytes(result) + + +class InvalidChecksum(Exception): + pass + + +def EncodeBase58Check(vchIn): + hash = Hash(vchIn) + return base_encode(vchIn + hash[0:4], base=58) + + +def DecodeBase58Check(psz): + vchRet = base_decode(psz, None, base=58) + key = vchRet[0:-4] + csum = vchRet[-4:] + hash = Hash(key) + cs32 = hash[0:4] + if cs32 != csum: + raise InvalidChecksum('expected {}, actual {}'.format(bh2u(cs32), bh2u(csum))) + else: + return key + + +# backwards compat +# extended WIF for segwit (used in 3.0.x; but still used internally) +# the keys in this dict should be a superset of what Imported Wallets can import +WIF_SCRIPT_TYPES = { + 'p2pkh':0, + 'p2wpkh':1, + 'p2wpkh-p2sh':2, + 'p2sh':5, + 'p2wsh':6, + 'p2wsh-p2sh':7 +} +WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES) + + +PURPOSE48_SCRIPT_TYPES = { + 'p2wsh-p2sh': 1, # specifically multisig + 'p2wsh': 2, # specifically multisig +} +PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES) + + +def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, + internal_use: bool=False) -> str: + # we only export secrets inside curve range + secret = ecc.ECPrivkey.normalize_secret_bytes(secret) + if internal_use: + prefix = bytes([(WIF_SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255]) + else: + prefix = bytes([constants.net.WIF_PREFIX]) + suffix = b'\01' if compressed else b'' + vchIn = prefix + secret + suffix + base58_wif = EncodeBase58Check(vchIn) + if internal_use: + return base58_wif + else: + return '{}:{}'.format(txin_type, base58_wif) + + +def deserialize_privkey(key: str) -> (str, bytes, bool): + if is_minikey(key): + return 'p2pkh', minikey_to_private_key(key), False + + txin_type = None + if ':' in key: + txin_type, key = key.split(sep=':', maxsplit=1) + if txin_type not in WIF_SCRIPT_TYPES: + raise BitcoinException('unknown script type: {}'.format(txin_type)) + try: + vch = DecodeBase58Check(key) + except BaseException: + neutered_privkey = str(key)[:3] + '..' + str(key)[-2:] + raise BitcoinException("cannot deserialize privkey {}" + .format(neutered_privkey)) + + if txin_type is None: + # keys exported in version 3.0.x encoded script type in first byte + prefix_value = vch[0] - constants.net.WIF_PREFIX + try: + txin_type = WIF_SCRIPT_TYPES_INV[prefix_value] + except KeyError: + raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0])) + else: + # all other keys must have a fixed first byte + if vch[0] != constants.net.WIF_PREFIX: + raise BitcoinException('invalid prefix ({}) for WIF key (2)'.format(vch[0])) + + if len(vch) not in [33, 34]: + raise BitcoinException('invalid vch len for WIF key: {}'.format(len(vch))) + compressed = len(vch) == 34 + secret_bytes = vch[1:33] + # we accept secrets outside curve range; cast into range here: + secret_bytes = ecc.ECPrivkey.normalize_secret_bytes(secret_bytes) + return txin_type, secret_bytes, compressed + + +def is_compressed(sec): + return deserialize_privkey(sec)[2] + + +def address_from_private_key(sec): + txin_type, privkey, compressed = deserialize_privkey(sec) + public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) + return pubkey_to_address(txin_type, public_key) + +def is_segwit_address(addr): + try: + witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr) + except Exception as e: + return False + return witprog is not None + +def is_b58_address(addr): + try: + addrtype, h = b58_address_to_hash160(addr) + except Exception as e: + return False + if addrtype not in [constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH]: + return False + return addr == hash160_to_b58_address(h, addrtype) + +def is_address(addr): + return is_segwit_address(addr) or is_b58_address(addr) + + +def is_private_key(key): + try: + k = deserialize_privkey(key) + return k is not False + except: + return False + + +########### end pywallet functions ####################### + +def is_minikey(text): + # Minikeys are typically 22 or 30 characters, but this routine + # permits any length of 20 or more provided the minikey is valid. + # A valid minikey must begin with an 'S', be in base58, and when + # suffixed with '?' have its SHA256 hash begin with a zero byte. + # They are widely used in Casascius physical bitcoins. + return (len(text) >= 20 and text[0] == 'S' + and all(ord(c) in __b58chars for c in text) + and sha256(text + '?')[0] == 0x00) + +def minikey_to_private_key(text): + return sha256(text) + + +###################################### BIP32 ############################## + +BIP32_PRIME = 0x800000a0 + + +def protect_against_invalid_ecpoint(func): + def func_wrapper(*args): + n = args[-1] + while True: + is_prime = n & BIP32_PRIME + try: + return func(*args[:-1], n=n) + except ecc.InvalidECPointException: + print_error('bip32 protect_against_invalid_ecpoint: skipping index') + n += 1 + is_prime2 = n & BIP32_PRIME + if is_prime != is_prime2: raise OverflowError() + return func_wrapper + + +# Child private key derivation function (from master private key) +# k = master private key (32 bytes) +# c = master chain code (extra entropy for key derivation) (32 bytes) +# n = the index of the key we want to derive. (only 32 bits will be used) +# If n is hardened (i.e. the 32nd bit is set), the resulting private key's +# corresponding public key can NOT be determined without the master private key. +# However, if n is not hardened, the resulting private key's corresponding +# public key can be determined without the master private key. +@protect_against_invalid_ecpoint +def CKD_priv(k, c, n): + if n < 0: raise ValueError('the bip32 index needs to be non-negative') + is_prime = n & BIP32_PRIME + return _CKD_priv(k, c, bfh(rev_hex(int_to_hex(n,4))), is_prime) + + +def _CKD_priv(k, c, s, is_prime): + try: + keypair = ecc.ECPrivkey(k) + except ecc.InvalidECPointException as e: + raise BitcoinException('Impossible xprv (not within curve order)') from e + cK = keypair.get_public_key_bytes(compressed=True) + data = bytes([0]) + k + s if is_prime else cK + s + I = hmac_oneshot(c, data, hashlib.sha512) + I_left = ecc.string_to_number(I[0:32]) + k_n = (I_left + ecc.string_to_number(k)) % ecc.CURVE_ORDER + if I_left >= ecc.CURVE_ORDER or k_n == 0: + raise ecc.InvalidECPointException() + k_n = ecc.number_to_string(k_n, ecc.CURVE_ORDER) + c_n = I[32:] + return k_n, c_n + +# Child public key derivation function (from public key only) +# K = master public key +# c = master chain code +# n = index of key we want to derive +# This function allows us to find the nth public key, as long as n is +# not hardened. If n is hardened, we need the master private key to find it. +@protect_against_invalid_ecpoint +def CKD_pub(cK, c, n): + if n < 0: raise ValueError('the bip32 index needs to be non-negative') + if n & BIP32_PRIME: raise Exception() + return _CKD_pub(cK, c, bfh(rev_hex(int_to_hex(n,4)))) + +# helper function, callable with arbitrary string. +# note: 's' does not need to fit into 32 bits here! (c.f. trustedcoin billing) +def _CKD_pub(cK, c, s): + I = hmac_oneshot(c, cK + s, hashlib.sha512) + pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(cK) + if pubkey.is_at_infinity(): + raise ecc.InvalidECPointException() + cK_n = pubkey.get_public_key_bytes(compressed=True) + c_n = I[32:] + return cK_n, c_n + + +def xprv_header(xtype, *, net=None): + if net is None: + net = constants.net + return bfh("%08x" % net.XPRV_HEADERS[xtype]) + + +def xpub_header(xtype, *, net=None): + if net is None: + net = constants.net + return bfh("%08x" % net.XPUB_HEADERS[xtype]) + + +def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, + child_number=b'\x00'*4, *, net=None): + if not ecc.is_secret_within_curve_range(k): + raise BitcoinException('Impossible xprv (not within curve order)') + xprv = xprv_header(xtype, net=net) \ + + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k + return EncodeBase58Check(xprv) + + +def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4, + child_number=b'\x00'*4, *, net=None): + xpub = xpub_header(xtype, net=net) \ + + bytes([depth]) + fingerprint + child_number + c + cK + return EncodeBase58Check(xpub) + + +class InvalidMasterKeyVersionBytes(BitcoinException): pass + + +def deserialize_xkey(xkey, prv, *, net=None): + if net is None: + net = constants.net + xkey = DecodeBase58Check(xkey) + if len(xkey) != 78: + raise BitcoinException('Invalid length for extended key: {}' + .format(len(xkey))) + depth = xkey[4] + fingerprint = xkey[5:9] + child_number = xkey[9:13] + c = xkey[13:13+32] + header = int('0x' + bh2u(xkey[0:4]), 16) + headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS + if header not in headers.values(): + raise InvalidMasterKeyVersionBytes('Invalid extended key format: {}' + .format(hex(header))) + xtype = list(headers.keys())[list(headers.values()).index(header)] + n = 33 if prv else 32 + K_or_k = xkey[13+n:] + if prv and not ecc.is_secret_within_curve_range(K_or_k): + raise BitcoinException('Impossible xprv (not within curve order)') + return xtype, depth, fingerprint, child_number, c, K_or_k + + +def deserialize_xpub(xkey, *, net=None): + return deserialize_xkey(xkey, False, net=net) + +def deserialize_xprv(xkey, *, net=None): + return deserialize_xkey(xkey, True, net=net) + +def xpub_type(x): + return deserialize_xpub(x)[0] + + +def is_xpub(text): + try: + deserialize_xpub(text) + return True + except: + return False + + +def is_xprv(text): + try: + deserialize_xprv(text) + return True + except: + return False + + +def xpub_from_xprv(xprv): + xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv) + cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True) + return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) + + +def bip32_root(seed, xtype): + I = hmac_oneshot(b"Bitcoin seed", seed, hashlib.sha512) + master_k = I[0:32] + master_c = I[32:] + # create xprv first, as that will check if master_k is within curve order + xprv = serialize_xprv(xtype, master_c, master_k) + cK = ecc.ECPrivkey(master_k).get_public_key_bytes(compressed=True) + xpub = serialize_xpub(xtype, master_c, cK) + return xprv, xpub + + +def xpub_from_pubkey(xtype, cK): + if cK[0] not in (0x02, 0x03): + raise ValueError('Unexpected first byte: {}'.format(cK[0])) + return serialize_xpub(xtype, b'\x00'*32, cK) + + +def bip32_derivation(s): + if not s.startswith('m/'): + raise ValueError('invalid bip32 derivation path: {}'.format(s)) + s = s[2:] + for n in s.split('/'): + if n == '': continue + i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n) + yield i + +def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]: + """Convert bip32 path to list of uint32 integers with prime flags + m/0/-1/1' -> [0, 0x80000001, 0x80000001] + + based on code in trezorlib + """ + path = [] + for x in n.split('/')[1:]: + if x == '': continue + prime = 0 + if x.endswith("'"): + x = x.replace('\'', '') + prime = BIP32_PRIME + if x.startswith('-'): + prime = BIP32_PRIME + path.append(abs(int(x)) | prime) + return path + +def is_bip32_derivation(x): + try: + [ i for i in bip32_derivation(x)] + return True + except : + return False + +def bip32_private_derivation(xprv, branch, sequence): + if not sequence.startswith(branch): + raise ValueError('incompatible branch ({}) and sequence ({})' + .format(branch, sequence)) + if branch == sequence: + return xprv, xpub_from_xprv(xprv) + xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv) + sequence = sequence[len(branch):] + for n in sequence.split('/'): + if n == '': continue + i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n) + parent_k = k + k, c = CKD_priv(k, c, i) + depth += 1 + parent_cK = ecc.ECPrivkey(parent_k).get_public_key_bytes(compressed=True) + fingerprint = hash_160(parent_cK)[0:4] + child_number = bfh("%08X"%i) + cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True) + xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) + xprv = serialize_xprv(xtype, c, k, depth, fingerprint, child_number) + return xprv, xpub + + +def bip32_public_derivation(xpub, branch, sequence): + xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub) + if not sequence.startswith(branch): + raise ValueError('incompatible branch ({}) and sequence ({})' + .format(branch, sequence)) + sequence = sequence[len(branch):] + for n in sequence.split('/'): + if n == '': continue + i = int(n) + parent_cK = cK + cK, c = CKD_pub(cK, c, i) + depth += 1 + fingerprint = hash_160(parent_cK)[0:4] + child_number = bfh("%08X"%i) + return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) + + +def bip32_private_key(sequence, k, chain): + for i in sequence: + k, chain = CKD_priv(k, chain, i) + return k diff --git a/electrum/blockchain.py b/electrum/blockchain.py new file mode 100644 index 000000000..bbabc2a1e --- /dev/null +++ b/electrum/blockchain.py @@ -0,0 +1,405 @@ +# Electrum - lightweight Bitcoin client +# Copyright (C) 2012 thomasv@ecdsa.org +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import os +import threading + +from . import util +from .bitcoin import Hash, hash_encode, int_to_hex, rev_hex +from . import constants +from .util import bfh, bh2u + +MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 + + +class MissingHeader(Exception): + pass + +class InvalidHeader(Exception): + pass + +def serialize_header(res): + s = int_to_hex(res.get('version'), 4) \ + + rev_hex(res.get('prev_block_hash')) \ + + rev_hex(res.get('merkle_root')) \ + + int_to_hex(int(res.get('timestamp')), 4) \ + + int_to_hex(int(res.get('bits')), 4) \ + + int_to_hex(int(res.get('nonce')), 4) + return s + +def deserialize_header(s, height): + if not s: + raise InvalidHeader('Invalid header: {}'.format(s)) + if len(s) != 80: + raise InvalidHeader('Invalid header length: {}'.format(len(s))) + hex_to_int = lambda s: int('0x' + bh2u(s[::-1]), 16) + h = {} + 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['block_height'] = height + return h + +def hash_header(header): + if header is None: + return '0' * 64 + if header.get('prev_block_hash') is None: + header['prev_block_hash'] = '00'*32 + return hash_encode(Hash(bfh(serialize_header(header)))) + + +blockchains = {} + +def read_blockchains(config): + blockchains[0] = Blockchain(config, 0, None) + fdir = os.path.join(util.get_headers_dir(config), 'forks') + util.make_dir(fdir) + l = filter(lambda x: x.startswith('fork_'), os.listdir(fdir)) + l = sorted(l, key = lambda x: int(x.split('_')[1])) + for filename in l: + forkpoint = int(filename.split('_')[2]) + parent_id = int(filename.split('_')[1]) + b = Blockchain(config, forkpoint, parent_id) + h = b.read_header(b.forkpoint) + if b.parent().can_connect(h, check_height=False): + blockchains[b.forkpoint] = b + else: + util.print_error("cannot connect", filename) + return blockchains + +def check_header(header): + if type(header) is not dict: + return False + for b in blockchains.values(): + if b.check_header(header): + return b + return False + +def can_connect(header): + for b in blockchains.values(): + if b.can_connect(header): + return b + return False + + +class Blockchain(util.PrintError): + """ + Manages blockchain headers and their verification + """ + + def __init__(self, config, forkpoint, parent_id): + self.config = config + self.catch_up = None # interface catching up + self.forkpoint = forkpoint + self.checkpoints = constants.net.CHECKPOINTS + self.parent_id = parent_id + assert parent_id != forkpoint + self.lock = threading.RLock() + with self.lock: + self.update_size() + + def with_lock(func): + def func_wrapper(self, *args, **kwargs): + with self.lock: + return func(self, *args, **kwargs) + return func_wrapper + + def parent(self): + return blockchains[self.parent_id] + + def get_max_child(self): + children = list(filter(lambda y: y.parent_id==self.forkpoint, blockchains.values())) + return max([x.forkpoint for x in children]) if children else None + + def get_forkpoint(self): + mc = self.get_max_child() + return mc if mc is not None else self.forkpoint + + def get_branch_size(self): + return self.height() - self.get_forkpoint() + 1 + + def get_name(self): + return self.get_hash(self.get_forkpoint()).lstrip('00')[0:10] + + def check_header(self, header): + header_hash = hash_header(header) + height = header.get('block_height') + return header_hash == self.get_hash(height) + + def fork(parent, header): + forkpoint = header.get('block_height') + self = Blockchain(parent.config, forkpoint, parent.forkpoint) + open(self.path(), 'w+').close() + self.save_header(header) + return self + + def height(self): + return self.forkpoint + self.size() - 1 + + def size(self): + with self.lock: + return self._size + + def update_size(self): + p = self.path() + self._size = os.path.getsize(p)//80 if os.path.exists(p) else 0 + + def verify_header(self, header, prev_hash, target): + _hash = hash_header(header) + 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 = self.target_to_bits(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, data): + num = len(data) // 80 + prev_hash = self.get_hash(index * 2016 - 1) + target = self.get_target(index-1) + for i in range(num): + raw_header = data[i*80:(i+1) * 80] + header = deserialize_header(raw_header, index*2016 + i) + self.verify_header(header, prev_hash, target) + prev_hash = hash_header(header) + + def path(self): + d = util.get_headers_dir(self.config) + filename = 'blockchain_headers' if self.parent_id is None else os.path.join('forks', 'fork_%d_%d'%(self.parent_id, self.forkpoint)) + return os.path.join(d, filename) + + @with_lock + def save_chunk(self, index, chunk): + chunk_within_checkpoint_region = index < len(self.checkpoints) + # chunks in checkpoint region are the responsibility of the 'main chain' + if chunk_within_checkpoint_region and self.parent_id is not None: + main_chain = blockchains[0] + main_chain.save_chunk(index, chunk) + return + + delta_height = (index * 2016 - self.forkpoint) + delta_bytes = delta_height * 80 + # if this chunk contains our forkpoint, only save the part after forkpoint + # (the part before is the responsibility of the parent) + if delta_bytes < 0: + chunk = chunk[-delta_bytes:] + delta_bytes = 0 + truncate = not chunk_within_checkpoint_region + self.write(chunk, delta_bytes, truncate) + self.swap_with_parent() + + @with_lock + def swap_with_parent(self): + if self.parent_id is None: + return + parent_branch_size = self.parent().height() - self.forkpoint + 1 + if parent_branch_size >= self.size(): + return + self.print_error("swap", self.forkpoint, self.parent_id) + parent_id = self.parent_id + forkpoint = self.forkpoint + parent = self.parent() + self.assert_headers_file_available(self.path()) + with open(self.path(), 'rb') as f: + my_data = f.read() + self.assert_headers_file_available(parent.path()) + with open(parent.path(), 'rb') as f: + f.seek((forkpoint - parent.forkpoint)*80) + parent_data = f.read(parent_branch_size*80) + self.write(parent_data, 0) + parent.write(my_data, (forkpoint - parent.forkpoint)*80) + # store file path + for b in blockchains.values(): + b.old_path = b.path() + # swap parameters + self.parent_id = parent.parent_id; parent.parent_id = parent_id + self.forkpoint = parent.forkpoint; parent.forkpoint = forkpoint + self._size = parent._size; parent._size = parent_branch_size + # move files + for b in blockchains.values(): + if b in [self, parent]: continue + if b.old_path != b.path(): + self.print_error("renaming", b.old_path, b.path()) + os.rename(b.old_path, b.path()) + # update pointers + blockchains[self.forkpoint] = self + blockchains[parent.forkpoint] = parent + + def assert_headers_file_available(self, path): + if os.path.exists(path): + return + elif not os.path.exists(util.get_headers_dir(self.config)): + raise FileNotFoundError('Electrum headers_dir does not exist. Was it deleted while running?') + else: + raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path)) + + def write(self, data, offset, truncate=True): + filename = self.path() + with self.lock: + self.assert_headers_file_available(filename) + with open(filename, 'rb+') as f: + if truncate and offset != self._size*80: + f.seek(offset) + f.truncate() + f.seek(offset) + f.write(data) + f.flush() + os.fsync(f.fileno()) + self.update_size() + + @with_lock + def save_header(self, header): + delta = header.get('block_height') - self.forkpoint + data = bfh(serialize_header(header)) + # headers are only _appended_ to the end: + assert delta == self.size() + assert len(data) == 80 + self.write(data, delta*80) + self.swap_with_parent() + + def read_header(self, height): + assert self.parent_id != self.forkpoint + if height < 0: + return + if height < self.forkpoint: + return self.parent().read_header(height) + if height > self.height(): + return + delta = height - self.forkpoint + name = self.path() + self.assert_headers_file_available(name) + with open(name, 'rb') as f: + f.seek(delta * 80) + h = f.read(80) + if len(h) < 80: + raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h))) + if h == bytes([0])*80: + return None + return deserialize_header(h, height) + + def get_hash(self, height): + if height == -1: + return '0000000000000000000000000000000000000000000000000000000000000000' + elif height == 0: + return constants.net.GENESIS + elif height < len(self.checkpoints) * 2016: + assert (height+1) % 2016 == 0, height + index = height // 2016 + h, t = self.checkpoints[index] + return h + else: + return hash_header(self.read_header(height)) + + def get_target(self, index): + # compute target from chunk x, used in chunk x+1 + if constants.net.TESTNET: + return 0 + if index == -1: + return MAX_TARGET + if index < len(self.checkpoints): + h, t = self.checkpoints[index] + return t + # new target + first = self.read_header(index * 2016) + last = self.read_header(index * 2016 + 2015) + if not first or not last: + raise MissingHeader() + bits = last.get('bits') + target = self.bits_to_target(bits) + nActualTimespan = last.get('timestamp') - first.get('timestamp') + nTargetTimespan = 14 * 24 * 60 * 60 + nActualTimespan = max(nActualTimespan, nTargetTimespan // 4) + nActualTimespan = min(nActualTimespan, nTargetTimespan * 4) + new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan) + return new_target + + def bits_to_target(self, bits): + bitsN = (bits >> 24) & 0xff + if not (bitsN >= 0x03 and bitsN <= 0x1d): + raise Exception("First part of bits should be in [0x03, 0x1d]") + bitsBase = bits & 0xffffff + if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff): + raise Exception("Second part of bits should be in [0x8000, 0x7fffff]") + return bitsBase << (8 * (bitsN-3)) + + def target_to_bits(self, target): + c = ("%064x" % target)[2:] + while c[:2] == '00' and len(c) > 6: + c = c[2:] + bitsN, bitsBase = len(c) // 2, int('0x' + c[:6], 16) + if bitsBase >= 0x800000: + bitsN += 1 + bitsBase >>= 8 + return bitsN << 24 | bitsBase + + def can_connect(self, header, check_height=True): + if header is None: + return False + height = header['block_height'] + if check_height and self.height() != height - 1: + #self.print_error("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) + #except MissingHeader: + # return False + #try: + # self.verify_header(header, prev_hash, target) + #except BaseException as e: + # return False + return True + + def connect_chunk(self, idx, hexdata): + try: + data = bfh(hexdata) + #self.verify_chunk(idx, data) + #self.print_error("validated chunk %d" % idx) + self.save_chunk(idx, data) + return True + except BaseException as e: + self.print_error('verify_chunk %d failed'%idx, str(e)) + return False + + def get_checkpoints(self): + # for each chunk, store the hash of the last block and the target after the chunk + cp = [] + n = self.height() // 2016 + for index in range(n): + h = self.get_hash((index+1) * 2016 -1) + #target = self.get_target(index) + target=0 + cp.append((h, target)) + return cp diff --git a/electrum/checkpoints.json b/electrum/checkpoints.json new file mode 100644 index 000000000..573ca3d69 --- /dev/null +++ b/electrum/checkpoints.json @@ -0,0 +1,6 @@ +[ + [ + "e7874ac02e4da5148fe44510c7f0c7242f52b3726992553ebbc8c7f303c473ab", + 0 + ] +] diff --git a/electrum/checkpoints_testnet.json b/electrum/checkpoints_testnet.json new file mode 100644 index 000000000..aaa4ea28e --- /dev/null +++ b/electrum/checkpoints_testnet.json @@ -0,0 +1,2662 @@ +[ + [ + "00000000864b744c5025331036aa4a16e9ed1cbb362908c625272150fa059b29", + 0 + ], + [ + "000000002e9ccffc999166ccf8d72129e1b2e9c754f6c90ad2f77cab0d9fb4c7", + 0 + ], + [ + "0000000009b9f0436a9c733e2c9a9d9c8fe3475d383bdc1beb7bfa995f90be70", + 0 + ], + [ + "000000000a9c9c79f246042b9e2819822287f2be7cd6487aecf7afab6a88bed5", + 0 + ], + [ + "000000003a7002e1247b0008cba36cd46f57cd7ce56ac9d9dc5644265064df09", + 0 + ], + [ + "00000000061e01e82afff6e7aaea4eb841b78cc0eed3af11f6706b14471fa9c8", + 0 + ], + [ + "000000003911e011ae2459e44d4581ac69ba703fb26e1421529bd326c538f12d", + 0 + ], + [ + "000000000a5984d6c73396fe40de392935f5fc2a8e48eedf38034ce0a3178a60", + 0 + ], + [ + "000000000786bdc642fa54c0a791d58b732ed5676516fffaeca04492be97c243", + 0 + ], + [ + "000000001359c49f9618f3ee69afbd1b3196f1832acc47557d42256fcc6b7f48", + 0 + ], + [ + "00000000270dde98d582af35dff5aed02087dad8529dc5c808c67573d6dabaf4", + 0 + ], + [ + "00000000425c160908c215c4adf998771a2d1c472051bc58320696f3a5eb0644", + 0 + ], + [ + "0000000006a5976471986377805d4a148d8822bb7f458138c83f167d197817c9", + 0 + ], + [ + "000000000318394ea17038ef369f3cccc79b3d7dfda957af6c8cd4a471ffa814", + 0 + ], + [ + "000000000ad4f9d0b8e86871478cc849f7bc42fb108ebec50e4a795afc284926", + 0 + ], + [ + "000000000207e63e68f2a7a4c067135883d726fd65e3620142fb9bdf50cce1f6", + 0 + ], + [ + "00000000003b426d2c12ee66b2eedb4dcc05d5e158685b222240d31e43687762", + 0 + ], + [ + "00000000017cf6ee86e3d483f9a978ded72be1fa5af37d287a71c5dfb87cdd83", + 0 + ], + [ + "00000000004b1d9fe16fc0c72cfa0395c98a3e460cd2affb8640e28bca295a4a", + 0 + ], + [ + "0000000046d191b09f7726e4f8bfaffed6c30734afbf1f95e6bddbe0b07d9e88", + 0 + ], + [ + "0000000082cec8200e9ea055c2991bf74560eb7e7140691ea53e7828dbdc9553", + 0 + ], + [ + "000000003775b96d6b362d4804afe2d9c3cf3cbb46a45c3ccc377c94e83edd23", + 0 + ], + [ + "00000000037835a92404acb2f18768a49d4f93685ead30aad6bb3b073f411e02", + 0 + ], + [ + "0000000006cf75d17706d1f62e6b08e6ba5facfde38a8920b7d808a6b6781ff2", + 0 + ], + [ + "0000000003dff257cdae43703fcd0ca91fda0970f5fc04258b4608fb1942a6f6", + 0 + ], + [ + "0000000000532d97d18867658e08c789f627535652382147e33bf8626d4131bc", + 0 + ], + [ + "000000000266dfb79bb11dedd0ae748505863ab3ab731269cd71a2c2fbd159b3", + 0 + ], + [ + "00000000349ff0119d5c0dd8ffad8bf41cd6126a88416148b81fa4dcaebc42e1", + 0 + ], + [ + "000000003c61939b4799eeea4335218d30de9b1071605126d719dce0f0d14810", + 0 + ], + [ + "000000003d9284570ed648d2b12ad24046ac8b9abcf05c4e9813ea110490cf73", + 0 + ], + [ + "0000000001360b66e6dc0ccfbd75356034e721ae55c3d5c71a58be5d281c252b", + 0 + ], + [ + "000000000c114f42504916bfb2ee26ed8307b3f7f74226c1cfe1f5302ec23d26", + 0 + ], + [ + "0000000007acac3fcf97b4ca81821263b704364adaa2736fce0a0722bfed4f8d", + 0 + ], + [ + "00000000059768ef7731d27f9c2be48c6e16d7cb56680625f08ff25ead504280", + 0 + ], + [ + "000000000351c8908f1f52518ce4bd251b896ca3fbccb69a2607db6624bafcfc", + 0 + ], + [ + "0000000068d7ccae048e212e9e2ecb4d944f583b4490df4fbf654b4915597052", + 0 + ], + [ + "000000000e2aaa36417187233ff55325473bd5b7a164b358da60c96d1920fd77", + 0 + ], + [ + "000000001eb11ef6dbe0647bc87a8d218f6e59c2b9690f17edcf0dbd39cd0308", + 0 + ], + [ + "00000000022e7855e24cc3fff67ce093242434a8ffa45882333a0f08a40aad9c", + 0 + ], + [ + "000000000210130ff4e3186258c09a8463c1e196f5c5432b4c7b6954e907bf63", + 0 + ], + [ + "0000000000e01372ede322bf88ee5ed8a46dd4fd8df832eca16180263fc8b1ef", + 0 + ], + [ + "00000000a0701896e26d5d884834b267512e0af52c92edc4bccf1c5c803d3c4f", + 0 + ], + [ + "00000000869fc8d9ac1588f3e5bdfd60253e9824083800b7794010e0e9c6b6fe", + 0 + ], + [ + "000000001d43b3165ec30736f28f0761600b092686f861db23ec38f2d92b0ec6", + 0 + ], + [ + "000000000ef4092da8c2056e5933de0e1530194c3ad941a9b393fbb26f98862e", + 0 + ], + [ + "0000000001e3fed39f70023909f962bea146b03bc8e94e5d19d7da93123f4f64", + 0 + ], + [ + "0000000000b4b8c877bbe3cde97649845290bb78999ecff4621b9bf2ab16aa2e", + 0 + ], + [ + "00000000006095ba3b4742883a0ec427a3fd685ffb65b987ea77ebfedea7da82", + 0 + ], + [ + "000000000168f0a76a6068a34fc042553aff4aa63b906028f28c2a4c327328e1", + 0 + ], + [ + "0000000000af10f3079b4989ac4ff0baaecab38220510cdae9672d6922e93919", + 0 + ], + [ + "0000000000312791ada0f6a4c5eaf2a1cd57cd06f5970a8ab49923817b862c35", + 0 + ], + [ + "000000000055f3d4f45c4d199d9c230cb2cfeb68c8e934cfd061bd616358655a", + 0 + ], + [ + "000000000036b6129bb5a786bfdd75cb4b932f7dcae9da469d3ba35096f1e821", + 0 + ], + [ + "00000000002fbccf271c13e486673251ecd7951ecc12ee73c4390e0ff09e9b59", + 0 + ], + [ + "0000000000314e297a81bf002fc40eb391d8883ea45ee4e782385aa0fdba6452", + 0 + ], + [ + "00000000d3c473819ec3b3c268f7b555df22772e407bc8f246a47cfc579ec61f", + 0 + ], + [ + "0000000075a438fda6bdb391263d0a2a6e8e68edd9dd8f70fe5734eab9351eb8", + 0 + ], + [ + "0000000017ebae0a2bec50008b4a4ea8839798cbd9ff228e76aba087d0ff1736", + 0 + ], + [ + "000000000800466ba31c0bbc12b125f16d05ed27788de045e25d6f093817d29c", + 0 + ], + [ + "00000000002163c41f2264f202e611aeb9ba6c0a3ee95cd8e5e7e571edc64edf", + 0 + ], + [ + "0000000000de9882d417786fce8c755cfaad17f40cda744d4badedfe5e414e31", + 0 + ], + [ + "00000000002af352cf41f60a5ebf033bf7e4967c0597cee706ba877b795aefb4", + 0 + ], + [ + "0000000000009ca0030f1dd0b09cc628f2d4d278c87b20781a1b136dc395debf", + 0 + ], + [ + "00000000ffd27370a76d06a0da0e3805f47e35e2cf584d73d2c5ecaa2e525642", + 0 + ], + [ + "00000000720da6910aa75099baa020cb8db37e1dc19cdff66152225b7609c23a", + 0 + ], + [ + "000000000a5c2cc704bce5e8527ce91bac7430c659624ecd86e6a1bb9b697962", + 0 + ], + [ + "00000000084273545134e9a06483c8fab00c2b0628056bb1967f310c74a971bc", + 0 + ], + [ + "0000000002f66f4da52804647b1c3e1f89d17bdb05e9cd4ebbd922007c773f21", + 0 + ], + [ + "00000000c46146c9d0a67a354b3f82947e52670a3bded6d8513ab34a68ae18bd", + 0 + ], + [ + "000000002f61c429d7dbe7bde75796086efe574998766806138710a2d6001eba", + 0 + ], + [ + "0000000001daf3e3e78a57df2c2d2ddd14093d10515925e75c818bec3bbd30c2", + 0 + ], + [ + "0000000002e133a7427a9aac6ceca969b27507c14111a45512cdf8f52a436de0", + 0 + ], + [ + "0000000000f7c4374d458666740de1d0e8c55229a209ced7c38e38708781487c", + 0 + ], + [ + "000000000035bb9ea329ba30b83eeb4ea6f57c2fe703b97f9b879f21e22643e0", + 0 + ], + [ + "00000000001220503e0aaee266bca85de09ce97b0091f24972d1ad1c8afe8609", + 0 + ], + [ + "000000000010a614c60457f8d2ae2bb826d037f52113252888fadda8ed773c9c", + 0 + ], + [ + "00000000585a8b882ecff8aa8434feeac4ef199ca669bd81ed473e37f0bb4528", + 0 + ], + [ + "000000009504ffdb5fe82ad88218fb5e75a8bc185247e30e22d23b9fd9b7f282", + 0 + ], + [ + "000000000ddec7d73bcd653168d82e34cf5746e006bccda8a9c031c3289b9568", + 0 + ], + [ + "000000000cb6620ee4e8cb8b6b4d51251e5961f7ae2e83538ab3a4fef3bcc773", + 0 + ], + [ + "000000000239224a0841738513c1eda712b73266ea958aa75f44a3985ebfab82", + 0 + ], + [ + "00000000002630c7c3586fcc19079300403c54dc293bcfdf8a9981f85a5c31bc", + 0 + ], + [ + "000000000028d8c34f44e51fd71f5401094a983f6566e6d08ce86ec5d1bd639c", + 0 + ], + [ + "00000000000dca95f1828adc3c37b4625f60aeb35a6614a4358322b7a6bc2f7d", + 0 + ], + [ + "00000000d72ec84fda18959ddc474d1a31a3a13b1d94695136c4810af8c01a0b", + 0 + ], + [ + "00000000327c29604996eb7f0a208160969ee4408a1cad277a956334f94e0f35", + 0 + ], + [ + "000000000e1bd41d009c1910fcfee7bf1cc1adb04b0b7a632ac36c1092f01bb7", + 0 + ], + [ + "000000000201a5afed48b9d095b949229e9882ef8bc96767be3097c87264dfb6", + 0 + ], + [ + "00000000003f28e8f3f9c80b1269bb0aa3b57501c12458550ef04fd43aca6a33", + 0 + ], + [ + "000000000029e09fc14e38a6a0103c8c67383f41af7d76998055682525f4ca89", + 0 + ], + [ + "00000000285ce297602995582ba5d32d583d618a6a92643566e25dd36cf2b7ab", + 0 + ], + [ + "00000000657045fa54fac52b8480dc84bd4c418940ba63679f4bd6add6a39962", + 0 + ], + [ + "0000000017b7bb58be05a47ff7c4ead27db750813d6bcf3f99cbcc35324cf445", + 0 + ], + [ + "00000000003a310e39b6df17f17450496b4f5c1593399bfa1ab8b4d39bac9b25", + 0 + ], + [ + "00000000000bfbc5294f003548a9636ebbcea3ba42577821266317676fbc363c", + 0 + ], + [ + "000000002329351dd70c24da2eea5ac19f65b6053c4611aa4eb93bcc2783c57e", + 0 + ], + [ + "000000004ce02f1005aa6fa4d158c6e4fce95ab053d88ae74881dd080c24e057", + 0 + ], + [ + "0000000000fdaaa54cdaade8cfb75245de0747c60c0307ad11be9fe154535565", + 0 + ], + [ + "0000000003dc49f7472f960eedb4fb2d1ccc8b0530ca6c75ed2bba9718b6f297", + 0 + ], + [ + "00000000014ca604d769d4b99fff03ae3ac84d1e8eb991c5dac7c3cd4d9e68ee", + 0 + ], + [ + "0000000000190ab8ecef3a3d5583563851672d81a4d4d952b8cf3bd503c655e5", + 0 + ], + [ + "00000000001204d263b607987fab11e1c19c94b7e3e674cc73cc2fb7b05fbf07", + 0 + ], + [ + "0000000000141e8d7f7ac359a8ae58e35ce6010c25ddd6f1881f41c0b939332e", + 0 + ], + [ + "00000000946344dd06ef5ddd13fb74f20c475daf911ff4e3f1dcdf64c330e274", + 0 + ], + [ + "00000000ec77a7892e48b85bcbaf404d16d7fc93747d7e9e3ba6195a9b6f1525", + 0 + ], + [ + "0000000018a305c04dea8e93e423ce9569872e0ec5af49d23a0e3872b0ad6297", + 0 + ], + [ + "00000000055e32c5f8a86c9a712eeb6440bbf9810ae6da12d0cea2493138a885", + 0 + ], + [ + "0000000001913fcbe67badbce4234e86e35a1ea867ecd69814b5f5ab039b7d4b", + 0 + ], + [ + "00000000002c71fe4403aee704720ceafd21f9f8c9c97a8bfbd25bb46223aa40", + 0 + ], + [ + "0000000000343a42da0c811836d0785c272591facd816f0e7fdcfb1109d8f9a8", + 0 + ], + [ + "00000000000309b182608b3eea7fafd0d72e3c79a0a3a9cda03cde3947e332e1", + 0 + ], + [ + "00000000000204cc04e421c3958a64d7bc024a474ce792d42ab5b48a5a6f3927", + 0 + ], + [ + "000000005eaa010e7255bd37e0b00780575074a74d889e17c4dbc578f917348d", + 0 + ], + [ + "00000000a0d425f62d9196c069286dc6635ded9d027de40070d397e45bd63e0e", + 0 + ], + [ + "000000003355fd37068ce2d5d2a94ef964eeb9b687f21f4a00850a3e6cc4a71f", + 0 + ], + [ + "000000000ca9148dabe9424cd8c96860c90d836ab25970a3e91856764e2e640c", + 0 + ], + [ + "0000000000bde23f829dde8edef35436be4b8978da21fd2c3a8100ef5334e3cc", + 0 + ], + [ + "000000000028bb26f1427fbfabeae65d55a9e59e18230713e40f0f7c9c2dee12", + 0 + ], + [ + "00000000002ac05422d254e597ee6b5e0f8be9b3e2f887486442d720c7766919", + 0 + ], + [ + "00000000000e36d0b6f187dd9601b1d1dcd987c3e0f6a081ffd039c7c5e32462", + 0 + ], + [ + "0000000000048d7b1f2a2a11fda34a5cfeea067ab03e482931e5a0f463f438ba", + 0 + ], + [ + "00000000f780ab88c8a4f4247573a749fbb087a4e3fb6a7d29926de8a9ab3462", + 0 + ], + [ + "000000000313bbe6a940e6a8c40ba091aa1ebbaad135bbbff3ed8ae07cf574d2", + 0 + ], + [ + "000000001d4ab29721aa2722482562670a0d71dc1eb73231c5dafb64756b04e8", + 0 + ], + [ + "0000000006588bcbdec38d19962b96cf0352cbf1b90f3379cc6787d018cdb96d", + 0 + ], + [ + "000000000022e79539a21ac24f9daa2cbddf2bb4a3125f88a5efc20d13ea856b", + 0 + ], + [ + "0000000000dd284b7fee584cc578a10fbe57e8efe6bf6ebacb23c0ac5d46cdf7", + 0 + ], + [ + "00000000001451143787f411c93d5506065c3fb597966f2fd7a4a5c078ee6aa2", + 0 + ], + [ + "00000000000ca977394af1e414dc1f9d83efa007f7226e11d3a00f59a1fdfad1", + 0 + ], + [ + "0000000000011f8caa80580e7a796bbce5b84e60731bf48e03c6ff5c6bba868e", + 0 + ], + [ + "000000000001705beb1376af1af08b437acef6befbe7d3b60c5fbaf6bb7f38c9", + 0 + ], + [ + "000000000000c838f1f45422d93ca9b5838368a37423efa8439ee24b2bf247a2", + 0 + ], + [ + "00000000000111ad857d31d07fdc8b32d17af2522c18bdaccfef449b29d17362", + 0 + ], + [ + "000000000000312a7718fc616b0ecfdbf6066f71ec1a4a8c43f50f02f61cc398", + 0 + ], + [ + "0000000000007d232b217a59b804ef67091c5720a5460c2c16bf97b97a24801e", + 0 + ], + [ + "000000000000177235c33695aced585685b4c500eb76e72caad02e17503900eb", + 0 + ], + [ + "00000000000037f5c5890da7a8e2acd2b0669ad7db648ac43140c637a1c81637", + 0 + ], + [ + "0000000000002123904063f223bc35135c426a4f9a0b74c1907e837b810f0321", + 0 + ], + [ + "0000000000000961db809da357d91a9341170fafef9f24896d8730bd05cf3f96", + 0 + ], + [ + "000000000d2e8fcd05eb874e98cfc3a6e239f6974950e6f50b0487513ecab760", + 0 + ], + [ + "00000000017e362508c8db23fae0431eaed708d9db13e48fd5d318066bf6733f", + 0 + ], + [ + "000000000011b2bc4fe36f90b7ba5a62f974db250bfdc285b70c71148023c7e3", + 0 + ], + [ + "000000000001be28570b378dd5dd2eb3aa495c229913b6757fe8900dfa3cce99", + 0 + ], + [ + "0000000000242bd0bb16d0a5324e0b4b5a83697dabb3b4a059084557478e50b9", + 0 + ], + [ + "0000000000d8ce69d18da32ed52e503d6b5ad48d970b90545f956b2d2af2edf6", + 0 + ], + [ + "0000000000366655bf0cb3dd0cd7801e0adbd26b5b441b77a9e3642597effb00", + 0 + ], + [ + "00000000000dc7aa00d4607ca8374d40d1187f1c084b620edb45fc39bc8d2db8", + 0 + ], + [ + "000000000003baf60d9c6e70a765cf517f66a124509191188e9547ad09edf68b", + 0 + ], + [ + "000000000000e0f476893b8fb4d37e855353075fde73dbc1fe181cc956349f19", + 0 + ], + [ + "00000000000032ed16b7de758abadf4a4fb2df7a101ff275c51f29e1555a89a5", + 0 + ], + [ + "0000000000000a564d03f0f2fe20f6fb5f038d931f732d817641cd7fff3b0acd", + 0 + ], + [ + "000000000000011aa4d0fdcea8d4ca85cd5d548e322e2b6abd17f8444be855c5", + 0 + ], + [ + "0000000000000610588540267a0eb544531047d4c8af0f21fca7cd3d96205cfc", + 0 + ], + [ + "00000000000002770dab5e14843149df8f76b8dc8458ed3ed2ed8a14a6e2e564", + 0 + ], + [ + "00000000000006b70ebc9f75bd32f466602cbd4b86c3c2d2379059542bb8bec6", + 0 + ], + [ + "00000000000000ef579af389fa7674f98a2371063fa8b218c5ca0ad94e21b896", + 0 + ], + [ + "000000000000021b6108dc988f9153383f9501ab9001109aa87902ddd4c8a4d1", + 0 + ], + [ + "000000000000022c02ff22bc0af5201f0e1a14a75879c494731e4fbf999218c8", + 0 + ], + [ + "000000000000032651c988edc1ccd08e82b888cbb8135e24a958ac0c0b640d5d", + 0 + ], + [ + "000000000000015aefdfa0790bed326c38c358c07aac0674f5b2e771258b8df3", + 0 + ], + [ + "00000000000000822e1534c86afef911b67d3fa20cf2b12d93d20d64005f54d7", + 0 + ], + [ + "00000000000000338b871276768c923b1c603fd6150bd054c2287e532e61de7f", + 0 + ], + [ + "00000000000002d0af52c0cae894bf836b61137ace2bd7500abd13a584c02741", + 0 + ], + [ + "000000006f8443a458f38d8731821c07a2fda0ecdbb1cf797f541844d468ce0c", + 0 + ], + [ + "0000000000b6fbd8b4e227f5514979a61d8b0b918d2adc154e585ca926386704", + 0 + ], + [ + "000000000f4f5e49b10278e27d9dee15b92f9d4a257138a206831e0c00188767", + 0 + ], + [ + "0000000002c7e9769bd8ae9906fc5682e937b5c31ab5b5b86e4d70af2c15a95c", + 0 + ], + [ + "0000000000f68a1db8cd387e0a2f93f45149fe1ee4a230bb386313bdd42058e8", + 0 + ], + [ + "0000000000f0f65c360c8f0f9853ad1142f16675dc1175d61afdbef977776b25", + 0 + ], + [ + "000000000004f734e634156511cbef7dfefebdf317e7488aa6c2562572d7ecb7", + 0 + ], + [ + "0000000000002a46a7a16787e8317dc567ae26816324c2035be0186ba54d5cb8", + 0 + ], + [ + "000000000001a593e6f01875b77e270163538d88452779bb557df7c2607c28e0", + 0 + ], + [ + "0000000000004f24cfafa10bd50a452535f64be577a6161e51c7c71542f654c4", + 0 + ], + [ + "00000000597cce73e84b63f08cfcb9b01f5e7621752d8c8e08fabbd6ab5c0dd5", + 0 + ], + [ + "000000007cad379df01247771fff471bc99faea1b86218602f45ab13efc5e9f6", + 0 + ], + [ + "000000000d6085aab25892be49c49d6c0a3949befdc3ddce2faa46b104e1e804", + 0 + ], + [ + "0000000002be5996786b42d6a229093896aea9966b1854ea261e01e84da1f420", + 0 + ], + [ + "00000000002684b72056e270b115d80b12b2f68eac7412355287226aecd9b5e0", + 0 + ], + [ + "0000000079ea27efb24366c87856a9e371c56fcbd59d09d3164a5c2fc15fcbca", + 0 + ], + [ + "000000001694120525dba4548ca54087544da1fbefa51c38f0208d683418825d", + 0 + ], + [ + "000000000693e80d372938f3553151ab9d0a9a6922182591c701df739dc9a502", + 0 + ], + [ + "0000000002950d9cb23c8511937811910b712f73d448e6fdc2e39e029b86848b", + 0 + ], + [ + "000000000091c40056c6a48f33db17764af89c01f62ae653aa5e494146164cee", + 0 + ], + [ + "00000000001f373c47e1a39af4e1ebcd8c88411ec49d6bd520c2781564070971", + 0 + ], + [ + "00000000000809ca4b2170c57958709b867095b1972d80a2ee55359fbd0940fe", + 0 + ], + [ + "0000000000038e7bd66fc3308447b1370dbdd0661c427c512bdbc641ff360fb2", + 0 + ], + [ + "000000009a3325df76e2de1fc1970cc2f241fa8a41da9ad745a0d9666d9ff51d", + 0 + ], + [ + "000000003176e92ff837bf43a48a995c1a321b166475f586ffb4b962e0254a4a", + 0 + ], + [ + "0000000001ae3292e81ca3859b75bccd5bff825cd9f496efd085160c716ed05e", + 0 + ], + [ + "00000000033bdac4f0d36bb912fba28bb5caa54d1b611759a10f79ff3c969cf2", + 0 + ], + [ + "00000000004c6db7fa0e2c9f08693abfeb128c5827b511a5c46c623a103b416b", + 0 + ], + [ + "00000000003d87f48bb95e9431760d0c5f4f93c77d02fce9dd1673e9f5b01029", + 0 + ], + [ + "00000000000e214fc3d8b97571eb75d248ca29f8e25a584c33de8488ceee72b0", + 0 + ], + [ + "00000000000133269b7159b828700d02de770a8cbd91f3d166e6bbc95d8e0dfc", + 0 + ], + [ + "000000000000cc92e2dd933a08f7fd87f84451627982fb66583587858217c059", + 0 + ], + [ + "00000000000030708136c20c4c8216314005b3cb5c551ded33b26cf64d2ff47d", + 0 + ], + [ + "00000000c472a1341d479ed02f31b699e448c035049a7092670b38f4ec6121f0", + 0 + ], + [ + "000000000a358834d6eed41b9b7161a338aba53828111414cdea7552ed15548a", + 0 + ], + [ + "000000000e13e77372daea775c8358916e57ed11835899c14e5140ed9be11089", + 0 + ], + [ + "00000000008252cd0931f94b2465bd4f93e4bfeec6697962c5b034cf3d12cf7c", + 0 + ], + [ + "00000000019812cd6cde3a43831234be71e68118be24a80161349b8b327acb5b", + 0 + ], + [ + "00000000005865499f301adfb59f8380743e4c3b3ab220ca4eb97dc6628df626", + 0 + ], + [ + "000000000015f77e1e61329560a4378eb401fa5bf0ef90b0a014a4d7857ca7a8", + 0 + ], + [ + "00000000e9cbcbb625e8a463ba8e7f14be46ba9538ffe93338784ccad3d992e8", + 0 + ], + [ + "000000000fb27169efcc2873cfaac223ebb91cc5e1e5ad7e9a312d42bedf7c42", + 0 + ], + [ + "000000000c9c96d62ebfbf3fa4003f1d46d175140ab084dee17e8125fa40f24a", + 0 + ], + [ + "000000000311e3a766b1ab2064b68a344a561eb496d595126808ffb166c71cc1", + 0 + ], + [ + "00000000677568c82262ac3a4ca3f909bdfb0b35145ad490fa3fbdc719d06b91", + 0 + ], + [ + "000000000ee77ba9ab657e51fd9140f5c9b46731d9341e98188f929c97d04746", + 0 + ], + [ + "0000000008a67eb9c91a6d74168f3f385270fa942ea00bdd31924d1b6ea11148", + 0 + ], + [ + "00000000017f93c9e0026e90d579e18c83b4a8557f0c00e9b85ab164cf4466c5", + 0 + ], + [ + "0000000000994efa379235c03711a8e6b29895d928b5fde96cb01c02374c0602", + 0 + ], + [ + "00000000b3be9f23c943d71d7c7dbdf6dd672d77a712f6c83e9796a85e4379f2", + 0 + ], + [ + "000000000713e1089b0b2bdcba462b740c9396f822f1c73e090713978a7f1314", + 0 + ], + [ + "0000000002fc44d358401a7ac9ce4ddcb17f3cbac08e40242e755e60ab2292ed", + 0 + ], + [ + "00000000021ef2c04fd30be7049f73b9a8353ac96a467dd5f0b9c1457be1bc5e", + 0 + ], + [ + "000000000023b95b440ccbbdcb914172cf675cd15d6111bd7f5a436a4925d36e", + 0 + ], + [ + "00000000001983521dbffd1b742a6d4b5dfda3f46579fbbdd83a2ebf9a039bec", + 0 + ], + [ + "0000000000044d53dbea312432e68fa90dc2148946f613216dbdeec86f6a67c1", + 0 + ], + [ + "00000000000107667692f12d21a55a72ff1dce828f96872e36c35bfbae475a8d", + 0 + ], + [ + "000000000000252d1d0c01744ec25af801ef7c57e2581c95295070b6a8a85bd5", + 0 + ], + [ + "000000001c1da54e16dc06158677024d9e74bff39bfaec83434ac33673fcc251", + 0 + ], + [ + "00000000b4d0c6ae86bfdf7ba4c205fc3e6b3b6d63836b85e30e9d8bac922301", + 0 + ], + [ + "000000002b16179cb022bf678bd847dd6fc1908d0df04abf0c7874981eb33ee7", + 0 + ], + [ + "000000000e6783554aae41856424d184dc4fa061f40676efd107e6f933a25641", + 0 + ], + [ + "00000000005ae4acbab519895b4b523d97a09e381c9e4b044e642f73b8c0f1b0", + 0 + ], + [ + "000000000010372b59c9595d947064804b75ab21868dd075a3842ab7d2df6181", + 0 + ], + [ + "00000000002f9f587ea304093be049d3142ac0c92f9c68928a4f82d12b929b69", + 0 + ], + [ + "000000000005d4cae51b3c76dc3c61bed0c265c4f228c0c4d1d3d147146c34eb", + 0 + ], + [ + "000000000001a5b6c0e0a0b485a490cb52ccdf9b22596656039b51545bb07be5", + 0 + ], + [ + "000000000000d723d0976338edf55d08edab995dd6283cbb688855f0dca6e8f5", + 0 + ], + [ + "00000000bfebfae90208a82c7fa06c0f61674dbf1e4f9162e370656c38d611bb", + 0 + ], + [ + "000000000c91cd144b2a92ab5024c87f70cc1d76a4a7f26a82a98c5aaad62850", + 0 + ], + [ + "00000000077c8114eb5cfb69c3924c699d0c70334360dd1daa95db0db4816953", + 0 + ], + [ + "000000000348a6443e091db8f68e88a10afad7c6e3e5392247902c4b4feade43", + 0 + ], + [ + "0000000000d63b70351e05829ad8a56336521b361b0d50eb7ea1f5b46c25b00a", + 0 + ], + [ + "00000000004658603163f0ede572120a1bbfce8d313aa282ae54d2ffd9fe9079", + 0 + ], + [ + "0000000000048063b410c793db34856f23acfb19a0ce72f5997fa572773378c8", + 0 + ], + [ + "00000000000228fb6e587fa593ff8b4764064bba8bfc2f43ba5b1f12af33d04a", + 0 + ], + [ + "00000000000082e3ddb75c0ea2a98922b1556ce10346f9bb0cedd97ccb3fdf62", + 0 + ], + [ + "00000000000005571b54d4886b44b81c21dfbefa554cd7c23430e5aeff6b5ae2", + 0 + ], + [ + "00000000306a603ca1a0d961e08e103a9f13f3615163c3373d1bd2a67cadc2a7", + 0 + ], + [ + "00000000195d93ba7ae19832b622de86ebdadf3c78f1751ef2b2e9b0e3a530d8", + 0 + ], + [ + "0000000000476d0d00cbc68bb20b4893f0e608b02a1e029b8c6c73e169c49e69", + 0 + ], + [ + "000000000051348044bc10fc05960c244c3ccd3b3b6c145ffd9958a1c8bc0215", + 0 + ], + [ + "0000000001e4df369203badca9aedc28c240d592b12d284ce0b0463fc7537c09", + 0 + ], + [ + "000000000091cc1ccd448b0ec9185618a84dea96f52477cfb9b9ca2b60cebe83", + 0 + ], + [ + "000000000024a50299c0ef0c6dec9c64336b6cf5c1a1b0013e22fd4fcee1d7d1", + 0 + ], + [ + "00000000000349248c1df06c3783d1270cd97ce7f605b9036fca0fdc2f0fbb96", + 0 + ], + [ + "000000000001afe6793e7427a3d780876d26eb7f2ded92563f991bf7302aea69", + 0 + ], + [ + "0000000000007148006e139e24d9fccc307661c9a0cbcd1af983487c2f0780c9", + 0 + ], + [ + "0000000000002734722a341984738177a3f6f264291424e4984f2128d921bf29", + 0 + ], + [ + "000000000109b02caaa95e49a477757a41a42daed40e92f54fa09e63f5538cd2", + 0 + ], + [ + "000000009a11c7ff8b8fa7fbff5a04c25906f701ab5bd67195736f9ccc839ab9", + 0 + ], + [ + "000000002b1d77f8e0cd60af1c62ef6d381e8905665b15a7fbc546d0c1a45e18", + 0 + ], + [ + "0000000002588cb017de9e2f23cea7edc5082f1b3faec890f9252d556efeac40", + 0 + ], + [ + "00000000008b07f177adc24a4b1a64d2dbcfbcc903ba861d493e11d6b33af7dc", + 0 + ], + [ + "0000000000bab8db5020aa8e052165275e8eb3e7c843533246bf6e4c8374757e", + 0 + ], + [ + "0000000000138488fdca8bfc327e6dbd6c72c5f1dc5868d9c0ea886665b9b56b", + 0 + ], + [ + "0000000000094021fc954efbf08be667fef1b817e8715d4093a561fc30264aa7", + 0 + ], + [ + "000000000000e8183e64072db79adfc6c09b650c4178001be3fade4050b06005", + 0 + ], + [ + "0000000000004c93e8661c75974cd191c68dd66999da4f70d039c0ba4a12b970", + 0 + ], + [ + "00000000000021c675b3ec404bb996f5e68f9eeceeac6946e5a6822987824d33", + 0 + ], + [ + "0000000000000ad85684d30f25d1ec34638f099df2f33b418a07307c68fe3c2d", + 0 + ], + [ + "000000000009c6add76ac42a1942c4ce74d25d1b8975d4e3ac8932185e785a44", + 0 + ], + [ + "000000001e7d828d354716881683eb6fb5caec5d91afce298e4e3bcee9574924", + 0 + ], + [ + "000000000a0e438ab203d8fd3e56100f2f14759f704bff6c699df0bb4e9aad64", + 0 + ], + [ + "000000000b7d5c2895df8bc1fdf5d31e0f663564cb5cff3b18642c44a71b6248", + 0 + ], + [ + "000000000193209ecd92fce00a75975446423d94a325ed525c15d5ab921da273", + 0 + ], + [ + "000000000020835bdc30ac67efdbc785d15186914bc14e86387f97450df46418", + 0 + ], + [ + "00000000000c9078321f0030214c75e170b01ec664d39bab1b1e48460a54eb63", + 0 + ], + [ + "00000000000ac68b63d486ade190dc9108eb3730d25e7537649fe21c30e0121f", + 0 + ], + [ + "000000000002a94dfc5f4b677b251a7a7647dbb99c0803df8658222227fe3e3f", + 0 + ], + [ + "000000000000b076bbef0e50593b1595ffb3d571e7ad95dbdf06dca8824ef7f3", + 0 + ], + [ + "000000000000167075c8bcd24233d25cd268271c0e8fcb6f301ee1b6f6ff0341", + 0 + ], + [ + "00000000013107aa587bcf12ac445330ff0325d73c5253f7e6a49ed8c50257bb", + 0 + ], + [ + "00000000090ff53d49c9ffd51511af8d5cba2038a8e25e3b17186b1bc941f43d", + 0 + ], + [ + "000000000d9e704d5607f77f8983cc56069571a3761d5bd5da55f05ec5d8e844", + 0 + ], + [ + "0000000002b2b4c0950fb6390f0ae860840e84eb0a82e5e8a9bc37c14bbf43b0", + 0 + ], + [ + "0000000000be10137a2434dce1d97850b768ce878c1c80ec905f6e9f21e65fa7", + 0 + ], + [ + "00000000005cd966f80183d4c048e63a5c14f649298dfd261d989d9e3c026bf4", + 0 + ], + [ + "00000000000e8f30e55006a4082380c4b1a372b7ad919d3a9b0a52fe5ee881d3", + 0 + ], + [ + "0000000000018c70a4c27bdba237ad19ebae5d3ca23f1394ccc746d73669a1c4", + 0 + ], + [ + "0000000000022acc8432c883953227786f7a6560aeaf0176d232c8affa5b25b4", + 0 + ], + [ + "0000000000001854e95b28b4efcb2cfeb08c76d8cf1fb03f2055b3fb758f3a1c", + 0 + ], + [ + "000000000000187080c2c39f5a3ea8be72ac4d3ec0d16b21cd34f1541bef23be", + 0 + ], + [ + "0000000000001593766a3c63b524f658ec7690df467cc7bbcebbdb56385500d4", + 0 + ], + [ + "00000000000012d6966dc51a41f2c617192169ec8418405e164ba83b9f7ecdfe", + 0 + ], + [ + "0000000000001d0c7d0a2605e127b00448b71e756ad96625116ab8ca18f74900", + 0 + ], + [ + "000000000009cb439ea49282d257595ad1f7602856c16cc26fff423f7783c792", + 0 + ], + [ + "0000000000889282b98336c994d7420a639221e0484b511227fd616d78dbd028", + 0 + ], + [ + "000000000071a4a2ad6767864bd21239c74c9912a40ca9fd3b209e21b66460d9", + 0 + ], + [ + "0000000000f3ed2c3c9a7c3a7291e859cecba8cf9243d23a4892e6be8ea9b70f", + 0 + ], + [ + "00000000006a4258ffdff8b7f6f4f685ce18c6eb1d7a1cf501ca9e02fcb7620a", + 0 + ], + [ + "00000000004af78f1a109d1267a9c24d69c6a4b30fea49f0efa6c8834cf394f9", + 0 + ], + [ + "0000000000193bf3efbb145747198470a81b2cd33c991057676742d5c22a64b2", + 0 + ], + [ + "000000000006b436798c7e4a8c3bdbf054a66707feee5a18ce9ca57eb95bb48a", + 0 + ], + [ + "0000000000001db50c7caa3a02ea4f173343f958f334a8bf3f8638add9e69b34", + 0 + ], + [ + "0000000000003c621629cc0bcec5968d61d2e42c6673de4d46555118ad5001d8", + 0 + ], + [ + "0000000000001262bef2918265f6dd4534013a4650444054fb4f5e490c5ed57b", + 0 + ], + [ + "0000000000000120ceee972d70cc84430006645997c7337976c673bd75cbef2b", + 0 + ], + [ + "00000000ba16134dc0c418a116b97ad5deccd6bf6e3daa028a8a6a80d7823faf", + 0 + ], + [ + "00000000a1a00d6d6fe0660e63402a5a7c7248589211594d37fd800456ce84b6", + 0 + ], + [ + "00000000394766cec78f962c29aaa715b66e3ad34e1f2323dba45e087cb3b395", + 0 + ], + [ + "0000000008b15a3020676f5e084210ecc05f646885eca1cf6a10e9ae9e3995cc", + 0 + ], + [ + "0000000002cf7eb98abe784f6e516670a88b9028a6faabfd099a364c2dc5c42b", + 0 + ], + [ + "000000000054015fec337a9ee43eea501d2292f031f5bc1f09758d20f5cd3135", + 0 + ], + [ + "0000000000068d24d31a9f1192d848155a2f90939627bc456c9a337135a923fa", + 0 + ], + [ + "000000000006262bd09358258edcc455f9ba46b7f9d6e69d0f6b9da89488a4a5", + 0 + ], + [ + "000000000002327bf77ae67961463ea98a78dab06c24ac7d58b1727c5f856626", + 0 + ], + [ + "0000000000006672235c1606fbacd7861b16b267d203b4d687708eeb1fc25e6d", + 0 + ], + [ + "000000000000ac0c9a39a47313a8715f125c46d6ea8be8741b99b1db4a8aae47", + 0 + ], + [ + "0000000000007e93f6578e7856aae0ecf6341e1312664d9e1d812ff254c37ae6", + 0 + ], + [ + "0000000000002a980acdb1443926875e7d4a57859b2b45ce3fa92c7716319f62", + 0 + ], + [ + "0000000000683bfd82c63514bc58a80daf699a6bcd040bb2a499540baf52463d", + 0 + ], + [ + "00000000373e6262928d7a6cac965b294aef35f90b72c85100ef91501775e06a", + 0 + ], + [ + "0000000000f7bc44061b65c62d4d7747138df127dd2a30f583c3ebb66a25c7a4", + 0 + ], + [ + "000000000212a71c38d0e13ab7c5646c949d4b7ca23afedbe351a43b7607043b", + 0 + ], + [ + "0000000000a836e88f76ee5dcca1e884572f32f4460a3b024280738d76e98ced", + 0 + ], + [ + "0000000000413f6c1b1c9841961636bb3290f2410ba0731f3522c4ff3faa2e0e", + 0 + ], + [ + "0000000000082336107412226110ab2a53016d4faad4deec048828507a300248", + 0 + ], + [ + "000000000000a91e7a3f35a23f01621dd051e314da617714991467131808d3bf", + 0 + ], + [ + "000000000000cd6576950f6f238227c3ba7f62405ed1bf3af4878c6dc1b04635", + 0 + ], + [ + "0000000000674099e9741e44da03e9531402a2607a19a65660b57470340828db", + 0 + ], + [ + "0000000030c4744001ae85f9e6b46ed0664449927b86b8fbf25b22b851d23671", + 0 + ], + [ + "00000000002f5095ad1a12eb9eedf88ce1e7268368461b6b4e10051148f436cb", + 0 + ], + [ + "000000000057d3e2a77eadb8b9613cb839ab02a96094dd5d0a6d1f09026c3936", + 0 + ], + [ + "00000000004e0a28be887d6ed037cd9102cbbda7d6c9e584ba51f2c2dce96232", + 0 + ], + [ + "0000000000211346d8099f7ecea72481c4cd45591f5e0d7e347725ac2162f142", + 0 + ], + [ + "0000000000199ae9fc06c5acee766db6033b86f76c266cadefe1461c611c2198", + 0 + ], + [ + "00000000004c9e5748558d4f5a75bc824171e3b958152dfd6844330f1e907f8c", + 0 + ], + [ + "0000000000137addf1521361dad1ee007eb9e6dd4eb8441492ebfaa3c240d556", + 0 + ], + [ + "000000000054d4c77bb7964e5327c35760d87b890ea336aec5ecdeb783350738", + 0 + ], + [ + "00000000006b7b06d04818e97a4df66164b471912f88d9cd02de4af6c8bbe74f", + 0 + ], + [ + "0000000000380fa9858e3e90335c061a3776a26bee1e8b6851de33ec63670782", + 0 + ], + [ + "00000000000842598b03fb79ce7386e9f9181a02dcf1effc8f70d3ff7368ccd5", + 0 + ], + [ + "000000000003d3475edecd733fc7b82432882d9c9f1350a98ef8921b87db4dec", + 0 + ], + [ + "00000000000000e330a8d57a38dbcc0b0a5dc7a4210f231b8082b9be5f9e4bce", + 0 + ], + [ + "000000000000218ff87fd50cfba2fd04203a78d2600cb2c4dcb039d803426e19", + 0 + ], + [ + "00000000007c96e6e3ed3146260348ac79ea7dc2ec2ae6bf8dc203400a37721d", + 0 + ], + [ + "000000005abaa10bf7260470c28ba32f1755b4cfd3734aad580681e39a9605a5", + 0 + ], + [ + "00000000005e77c226e6fffccafa56055e68f0ea0a30101e6a243ab9b3e07db0", + 0 + ], + [ + "0000000000e989fe27f85b89c1e852d7bc94b09033cc6c8b32fbbbd9383a9ae1", + 0 + ], + [ + "000000000091a1e962438583146293ef34156962445ffc5e81e4d0fe327d37ac", + 0 + ], + [ + "0000000000477978a6903217e2817d10e99bdfedb4f8bc396b96fd5b0b93b522", + 0 + ], + [ + "00000000000bfd9e5f13a9c03c48e8b58a937cf1ae2849160f1ca11f8fcced3c", + 0 + ], + [ + "00000000000158dd3c31b6379887b4353ef2898c03b7ce55458fcd57cb6f0639", + 0 + ], + [ + "00000000000029d7009eb56b9d38366005576b82a9b59fc845522a34ad36a38a", + 0 + ], + [ + "0000000000e6e207a82b8ad7136352204bb8e9ccfcd25885a715d3c65cbee997", + 0 + ], + [ + "0000000000fadc4429f50fc534ccac4db5e51a313df25034d6c5c25f7e83448c", + 0 + ], + [ + "000000000019c58defcfdab6c6ab9497685e61118effda4c2613bf44be19fcbd", + 0 + ], + [ + "000000000006cf444d846093c5045d42ddc0986ca805f261476d0fd2eb474c39", + 0 + ], + [ + "0000000000d0856a3d6a1e5b1ac7e388cc029bd8410b3b1489598974fe470568", + 0 + ], + [ + "00000000003d9aae63ed532b78082ca5386211e22410fd24ebd5318d1a4cd1da", + 0 + ], + [ + "00000000000345003879f86021a6d5e3fe93813246818c145947b7e225691177", + 0 + ], + [ + "00000000000175393730cde3e49de7af2b81ae736eee005a9f9c4a1e878c52ec", + 0 + ], + [ + "00000000000087a8c621c879aec2a897258632d6aa631b9a38ba4d564e08682a", + 0 + ], + [ + "0000000000002ea641b2975935bd9caf337b51ac9f9bb90a54f6ea6ee5d3112b", + 0 + ], + [ + "0000000000000c544f9b6a8cbab6d25caf949875622bf75139234850b10affe1", + 0 + ], + [ + "0000000000000f66fc4e37232a29f3389c493863a980d58a1d570eddd5268999", + 0 + ], + [ + "00000000001213fe2bbb8aacb1fc14983586e09db964151cb507956a81b35f25", + 0 + ], + [ + "0000000000ba82c2160602ddc1913bc4c133ad0af8848e014367c84110d00e05", + 0 + ], + [ + "0000000000b7a98b364b1cf9521275a915c7a1b3a0f0c052c7d8efb620ec0870", + 0 + ], + [ + "000000000047dc62db23540ab4aee43e54812aedb623a2a158aa3244fc784722", + 0 + ], + [ + "00000000005291002da10e53c3855882251a6e5a425b5e639ef9be3bd05767ca", + 0 + ], + [ + "00000000005ffbcbc0d9b380584bdc78050a6f0c3582b4c9c5103a150cbc71f5", + 0 + ], + [ + "00000000000a7a69cc06b0a68b27a8fa5d29727ec3b6db8d32d61cf7489b5ff3", + 0 + ], + [ + "000000000007212eb8c49758d98cefaa6098da2b877a6055be341f5f7c0ad301", + 0 + ], + [ + "000000000068d1099d8cf3f43f6d164f2925b1d52ede75640cc65ca020e1de1c", + 0 + ], + [ + "0000000008d5ddef4468a4414bd08184c2eba0ec536b85a743b1091828a6a884", + 0 + ], + [ + "000000000acae40db93b589783b0cde70b98552955cb3c12f08de1b417d9008d", + 0 + ], + [ + "000000000066a51eaa3a54036f338719da3d5779180c0bc3787b533410de90e5", + 0 + ], + [ + "00000000008b521677a6e897950aac69640e52efb01b7af10bba3820ecd09a89", + 0 + ], + [ + "00000000001823f0e399311cab0fcf57403e094feebf99b22030bafd2004da87", + 0 + ], + [ + "00000000000bf821c2abf5bcd00ca96439ddf5b0b593be5601145fda5338efdc", + 0 + ], + [ + "000000000003f4fd19b2af0141289177014ecc6dce6ea8fb50bab93d4a291095", + 0 + ], + [ + "00000000000011842d892a02e55ca594caddc9f3cea1979ddffefc070eda8498", + 0 + ], + [ + "000000000000208aa0259d20f51c0e7b8895e18a93aea79af9b3832e710ef134", + 0 + ], + [ + "00000000000007218f849e72dee1f7fb6fcf36f3b6745c6468187ed2ed13287f", + 0 + ], + [ + "00000000000f79f656cae641c2b74554c6ecd673c0c7550671c4c2af940661b3", + 0 + ], + [ + "0000000000199b4d178c05fd1c3154c9a4632eadc7bfc734c4522176c977ce8a", + 0 + ], + [ + "00000000085d0682d481635cb2e6de2e4d9884589455a86194f0b222f9acb3c6", + 0 + ], + [ + "00000000015972a5a6786a14b009bf582c4bbf7b9854591dd8d26f82b43ddaef", + 0 + ], + [ + "000000000064bf72b7bdbfcbe96dbbd0efcaf7aa94c0f92cb4e6662819468fe4", + 0 + ], + [ + "00000000003df36b7962bb4ad62266c462382eddc93f4bfeac464b95f7a89ee9", + 0 + ], + [ + "000000000006516d3a9f424eb61db5dfb85aeee29708b78c65d24827bd926263", + 0 + ], + [ + "000000000001c1709fe1b294712638db356e89155650f6fbecde79ec47a92af7", + 0 + ], + [ + "000000000000dfc23251344b593c16c28cd195abcb337519d7bc82175721a033", + 0 + ], + [ + "0000000000000aae2dd2bf0b8581d137fcfa3d9c4cadbe3ef3834d7cae4268c0", + 0 + ], + [ + "000000000000092a5baff3d9a5ae87689b2afe668e71bac3b342c7d383f0060f", + 0 + ], + [ + "00000000000fa906eeff7d2e126698d88b8cda01d32ea2c039c26984daaa17a3", + 0 + ], + [ + "00000000002d4315e5bdc2bcfdb245b914130764a50943a2b2e02ea3acf5c47b", + 0 + ], + [ + "0000000000fc2bc9bb83e04cbe922d64719295bfef6320027725402306bcf1a0", + 0 + ], + [ + "000000000142690e7c334b97612746d6db208e6153bdfa8479d86d1b575feacd", + 0 + ], + [ + "0000000000629a7820e8cdbbed18dcfe16c992152badc745ca73b9b34e53fb0d", + 0 + ], + [ + "000000000023c2e9dbf3fe03248e40f4ec3fb2dc81ac573d5a6a4f490c701877", + 0 + ], + [ + "000000000013658a43b6d1c4be95fa36e32d3edf80716de3a8f7e98858016adb", + 0 + ], + [ + "000000000007c847295d8c4b6da9d8a64b57c3a2307e64387bf8882b9d35d6de", + 0 + ], + [ + "0000000000032bf90b823332af80bd2ea18f411f081c7dca8f2fe79d9215526b", + 0 + ], + [ + "000000000000001bc0655da6f24c6952e811006897a0c6dd8b6bd94f178636c8", + 0 + ], + [ + "0000000000001e1d09b15393190cf686e25488db7fcbc2f1ebacc8165fe6e3a0", + 0 + ], + [ + "00000000000cc79ae066badb4157def4067057cefd705bf87f1d832845a7ab36", + 0 + ], + [ + "000000000014408398244b94b4eff6b54875802ede6df2d1d21915333a195719", + 0 + ], + [ + "0000000000114135a1bc757110c05162fa649b694db9569be117e34832c87257", + 0 + ], + [ + "00000000009b15fb2bcee1af904989ba0761e4cddc6b3ee214c0bb07dac6211f", + 0 + ], + [ + "000000000012be506dde2c54adf355bdb41a457b0abec436202a3be73f0b052c", + 0 + ], + [ + "00000000000963760ceb5fc65570650d494805e05c9d753f3ea6d44247ad3d08", + 0 + ], + [ + "00000000000bfec54977673f68b6fe5f088398e697d778fa7987f8bab6a70825", + 0 + ], + [ + "000000000000e7f428bb413c17032c0031af0d26133ba93f744a5a0c16cf7e1a", + 0 + ], + [ + "00000000000036bc80378323c6eaff8ab350b6d89955f602960cb7c93d2feb4c", + 0 + ], + [ + "00000000000f0d5edcaeba823db17f366be49a80d91d15b77747c2e017b8c20a", + 0 + ], + [ + "00000000001ff8fd57798082ab5a7452ada211e1c3be38745155505601498829", + 0 + ], + [ + "000000000020f960b535eac585e5810ad64f158c1142f0eecd925c8058172933", + 0 + ], + [ + "0000000000067bd89409368d221507a160e5c45972eeb01efe210054fe8e7d85", + 0 + ], + [ + "00000000003521f2d5ea3232d4835ca6c6bae083ba90458f67d4cd765ce93b09", + 0 + ], + [ + "000000000005ab3ff3a0c484eff7b571fb78ce27d93f77a480074232e5ce0c1d", + 0 + ], + [ + "00000000001048c9eca7cc1cbb86946c04498052071f7e7c775bba565ada337c", + 0 + ], + [ + "00000000000154caacde41be616f924d7d478812148242fba85605eefec9ac61", + 0 + ], + [ + "000000000000c34f75bd6f338c0206a31a8d5021cc2ded51e88a6ef4fe686d10", + 0 + ], + [ + "0000000000001e0581d86c49a6ca14ba88639ef908abb09210b57989e06b1a1f", + 0 + ], + [ + "0000000000d0e6dc0bf830b50bde3e400e16ec4f772f92a55390e62d4aa73af3", + 0 + ], + [ + "00000000069c2501a2f32cc69af72a602ff674438ae04dd05516f72a71b9ab26", + 0 + ], + [ + "0000000000c926b38954550c9b8d363ff058c2eb135eebdb3e640cfa67df803d", + 0 + ], + [ + "000000000011e9ad9c18e9e2095c3662af5be1e918dff653758583aa45dc8197", + 0 + ], + [ + "0000000000f311624ff4dcdf07400d0d2fec8b16b14c1c16babc377a2d85ad21", + 0 + ], + [ + "00000000002e455cabfdc2a8955e8ddfe717b12efe5b80937b0c0ad6ac977fc5", + 0 + ], + [ + "00000000000fed8889a22339b340f599ac7908e790bfc3cfca9b78078a52d228", + 0 + ], + [ + "0000000000012ca4492956b3f859b00e5db14b54d422cd95c68c7150743db365", + 0 + ], + [ + "0000000000004c58e8f7bac59eb4a036764a4d8e0da51c0290858ab14fb72481", + 0 + ], + [ + "0000000000002f60bc99563ff5b4b800c176fe8bde95e8f968fd6b53d74c9cef", + 0 + ], + [ + "0000000000000bffd10a3fb0b5b86d8b2561f39d07f8a4c41dfa08e3e49b7db5", + 0 + ], + [ + "00000000000006a296be9cd8fd4e3145c146863adbe08b71831abb8a869d032c", + 0 + ], + [ + "0000000000000c557f496e82891039ff22e277bd604be6e2e8b95e519bee91f9", + 0 + ], + [ + "0000000000399b30d2111c4bf3051c1f7f2f35bba7ff290d92393341ae47df55", + 0 + ], + [ + "000000001f88733439e4e8d3c474504aed62037faa16f3845b4c671f69732e26", + 0 + ], + [ + "0000000018aa2f93d2ab76a7e2f1bf5b565b4a1b0ececb6ee46490984f6c0d4b", + 0 + ], + [ + "0000000005e22674fcf65ce7be896a0557205ab26d1f76d73a717f5f14a6d6ad", + 0 + ], + [ + "0000000000223d866b324c097973210f8fc715c9535908359d61d8e1ab2f0100", + 0 + ], + [ + "00000000002b321fd6452ab43849bd7a781953ec4485554e0fdc579f2a52c90a", + 0 + ], + [ + "0000000000173132748c51b5754b0341232325bd118455bf3c8d25164d3eb92a", + 0 + ], + [ + "00000000000143158cdea5fbb9453bbe1a7a900e6feba1e2193e4f5c106d9fba", + 0 + ], + [ + "0000000000014677751456af5630025b3d9921a4eafb4d36a06498f0c6a84c56", + 0 + ], + [ + "000000000000243976cf2d30ecd3cb1fd0b805fba4da92d2758f78e1c6f8ae92", + 0 + ], + [ + "0000000000001323db1ab3f247bcb1e92592004b43e4bed0966ed09f675cf269", + 0 + ], + [ + "000000000000017a410c22c4b6caf710f5ccf005d644caf276ea8626a538798d", + 0 + ], + [ + "0000000000170b2b1374e3a0dfdce2fbc5e302e1e0e9fb419dc057c9959902d1", + 0 + ], + [ + "000000000015b4fad4d929630487680cda2d3aada138c58cc08241ef6dd4ab09", + 0 + ], + [ + "00000000000abebab869f1620843d413a3d9e06dc7d9f5201a414d547ace1f99", + 0 + ], + [ + "00000000000b0bdaf05c2fe8b12ebd2372f49d8eabcfbccdadd68b5e5b7c9565", + 0 + ], + [ + "00000000000ca1af42ee1be2c8895d94f39dab5fcdbe0b4b4065f4be534e7294", + 0 + ], + [ + "000000000069d0cc8c0452bf86cff87db05232f801a162acab2d080d6e4e9ea9", + 0 + ], + [ + "000000000019c7f7685f5bdc3afbb5e978cb3f4f70fea7b2b410139741303b53", + 0 + ], + [ + "00000000000d3874ce21db78f4d1883ad9ae8b26c1d7c13f3d723ff85629d595", + 0 + ], + [ + "0000000000033f87c25275ff72b58630d8da90221f2c84bcbd77c8e615709f8b", + 0 + ], + [ + "000000000000dc72adaaae6483eb6737de7d21b3a24b2426330e80b078ceaed1", + 0 + ], + [ + "00000000000002fb1337228db02ac464565271f22f045c1b6ee5e449f057a829", + 0 + ], + [ + "00000000000001902376ff640d3088899af0819dbd15f602156a13ac2fc8e94e", + 0 + ], + [ + "000000000000007ee49761a1c8284a3b8acefa39e37e455be4773d648e2db794", + 0 + ], + [ + "00000000000005b4d495a77f57018dbc72bf47993d494349329a3c653f04ab93", + 0 + ], + [ + "000000000000009dcb3ae6d68828e2f5ccfd58780abb260354e74484106f81ce", + 0 + ], + [ + "00000000a3ceb118021fb42d39be52db951c6f852bb9a241046e972706f7329a", + 0 + ], + [ + "00000000574e8e1c27fa54c77b4e7cd1b79de070f0d3ad5b383206ab9777d983", + 0 + ], + [ + "0000000039d562f640c1743421d53e7e04c3e8ba222c339fff6f3d25b1d4a7fe", + 0 + ], + [ + "000000000001cb1559d55c697871e18d5c26800f77fb11587241bfbec3b15e26", + 0 + ], + [ + "000000000006e01a93090319756c7ca826ef655feb0cc2ef9abcc59d67de5e5b", + 0 + ], + [ + "000000000000a81aaf5a4c013032638a077af6aad8bc449d74daef8ad3a74419", + 0 + ], + [ + "00000000000087d0574963c1582f2161298e2de5e48f74566291ef9afc2be24a", + 0 + ], + [ + "0000000000033251e71c347cd663945fb68efe82a8c6666c0b41e93f1c46658d", + 0 + ], + [ + "000000000000f592857e6f0e4711b5b93fdf95f2b21a5963bde15be750a07908", + 0 + ], + [ + "0000000000004353c8426e18b942a5012934ddac8322b86d6ab98ed7c0ee86ed", + 0 + ], + [ + "00000000004f027845b699f42e7d0d30c530e99524c5f97186ce6a250a5fac42", + 0 + ], + [ + "000000002fc6407edc060df90785082834867331e6746a43ed34a26fbdc5df64", + 0 + ], + [ + "0000000000048733007c91ea3665bd4e1653b10799e3f43abee0fe830ffbb3ad", + 0 + ], + [ + "0000000000025a9b1c5afceba0c78c4b0320797acdc1ad50b4e040f148fbff7f", + 0 + ], + [ + "00000000007ca6d026d27387edc1c5570de41c61bacbcb1dad2c0f300b49e637", + 0 + ], + [ + "00000000000258f683a77ad509da82a4fab24188fdb4b4690e212c50794a9abb", + 0 + ], + [ + "0000000000015111bce7b6ac13c930484e14e31e13e43355cb4d63c8f1782440", + 0 + ], + [ + "000000000001ca074fdecac7749d95f28f10c83a7e13787fd865bfbe505382bc", + 0 + ], + [ + "0000000000001c11a6505dd44ab405fdc07ddfc015f3c1166a5d9352ab58b52c", + 0 + ], + [ + "0000000000000c83f7f8e1cab4efa08d6c68c4555fb6ab542e01b87edd8f56ac", + 0 + ], + [ + "00000000000009561d0ceba15388573d2a994aff24512ec3ed7d7881aa0997dd", + 0 + ], + [ + "00000000007dc7cfbbb94db1fbc076a70a1252fd595686b4d75b2ea77ed6ee9e", + 0 + ], + [ + "00000000000251feb68a8c90852f73aeb29ebda191038737b7edd37c9475f4ac", + 0 + ], + [ + "0000000000013f9a97045ea9047654e514951288911b2c3986787c27bab49106", + 0 + ], + [ + "0000000006e8c37735c61f22bec69f4cb7eba03172349e7012b7704652f3e83a", + 0 + ], + [ + "0000000001f341add5657043d8e50e53ba079fe24966a2668f904be5579c84b9", + 0 + ], + [ + "000000000029a6275cd477d77939424bd183c2f1308a9912f45aa7cc9ed13b56", + 0 + ], + [ + "00000000000a0336239e5e1faedf5bd2eedf38c9a5ba34a832356aea70aeb102", + 0 + ], + [ + "000000000003c1a2b25093a64eb624055f6a3a26e18b8e7ea2d9382ec7a3609a", + 0 + ], + [ + "000000000001bd89bf7e8740ce22adfa6e8793bd1716a647e558ed1742ee8329", + 0 + ], + [ + "0000000000001320421f1bb2c94000e11a621f581fc277c0e2911c3b89f680bd", + 0 + ], + [ + "000000000054ce90a949f5ae2d43c4ace599668c6ccbc50620f6d5705922ea7c", + 0 + ], + [ + "00000000200d16fea4857e6b73169cc593421a57971acdbcaf87a31d7d8d72c8", + 0 + ], + [ + "0000000000e75602181c88f713b91c49de291ed878be305d25b75c0ec5fbe942", + 0 + ], + [ + "000000000081f8169c3c3665f20351dc0fe499612ae232ec0b55858a8e5dc6e9", + 0 + ], + [ + "0000000000d7ad232e7593fb435d125343b8113bbdb3705ab58ac0e18c26cc79", + 0 + ], + [ + "0000000000076df615d887e33193ca2dc0f2fc0e70744512c95da6242e9b1a81", + 0 + ], + [ + "0000000000084a62093d1929843e74456686429b698a7ea9b1901c1565779f58", + 0 + ], + [ + "00000000000251d1da01e9de9fcaf3ca3a64bff78a5faf51a8e697dfab6b5e4b", + 0 + ], + [ + "000000000000609a8798996b1f1fe0b66060a628eadc380d0d369a2318c2d0ec", + 0 + ], + [ + "00000000000014770aeab044a022e86d888a6ede75b6474022c71aead3a1db74", + 0 + ], + [ + "00000000000004101d04ebc90ade5d4b911aa13c038ecf25e9887d877203ddb8", + 0 + ], + [ + "000000007c700410b61eb7ff1aaccbfc3a79e4e4484ad7a2b0eda4d91dc4b613", + 0 + ], + [ + "00000000055ff438a031413ee042fd3c0a2b69be98690542806ff123b7988024", + 0 + ], + [ + "000000002eca5f9f2c3b656d2550662fdee4c95da133eade51a5cae653bc69fe", + 0 + ], + [ + "000000000c679b76ccf0c5b943095fdee8fa466311edbea2c4a05f9430ffef3f", + 0 + ], + [ + "00000000007c6f494e32d5d9de58fa008a770fdc0a7b4a141be5b7c2de3ab970", + 0 + ], + [ + "0000000000d5dcd5a26c8ad29c1293e70401e2f90d8288469df3816b8cc6d4aa", + 0 + ], + [ + "00000000000d754d94f36cacbfb620710672afb1558499cabe17ca62c54a7d3a", + 0 + ], + [ + "000000000004096bb78fba714b130f7f1f929e2803c75a7a85619f7a2b86567f", + 0 + ], + [ + "0000000000020e686c38d44c35896df35f9f1b7723a82a826a5e2393c25ef68c", + 0 + ], + [ + "000000000000504f9af6885c0cb6484109ea205a956c8efae9557a1f5b9233da", + 0 + ], + [ + "0000000000000e8746e52e4320ec17e66434a3936a3825f7046fe874e92275fb", + 0 + ], + [ + "0000000000000f48d818a9a026270c9f733f629959bea25192596d59874b1ce2", + 0 + ], + [ + "00000000eaa9214cb05b241828a1cfb0c4209fb7ea64429815d61f7c1d98939e", + 0 + ], + [ + "000000001f7f915a6002cce4edd5cba392307f3a199a520ee8937327a9135162", + 0 + ], + [ + "0000000009674ee0c606d687bdcddf8e023462927e2902b3381bc4bb862a7397", + 0 + ], + [ + "0000000001f3f3528c083a4b11eb2f04d8bbeca92b57f05d8282909bde78bc77", + 0 + ], + [ + "000000000131917ac459aefb91774dbb42caeca497afc0cfd1766e0338cc7f88", + 0 + ], + [ + "000000000027634444081e1289354cb50034a506bb306a2ac1d8280683771c5c", + 0 + ], + [ + "000000000017a852acff78fbee573329d45bb8b121e9f6fc1e4f687bb3778ada", + 0 + ], + [ + "000000000006789e1a00eca982fb2827f680b254c4a0ecb005af4464f3585a02", + 0 + ], + [ + "0000000000015d2e9f54b1e9419d6b32ce68ae626cdd7f2a1954f22ca39ae0fa", + 0 + ], + [ + "0000000000002f7893bc169165ed9fefb434b6201103f23cc84a747a68ff8797", + 0 + ], + [ + "00000000000008471ccf356a18dd48aa12506ef0b6162cb8f98a8d8bb0465902", + 0 + ], + [ + "0000000000000596f00b9db53c4111bcde16f3781471c5307af1a996e34ec20a", + 0 + ], + [ + "000000000000007b5d2406f64f5f5833c063a6906552e815e603140c00bca951", + 0 + ], + [ + "0000000093ca5d935740a1b25f10ce092fd777c2bb521f3156619389ae78931e", + 0 + ], + [ + "00000000292f3a48559527341f72400a0f8a783aebcaae5bfa0e390dfaa5286b", + 0 + ], + [ + "000000001e852ed7ddf0108d1fce0f4f686f43c8c1b85bcb12c43e564dc7630e", + 0 + ], + [ + "000000000c4bea8fb1e7f3a1f3e6c6b3f71388c0ec7eef3de381853767e89f87", + 0 + ], + [ + "00000000029ef31a21711b55c4300efa38ace0b706091e373f48285286f2c578", + 0 + ], + [ + "0000000000979060786bb008f193d3917e28667bb1b28329f3adadc172e4cce7", + 0 + ], + [ + "000000000019030ceb98013b1627517b45b04ee055ef445813bbebaa25fa1ed3", + 0 + ], + [ + "00000000000adf202247bb794fc9a3c82cd8767143f1e6ed5f60940ee18b09a8", + 0 + ], + [ + "000000000000b19061e2481d8be6183b3d881b0d58601072d2a32729435f6af3", + 0 + ], + [ + "0000000000007a6d34f59b29e8d4da53e51e3414acd18527466d064945fe19fc", + 0 + ], + [ + "0000000000002e66ca213a2c3e9eb5fa62de29feb83880a0bd29f90fca8ad199", + 0 + ], + [ + "0000000000000b4ca10aa100728d0928f37db5296303db1b74ffe29e4a17b6cd", + 0 + ], + [ + "0000000000000143309f6b19567955743775f61f8dc6932c0b46cf5fb11c6c72", + 0 + ], + [ + "00000000000000b04d5409b3ac60cc18c0b9a3d58b303594635a8f75a9d2abd5", + 0 + ], + [ + "000000000000040a2699f62a552703a278608248c2ce823f4cd8845376e9a371", + 0 + ], + [ + "00000000000005cfcb850db7e83d4963994f958bae9b1de1483f5aeb3d449925", + 0 + ], + [ + "00000000000190f80220e70c1481153671a7c90fd856988c183ab0e3d9313df8", + 0 + ], + [ + "000000009374563a06178641d06776f66554c2a094b5319f0801fe35cef72ccf", + 0 + ], + [ + "00000000003e4e6e5e8e4a89e7de50eed104d4a49d2992ff101b6740beec7cb5", + 0 + ], + [ + "0000000000618cd377d14aaa441cbdb92527894f98da316eca81664f8ab5488d", + 0 + ], + [ + "00000000000d977ab2897885fee712f58612fce8c10ffbe9400326fe3429b77b", + 0 + ], + [ + "00000000000c3575b487dd0f938c5bc744fa65ca4ca3a9c981b8bda903ec110b", + 0 + ], + [ + "0000000000247ac689595ed8d62678bfe53e5af13c0f5455e558f5e6bb375c16", + 0 + ], + [ + "0000000000093d175376aa621176511f335a48f824b66d998e8082f85134a48b", + 0 + ], + [ + "000000000000c0c0448fe922f2c737946297d35f2c25ad7cc223e11bbe58e1f8", + 0 + ], + [ + "00000000000016abe4e7c10ddb658bb089b2ef3b1de3f3329097cf679eedf2b5", + 0 + ], + [ + "000000000000242757cea5b68c52b83dd8c2eb9257492074fc69dfa30bd4cbf4", + 0 + ], + [ + "00000000000006813f3dd7726a509fbe3101835db155dfd35d44aeae6aedb316", + 0 + ], + [ + "000000000000053cc4f39cff1c8cee1aff7e289a85dee84164d2d981afc8f17a", + 0 + ], + [ + "00000000000000789724805cf1d37ef689acf52c47a460507f540d5e5ca79bfa", + 0 + ], + [ + "00000000000003d71618bb8952887f65540270a5e54d6246b9419e08831b5e4e", + 0 + ], + [ + "0000000000000251a513a33eadfad67c015f6e3b291dfd0ae1cc4bb3a43006dc", + 0 + ], + [ + "00000000968009e3f8d6e6071e7def68298307717a9af6c2d44986deaae297d5", + 0 + ], + [ + "0000000062bcacb734df83bbfa3e1b9a8dfa570ecffb6c29eaaf8e9498cccd30", + 0 + ], + [ + "000000001d4618c0931bd3c25ee592c35341f30ff3b549a671f637b3c26ef414", + 0 + ], + [ + "000000000418b329df96a004f1b652ad06a7ca295f9f2e711c412d00493f5a86", + 0 + ], + [ + "000000000302bfb88e9027237d023c4b969e106c9a7a23a84103776de7880836", + 0 + ], + [ + "000000000069b9f7d9134fd93c8b7e3af8b26bbcbb5553af02fb6ed644d7fca5", + 0 + ], + [ + "00000000000411ec444240ee91e2777ad18b80dee854e3e838e32209e84774fa", + 0 + ], + [ + "0000000000007c73f322eba4dee5463305227c7e1a8139f1b7b296444f265052", + 0 + ], + [ + "00000000000129adf0f9c0242aedbb9d87935d67ee4ddea758c00344d4b6a29e", + 0 + ], + [ + "000000000000343594e671158b6e1b4b6499f6ad66e2aeabf1f6d295d3dba850", + 0 + ], + [ + "000000000000320f0d5c22ba22b588b97a0e02273034bcd53669b1c8c4eeda1b", + 0 + ], + [ + "0000000000001e8cdb2d98471a5c60bdbddbe644b9ad08e17a97b3a7dce1e332", + 0 + ], + [ + "0000000000000026c9994ccdd027e86f51a2e36812f754bd855a7f9b1ca56511", + 0 + ], + [ + "00000000000002746a820a2c08b35b8d0493c4b5d468fcc971b9c88003e84849", + 0 + ], + [ + "000000000002949f844e92645df73ce9c093e5aac0d962a0fa13eb076eec835c", + 0 + ], + [ + "00000000000156fbda67468ae2863993b98a41227c420246e4bc4e68c84df0e8", + 0 + ], + [ + "000000000003b43c6c807122c8dd10e2a0cffbf72946f41c97c1ab82d416f74d", + 0 + ], + [ + "000000000004e0635c2438b1b649007e5d424b3de846299a8db53049ebf4da0c", + 0 + ], + [ + "00000000000258e4b79e3cca2ab7d12b35ba77fc491572f2e794f0a10f5236d9", + 0 + ], + [ + "0000000000f5816875d9fece105e499b0467b8fb23ea973c48d828a235acdebd", + 0 + ], + [ + "000000000001353bbaec810af7a4c74b4964ae072361c0889ed6d59cf16db6fd", + 0 + ], + [ + "00000000000b354d8c389473670ca6bed7e3dffa069f270d35ec9dad810af141", + 0 + ], + [ + "000000000002fa1f39e7cd8730fa08085ba2b532146ad1ef3b400a13e835ca36", + 0 + ], + [ + "000000000000d2c7943eee59652a9783bff27e474a92ec206c5c6e3cdd58d0d7", + 0 + ], + [ + "00000000000036034181b4d9a84a97490b49fbee4262b9cfb25a7bfc9c0eec9f", + 0 + ], + [ + "00000000000007deb59381cce692f152fc902732d96a7e7d463bc83915b37c0a", + 0 + ], + [ + "00000000ea7d32833462c0f72ade0cae4766e6065caa4e510331929c56d16632", + 0 + ], + [ + "000000000068fce0ddd370d4c8f9129a7bc7843e75fc57666202d3b90239e269", + 0 + ], + [ + "0000000026b4a2212c9c9493f8bd9d5331cab6d8eda8ee017410e58a783ca069", + 0 + ], + [ + "0000000009535ea2dc7e83c31cd17f1db1bb78b0a678fc0610844273de143bf5", + 0 + ], + [ + "00000000008607cbd5baca91d5b8b82ee965aace335744a3e21578af22bee8ba", + 0 + ], + [ + "000000000030dcedae0f5e98c4e176f9569ce76c4d4135bb028fc3144ef381d9", + 0 + ], + [ + "0000000000297c3f0e3fa85731222ba934a955bf513247a72a33c74c498cadbe", + 0 + ], + [ + "0000000000020a0d4a1e8120cbdb486e758b58919c9df12e0edc8ca1f2795e94", + 0 + ], + [ + "000000000000078773afc9023182bfb6534a60158672e6bc6e8aa5052854da80", + 0 + ], + [ + "00000000000102ecdd67800807d9e137357805b9bbf8a439ed86bde5b19fbeb7", + 0 + ], + [ + "0000000000005c3d2e3c7ee737c67ab465533acb233e0df902c1525fc11c3a55", + 0 + ], + [ + "0000000000001a77771650cdbbceff87caa4461391ba6a4ddc9815b5b0ab47b0", + 0 + ], + [ + "000000000000071ec390bbd28fa2a84e52ab5b32901d0723d22646b04ae01dc3", + 0 + ], + [ + "00000000000005c3ec3194f710c6f26ee736d59cc935ddfa574440f39846433a", + 0 + ], + [ + "00000000000001cc3df6924591939269d61ead563b9eb68402a2ca01d7ff99e2", + 0 + ], + [ + "000000008c778b3554ceaf3a13a856acbfe46b5750fb86fd92ba30651c2852f4", + 0 + ], + [ + "00000000107ca31f75f8ea76073dda3c33330b2706c1ec20c3ec240e853b65c5", + 0 + ], + [ + "0000000006ba99b08e7f2869ce113e2ad7464891de7b4cfa96f330d706a2da46", + 0 + ], + [ + "000000000f31036bd51b2818f6dfb90ada9be5019abf55fb15694b181e269865", + 0 + ], + [ + "00000000004fcc101bc47eb7a379b9f608d5c00ac04d2d0ea165ae2937070796", + 0 + ], + [ + "000000000044d5ca3eda838edef0df7e69e1934047f8482822ce58ff7a18466d", + 0 + ], + [ + "000000000029bdfb157be6d400c4dd3370d98afdd8cd3db6f1ada8c19bbf4650", + 0 + ], + [ + "000000000005e9699ad8035caa4f73af781ac2040c87b8aa77459b3607209aa8", + 0 + ], + [ + "000000000001c0ba033f7d85beeaa167c9bde0e192240653a7ff6d9b81679842", + 0 + ], + [ + "0000000000000e0176111f29e800b49c7b8c7226dbbf4df715f1a4f06bcaaa49", + 0 + ], + [ + "00000000ac3bb2cf42192e9053f5384355228a2b3d70b4ece4d45773a5d5ddd2", + 0 + ], + [ + "000000000f29f7b60842b1044b2db7998e9bcbd92f8ec6fe8d159c6d582f1f1a", + 0 + ], + [ + "00000000352f86bc5f9760961a25de009940508bb2cd0b37f378fbc87dc97eef", + 0 + ], + [ + "000000000e9b3086008679ed57f59857f64c3954368ba1088117dbf88d5839cd", + 0 + ], + [ + "000000000015324bd8fed0e61b62bd1d6c663b862cb98ea03c494a92e4a8d0af", + 0 + ], + [ + "000000000020475a181b7a084b341860a72fc0c1fdfcc13a85adeb0471444b0f", + 0 + ], + [ + "0000000000031905c508a975707b74f24e733880382775ee0e6250666473e1d8", + 0 + ], + [ + "000000000000ca38b15d2ea33a6eef505a9c661540a18882f79ba9a3c575a9bd", + 0 + ], + [ + "000000000002739979a7a89fa279303b6606885e750b19e91ed637d7f222b392", + 0 + ], + [ + "00000000000091e935fc266facc2c92759d5468a39aee5be6b76b519a9bc7567", + 0 + ], + [ + "00000000000006e339938254208203b67c3c400f703fc29535fc646699e36e58", + 0 + ], + [ + "00000000000008f6f1d1150d77f93a7f1baa24b65ceb471b1825b2e92ca6997c", + 0 + ], + [ + "000000000000004894e1edcc5421dbcec77d47c5c50bf27b2cff3f1c242c9eb3", + 0 + ], + [ + "000000000000054e97fb5e1a8bd7900f7c329385895761aaa40d11b3c75b0c8e", + 0 + ], + [ + "0000000000000600f4bcc5a89527eede43d1d3342dc12eee1371ab534b0102dc", + 0 + ], + [ + "00000000d1ad5c3ef8c3bb4610b34c264e4ca1ea51c4c8bac18b215e7dc96948", + 0 + ], + [ + "0000000062f6a07ae11f9724b8ba9dc2b7348ffd02b59edd3cd2bf387fab9723", + 0 + ], + [ + "000000000014e4c97c9b09ff20203213f3336b0927fd19d214cef1f544756e39", + 0 + ], + [ + "0000000000d004681880e127aed3fa73255a2e75c2e5c8580cd555526614b294", + 0 + ], + [ + "000000000008093189bba28d40662d6964afc1c0fc9b5c1681bbe32e8bee6c0b", + 0 + ], + [ + "00000000002df10cf8165b2204ef4db6721c8c2119d60463b040fbc81c266bbf", + 0 + ], + [ + "00000000000c28c789e7cd9800b98c1dd32e2dda54048116ff47ed856a14acfb", + 0 + ], + [ + "000000000003e8e7755d9b8299b28c71d9f0e18909f25bc9f3eeec3464ece1dd", + 0 + ], + [ + "0000000000004b95a0103abe2cb97806caca76f6922d9c5df003cf4a467df822", + 0 + ], + [ + "0000000000005f12d2ab72bfa715860444c281265ef77e09dc2d041ce89506c0", + 0 + ], + [ + "00000000000016eeedb3f367daaee93334188db877fb01cd0282b990f60812b3", + 0 + ], + [ + "00000000000001daf3bd8306b6f6899af8aa656d87ac2aa37d493fdcb0cb3000", + 0 + ], + [ + "0000000000000390b86892ad0bed9b520783056961cad7362ace8049aa00471c", + 0 + ], + [ + "00000000000002105d01b4de7d3e3ada9c757a239151d50b5dd193e3951a23cc", + 0 + ], + [ + "00000000000002362fa802df308201a4b1fff2fd8a91892915a46f5d54098ff4", + 0 + ], + [ + "00000000000004fb8aa6c6aecb64b9d8d7e691a6cd56fad69fc5278b9e8d98cb", + 0 + ], + [ + "00000000000000ce3bd9752b2508ddae1ee71332e905163a3c0d7e10b8c472f7", + 0 + ], + [ + "00000000000002d0d8520982f15a45d4a405334c61886b6d13d95843386af647", + 0 + ], + [ + "00000000cafd25502ad67d5d409edfc98f5bbd3173e86e085c69658d58da5f70", + 0 + ], + [ + "00000000b01e0675317a29a07731ea092fa029016a40ed8bb4fc17cde50eda05", + 0 + ], + [ + "000000002676805396ed2883ccc8ad401aa0a974627559fbae2416ba5c54999c", + 0 + ], + [ + "0000000000030ab759158f3d425824228dc5c91f32db91d404bee29ee3a41878", + 0 + ], + [ + "00000000000da1c8040ec08e7490fb201ca1fb3571f29c0efd3351ae197d3017", + 0 + ], + [ + "000000000004e3cba890c16ffc7d1c019d4ab88afa39315164e1b08b8e6a9330", + 0 + ], + [ + "00000000000bdcfb630b43977be44529e54daa02d199014a0967deac669bd060", + 0 + ], + [ + "000000000007254038f9c621d6df0d9fbd90b5697e4170cd6090daaf579f3790", + 0 + ], + [ + "000000000002263e27ea1cec943632bf469a28b067f0bfde3b9a6b48540981b4", + 0 + ], + [ + "000000000000f194a8d17e683d17f222d23a9032f034d4dc4497263fd785dfa0", + 0 + ], + [ + "00000000000036e359b7b07044e3cd5b132a3c72501a0f3f9ccde167f5316bba", + 0 + ], + [ + "0000000000000b10e98a90e0fd1ffbf7d5fc5a76e8e6e960c6fb158711af6f48", + 0 + ], + [ + "0000000000000104e1e4303b8dae78389bb4e6c38f3eb3fe42aec6464bd5c397", + 0 + ], + [ + "00000000000000bde368a635921f5ad25aeb4b784651de24d624cf20c27691c7", + 0 + ], + [ + "0000000081a626a33cff134e7e56dc0f0a67b1735c96256774885d5d095807c0", + 0 + ], + [ + "0000000055d357cdf39130eb767f416101e79025515906bea528f43cb6446920", + 0 + ], + [ + "0000000012558b30f9c1a156fd80b02451e8dfcc7fe0350fb4adeeb84951a0a6", + 0 + ], + [ + "000000000001a4868924fc7cca0334ffc4dd49c07fb841c1da059a7c219bdf95", + 0 + ], + [ + "00000000000010086bd2bba88c71b08cfc7e24183d610a2803e6d382049d52c0", + 0 + ], + [ + "0000000000018c83992fe05d820b097228de93787e3f59e65cb89ad4c385e364", + 0 + ], + [ + "00000000000023ab80324770ff4c6802d09e5e1e7de78d2a8e64783904d47f19", + 0 + ], + [ + "000000000000287fa294ea557835d8c98bfe94c4d8b18d5b10f1b62d68957113", + 0 + ] +] \ No newline at end of file diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py new file mode 100644 index 000000000..5a69cccfe --- /dev/null +++ b/electrum/coinchooser.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2015 kyuupichan@gmail +# +# 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 collections import defaultdict, namedtuple +from math import floor, log10 + +from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address +from .transaction import Transaction, TxOutput +from .util import NotEnoughFunds, PrintError + + +# A simple deterministic PRNG. Used to deterministically shuffle a +# set of coins - the same set of coins should produce the same output. +# Although choosing UTXOs "randomly" we want it to be deterministic, +# so if sending twice from the same UTXO set we choose the same UTXOs +# to spend. This prevents attacks on users by malicious or stale +# servers. +class PRNG: + def __init__(self, seed): + self.sha = sha256(seed) + self.pool = bytearray() + + def get_bytes(self, n): + while len(self.pool) < n: + self.pool.extend(self.sha) + self.sha = sha256(self.sha) + result, self.pool = self.pool[:n], self.pool[n:] + return result + + def randint(self, start, end): + # Returns random integer in [start, end) + n = end - start + r = 0 + p = 1 + while p < n: + r = self.get_bytes(1)[0] + (r << 8) + p = p << 8 + return start + (r % n) + + def choice(self, seq): + return seq[self.randint(0, len(seq))] + + def shuffle(self, x): + for i in reversed(range(1, len(x))): + # pick an element in x[:i+1] with which to exchange x[i] + j = self.randint(0, i+1) + x[i], x[j] = x[j], x[i] + + +Bucket = namedtuple('Bucket', + ['desc', + 'weight', # as in BIP-141 + 'value', # in satoshis + 'coins', # UTXOs + 'min_height', # min block height where a coin was confirmed + 'witness']) # whether any coin uses segwit + +def strip_unneeded(bkts, sufficient_funds): + '''Remove buckets that are unnecessary in achieving the spend amount''' + bkts = sorted(bkts, key = lambda bkt: bkt.value) + for i in range(len(bkts)): + if not sufficient_funds(bkts[i + 1:]): + return bkts[i:] + # Shouldn't get here + return bkts + +class CoinChooserBase(PrintError): + + enable_output_value_rounding = False + + def keys(self, coins): + raise NotImplementedError + + def bucketize_coins(self, coins): + keys = self.keys(coins) + buckets = defaultdict(list) + for key, coin in zip(keys, coins): + buckets[key].append(coin) + + def make_Bucket(desc, coins): + witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in coins) + # note that we're guessing whether the tx uses segwit based + # on this single bucket + weight = sum(Transaction.estimated_input_weight(coin, witness) + for coin in coins) + value = sum(coin['value'] for coin in coins) + min_height = min(coin['height'] for coin in coins) + return Bucket(desc, weight, value, coins, min_height, witness) + + return list(map(make_Bucket, buckets.keys(), buckets.values())) + + def penalty_func(self, tx): + def penalty(candidate): + return 0 + return penalty + + def change_amounts(self, tx, count, fee_estimator, dust_threshold): + # Break change up if bigger than max_change + output_amounts = [o.value for o in tx.outputs()] + # Don't split change of less than 0.02 BTC + max_change = max(max(output_amounts) * 1.25, 0.02 * COIN) + + # Use N change outputs + for n in range(1, count + 1): + # How much is left if we add this many change outputs? + change_amount = max(0, tx.get_fee() - fee_estimator(n)) + if change_amount // n <= max_change: + break + + # Get a handle on the precision of the output amounts; round our + # change to look similar + def trailing_zeroes(val): + s = str(val) + return len(s) - len(s.rstrip('0')) + + zeroes = [trailing_zeroes(i) for i in output_amounts] + min_zeroes = min(zeroes) + max_zeroes = max(zeroes) + + if n > 1: + zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1) + else: + # if there is only one change output, this will ensure that we aim + # to have one that is exactly as precise as the most precise output + zeroes = [min_zeroes] + + # Calculate change; randomize it a bit if using more than 1 output + remaining = change_amount + amounts = [] + while n > 1: + average = remaining / n + amount = self.p.randint(int(average * 0.7), int(average * 1.3)) + precision = min(self.p.choice(zeroes), int(floor(log10(amount)))) + amount = int(round(amount, -precision)) + amounts.append(amount) + remaining -= amount + n -= 1 + + # Last change output. Round down to maximum precision but lose + # no more than 10**max_dp_to_round_for_privacy + # e.g. a max of 2 decimal places means losing 100 satoshis to fees + max_dp_to_round_for_privacy = 2 if self.enable_output_value_rounding else 0 + N = pow(10, min(max_dp_to_round_for_privacy, zeroes[0])) + amount = (remaining // N) * N + amounts.append(amount) + + assert sum(amounts) <= change_amount + + return amounts + + def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold): + amounts = self.change_amounts(tx, len(change_addrs), fee_estimator, + dust_threshold) + assert min(amounts) >= 0 + assert len(change_addrs) >= len(amounts) + # If change is above dust threshold after accounting for the + # size of the change output, add it to the transaction. + dust = sum(amount for amount in amounts if amount < dust_threshold) + amounts = [amount for amount in amounts if amount >= dust_threshold] + change = [TxOutput(TYPE_ADDRESS, addr, amount) + for addr, amount in zip(change_addrs, amounts)] + self.print_error('change:', change) + if dust: + self.print_error('not keeping dust', dust) + return change + + def make_tx(self, coins, outputs, change_addrs, fee_estimator, + dust_threshold): + """Select unspent coins to spend to pay outputs. If the change is + greater than dust_threshold (after adding the change output to + the transaction) it is kept, otherwise none is sent and it is + added to the transaction fee. + + Note: fee_estimator expects virtual bytes + """ + + # Deterministic randomness from coins + utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins] + self.p = PRNG(''.join(sorted(utxos))) + + # Copy the outputs so when adding change we don't modify "outputs" + tx = Transaction.from_io([], outputs[:]) + # Weight of the transaction with no inputs and no change + # Note: this will use legacy tx serialization as the need for "segwit" + # would be detected from inputs. The only side effect should be that the + # marker and flag are excluded, which is compensated in get_tx_weight() + base_weight = tx.estimated_weight() + spent_amount = tx.output_value() + + def fee_estimator_w(weight): + return fee_estimator(Transaction.virtual_size_from_weight(weight)) + + def get_tx_weight(buckets): + total_weight = base_weight + sum(bucket.weight for bucket in buckets) + is_segwit_tx = any(bucket.witness for bucket in buckets) + if is_segwit_tx: + total_weight += 2 # marker and flag + # non-segwit inputs were previously assumed to have + # a witness of '' instead of '00' (hex) + # note that mixed legacy/segwit buckets are already ok + num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins) + for bucket in buckets) + total_weight += num_legacy_inputs + + return total_weight + + def sufficient_funds(buckets): + '''Given a list of buckets, return True if it has enough + value to pay for the transaction''' + total_input = sum(bucket.value for bucket in buckets) + total_weight = get_tx_weight(buckets) + return total_input >= spent_amount + fee_estimator_w(total_weight) + + # Collect the coins into buckets, choose a subset of the buckets + buckets = self.bucketize_coins(coins) + buckets = self.choose_buckets(buckets, sufficient_funds, + self.penalty_func(tx)) + + tx.add_inputs([coin for b in buckets for coin in b.coins]) + tx_weight = get_tx_weight(buckets) + + # change is sent back to sending address unless specified + if not change_addrs: + change_addrs = [tx.inputs()[0]['address']] + # note: this is not necessarily the final "first input address" + # because the inputs had not been sorted at this point + assert is_address(change_addrs[0]) + + # This takes a count of change outputs and returns a tx fee + output_weight = 4 * Transaction.estimated_output_size(change_addrs[0]) + fee = lambda count: fee_estimator_w(tx_weight + count * output_weight) + change = self.change_outputs(tx, change_addrs, fee, dust_threshold) + tx.add_outputs(change) + + self.print_error("using %d inputs" % len(tx.inputs())) + self.print_error("using buckets:", [bucket.desc for bucket in buckets]) + + return tx + + def choose_buckets(self, buckets, sufficient_funds, penalty_func): + raise NotImplemented('To be subclassed') + + +class CoinChooserRandom(CoinChooserBase): + + def bucket_candidates_any(self, buckets, sufficient_funds): + '''Returns a list of bucket sets.''' + if not buckets: + raise NotEnoughFunds() + + candidates = set() + + # Add all singletons + for n, bucket in enumerate(buckets): + if sufficient_funds([bucket]): + candidates.add((n, )) + + # And now some random ones + attempts = min(100, (len(buckets) - 1) * 10 + 1) + permutation = list(range(len(buckets))) + for i in range(attempts): + # Get a random permutation of the buckets, and + # incrementally combine buckets until sufficient + self.p.shuffle(permutation) + bkts = [] + for count, index in enumerate(permutation): + bkts.append(buckets[index]) + if sufficient_funds(bkts): + candidates.add(tuple(sorted(permutation[:count + 1]))) + break + else: + # FIXME this assumes that the effective value of any bkt is >= 0 + # we should make sure not to choose buckets with <= 0 eff. val. + raise NotEnoughFunds() + + candidates = [[buckets[n] for n in c] for c in candidates] + return [strip_unneeded(c, sufficient_funds) for c in candidates] + + def bucket_candidates_prefer_confirmed(self, buckets, sufficient_funds): + """Returns a list of bucket sets preferring confirmed coins. + + Any bucket can be: + 1. "confirmed" if it only contains confirmed coins; else + 2. "unconfirmed" if it does not contain coins with unconfirmed parents + 3. other: e.g. "unconfirmed parent" or "local" + + This method tries to only use buckets of type 1, and if the coins there + are not enough, tries to use the next type but while also selecting + all buckets of all previous types. + """ + conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0] + unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0] + other_buckets = [bkt for bkt in buckets if bkt.min_height < 0] + + bucket_sets = [conf_buckets, unconf_buckets, other_buckets] + already_selected_buckets = [] + + for bkts_choose_from in bucket_sets: + try: + def sfunds(bkts): + return sufficient_funds(already_selected_buckets + bkts) + + candidates = self.bucket_candidates_any(bkts_choose_from, sfunds) + break + except NotEnoughFunds: + already_selected_buckets += bkts_choose_from + else: + raise NotEnoughFunds() + + candidates = [(already_selected_buckets + c) for c in candidates] + return [strip_unneeded(c, sufficient_funds) for c in candidates] + + def choose_buckets(self, buckets, sufficient_funds, penalty_func): + candidates = self.bucket_candidates_prefer_confirmed(buckets, sufficient_funds) + penalties = [penalty_func(cand) for cand in candidates] + winner = candidates[penalties.index(min(penalties))] + self.print_error("Bucket sets:", len(buckets)) + self.print_error("Winning penalty:", min(penalties)) + return winner + +class CoinChooserPrivacy(CoinChooserRandom): + """Attempts to better preserve user privacy. + First, if any coin is spent from a user address, all coins are. + Compared to spending from other addresses to make up an amount, this reduces + information leakage about sender holdings. It also helps to + reduce blockchain UTXO bloat, and reduce future privacy loss that + would come from reusing that address' remaining UTXOs. + Second, it penalizes change that is quite different to the sent amount. + Third, it penalizes change that is too big. + """ + + def keys(self, coins): + return [coin['address'] for coin in coins] + + def penalty_func(self, tx): + min_change = min(o.value for o in tx.outputs()) * 0.75 + max_change = max(o.value for o in tx.outputs()) * 1.33 + spent_amount = sum(o.value for o in tx.outputs()) + + def penalty(buckets): + badness = len(buckets) - 1 + total_input = sum(bucket.value for bucket in buckets) + # FIXME "change" here also includes fees + change = float(total_input - spent_amount) + # Penalize change not roughly in output range + if change < min_change: + badness += (min_change - change) / (min_change + 10000) + elif change > max_change: + badness += (change - max_change) / (max_change + 10000) + # Penalize large change; 5 BTC excess ~= using 1 more input + badness += change / (COIN * 5) + return badness + + return penalty + + +COIN_CHOOSERS = { + 'Privacy': CoinChooserPrivacy, +} + +def get_name(config): + kind = config.get('coin_chooser') + if not kind in COIN_CHOOSERS: + kind = 'Privacy' + return kind + +def get_coin_chooser(config): + klass = COIN_CHOOSERS[get_name(config)] + coinchooser = klass() + coinchooser.enable_output_value_rounding = config.get('coin_chooser_output_rounding', False) + return coinchooser diff --git a/electrum/commands.py b/electrum/commands.py new file mode 100644 index 000000000..89e7a619b --- /dev/null +++ b/electrum/commands.py @@ -0,0 +1,895 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcore client +# Copyright (C) 2011 thomasv@gitorious +# +# 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 sys +import datetime +import copy +import argparse +import json +import ast +import base64 +from functools import wraps +from decimal import Decimal + +from .import util, ecc +from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode +from . import bitcoin +from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS +from .i18n import _ +from .transaction import Transaction, multisig_script, TxOutput +from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED +from .plugin import run_hook + +known_commands = {} + + +def satoshis(amount): + # satoshi conversion must not be performed by the parser + return int(COIN*Decimal(amount)) if amount not in ['!', None] else amount + + +class Command: + def __init__(self, func, s): + self.name = func.__name__ + self.requires_network = 'n' in s + self.requires_wallet = 'w' in s + self.requires_password = 'p' in s + self.description = func.__doc__ + self.help = self.description.split('.')[0] if self.description else None + varnames = func.__code__.co_varnames[1:func.__code__.co_argcount] + self.defaults = func.__defaults__ + if self.defaults: + n = len(self.defaults) + self.params = list(varnames[:-n]) + self.options = list(varnames[-n:]) + else: + self.params = list(varnames) + self.options = [] + self.defaults = [] + + +def command(s): + def decorator(func): + global known_commands + name = func.__name__ + known_commands[name] = Command(func, s) + @wraps(func) + def func_wrapper(*args, **kwargs): + c = known_commands[func.__name__] + wallet = args[0].wallet + password = kwargs.get('password') + if c.requires_wallet and wallet is None: + raise Exception("wallet not loaded. Use 'electrum daemon load_wallet'") + if c.requires_password and password is None and wallet.has_password(): + return {'error': 'Password required' } + return func(*args, **kwargs) + return func_wrapper + return decorator + + +class Commands: + + def __init__(self, config, wallet, network, callback = None): + self.config = config + self.wallet = wallet + self.network = network + self._callback = callback + + def _run(self, method, args, password_getter): + # this wrapper is called from the python console + cmd = known_commands[method] + if cmd.requires_password and self.wallet.has_password(): + password = password_getter() + if password is None: + return + else: + password = None + + f = getattr(self, method) + if cmd.requires_password: + result = f(*args, **{'password':password}) + else: + result = f(*args) + + if self._callback: + self._callback() + return result + + @command('') + def commands(self): + """List of commands""" + return ' '.join(sorted(known_commands.keys())) + + @command('') + def create(self, segwit=False): + """Create a new wallet""" + raise Exception('Not a JSON-RPC command') + + @command('wn') + def restore(self, text): + """Restore a wallet from text. Text can be a seed phrase, a master + public key, a master private key, a list of bitcore addresses + or bitcore private keys. If you want to be prompted for your + seed, type '?' or ':' (concealed) """ + raise Exception('Not a JSON-RPC command') + + @command('wp') + def password(self, password=None, new_password=None): + """Change wallet password. """ + if self.wallet.storage.is_encrypted_with_hw_device() and new_password: + raise Exception("Can't change the password of a wallet encrypted with a hw device.") + b = self.wallet.storage.is_encrypted() + self.wallet.update_password(password, new_password, b) + self.wallet.storage.write() + return {'password':self.wallet.has_password()} + + @command('') + def getconfig(self, key): + """Return a configuration variable. """ + return self.config.get(key) + + @classmethod + def _setconfig_normalize_value(cls, key, value): + if key not in ('rpcuser', 'rpcpassword'): + value = json_decode(value) + try: + value = ast.literal_eval(value) + except: + pass + return value + + @command('') + def setconfig(self, key, value): + """Set a configuration variable. 'value' may be a string or a Python expression.""" + value = self._setconfig_normalize_value(key, value) + self.config.set_key(key, value) + return True + + @command('') + def make_seed(self, nbits=132, language=None, segwit=False): + """Create a seed""" + from .mnemonic import Mnemonic + t = 'segwit' if segwit else 'standard' + s = Mnemonic(language).make_seed(t, nbits) + return s + + @command('n') + def getaddresshistory(self, address): + """Return the transaction history of any address. Note: This is a + walletless server query, results are not checked by SPV. + """ + sh = bitcoin.address_to_scripthash(address) + return self.network.get_history_for_scripthash(sh) + + @command('w') + def listunspent(self): + """List unspent outputs. Returns the list of unspent transaction + outputs in your wallet.""" + l = copy.deepcopy(self.wallet.get_utxos()) + for i in l: + v = i["value"] + i["value"] = str(Decimal(v)/COIN) if v is not None else None + return l + + @command('n') + def getaddressunspent(self, address): + """Returns the UTXO list of any address. Note: This + is a walletless server query, results are not checked by SPV. + """ + sh = bitcoin.address_to_scripthash(address) + return self.network.listunspent_for_scripthash(sh) + + @command('') + def serialize(self, jsontx): + """Create a transaction from json inputs. + Inputs must have a redeemPubkey. + Outputs must be a list of {'address':address, 'value':satoshi_amount}. + """ + keypairs = {} + inputs = jsontx.get('inputs') + outputs = jsontx.get('outputs') + locktime = jsontx.get('lockTime', 0) + for txin in inputs: + if txin.get('output'): + prevout_hash, prevout_n = txin['output'].split(':') + txin['prevout_n'] = int(prevout_n) + txin['prevout_hash'] = prevout_hash + sec = txin.get('privkey') + if sec: + txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) + pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) + keypairs[pubkey] = privkey, compressed + txin['type'] = txin_type + txin['x_pubkeys'] = [pubkey] + txin['signatures'] = [None] + txin['num_sig'] = 1 + + outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs] + tx = Transaction.from_io(inputs, outputs, locktime=locktime) + tx.sign(keypairs) + return tx.as_dict() + + @command('wp') + def signtransaction(self, tx, privkey=None, password=None): + """Sign a transaction. The wallet keys will be used unless a private key is provided.""" + tx = Transaction(tx) + if privkey: + txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey) + pubkey_bytes = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed) + h160 = bitcoin.hash_160(pubkey_bytes) + x_pubkey = 'fd' + bh2u(b'\x00' + h160) + tx.sign({x_pubkey:(privkey2, compressed)}) + else: + self.wallet.sign_transaction(tx, password) + return tx.as_dict() + + @command('') + def deserialize(self, tx): + """Deserialize a serialized transaction""" + tx = Transaction(tx) + return tx.deserialize() + + @command('n') + def broadcast(self, tx): + """Broadcast a transaction to the network. """ + tx = Transaction(tx) + return self.network.broadcast_transaction(tx) + + @command('') + def createmultisig(self, num, pubkeys): + """Create multisig address""" + assert isinstance(pubkeys, list), (type(num), type(pubkeys)) + redeem_script = multisig_script(pubkeys, num) + address = bitcoin.hash160_to_p2sh(hash_160(bfh(redeem_script))) + return {'address':address, 'redeemScript':redeem_script} + + @command('w') + def freeze(self, address): + """Freeze address. Freeze the funds at one of your wallet\'s addresses""" + return self.wallet.set_frozen_state([address], True) + + @command('w') + def unfreeze(self, address): + """Unfreeze address. Unfreeze the funds at one of your wallet\'s address""" + return self.wallet.set_frozen_state([address], False) + + @command('wp') + def getprivatekeys(self, address, password=None): + """Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.""" + if isinstance(address, str): + address = address.strip() + if is_address(address): + return self.wallet.export_private_key(address, password)[0] + domain = address + return [self.wallet.export_private_key(address, password)[0] for address in domain] + + @command('w') + def ismine(self, address): + """Check if address is in wallet. Return true if and only address is in wallet""" + return self.wallet.is_mine(address) + + @command('') + def dumpprivkeys(self): + """Deprecated.""" + return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '" + + @command('') + def validateaddress(self, address): + """Check that an address is valid. """ + return is_address(address) + + @command('w') + def getpubkeys(self, address): + """Return the public keys for a wallet address. """ + return self.wallet.get_public_keys(address) + + @command('w') + def getbalance(self): + """Return the balance of your wallet. """ + c, u, x = self.wallet.get_balance() + out = {"confirmed": str(Decimal(c)/COIN)} + if u: + out["unconfirmed"] = str(Decimal(u)/COIN) + if x: + out["unmatured"] = str(Decimal(x)/COIN) + return out + + @command('n') + def getaddressbalance(self, address): + """Return the balance of any address. Note: This is a walletless + server query, results are not checked by SPV. + """ + sh = bitcoin.address_to_scripthash(address) + out = self.network.get_balance_for_scripthash(sh) + out["confirmed"] = str(Decimal(out["confirmed"])/COIN) + out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN) + return out + + @command('n') + def getmerkle(self, txid, height): + """Get Merkle branch of a transaction included in a block. Electrum + uses this to verify transactions (Simple Payment Verification).""" + return self.network.get_merkle_for_transaction(txid, int(height)) + + @command('n') + def getservers(self): + """Return the list of available servers""" + return self.network.get_servers() + + @command('') + def version(self): + """Return the version of Electrum.""" + from .version import ELECTRUM_VERSION + return ELECTRUM_VERSION + + @command('w') + def getmpk(self): + """Get master public key. Return your wallet\'s master public key""" + return self.wallet.get_master_public_key() + + @command('wp') + def getmasterprivate(self, password=None): + """Get master private key. Return your wallet\'s master private key""" + return str(self.wallet.keystore.get_master_private_key(password)) + + @command('wp') + def getseed(self, password=None): + """Get seed phrase. Print the generation seed of your wallet.""" + s = self.wallet.get_seed(password) + return s + + @command('wp') + def importprivkey(self, privkey, password=None): + """Import a private key.""" + if not self.wallet.can_import_privkey(): + return "Error: This type of wallet cannot import private keys. Try to create a new wallet with that key." + try: + addr = self.wallet.import_private_key(privkey, password) + out = "Keypair imported: " + addr + except BaseException as e: + out = "Error: " + str(e) + return out + + def _resolver(self, x): + if x is None: + return None + out = self.wallet.contacts.resolve(x) + if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False: + raise Exception('cannot verify alias', x) + return out['address'] + + @command('n') + def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100): + """Sweep private keys. Returns a transaction that spends UTXOs from + privkey to a destination address. The transaction is not + broadcasted.""" + from .wallet import sweep + tx_fee = satoshis(fee) + privkeys = privkey.split() + self.nocheck = nocheck + #dest = self._resolver(destination) + tx = sweep(privkeys, self.network, self.config, destination, tx_fee, imax) + return tx.as_dict() if tx else None + + @command('wp') + def signmessage(self, address, message, password=None): + """Sign a message with a key. Use quotes if your message contains + whitespaces""" + sig = self.wallet.sign_message(address, message, password) + return base64.b64encode(sig).decode('ascii') + + @command('') + def verifymessage(self, address, signature, message): + """Verify a signature.""" + sig = base64.b64decode(signature) + message = util.to_bytes(message) + return ecc.verify_message_with_address(address, sig, message) + + def _mktx(self, outputs, fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime=None): + self.nocheck = nocheck + change_addr = self._resolver(change_addr) + domain = None if domain is None else map(self._resolver, domain) + final_outputs = [] + for address, amount in outputs: + address = self._resolver(address) + amount = satoshis(amount) + final_outputs.append(TxOutput(TYPE_ADDRESS, address, amount)) + + coins = self.wallet.get_spendable_coins(domain, self.config) + tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) + if locktime != None: + tx.locktime = locktime + if rbf is None: + rbf = self.config.get('use_rbf', True) + if rbf: + tx.set_rbf(True) + if not unsigned: + self.wallet.sign_transaction(tx, password) + return tx + + @command('wp') + def payto(self, destination, amount, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=None, password=None, locktime=None): + """Create a transaction. """ + tx_fee = satoshis(fee) + domain = from_addr.split(',') if from_addr else None + tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime) + return tx.as_dict() + + @command('wp') + def paytomany(self, outputs, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=None, password=None, locktime=None): + """Create a multi-output transaction. """ + tx_fee = satoshis(fee) + domain = from_addr.split(',') if from_addr else None + tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime) + return tx.as_dict() + + @command('w') + def history(self, year=None, show_addresses=False, show_fiat=False): + """Wallet history. Returns the transaction history of your wallet.""" + kwargs = {'show_addresses': show_addresses} + if year: + import time + start_date = datetime.datetime(year, 1, 1) + end_date = datetime.datetime(year+1, 1, 1) + kwargs['from_timestamp'] = time.mktime(start_date.timetuple()) + kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) + if show_fiat: + from .exchange_rate import FxThread + fx = FxThread(self.config, None) + kwargs['fx'] = fx + return json_encode(self.wallet.get_full_history(**kwargs)) + + @command('w') + def setlabel(self, key, label): + """Assign a label to an item. Item may be a bitcore address or a + transaction ID""" + self.wallet.set_label(key, label) + + @command('w') + def listcontacts(self): + """Show your list of contacts""" + return self.wallet.contacts + + @command('w') + def getalias(self, key): + """Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record.""" + return self.wallet.contacts.resolve(key) + + @command('w') + def searchcontacts(self, query): + """Search through contacts, return matching entries. """ + results = {} + for key, value in self.wallet.contacts.items(): + if query.lower() in key.lower(): + results[key] = value + return results + + @command('w') + def listaddresses(self, receiving=False, change=False, labels=False, frozen=False, unused=False, funded=False, balance=False): + """List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results.""" + out = [] + for addr in self.wallet.get_addresses(): + if frozen and not self.wallet.is_frozen(addr): + continue + if receiving and self.wallet.is_change(addr): + continue + if change and not self.wallet.is_change(addr): + continue + if unused and self.wallet.is_used(addr): + continue + if funded and self.wallet.is_empty(addr): + continue + item = addr + if labels or balance: + item = (item,) + if balance: + item += (format_satoshis(sum(self.wallet.get_addr_balance(addr))),) + if labels: + item += (repr(self.wallet.labels.get(addr, '')),) + out.append(item) + return out + + @command('n') + def gettransaction(self, txid): + """Retrieve a transaction. """ + if self.wallet and txid in self.wallet.transactions: + tx = self.wallet.transactions[txid] + else: + raw = self.network.get_transaction(txid) + if raw: + tx = Transaction(raw) + else: + raise Exception("Unknown transaction") + return tx.as_dict() + + @command('') + def encrypt(self, pubkey, message): + """Encrypt a message with a public key. Use quotes if the message contains whitespaces.""" + public_key = ecc.ECPubkey(bfh(pubkey)) + encrypted = public_key.encrypt_message(message) + return encrypted + + @command('wp') + def decrypt(self, pubkey, encrypted, password=None): + """Decrypt a message encrypted with a public key.""" + return self.wallet.decrypt_message(pubkey, encrypted, password) + + def _format_request(self, out): + pr_str = { + PR_UNKNOWN: 'Unknown', + PR_UNPAID: 'Pending', + PR_PAID: 'Paid', + PR_EXPIRED: 'Expired', + } + out['amount (BTX)'] = format_satoshis(out.get('amount')) + out['status'] = pr_str[out.get('status', PR_UNKNOWN)] + return out + + @command('w') + def getrequest(self, key): + """Return a payment request""" + r = self.wallet.get_payment_request(key, self.config) + if not r: + raise Exception("Request not found") + return self._format_request(r) + + #@command('w') + #def ackrequest(self, serialized): + # """""" + # pass + + @command('w') + def listrequests(self, pending=False, expired=False, paid=False): + """List the payment requests you made.""" + out = self.wallet.get_sorted_requests(self.config) + if pending: + f = PR_UNPAID + elif expired: + f = PR_EXPIRED + elif paid: + f = PR_PAID + else: + f = None + if f is not None: + out = list(filter(lambda x: x.get('status')==f, out)) + return list(map(self._format_request, out)) + + @command('w') + def createnewaddress(self): + """Create a new receiving address, beyond the gap limit of the wallet""" + return self.wallet.create_new_address(False) + + @command('w') + def getunusedaddress(self): + """Returns the first unused address of the wallet, or None if all addresses are used. + An address is considered as used if it has received a transaction, or if it is used in a payment request.""" + return self.wallet.get_unused_address() + + @command('w') + def addrequest(self, amount, memo='', expiration=None, force=False): + """Create a payment request, using the first unused address of the wallet. + The address will be considered as used after this operation. + If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet.""" + addr = self.wallet.get_unused_address() + if addr is None: + if force: + addr = self.wallet.create_new_address(False) + else: + return False + amount = satoshis(amount) + expiration = int(expiration) if expiration else None + req = self.wallet.make_payment_request(addr, amount, memo, expiration) + self.wallet.add_payment_request(req, self.config) + out = self.wallet.get_payment_request(addr, self.config) + return self._format_request(out) + + @command('w') + def addtransaction(self, tx): + """ Add a transaction to the wallet history """ + tx = Transaction(tx) + if not self.wallet.add_transaction(tx.txid(), tx): + return False + self.wallet.save_transactions() + return tx.txid() + + @command('wp') + def signrequest(self, address, password=None): + "Sign payment request with an OpenAlias" + alias = self.config.get('alias') + if not alias: + raise Exception('No alias in your configuration') + alias_addr = self.wallet.contacts.resolve(alias)['address'] + self.wallet.sign_payment_request(address, alias, alias_addr, password) + + @command('w') + def rmrequest(self, address): + """Remove a payment request""" + return self.wallet.remove_payment_request(address, self.config) + + @command('w') + def clearrequests(self): + """Remove all payment requests""" + for k in list(self.wallet.receive_requests.keys()): + self.wallet.remove_payment_request(k, self.config) + + @command('n') + def notify(self, address, URL): + """Watch an address. Every time the address changes, a http POST is sent to the URL.""" + def callback(x): + import urllib.request + headers = {'content-type':'application/json'} + data = {'address':address, 'status':x.get('result')} + serialized_data = util.to_bytes(json.dumps(data)) + try: + req = urllib.request.Request(URL, serialized_data, headers) + response_stream = urllib.request.urlopen(req, timeout=5) + util.print_error('Got Response for %s' % address) + except BaseException as e: + util.print_error(str(e)) + self.network.subscribe_to_addresses([address], callback) + return True + + @command('wn') + def is_synchronized(self): + """ return wallet synchronization status """ + return self.wallet.is_up_to_date() + + @command('n') + def getfeerate(self, fee_method=None, fee_level=None): + """Return current suggested fee rate (in sat/kvByte), according to config + settings or supplied parameters. + """ + if fee_method is None: + dyn, mempool = None, None + elif fee_method.lower() == 'static': + dyn, mempool = False, False + elif fee_method.lower() == 'eta': + dyn, mempool = True, False + elif fee_method.lower() == 'mempool': + dyn, mempool = True, True + else: + raise Exception('Invalid fee estimation method: {}'.format(fee_method)) + if fee_level is not None: + fee_level = Decimal(fee_level) + return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level) + + @command('') + def help(self): + # for the python console + return sorted(known_commands.keys()) + +param_descriptions = { + 'privkey': 'Private key. Type \'?\' to get a prompt.', + 'destination': 'Bitcore address, contact or alias', + 'address': 'Bitcore address', + 'seed': 'Seed phrase', + 'txid': 'Transaction ID', + 'pos': 'Position', + 'height': 'Block height', + 'tx': 'Serialized transaction (hexadecimal)', + 'key': 'Variable name', + 'pubkey': 'Public key', + 'message': 'Clear text message. Use quotes if it contains spaces.', + 'encrypted': 'Encrypted message', + 'amount': 'Amount to be sent (in BTX). Type \'!\' to send the maximum available.', + 'requested_amount': 'Requested amount (in BTX).', + 'outputs': 'list of ["address", amount]', + 'redeem_script': 'redeem script (hexadecimal)', +} + +command_options = { + 'password': ("-W", "Password"), + 'new_password':(None, "New Password"), + 'receiving': (None, "Show only receiving addresses"), + 'change': (None, "Show only change addresses"), + 'frozen': (None, "Show only frozen addresses"), + 'unused': (None, "Show only unused addresses"), + 'funded': (None, "Show only funded addresses"), + 'balance': ("-b", "Show the balances of listed addresses"), + 'labels': ("-l", "Show the labels of listed addresses"), + 'nocheck': (None, "Do not verify aliases"), + 'imax': (None, "Maximum number of inputs"), + 'fee': ("-f", "Transaction fee (in BTX)"), + 'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."), + 'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"), + 'nbits': (None, "Number of bits of entropy"), + 'segwit': (None, "Create segwit seed"), + 'language': ("-L", "Default language for wordlist"), + 'privkey': (None, "Private key. Set to '?' to get a prompt."), + 'unsigned': ("-u", "Do not sign transaction"), + 'rbf': (None, "Replace-by-fee transaction"), + 'locktime': (None, "Set locktime block number"), + 'domain': ("-D", "List of addresses"), + 'memo': ("-m", "Description of the request"), + 'expiration': (None, "Time in seconds"), + '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."), + 'expired': (None, "Show only expired requests."), + 'paid': (None, "Show only paid requests."), + 'show_addresses': (None, "Show input and output addresses"), + 'show_fiat': (None, "Show fiat value of transactions"), + 'year': (None, "Show history for a given year"), + 'fee_method': (None, "Fee estimation method to use"), + 'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position") +} + + +# don't use floats because of rounding errors +from .transaction import tx_from_str +json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x))) +arg_types = { + 'num': int, + 'nbits': int, + 'imax': int, + 'year': int, + 'tx': tx_from_str, + 'pubkeys': json_loads, + 'jsontx': json_loads, + 'inputs': json_loads, + 'outputs': json_loads, + 'fee': lambda x: str(Decimal(x)) if x is not None else None, + 'amount': lambda x: str(Decimal(x)) if x != '!' else '!', + 'locktime': int, + 'fee_method': str, + 'fee_level': json_loads, +} + +config_variables = { + + 'addrequest': { + 'requests_dir': 'directory where a bip70 file will be written.', + '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 bitcore: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', + }, + 'listrequests':{ + 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcore: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', + } +} + +def set_default_subparser(self, name, args=None): + """see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand""" + subparser_found = False + for arg in sys.argv[1:]: + if arg in ['-h', '--help']: # global help if no subparser + break + else: + for x in self._subparsers._actions: + if not isinstance(x, argparse._SubParsersAction): + continue + for sp_name in x._name_parser_map.keys(): + if sp_name in sys.argv[1:]: + subparser_found = True + if not subparser_found: + # insert default in first position, this implies no + # global options without a sub_parsers specified + if args is None: + sys.argv.insert(1, name) + else: + args.insert(0, name) + +argparse.ArgumentParser.set_default_subparser = set_default_subparser + + +# workaround https://bugs.python.org/issue23058 +# see https://github.com/nickstenning/honcho/pull/121 + +def subparser_call(self, parser, namespace, values, option_string=None): + from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR + parser_name = values[0] + arg_strings = values[1:] + # set the parser name if requested + if self.dest is not SUPPRESS: + setattr(namespace, self.dest, parser_name) + # select the parser + try: + parser = self._name_parser_map[parser_name] + except KeyError: + tup = parser_name, ', '.join(self._name_parser_map) + msg = _('unknown parser {!r} (choices: {})').format(*tup) + raise ArgumentError(self, msg) + # parse all the remaining options into the namespace + # store any unrecognized options on the object, so that the top + # level parser can decide what to do with them + namespace, arg_strings = parser.parse_known_args(arg_strings, namespace) + if arg_strings: + vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, []) + getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings) + +argparse._SubParsersAction.__call__ = subparser_call + + +def add_network_options(parser): + parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only") + parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)") + parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http") + parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers") + +def add_global_options(parser): + group = parser.add_argument_group('global options') + # const is for when no argument is given to verbosity + # default is for when the flag is missing + group.add_argument("-v", dest="verbosity", help="Set verbosity filter", default='', const='*', nargs='?') + group.add_argument("-D", "--dir", dest="electrum_path", help="electrum directory") + group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum-btx_data' directory") + group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path") + group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet") + group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest") + group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet") + +def get_parser(): + # create main parser + parser = argparse.ArgumentParser( + epilog="Run 'electrum help ' to see the help for a command") + add_global_options(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="bitcore 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("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline") + 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") + add_network_options(parser_gui) + add_global_options(parser_gui) + # daemon + parser_daemon = subparsers.add_parser('daemon', help="Run Daemon") + parser_daemon.add_argument("subcommand", choices=['start', 'status', 'stop', 'load_wallet', 'close_wallet'], nargs='?') + #parser_daemon.set_defaults(func=run_daemon) + add_network_options(parser_daemon) + add_global_options(parser_daemon) + # commands + for cmdname in sorted(known_commands.keys()): + cmd = known_commands[cmdname] + p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description) + add_global_options(p) + if cmdname == 'restore': + p.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline") + for optname, default in zip(cmd.options, cmd.defaults): + a, help = command_options[optname] + b = '--' + optname + action = "store_true" if type(default) is bool else 'store' + args = (a, b) if a else (b,) + if action == 'store': + _type = arg_types.get(optname, str) + p.add_argument(*args, dest=optname, action=action, default=default, help=help, type=_type) + else: + p.add_argument(*args, dest=optname, action=action, default=default, help=help) + + for param in cmd.params: + h = param_descriptions.get(param, '') + _type = arg_types.get(param, str) + p.add_argument(param, help=h, type=_type) + + cvh = config_variables.get(cmdname) + if cvh: + group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)') + for k, v in cvh.items(): + group.add_argument(k, nargs='?', help=v) + + # 'gui' is the default command + parser.set_default_subparser('gui') + return parser diff --git a/electrum/constants.py b/electrum/constants.py new file mode 100644 index 000000000..ec432f28e --- /dev/null +++ b/electrum/constants.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2018 The Electrum developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import json + + +def read_json(filename, default): + path = os.path.join(os.path.dirname(__file__), filename) + try: + with open(path, 'r') as f: + r = json.loads(f.read()) + except: + r = default + return r + + +class BitcoinMainnet: + + TESTNET = False + WIF_PREFIX = 0x80 + ADDRTYPE_P2PKH = 3 + ADDRTYPE_P2SH = 125 + SEGWIT_HRP = "btx" + GENESIS = "604148281e5c4b7f2487e5d03cd60d8e6f69411d613f6448034508cea52e9574" + DEFAULT_PORTS = {'t': '50001', 's': '50002'} + DEFAULT_SERVERS = read_json('servers.json', {}) + CHECKPOINTS = read_json('checkpoints.json', []) + + XPRV_HEADERS = { + 'standard': 0x0488ade4, # xprv + 'p2wpkh-p2sh': 0x049d7878, # yprv + 'p2wsh-p2sh': 0x0295b005, # Yprv + 'p2wpkh': 0x04b2430c, # zprv + 'p2wsh': 0x02aa7a99, # Zprv + } + XPUB_HEADERS = { + 'standard': 0x0488b21e, # xpub + 'p2wpkh-p2sh': 0x049d7cb2, # ypub + 'p2wsh-p2sh': 0x0295b43f, # Ypub + 'p2wpkh': 0x04b24746, # zpub + 'p2wsh': 0x02aa7ed3, # Zpub + } + BIP44_COIN_TYPE = 160 + + +class BitcoinTestnet: + + TESTNET = True + WIF_PREFIX = 0xef + ADDRTYPE_P2PKH = 111 + ADDRTYPE_P2SH = 196 + SEGWIT_HRP = "tb" + GENESIS = "02c5d66e8edb49984eb743c798bca069466ce457b7febfa3c3a01b33353b7bc6" + DEFAULT_PORTS = {'t': '51001', 's': '51002'} + DEFAULT_SERVERS = read_json('servers_testnet.json', {}) + CHECKPOINTS = read_json('checkpoints_testnet.json', []) + + XPRV_HEADERS = { + 'standard': 0x04358394, # tprv + 'p2wpkh-p2sh': 0x044a4e28, # uprv + 'p2wsh-p2sh': 0x024285b5, # Uprv + 'p2wpkh': 0x045f18bc, # vprv + 'p2wsh': 0x02575048, # Vprv + } + XPUB_HEADERS = { + 'standard': 0x043587cf, # tpub + 'p2wpkh-p2sh': 0x044a5262, # upub + 'p2wsh-p2sh': 0x024289ef, # Upub + 'p2wpkh': 0x045f1cf6, # vpub + 'p2wsh': 0x02575483, # Vpub + } + BIP44_COIN_TYPE = 1 + + +class BitcoinRegtest(BitcoinTestnet): + + SEGWIT_HRP = "bcrt" + GENESIS = "604148281e5c4b7f2487e5d03cd60d8e6f69411d613f6448034508cea52e9574" + DEFAULT_SERVERS = read_json('servers_regtest.json', {}) + CHECKPOINTS = [] + + +class BitcoinSimnet(BitcoinTestnet): + + SEGWIT_HRP = "sb" + GENESIS = "683e86bd5c6d110d91b94b97137ba6bfe02dbbdb8e3dff722a669b5d69d77af6" + DEFAULT_SERVERS = read_json('servers_regtest.json', {}) + CHECKPOINTS = [] + + +# don't import net directly, import the module instead (so that net is singleton) +net = BitcoinMainnet + +def set_simnet(): + global net + net = BitcoinSimnet + +def set_mainnet(): + global net + net = BitcoinMainnet + + +def set_testnet(): + global net + net = BitcoinTestnet + + +def set_regtest(): + global net + net = BitcoinRegtest diff --git a/electrum/contacts.py b/electrum/contacts.py new file mode 100644 index 000000000..03b8d3ecc --- /dev/null +++ b/electrum/contacts.py @@ -0,0 +1,135 @@ +# 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 re +import dns +from dns.exception import DNSException +import json +import traceback +import sys + +from . import bitcoin +from . import dnssec +from .util import export_meta, import_meta, print_error, to_string + + +class Contacts(dict): + + def __init__(self, storage): + self.storage = storage + d = self.storage.get('contacts', {}) + try: + self.update(d) + except: + return + # backward compatibility + for k, v in self.items(): + _type, n = v + if _type == 'address' and bitcoin.is_address(n): + self.pop(k) + self[n] = ('address', k) + + def save(self): + self.storage.put('contacts', dict(self)) + + def import_file(self, path): + import_meta(path, self._validate, self.load_meta) + + def load_meta(self, data): + self.update(data) + self.save() + + def export_file(self, filename): + export_meta(self, filename) + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + self.save() + + def pop(self, key): + if key in self.keys(): + dict.pop(self, key) + self.save() + + def resolve(self, k): + if bitcoin.is_address(k): + return { + 'address': k, + 'type': 'address' + } + if k in self.keys(): + _type, addr = self[k] + if _type == 'address': + return { + 'address': addr, + 'type': 'contact' + } + out = self.resolve_openalias(k) + if out: + address, name, validated = out + return { + 'address': address, + 'name': name, + 'type': 'openalias', + 'validated': validated + } + raise Exception("Invalid Bitcoin address or alias", k) + + def resolve_openalias(self, url): + # support email-style addresses, per the OA standard + url = url.replace('@', '.') + try: + records, validated = dnssec.query(url, dns.rdatatype.TXT) + except DNSException as e: + print_error('Error resolving openalias: ', str(e)) + return None + prefix = 'btc' + for record in records: + string = to_string(record.strings[0], 'utf8') + if string.startswith('oa1:' + prefix): + address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') + name = self.find_regex(string, r'recipient_name=([^;]+)') + if not name: + name = address + if not address: + continue + return address, name, validated + + def find_regex(self, haystack, needle): + regex = re.compile(needle) + try: + return regex.search(haystack).groups()[0] + except AttributeError: + return None + + def _validate(self, data): + for k, v in list(data.items()): + if k == 'contacts': + return self._validate(v) + if not bitcoin.is_address(k): + data.pop(k) + else: + _type, _ = v + if _type != 'address': + data.pop(k) + return data + diff --git a/electrum/crypto.py b/electrum/crypto.py new file mode 100644 index 000000000..de8b6b5d7 --- /dev/null +++ b/electrum/crypto.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2018 The Electrum developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import base64 +import os +import hashlib +import hmac + +import pyaes + +from .util import assert_bytes, InvalidPassword, to_bytes, to_string + + +try: + from Cryptodome.Cipher import AES +except: + AES = None + + +class InvalidPadding(Exception): + pass + + +def append_PKCS7_padding(data): + assert_bytes(data) + padlen = 16 - (len(data) % 16) + return data + bytes([padlen]) * padlen + + +def strip_PKCS7_padding(data): + assert_bytes(data) + if len(data) % 16 != 0 or len(data) == 0: + raise InvalidPadding("invalid length") + padlen = data[-1] + if padlen > 16: + raise InvalidPadding("invalid padding byte (large)") + for i in data[-padlen:]: + if i != padlen: + raise InvalidPadding("invalid padding byte (inconsistent)") + return data[0:-padlen] + + +def aes_encrypt_with_iv(key, iv, data): + assert_bytes(key, iv, data) + data = append_PKCS7_padding(data) + if AES: + e = AES.new(key, AES.MODE_CBC, iv).encrypt(data) + else: + aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) + aes = pyaes.Encrypter(aes_cbc, padding=pyaes.PADDING_NONE) + e = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer + return e + + +def aes_decrypt_with_iv(key, iv, data): + assert_bytes(key, iv, data) + if AES: + cipher = AES.new(key, AES.MODE_CBC, iv) + data = cipher.decrypt(data) + else: + aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) + aes = pyaes.Decrypter(aes_cbc, padding=pyaes.PADDING_NONE) + data = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer + try: + return strip_PKCS7_padding(data) + except InvalidPadding: + raise InvalidPassword() + + +def EncodeAES(secret, s): + assert_bytes(s) + iv = bytes(os.urandom(16)) + ct = aes_encrypt_with_iv(secret, iv, s) + e = iv + ct + return base64.b64encode(e) + +def DecodeAES(secret, e): + e = bytes(base64.b64decode(e)) + iv, e = e[:16], e[16:] + s = aes_decrypt_with_iv(secret, iv, e) + return s + +def pw_encode(s, password): + if password: + secret = Hash(password) + return EncodeAES(secret, to_bytes(s, "utf8")).decode('utf8') + else: + return s + +def pw_decode(s, password): + if password is not None: + secret = Hash(password) + try: + d = to_string(DecodeAES(secret, s), "utf8") + except Exception: + raise InvalidPassword() + return d + else: + return s + + +def sha256(x: bytes) -> bytes: + x = to_bytes(x, 'utf8') + return bytes(hashlib.sha256(x).digest()) + + +def Hash(x: bytes) -> bytes: + x = to_bytes(x, 'utf8') + out = bytes(sha256(sha256(x))) + return out + + +def hash_160(x: bytes) -> bytes: + try: + md = hashlib.new('ripemd160') + md.update(sha256(x)) + return md.digest() + except BaseException: + from . import ripemd + md = ripemd.new(sha256(x)) + return md.digest() + + +def hmac_oneshot(key: bytes, msg: bytes, digest) -> bytes: + if hasattr(hmac, 'digest'): + # requires python 3.7+; faster + return hmac.digest(key, msg, digest) + else: + return hmac.new(key, msg, digest).digest() diff --git a/electrum/currencies.json b/electrum/currencies.json new file mode 100644 index 000000000..0d4aa2023 --- /dev/null +++ b/electrum/currencies.json @@ -0,0 +1,36 @@ +{ + "CoinMarketCap": [ + "AUD", + "BRL", + "CAD", + "CHF", + "CLP", + "CNY", + "CZK", + "DKK", + "EUR", + "GBP", + "HKD", + "HUF", + "IDR", + "ILS", + "INR", + "JPY", + "KRW", + "MXN", + "MYR", + "NOK", + "NZD", + "PHP", + "PKR", + "PLN", + "RUB", + "SEK", + "SGD", + "THB", + "TRY", + "TWD", + "USD", + "ZAR" + ] +} \ No newline at end of file diff --git a/electrum/daemon.py b/electrum/daemon.py new file mode 100644 index 000000000..ccdce4a0a --- /dev/null +++ b/electrum/daemon.py @@ -0,0 +1,316 @@ +#!/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 ast +import os +import time +import traceback +import sys + +# from jsonrpc import JSONRPCResponseManager +import jsonrpclib +from .jsonrpc import VerifyingJSONRPCServer + +from .version import ELECTRUM_VERSION +from .network import Network +from .util import json_decode, DaemonThread +from .util import print_error, to_string +from .wallet import Wallet +from .storage import WalletStorage +from .commands import known_commands, Commands +from .simple_config import SimpleConfig +from .exchange_rate import FxThread +from .plugin import run_hook + + +def get_lockfile(config): + return os.path.join(config.path, 'daemon') + + +def remove_lockfile(lockfile): + os.unlink(lockfile) + + +def get_fd_or_server(config): + '''Tries to create the lockfile, using O_EXCL to + prevent races. If it succeeds it returns the FD. + Otherwise try and connect to the server specified in the lockfile. + If this succeeds, the server is returned. Otherwise remove the + lockfile and try again.''' + lockfile = get_lockfile(config) + while True: + try: + return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644), None + except OSError: + pass + server = get_server(config) + if server is not None: + return None, server + # Couldn't connect; remove lockfile and try again. + remove_lockfile(lockfile) + + +def get_server(config): + lockfile = get_lockfile(config) + while True: + create_time = None + try: + with open(lockfile) as f: + (host, port), create_time = ast.literal_eval(f.read()) + rpc_user, rpc_password = get_rpc_credentials(config) + if rpc_password == '': + # authentication disabled + server_url = 'http://%s:%d' % (host, port) + else: + server_url = 'http://%s:%s@%s:%d' % ( + rpc_user, rpc_password, host, port) + server = jsonrpclib.Server(server_url) + # Test daemon is running + server.ping() + return server + except Exception as e: + print_error("[get_server]", e) + if not create_time or create_time < time.time() - 1.0: + return None + # Sleep a bit and try again; it might have just been started + time.sleep(1.0) + + +def get_rpc_credentials(config): + rpc_user = config.get('rpcuser', None) + rpc_password = config.get('rpcpassword', None) + if rpc_user is None or rpc_password is None: + rpc_user = 'user' + import ecdsa, base64 + bits = 128 + nbytes = bits // 8 + (bits % 8 > 0) + pw_int = ecdsa.util.randrange(pow(2, bits)) + pw_b64 = base64.b64encode( + pw_int.to_bytes(nbytes, 'big'), b'-_') + rpc_password = to_string(pw_b64, 'ascii') + config.set_key('rpcuser', rpc_user) + config.set_key('rpcpassword', rpc_password, save=True) + elif rpc_password == '': + from .util import print_stderr + print_stderr('WARNING: RPC authentication is disabled.') + return rpc_user, rpc_password + + +class Daemon(DaemonThread): + + def __init__(self, config, fd, is_gui): + DaemonThread.__init__(self) + self.config = config + if config.get('offline'): + self.network = None + else: + self.network = Network(config) + self.network.start() + self.fx = FxThread(config, self.network) + if self.network: + self.network.add_jobs([self.fx]) + self.gui = None + self.wallets = {} + # Setup JSONRPC server + self.init_server(config, fd, is_gui) + + def init_server(self, config, fd, is_gui): + host = config.get('rpchost', '127.0.0.1') + port = config.get('rpcport', 0) + + rpc_user, rpc_password = get_rpc_credentials(config) + try: + server = VerifyingJSONRPCServer((host, port), logRequests=False, + rpc_user=rpc_user, rpc_password=rpc_password) + except Exception as e: + self.print_error('Warning: cannot initialize RPC server on host', host, e) + self.server = None + os.close(fd) + return + os.write(fd, bytes(repr((server.socket.getsockname(), time.time())), 'utf8')) + os.close(fd) + self.server = server + server.timeout = 0.1 + server.register_function(self.ping, 'ping') + if is_gui: + server.register_function(self.run_gui, 'gui') + else: + server.register_function(self.run_daemon, 'daemon') + self.cmd_runner = Commands(self.config, None, self.network) + for cmdname in known_commands: + server.register_function(getattr(self.cmd_runner, cmdname), cmdname) + server.register_function(self.run_cmdline, 'run_cmdline') + + def ping(self): + return True + + def run_daemon(self, config_options): + config = SimpleConfig(config_options) + sub = config.get('subcommand') + assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet'] + if sub in [None, 'start']: + response = "Daemon already running" + elif sub == 'load_wallet': + path = config.get_wallet_path() + wallet = self.load_wallet(path, config.get('password')) + if wallet is not None: + self.cmd_runner.wallet = wallet + run_hook('load_wallet', wallet, None) + response = wallet is not None + elif sub == 'close_wallet': + path = config.get_wallet_path() + if path in self.wallets: + self.stop_wallet(path) + response = True + else: + response = False + elif sub == 'status': + if self.network: + p = self.network.get_parameters() + current_wallet = self.cmd_runner.wallet + current_wallet_path = current_wallet.storage.path \ + if current_wallet else None + response = { + 'path': self.network.config.path, + 'server': p[0], + 'blockchain_height': self.network.get_local_height(), + 'server_height': self.network.get_server_height(), + 'spv_nodes': len(self.network.get_interfaces()), + 'connected': self.network.is_connected(), + 'auto_connect': p[4], + 'version': ELECTRUM_VERSION, + 'wallets': {k: w.is_up_to_date() + for k, w in self.wallets.items()}, + 'current_wallet': current_wallet_path, + 'fee_per_kb': self.config.fee_per_kb(), + } + else: + response = "Daemon offline" + elif sub == 'stop': + self.stop() + response = "Daemon stopped" + return response + + def run_gui(self, config_options): + config = SimpleConfig(config_options) + if self.gui: + #if hasattr(self.gui, 'new_window'): + # path = config.get_wallet_path() + # self.gui.new_window(path, config.get('url')) + # response = "ok" + #else: + # response = "error: current GUI does not support multiple windows" + response = "error: Electrum GUI already running" + else: + response = "Error: Electrum is running in daemon mode. Please stop the daemon first." + return response + + def load_wallet(self, path, password): + # wizard will be launched if we return + if path in self.wallets: + wallet = self.wallets[path] + return wallet + storage = WalletStorage(path, manual_upgrades=True) + if not storage.file_exists(): + return + if storage.is_encrypted(): + if not password: + return + storage.decrypt(password) + if storage.requires_split(): + return + if storage.get_action(): + return + wallet = Wallet(storage) + wallet.start_threads(self.network) + self.wallets[path] = wallet + return wallet + + def add_wallet(self, wallet): + path = wallet.storage.path + self.wallets[path] = wallet + + def get_wallet(self, path): + return self.wallets.get(path) + + def stop_wallet(self, path): + wallet = self.wallets.pop(path) + wallet.stop_threads() + + def run_cmdline(self, config_options): + password = config_options.get('password') + new_password = config_options.get('new_password') + config = SimpleConfig(config_options) + # FIXME this is ugly... + config.fee_estimates = self.network.config.fee_estimates.copy() + config.mempool_fees = self.network.config.mempool_fees.copy() + cmdname = config.get('cmd') + cmd = known_commands[cmdname] + if cmd.requires_wallet: + path = config.get_wallet_path() + wallet = self.wallets.get(path) + if wallet is None: + return {'error': 'Wallet "%s" is not loaded. Use "electrum daemon load_wallet"'%os.path.basename(path) } + else: + wallet = None + # arguments passed to function + args = map(lambda x: config.get(x), cmd.params) + # decode json arguments + args = [json_decode(i) for i in args] + # options + kwargs = {} + for x in cmd.options: + kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x)) + cmd_runner = Commands(config, wallet, self.network) + func = getattr(cmd_runner, cmd.name) + result = func(*args, **kwargs) + return result + + def run(self): + while self.is_running(): + self.server.handle_request() if self.server else time.sleep(0.1) + for k, wallet in self.wallets.items(): + wallet.stop_threads() + if self.network: + self.print_error("shutting down network") + self.network.stop() + self.network.join() + self.on_stop() + + def stop(self): + self.print_error("stopping, removing lockfile") + remove_lockfile(get_lockfile(self.config)) + DaemonThread.stop(self) + + def init_gui(self, config, plugins): + gui_name = config.get('gui', 'qt') + if gui_name in ['lite', 'classic']: + gui_name = 'qt' + gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) + self.gui = gui.ElectrumGui(config, self, plugins) + try: + self.gui.main() + except BaseException as e: + traceback.print_exc(file=sys.stdout) + # app will exit now diff --git a/electrum/dnssec.py b/electrum/dnssec.py new file mode 100644 index 000000000..6a8ac9807 --- /dev/null +++ b/electrum/dnssec.py @@ -0,0 +1,272 @@ +#!/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. + +# Check DNSSEC trust chain. +# Todo: verify expiration dates +# +# Based on +# http://backreference.org/2010/11/17/dnssec-verification-with-dig/ +# https://github.com/rthalley/dnspython/blob/master/tests/test_dnssec.py + + +# import traceback +# import sys +import time +import struct + + +import dns.name +import dns.query +import dns.dnssec +import dns.message +import dns.resolver +import dns.rdatatype +import dns.rdtypes.ANY.NS +import dns.rdtypes.ANY.CNAME +import dns.rdtypes.ANY.DLV +import dns.rdtypes.ANY.DNSKEY +import dns.rdtypes.ANY.DS +import dns.rdtypes.ANY.NSEC +import dns.rdtypes.ANY.NSEC3 +import dns.rdtypes.ANY.NSEC3PARAM +import dns.rdtypes.ANY.RRSIG +import dns.rdtypes.ANY.SOA +import dns.rdtypes.ANY.TXT +import dns.rdtypes.IN.A +import dns.rdtypes.IN.AAAA + + +# Pure-Python version of dns.dnssec._validate_rsig +import ecdsa +from . import rsakey + + +def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None): + from dns.dnssec import ValidationFailure, ECDSAP256SHA256, ECDSAP384SHA384 + from dns.dnssec import _find_candidate_keys, _make_hash, _is_ecdsa, _is_rsa, _to_rdata, _make_algorithm_id + + if isinstance(origin, str): + origin = dns.name.from_text(origin, dns.name.root) + + for candidate_key in _find_candidate_keys(keys, rrsig): + if not candidate_key: + raise ValidationFailure('unknown key') + + # For convenience, allow the rrset to be specified as a (name, rdataset) + # tuple as well as a proper rrset + if isinstance(rrset, tuple): + rrname = rrset[0] + rdataset = rrset[1] + else: + rrname = rrset.name + rdataset = rrset + + if now is None: + now = time.time() + if rrsig.expiration < now: + raise ValidationFailure('expired') + if rrsig.inception > now: + raise ValidationFailure('not yet valid') + + hash = _make_hash(rrsig.algorithm) + + if _is_rsa(rrsig.algorithm): + keyptr = candidate_key.key + (bytes,) = struct.unpack('!B', keyptr[0:1]) + keyptr = keyptr[1:] + if bytes == 0: + (bytes,) = struct.unpack('!H', keyptr[0:2]) + keyptr = keyptr[2:] + rsa_e = keyptr[0:bytes] + rsa_n = keyptr[bytes:] + n = ecdsa.util.string_to_number(rsa_n) + e = ecdsa.util.string_to_number(rsa_e) + pubkey = rsakey.RSAKey(n, e) + sig = rrsig.signature + + elif _is_ecdsa(rrsig.algorithm): + if rrsig.algorithm == ECDSAP256SHA256: + curve = ecdsa.curves.NIST256p + key_len = 32 + digest_len = 32 + elif rrsig.algorithm == ECDSAP384SHA384: + curve = ecdsa.curves.NIST384p + key_len = 48 + digest_len = 48 + else: + # shouldn't happen + raise ValidationFailure('unknown ECDSA curve') + keyptr = candidate_key.key + x = ecdsa.util.string_to_number(keyptr[0:key_len]) + y = ecdsa.util.string_to_number(keyptr[key_len:key_len * 2]) + assert ecdsa.ecdsa.point_is_valid(curve.generator, x, y) + point = ecdsa.ellipticcurve.Point(curve.curve, x, y, curve.order) + verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point, curve) + r = rrsig.signature[:key_len] + s = rrsig.signature[key_len:] + sig = ecdsa.ecdsa.Signature(ecdsa.util.string_to_number(r), + ecdsa.util.string_to_number(s)) + + else: + raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm) + + hash.update(_to_rdata(rrsig, origin)[:18]) + hash.update(rrsig.signer.to_digestable(origin)) + + if rrsig.labels < len(rrname) - 1: + suffix = rrname.split(rrsig.labels + 1)[1] + rrname = dns.name.from_text('*', suffix) + rrnamebuf = rrname.to_digestable(origin) + rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass, + rrsig.original_ttl) + rrlist = sorted(rdataset); + for rr in rrlist: + hash.update(rrnamebuf) + hash.update(rrfixed) + rrdata = rr.to_digestable(origin) + rrlen = struct.pack('!H', len(rrdata)) + hash.update(rrlen) + hash.update(rrdata) + + digest = hash.digest() + + if _is_rsa(rrsig.algorithm): + digest = _make_algorithm_id(rrsig.algorithm) + digest + if pubkey.verify(bytearray(sig), bytearray(digest)): + return + + elif _is_ecdsa(rrsig.algorithm): + diglong = ecdsa.util.string_to_number(digest) + if verifying_key.pubkey.verifies(diglong, sig): + return + + else: + raise ValidationFailure('unknown algorithm %s' % rrsig.algorithm) + + raise ValidationFailure('verify failure') + + +# replace validate_rrsig +dns.dnssec._validate_rrsig = python_validate_rrsig +dns.dnssec.validate_rrsig = python_validate_rrsig +dns.dnssec.validate = dns.dnssec._validate + + + +from .util import print_error + + +# hard-coded trust anchors (root KSKs) +trust_anchors = [ + # KSK-2017: + dns.rrset.from_text('.', 1 , 'IN', 'DNSKEY', '257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU='), + # KSK-2010: + dns.rrset.from_text('.', 15202, 'IN', 'DNSKEY', '257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0='), +] + + +def check_query(ns, sub, _type, keys): + q = dns.message.make_query(sub, _type, want_dnssec=True) + response = dns.query.tcp(q, ns, timeout=5) + assert response.rcode() == 0, 'No answer' + answer = response.answer + assert len(answer) != 0, ('No DNS record found', sub, _type) + assert len(answer) != 1, ('No DNSSEC record found', sub, _type) + if answer[0].rdtype == dns.rdatatype.RRSIG: + rrsig, rrset = answer + elif answer[1].rdtype == dns.rdatatype.RRSIG: + rrset, rrsig = answer + else: + raise Exception('No signature set in record') + if keys is None: + keys = {dns.name.from_text(sub):rrset} + dns.dnssec.validate(rrset, rrsig, keys) + return rrset + + +def get_and_validate(ns, url, _type): + # get trusted root key + root_rrset = None + for dnskey_rr in trust_anchors: + try: + # Check if there is a valid signature for the root dnskey + root_rrset = check_query(ns, '', dns.rdatatype.DNSKEY, {dns.name.root: dnskey_rr}) + break + except dns.dnssec.ValidationFailure: + # It's OK as long as one key validates + continue + if not root_rrset: + raise dns.dnssec.ValidationFailure('None of the trust anchors found in DNS') + keys = {dns.name.root: root_rrset} + # top-down verification + parts = url.split('.') + for i in range(len(parts), 0, -1): + sub = '.'.join(parts[i-1:]) + name = dns.name.from_text(sub) + # If server is authoritative, don't fetch DNSKEY + query = dns.message.make_query(sub, dns.rdatatype.NS) + response = dns.query.udp(query, ns, 3) + assert response.rcode() == dns.rcode.NOERROR, "query error" + rrset = response.authority[0] if len(response.authority) > 0 else response.answer[0] + rr = rrset[0] + if rr.rdtype == dns.rdatatype.SOA: + continue + # get DNSKEY (self-signed) + rrset = check_query(ns, sub, dns.rdatatype.DNSKEY, None) + # get DS (signed by parent) + ds_rrset = check_query(ns, sub, dns.rdatatype.DS, keys) + # verify that a signed DS validates DNSKEY + for ds in ds_rrset: + for dnskey in rrset: + htype = 'SHA256' if ds.digest_type == 2 else 'SHA1' + good_ds = dns.dnssec.make_ds(name, dnskey, htype) + if ds == good_ds: + break + else: + continue + break + else: + raise Exception("DS does not match DNSKEY") + # set key for next iteration + keys = {name: rrset} + # get TXT record (signed by zone) + rrset = check_query(ns, url, _type, keys) + return rrset + + +def query(url, rtype): + # 8.8.8.8 is Google's public DNS server + nameservers = ['8.8.8.8'] + ns = nameservers[0] + try: + out = get_and_validate(ns, url, rtype) + validated = True + except BaseException as e: + #traceback.print_exc(file=sys.stderr) + print_error("DNSSEC error:", str(e)) + resolver = dns.resolver.get_default_resolver() + out = resolver.query(url, rtype) + validated = False + return out, validated diff --git a/electrum/ecc.py b/electrum/ecc.py new file mode 100644 index 000000000..b7f6c643a --- /dev/null +++ b/electrum/ecc.py @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2018 The Electrum developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import base64 +import hmac +import hashlib +from typing import Union + + +import ecdsa +from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1 +from ecdsa.curves import SECP256k1 +from ecdsa.ellipticcurve import Point +from ecdsa.util import string_to_number, number_to_string + +from .util import bfh, bh2u, assert_bytes, print_error, to_bytes, InvalidPassword, profiler +from .crypto import (Hash, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot) +from .ecc_fast import do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1 + + +do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1() + +CURVE_ORDER = SECP256k1.order + + +def generator(): + return ECPubkey.from_point(generator_secp256k1) + + +def point_at_infinity(): + return ECPubkey(None) + + +def sig_string_from_der_sig(der_sig, order=CURVE_ORDER): + r, s = ecdsa.util.sigdecode_der(der_sig, order) + return ecdsa.util.sigencode_string(r, s, order) + + +def der_sig_from_sig_string(sig_string, order=CURVE_ORDER): + r, s = ecdsa.util.sigdecode_string(sig_string, order) + return ecdsa.util.sigencode_der_canonize(r, s, order) + + +def der_sig_from_r_and_s(r, s, order=CURVE_ORDER): + return ecdsa.util.sigencode_der_canonize(r, s, order) + + +def get_r_and_s_from_der_sig(der_sig, order=CURVE_ORDER): + r, s = ecdsa.util.sigdecode_der(der_sig, order) + return r, s + + +def get_r_and_s_from_sig_string(sig_string, order=CURVE_ORDER): + r, s = ecdsa.util.sigdecode_string(sig_string, order) + return r, s + + +def sig_string_from_r_and_s(r, s, order=CURVE_ORDER): + return ecdsa.util.sigencode_string_canonize(r, s, order) + + +def point_to_ser(P, compressed=True) -> bytes: + if isinstance(P, tuple): + assert len(P) == 2, 'unexpected point: %s' % P + x, y = P + else: + x, y = P.x(), P.y() + if x is None or y is None: # infinity + return None + if compressed: + return bfh(('%02x' % (2+(y&1))) + ('%064x' % x)) + return bfh('04'+('%064x' % x)+('%064x' % y)) + + +def get_y_coord_from_x(x, odd=True): + curve = curve_secp256k1 + _p = curve.p() + _a = curve.a() + _b = curve.b() + for offset in range(128): + Mx = x + offset + My2 = pow(Mx, 3, _p) + _a * pow(Mx, 2, _p) + _b % _p + My = pow(My2, (_p + 1) // 4, _p) + if curve.contains_point(Mx, My): + if odd == bool(My & 1): + return My + return _p - My + raise Exception('ECC_YfromX: No Y found') + + +def ser_to_point(ser: bytes) -> (int, int): + if ser[0] not in (0x02, 0x03, 0x04): + raise ValueError('Unexpected first byte: {}'.format(ser[0])) + if ser[0] == 0x04: + return string_to_number(ser[1:33]), string_to_number(ser[33:]) + x = string_to_number(ser[1:]) + return x, get_y_coord_from_x(x, ser[0] == 0x03) + + +def _ser_to_python_ecdsa_point(ser: bytes) -> ecdsa.ellipticcurve.Point: + x, y = ser_to_point(ser) + try: + return Point(curve_secp256k1, x, y, CURVE_ORDER) + except: + raise InvalidECPointException() + + +class InvalidECPointException(Exception): + """e.g. not on curve, or infinity""" + + +class _MyVerifyingKey(ecdsa.VerifyingKey): + @classmethod + def from_signature(klass, sig, recid, h, curve): # TODO use libsecp?? + """ See http://www.secg.org/download/aid-780/sec1-v2.pdf, chapter 4.1.6 """ + from ecdsa import util, numbertheory + from . import msqr + curveFp = curve.curve + G = curve.generator + order = G.order() + # extract r,s from signature + r, s = util.sigdecode_string(sig, order) + # 1.1 + x = r + (recid//2) * order + # 1.3 + alpha = ( x * x * x + curveFp.a() * x + curveFp.b() ) % curveFp.p() + beta = msqr.modular_sqrt(alpha, curveFp.p()) + y = beta if (beta - recid) % 2 == 0 else curveFp.p() - beta + # 1.4 the constructor checks that nR is at infinity + try: + R = Point(curveFp, x, y, order) + except: + raise InvalidECPointException() + # 1.5 compute e from message: + e = string_to_number(h) + minus_e = -e % order + # 1.6 compute Q = r^-1 (sR - eG) + inv_r = numbertheory.inverse_mod(r,order) + try: + Q = inv_r * ( s * R + minus_e * G ) + except: + raise InvalidECPointException() + return klass.from_public_point( Q, curve ) + + +class _MySigningKey(ecdsa.SigningKey): + """Enforce low S values in signatures""" + + def sign_number(self, number, entropy=None, k=None): + r, s = ecdsa.SigningKey.sign_number(self, number, entropy, k) + if s > CURVE_ORDER//2: + s = CURVE_ORDER - s + return r, s + + +class _PubkeyForPointAtInfinity: + point = ecdsa.ellipticcurve.INFINITY + + +class ECPubkey(object): + + def __init__(self, b: bytes): + if b is not None: + assert_bytes(b) + point = _ser_to_python_ecdsa_point(b) + self._pubkey = ecdsa.ecdsa.Public_key(generator_secp256k1, point) + else: + self._pubkey = _PubkeyForPointAtInfinity() + + @classmethod + def from_sig_string(cls, sig_string: bytes, recid: int, msg_hash: bytes): + assert_bytes(sig_string) + if len(sig_string) != 64: + raise Exception('Wrong encoding') + if recid < 0 or recid > 3: + raise ValueError('recid is {}, but should be 0 <= recid <= 3'.format(recid)) + ecdsa_verifying_key = _MyVerifyingKey.from_signature(sig_string, recid, msg_hash, curve=SECP256k1) + ecdsa_point = ecdsa_verifying_key.pubkey.point + return ECPubkey.from_point(ecdsa_point) + + @classmethod + def from_signature65(cls, sig: bytes, msg_hash: bytes): + if len(sig) != 65: + raise Exception("Wrong encoding") + nV = sig[0] + if nV < 27 or nV >= 35: + raise Exception("Bad encoding") + if nV >= 31: + compressed = True + nV -= 4 + else: + compressed = False + recid = nV - 27 + return cls.from_sig_string(sig[1:], recid, msg_hash), compressed + + @classmethod + def from_point(cls, point): + _bytes = point_to_ser(point, compressed=False) # faster than compressed + return ECPubkey(_bytes) + + def get_public_key_bytes(self, compressed=True): + if self.is_at_infinity(): raise Exception('point is at infinity') + return point_to_ser(self.point(), compressed) + + def get_public_key_hex(self, compressed=True): + return bh2u(self.get_public_key_bytes(compressed)) + + def point(self) -> (int, int): + return self._pubkey.point.x(), self._pubkey.point.y() + + def __mul__(self, other: int): + if not isinstance(other, int): + raise TypeError('multiplication not defined for ECPubkey and {}'.format(type(other))) + ecdsa_point = self._pubkey.point * other + return self.from_point(ecdsa_point) + + def __rmul__(self, other: int): + return self * other + + def __add__(self, other): + if not isinstance(other, ECPubkey): + raise TypeError('addition not defined for ECPubkey and {}'.format(type(other))) + ecdsa_point = self._pubkey.point + other._pubkey.point + return self.from_point(ecdsa_point) + + def __eq__(self, other): + return self._pubkey.point.x() == other._pubkey.point.x() \ + and self._pubkey.point.y() == other._pubkey.point.y() + + def __ne__(self, other): + return not (self == other) + + def verify_message_for_address(self, sig65: bytes, message: bytes) -> None: + assert_bytes(message) + h = Hash(msg_magic(message)) + public_key, compressed = self.from_signature65(sig65, h) + # check public key + if public_key != self: + raise Exception("Bad signature") + # check message + self.verify_message_hash(sig65[1:], h) + + def verify_message_hash(self, sig_string: bytes, msg_hash: bytes) -> None: + assert_bytes(sig_string) + if len(sig_string) != 64: + raise Exception('Wrong encoding') + ecdsa_point = self._pubkey.point + verifying_key = _MyVerifyingKey.from_public_point(ecdsa_point, curve=SECP256k1) + verifying_key.verify_digest(sig_string, msg_hash, sigdecode=ecdsa.util.sigdecode_string) + + def encrypt_message(self, message: bytes, magic: bytes = b'BIE1'): + """ + ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac + """ + assert_bytes(message) + + randint = ecdsa.util.randrange(CURVE_ORDER) + ephemeral_exponent = number_to_string(randint, CURVE_ORDER) + ephemeral = ECPrivkey(ephemeral_exponent) + ecdh_key = (self * ephemeral.secret_scalar).get_public_key_bytes(compressed=True) + key = hashlib.sha512(ecdh_key).digest() + iv, key_e, key_m = key[0:16], key[16:32], key[32:] + ciphertext = aes_encrypt_with_iv(key_e, iv, message) + ephemeral_pubkey = ephemeral.get_public_key_bytes(compressed=True) + encrypted = magic + ephemeral_pubkey + ciphertext + mac = hmac_oneshot(key_m, encrypted, hashlib.sha256) + + return base64.b64encode(encrypted + mac) + + @classmethod + def order(cls): + return CURVE_ORDER + + def is_at_infinity(self): + return self == point_at_infinity() + + +def msg_magic(message: bytes) -> bytes: + from .bitcoin import var_int + length = bfh(var_int(len(message))) + return b"\x18BitCore Signed Message:\n" + length + message + + +def verify_message_with_address(address: str, sig65: bytes, message: bytes): + from .bitcoin import pubkey_to_address + assert_bytes(sig65, message) + try: + h = Hash(msg_magic(message)) + public_key, compressed = ECPubkey.from_signature65(sig65, h) + # check public key using the address + pubkey_hex = public_key.get_public_key_hex(compressed) + for txin_type in ['p2pkh','p2wpkh','p2wpkh-p2sh']: + addr = pubkey_to_address(txin_type, pubkey_hex) + if address == addr: + break + else: + raise Exception("Bad signature") + # check message + public_key.verify_message_hash(sig65[1:], h) + return True + except Exception as e: + print_error("Verification error: {0}".format(e)) + return False + + +def is_secret_within_curve_range(secret: Union[int, bytes]) -> bool: + if isinstance(secret, bytes): + secret = string_to_number(secret) + return 0 < secret < CURVE_ORDER + + +class ECPrivkey(ECPubkey): + + def __init__(self, privkey_bytes: bytes): + assert_bytes(privkey_bytes) + if len(privkey_bytes) != 32: + raise Exception('unexpected size for secret. should be 32 bytes, not {}'.format(len(privkey_bytes))) + secret = string_to_number(privkey_bytes) + if not is_secret_within_curve_range(secret): + raise InvalidECPointException('Invalid secret scalar (not within curve order)') + self.secret_scalar = secret + + point = generator_secp256k1 * secret + super().__init__(point_to_ser(point)) + self._privkey = ecdsa.ecdsa.Private_key(self._pubkey, secret) + + @classmethod + def from_secret_scalar(cls, secret_scalar: int): + secret_bytes = number_to_string(secret_scalar, CURVE_ORDER) + return ECPrivkey(secret_bytes) + + @classmethod + def from_arbitrary_size_secret(cls, privkey_bytes: bytes): + """This method is only for legacy reasons. Do not introduce new code that uses it. + Unlike the default constructor, this method does not require len(privkey_bytes) == 32, + and the secret does not need to be within the curve order either. + """ + return ECPrivkey(cls.normalize_secret_bytes(privkey_bytes)) + + @classmethod + def normalize_secret_bytes(cls, privkey_bytes: bytes) -> bytes: + scalar = string_to_number(privkey_bytes) % CURVE_ORDER + if scalar == 0: + raise Exception('invalid EC private key scalar: zero') + privkey_32bytes = number_to_string(scalar, CURVE_ORDER) + return privkey_32bytes + + def sign(self, data: bytes, sigencode=None, sigdecode=None) -> bytes: + if sigencode is None: + sigencode = sig_string_from_r_and_s + if sigdecode is None: + sigdecode = get_r_and_s_from_sig_string + private_key = _MySigningKey.from_secret_exponent(self.secret_scalar, curve=SECP256k1) + sig = private_key.sign_digest_deterministic(data, hashfunc=hashlib.sha256, sigencode=sigencode) + public_key = private_key.get_verifying_key() + if not public_key.verify_digest(sig, data, sigdecode=sigdecode): + raise Exception('Sanity check verifying our own signature failed.') + return sig + + def sign_transaction(self, hashed_preimage: bytes) -> bytes: + return self.sign(hashed_preimage, + sigencode=der_sig_from_r_and_s, + sigdecode=get_r_and_s_from_der_sig) + + def sign_message(self, message: bytes, is_compressed: bool) -> bytes: + def bruteforce_recid(sig_string): + for recid in range(4): + sig65 = construct_sig65(sig_string, recid, is_compressed) + try: + self.verify_message_for_address(sig65, message) + return sig65, recid + except Exception as e: + continue + else: + raise Exception("error: cannot sign message. no recid fits..") + + message = to_bytes(message, 'utf8') + msg_hash = Hash(msg_magic(message)) + sig_string = self.sign(msg_hash, + sigencode=sig_string_from_r_and_s, + sigdecode=get_r_and_s_from_sig_string) + sig65, recid = bruteforce_recid(sig_string) + return sig65 + + def decrypt_message(self, encrypted, magic=b'BIE1'): + encrypted = base64.b64decode(encrypted) + if len(encrypted) < 85: + raise Exception('invalid ciphertext: length') + magic_found = encrypted[:4] + ephemeral_pubkey_bytes = encrypted[4:37] + ciphertext = encrypted[37:-32] + mac = encrypted[-32:] + if magic_found != magic: + raise Exception('invalid ciphertext: invalid magic bytes') + try: + ecdsa_point = _ser_to_python_ecdsa_point(ephemeral_pubkey_bytes) + except AssertionError as e: + raise Exception('invalid ciphertext: invalid ephemeral pubkey') from e + if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, ecdsa_point.x(), ecdsa_point.y()): + raise Exception('invalid ciphertext: invalid ephemeral pubkey') + ephemeral_pubkey = ECPubkey.from_point(ecdsa_point) + ecdh_key = (ephemeral_pubkey * self.secret_scalar).get_public_key_bytes(compressed=True) + key = hashlib.sha512(ecdh_key).digest() + iv, key_e, key_m = key[0:16], key[16:32], key[32:] + if mac != hmac_oneshot(key_m, encrypted[:-32], hashlib.sha256): + raise InvalidPassword() + return aes_decrypt_with_iv(key_e, iv, ciphertext) + + +def construct_sig65(sig_string, recid, is_compressed): + comp = 4 if is_compressed else 0 + return bytes([27 + recid + comp]) + sig_string diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py new file mode 100644 index 000000000..10ed30096 --- /dev/null +++ b/electrum/ecc_fast.py @@ -0,0 +1,223 @@ +# taken (with minor modifications) from pycoin +# https://github.com/richardkiss/pycoin/blob/01b1787ed902df23f99a55deb00d8cd076a906fe/pycoin/ecdsa/native/secp256k1.py + +import os +import sys +import traceback +import ctypes +from ctypes.util import find_library +from ctypes import ( + byref, c_byte, c_int, c_uint, c_char_p, c_size_t, c_void_p, create_string_buffer, CFUNCTYPE, POINTER +) + +import ecdsa + +from .util import print_stderr, print_error + + +SECP256K1_FLAGS_TYPE_MASK = ((1 << 8) - 1) +SECP256K1_FLAGS_TYPE_CONTEXT = (1 << 0) +SECP256K1_FLAGS_TYPE_COMPRESSION = (1 << 1) +# /** The higher bits contain the actual data. Do not use directly. */ +SECP256K1_FLAGS_BIT_CONTEXT_VERIFY = (1 << 8) +SECP256K1_FLAGS_BIT_CONTEXT_SIGN = (1 << 9) +SECP256K1_FLAGS_BIT_COMPRESSION = (1 << 8) + +# /** Flags to pass to secp256k1_context_create. */ +SECP256K1_CONTEXT_VERIFY = (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_VERIFY) +SECP256K1_CONTEXT_SIGN = (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_SIGN) +SECP256K1_CONTEXT_NONE = (SECP256K1_FLAGS_TYPE_CONTEXT) + +SECP256K1_EC_COMPRESSED = (SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION) +SECP256K1_EC_UNCOMPRESSED = (SECP256K1_FLAGS_TYPE_COMPRESSION) + + +def load_library(): + if sys.platform == 'darwin': + library_path = 'libsecp256k1.0.dylib' + elif sys.platform in ('windows', 'win32'): + library_path = 'libsecp256k1.dll' + elif 'ANDROID_DATA' in os.environ: + library_path = 'libsecp256k1.so' + else: + library_path = 'libsecp256k1.so.0' + + secp256k1 = ctypes.cdll.LoadLibrary(library_path) + if not secp256k1: + print_stderr('[ecc] warning: libsecp256k1 library failed to load') + return None + + try: + secp256k1.secp256k1_context_create.argtypes = [c_uint] + secp256k1.secp256k1_context_create.restype = c_void_p + + secp256k1.secp256k1_context_randomize.argtypes = [c_void_p, c_char_p] + secp256k1.secp256k1_context_randomize.restype = c_int + + secp256k1.secp256k1_ec_pubkey_create.argtypes = [c_void_p, c_void_p, c_char_p] + secp256k1.secp256k1_ec_pubkey_create.restype = c_int + + secp256k1.secp256k1_ecdsa_sign.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p, c_void_p, c_void_p] + secp256k1.secp256k1_ecdsa_sign.restype = c_int + + secp256k1.secp256k1_ecdsa_verify.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p] + secp256k1.secp256k1_ecdsa_verify.restype = c_int + + secp256k1.secp256k1_ec_pubkey_parse.argtypes = [c_void_p, c_char_p, c_char_p, c_size_t] + secp256k1.secp256k1_ec_pubkey_parse.restype = c_int + + secp256k1.secp256k1_ec_pubkey_serialize.argtypes = [c_void_p, c_char_p, c_void_p, c_char_p, c_uint] + secp256k1.secp256k1_ec_pubkey_serialize.restype = c_int + + secp256k1.secp256k1_ecdsa_signature_parse_compact.argtypes = [c_void_p, c_char_p, c_char_p] + secp256k1.secp256k1_ecdsa_signature_parse_compact.restype = c_int + + secp256k1.secp256k1_ecdsa_signature_normalize.argtypes = [c_void_p, c_char_p, c_char_p] + secp256k1.secp256k1_ecdsa_signature_normalize.restype = c_int + + secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [c_void_p, c_char_p, c_char_p] + secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = c_int + + secp256k1.secp256k1_ec_pubkey_tweak_mul.argtypes = [c_void_p, c_char_p, c_char_p] + secp256k1.secp256k1_ec_pubkey_tweak_mul.restype = c_int + + secp256k1.ctx = secp256k1.secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY) + r = secp256k1.secp256k1_context_randomize(secp256k1.ctx, os.urandom(32)) + if r: + return secp256k1 + else: + print_stderr('[ecc] warning: secp256k1_context_randomize failed') + return None + except (OSError, AttributeError): + #traceback.print_exc(file=sys.stderr) + print_stderr('[ecc] warning: libsecp256k1 library was found and loaded but there was an error when using it') + return None + + +class _patched_functions: + prepared_to_patch = False + monkey_patching_active = False + + +def _prepare_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1(): + if not _libsecp256k1: + return + + # save original functions so that we can undo patching (needed for tests) + _patched_functions.orig_sign = staticmethod(ecdsa.ecdsa.Private_key.sign) + _patched_functions.orig_verify = staticmethod(ecdsa.ecdsa.Public_key.verifies) + _patched_functions.orig_mul = staticmethod(ecdsa.ellipticcurve.Point.__mul__) + + curve_secp256k1 = ecdsa.ecdsa.curve_secp256k1 + curve_order = ecdsa.curves.SECP256k1.order + point_at_infinity = ecdsa.ellipticcurve.INFINITY + + def mul(self: ecdsa.ellipticcurve.Point, other: int): + if self.curve() != curve_secp256k1: + # this operation is not on the secp256k1 curve; use original implementation + return _patched_functions.orig_mul(self, other) + other %= curve_order + if self == point_at_infinity or other == 0: + return point_at_infinity + pubkey = create_string_buffer(64) + public_pair_bytes = b'\4' + self.x().to_bytes(32, byteorder="big") + self.y().to_bytes(32, byteorder="big") + r = _libsecp256k1.secp256k1_ec_pubkey_parse( + _libsecp256k1.ctx, pubkey, public_pair_bytes, len(public_pair_bytes)) + if not r: + return False + r = _libsecp256k1.secp256k1_ec_pubkey_tweak_mul(_libsecp256k1.ctx, pubkey, other.to_bytes(32, byteorder="big")) + if not r: + return point_at_infinity + + pubkey_serialized = create_string_buffer(65) + pubkey_size = c_size_t(65) + _libsecp256k1.secp256k1_ec_pubkey_serialize( + _libsecp256k1.ctx, pubkey_serialized, byref(pubkey_size), pubkey, SECP256K1_EC_UNCOMPRESSED) + x = int.from_bytes(pubkey_serialized[1:33], byteorder="big") + y = int.from_bytes(pubkey_serialized[33:], byteorder="big") + return ecdsa.ellipticcurve.Point(curve_secp256k1, x, y, curve_order) + + def sign(self: ecdsa.ecdsa.Private_key, hash: int, random_k: int): + # note: random_k is ignored + if self.public_key.curve != curve_secp256k1: + # this operation is not on the secp256k1 curve; use original implementation + return _patched_functions.orig_sign(self, hash, random_k) + secret_exponent = self.secret_multiplier + nonce_function = None + sig = create_string_buffer(64) + sig_hash_bytes = hash.to_bytes(32, byteorder="big") + _libsecp256k1.secp256k1_ecdsa_sign( + _libsecp256k1.ctx, sig, sig_hash_bytes, secret_exponent.to_bytes(32, byteorder="big"), nonce_function, None) + compact_signature = create_string_buffer(64) + _libsecp256k1.secp256k1_ecdsa_signature_serialize_compact(_libsecp256k1.ctx, compact_signature, sig) + r = int.from_bytes(compact_signature[:32], byteorder="big") + s = int.from_bytes(compact_signature[32:], byteorder="big") + return ecdsa.ecdsa.Signature(r, s) + + def verify(self: ecdsa.ecdsa.Public_key, hash: int, signature: ecdsa.ecdsa.Signature): + if self.curve != curve_secp256k1: + # this operation is not on the secp256k1 curve; use original implementation + return _patched_functions.orig_verify(self, hash, signature) + sig = create_string_buffer(64) + input64 = signature.r.to_bytes(32, byteorder="big") + signature.s.to_bytes(32, byteorder="big") + r = _libsecp256k1.secp256k1_ecdsa_signature_parse_compact(_libsecp256k1.ctx, sig, input64) + if not r: + return False + r = _libsecp256k1.secp256k1_ecdsa_signature_normalize(_libsecp256k1.ctx, sig, sig) + + public_pair_bytes = b'\4' + self.point.x().to_bytes(32, byteorder="big") + self.point.y().to_bytes(32, byteorder="big") + pubkey = create_string_buffer(64) + r = _libsecp256k1.secp256k1_ec_pubkey_parse( + _libsecp256k1.ctx, pubkey, public_pair_bytes, len(public_pair_bytes)) + if not r: + return False + + return 1 == _libsecp256k1.secp256k1_ecdsa_verify(_libsecp256k1.ctx, sig, hash.to_bytes(32, byteorder="big"), pubkey) + + # save new functions so that we can (re-)do patching + _patched_functions.fast_sign = sign + _patched_functions.fast_verify = verify + _patched_functions.fast_mul = mul + + _patched_functions.prepared_to_patch = True + + +def do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1(): + if not _libsecp256k1: + # FIXME print_error will always print as 'verbosity' is not yet initialised + print_error('[ecc] info: libsecp256k1 library not available, falling back to python-ecdsa. ' + 'This means signing operations will be slower.') + return + if not _patched_functions.prepared_to_patch: + raise Exception("can't patch python-ecdsa without preparations") + ecdsa.ecdsa.Private_key.sign = _patched_functions.fast_sign + ecdsa.ecdsa.Public_key.verifies = _patched_functions.fast_verify + ecdsa.ellipticcurve.Point.__mul__ = _patched_functions.fast_mul + # ecdsa.ellipticcurve.Point.__add__ = ... # TODO?? + + _patched_functions.monkey_patching_active = True + + +def undo_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1(): + if not _libsecp256k1: + return + if not _patched_functions.prepared_to_patch: + raise Exception("can't patch python-ecdsa without preparations") + ecdsa.ecdsa.Private_key.sign = _patched_functions.orig_sign + ecdsa.ecdsa.Public_key.verifies = _patched_functions.orig_verify + ecdsa.ellipticcurve.Point.__mul__ = _patched_functions.orig_mul + + _patched_functions.monkey_patching_active = False + + +def is_using_fast_ecc(): + return _patched_functions.monkey_patching_active + + +try: + _libsecp256k1 = load_library() +except: + _libsecp256k1 = None + #traceback.print_exc(file=sys.stderr) + +_prepare_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1() diff --git a/electrum/electrum b/electrum/electrum new file mode 120000 index 000000000..74bf81ab6 --- /dev/null +++ b/electrum/electrum @@ -0,0 +1 @@ +../run_electrum \ No newline at end of file diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py new file mode 100644 index 000000000..9c2eb6ed9 --- /dev/null +++ b/electrum/exchange_rate.py @@ -0,0 +1,578 @@ +from datetime import datetime +import inspect +import requests +import sys +import os +import json +from threading import Thread +import time +import csv +import decimal +from decimal import Decimal + +from .bitcoin import COIN +from .i18n import _ +from .util import PrintError, ThreadJob, make_dir + + +# See https://en.wikipedia.org/wiki/ISO_4217 +CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, + 'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0, + 'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3, + 'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0, + 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0, + 'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0} + + +class ExchangeBase(PrintError): + + def __init__(self, on_quotes, on_history): + self.history = {} + self.quotes = {} + self.on_quotes = on_quotes + self.on_history = on_history + + def get_json(self, site, get_string): + # APIs must have https + url = ''.join(['https://', site, get_string]) + response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}, timeout=10) + return response.json() + + def get_csv(self, site, get_string): + url = ''.join(['https://', site, get_string]) + response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}) + reader = csv.DictReader(response.content.decode().split('\n')) + return list(reader) + + def name(self): + return self.__class__.__name__ + + def update_safe(self, ccy): + try: + self.print_error("getting fx quotes for", ccy) + self.quotes = self.get_rates(ccy) + self.print_error("received fx quotes") + except BaseException as e: + self.print_error("failed fx quotes:", e) + self.on_quotes() + + def update(self, ccy): + t = Thread(target=self.update_safe, args=(ccy,)) + t.setDaemon(True) + t.start() + + def read_historical_rates(self, ccy, cache_dir): + filename = os.path.join(cache_dir, self.name() + '_'+ ccy) + if os.path.exists(filename): + timestamp = os.stat(filename).st_mtime + try: + with open(filename, 'r', encoding='utf-8') as f: + h = json.loads(f.read()) + h['timestamp'] = timestamp + except: + h = None + else: + h = None + if h: + self.history[ccy] = h + self.on_history() + return h + + def get_historical_rates_safe(self, ccy, cache_dir): + try: + self.print_error("requesting fx history for", ccy) + h = self.request_history(ccy) + self.print_error("received fx history for", ccy) + except BaseException as e: + self.print_error("failed fx history:", e) + return + filename = os.path.join(cache_dir, self.name() + '_' + ccy) + with open(filename, 'w', encoding='utf-8') as f: + f.write(json.dumps(h)) + h['timestamp'] = time.time() + self.history[ccy] = h + self.on_history() + + def get_historical_rates(self, ccy, cache_dir): + if ccy not in self.history_ccys(): + return + h = self.history.get(ccy) + if h is None: + h = self.read_historical_rates(ccy, cache_dir) + if h is None or h['timestamp'] < time.time() - 24*3600: + t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir)) + t.setDaemon(True) + t.start() + + def history_ccys(self): + return [] + + def historical_rate(self, ccy, d_t): + return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'), 'NaN') + + def get_currencies(self): + rates = self.get_rates('') + return sorted([str(a) for (a, b) in rates.items() if b is not None and len(a)==3]) + +class BitcoinAverage(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short') + return dict([(r.replace("BTC", ""), Decimal(json[r]['last'])) + for r in json if r != 'timestamp']) + + def history_ccys(self): + return ['AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'EUR', 'GBP', 'IDR', 'ILS', + 'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD', + 'ZAR'] + + def request_history(self, ccy): + history = self.get_csv('apiv2.bitcoinaverage.com', + "/indices/global/history/BTC%s?period=alltime&format=csv" % ccy) + return dict([(h['DateTime'][:10], h['Average']) + for h in history]) + + +class Bitcointoyou(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('bitcointoyou.com', "/API/ticker.aspx") + return {'BRL': Decimal(json['ticker']['last'])} + + def history_ccys(self): + return ['BRL'] + + +class BitcoinVenezuela(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('api.bitcoinvenezuela.com', '/') + rates = [(r, json['BTC'][r]) for r in json['BTC'] + if json['BTC'][r] is not None] # Giving NULL for LTC + return dict(rates) + + def history_ccys(self): + return ['ARS', 'EUR', 'USD', 'VEF'] + + def request_history(self, ccy): + return self.get_json('api.bitcoinvenezuela.com', + "/historical/index.php?coin=BTC")[ccy +'_BTC'] + + +class Bitbank(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('public.bitbank.cc', '/btc_jpy/ticker') + return {'JPY': Decimal(json['data']['last'])} + + +class BitFlyer(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('bitflyer.jp', '/api/echo/price') + return {'JPY': Decimal(json['mid'])} + + +class Bitmarket(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json') + return {'PLN': Decimal(json['last'])} + + +class BitPay(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('bitpay.com', '/api/rates') + return dict([(r['code'], Decimal(r['rate'])) for r in json]) + + +class Bitso(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('api.bitso.com', '/v2/ticker') + return {'MXN': Decimal(json['last'])} + + +class BitStamp(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('www.bitstamp.net', '/api/ticker/') + return {'USD': Decimal(json['last'])} + + +class Bitvalor(ExchangeBase): + + def get_rates(self,ccy): + json = self.get_json('api.bitvalor.com', '/v1/ticker.json') + return {'BRL': Decimal(json['ticker_1h']['total']['last'])} + + +class BlockchainInfo(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('blockchain.info', '/ticker') + return dict([(r, Decimal(json[r]['15m'])) for r in json]) + + +class BTCChina(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('data.btcchina.com', '/data/ticker') + return {'CNY': Decimal(json['ticker']['last'])} + + +class BTCParalelo(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('btcparalelo.com', '/api/price') + return {'VEF': Decimal(json['price'])} + + +class Coinbase(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('coinbase.com', + '/api/v1/currencies/exchange_rates') + return dict([(r[7:].upper(), Decimal(json[r])) + for r in json if r.startswith('btc_to_')]) + + +class CoinDesk(ExchangeBase): + + def get_currencies(self): + dicts = self.get_json('api.coindesk.com', + '/v1/bpi/supported-currencies.json') + return [d['currency'] for d in dicts] + + def get_rates(self, ccy): + json = self.get_json('api.coindesk.com', + '/v1/bpi/currentprice/%s.json' % ccy) + result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])} + return result + + def history_starts(self): + return { 'USD': '2012-11-30', 'EUR': '2013-09-01' } + + def history_ccys(self): + return self.history_starts().keys() + + def request_history(self, ccy): + start = self.history_starts()[ccy] + end = datetime.today().strftime('%Y-%m-%d') + # Note ?currency and ?index don't work as documented. Sigh. + query = ('/v1/bpi/historical/close.json?start=%s&end=%s' + % (start, end)) + json = self.get_json('api.coindesk.com', query) + return json['bpi'] + + +class Coinsecure(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('api.coinsecure.in', '/v0/noauth/newticker') + return {'INR': Decimal(json['lastprice'] / 100.0 )} + + +class Foxbit(ExchangeBase): + + def get_rates(self,ccy): + json = self.get_json('api.bitvalor.com', '/v1/ticker.json') + return {'BRL': Decimal(json['ticker_1h']['exchanges']['FOX']['last'])} + + +class itBit(ExchangeBase): + + def get_rates(self, ccy): + ccys = ['USD', 'EUR', 'SGD'] + json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy) + result = dict.fromkeys(ccys) + if ccy in ccys: + result[ccy] = Decimal(json['lastPrice']) + return result + + +class Kraken(ExchangeBase): + + def get_rates(self, ccy): + ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY'] + pairs = ['XBT%s' % c for c in ccys] + json = self.get_json('api.kraken.com', + '/0/public/Ticker?pair=%s' % ','.join(pairs)) + return dict((k[-3:], Decimal(float(v['c'][0]))) + for k, v in json['result'].items()) + + +class LocalBitcoins(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('localbitcoins.com', + '/bitcoinaverage/ticker-all-currencies/') + return dict([(r, Decimal(json[r]['rates']['last'])) for r in json]) + + +class MercadoBitcoin(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('api.bitvalor.com', '/v1/ticker.json') + return {'BRL': Decimal(json['ticker_1h']['exchanges']['MBT']['last'])} + + +class NegocieCoins(ExchangeBase): + + def get_rates(self,ccy): + json = self.get_json('api.bitvalor.com', '/v1/ticker.json') + return {'BRL': Decimal(json['ticker_1h']['exchanges']['NEG']['last'])} + +class TheRockTrading(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('api.therocktrading.com', + '/v1/funds/BTCEUR/ticker') + return {'EUR': Decimal(json['last'])} + +class Unocoin(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('www.unocoin.com', 'trade?buy') + return {'INR': Decimal(json)} + + +class WEX(ExchangeBase): + + def get_rates(self, ccy): + json_eur = self.get_json('wex.nz', '/api/3/ticker/btc_eur') + json_rub = self.get_json('wex.nz', '/api/3/ticker/btc_rur') + json_usd = self.get_json('wex.nz', '/api/3/ticker/btc_usd') + return {'EUR': Decimal(json_eur['btc_eur']['last']), + 'RUB': Decimal(json_rub['btc_rur']['last']), + 'USD': Decimal(json_usd['btc_usd']['last'])} + +class CoinMarketCap(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json("api.coinmarketcap.com", "/v1/ticker/bitcore/?convert=" + ccy) + return {ccy: Decimal(json[0]["price_" + ccy.lower()])} + + +class Winkdex(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('winkdex.com', '/api/v0/price') + return {'USD': Decimal(json['price'] / 100.0)} + + def history_ccys(self): + return ['USD'] + + def request_history(self, ccy): + json = self.get_json('winkdex.com', + "/api/v0/series?start_time=1342915200") + history = json['series'][0]['results'] + return dict([(h['timestamp'][:10], h['price'] / 100.0) + for h in history]) + + +class Zaif(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy') + return {'JPY': Decimal(json['last_price'])} + + +def dictinvert(d): + inv = {} + for k, vlist in d.items(): + for v in vlist: + keys = inv.setdefault(v, []) + keys.append(k) + return inv + +def get_exchanges_and_currencies(): + import os, json + path = os.path.join(os.path.dirname(__file__), 'currencies.json') + try: + with open(path, 'r', encoding='utf-8') as f: + return json.loads(f.read()) + except: + pass + d = {} + is_exchange = lambda obj: (inspect.isclass(obj) + and issubclass(obj, ExchangeBase) + and obj != ExchangeBase) + exchanges = dict(inspect.getmembers(sys.modules[__name__], is_exchange)) + for name, klass in exchanges.items(): + exchange = klass(None, None) + try: + d[name] = exchange.get_currencies() + print(name, "ok") + except: + print(name, "error") + continue + with open(path, 'w', encoding='utf-8') as f: + f.write(json.dumps(d, indent=4, sort_keys=True)) + return d + + +CURRENCIES = get_exchanges_and_currencies() + + +def get_exchanges_by_ccy(history=True): + if not history: + return dictinvert(CURRENCIES) + d = {} + exchanges = CURRENCIES.keys() + for name in exchanges: + klass = globals()[name] + exchange = klass(None, None) + d[name] = exchange.history_ccys() + return dictinvert(d) + + +class FxThread(ThreadJob): + + def __init__(self, config, network): + self.config = config + self.network = network + self.ccy = self.get_currency() + self.history_used_spot = False + self.ccy_combo = None + self.hist_checkbox = None + self.cache_dir = os.path.join(config.path, 'cache') + self.set_exchange(self.config_exchange()) + make_dir(self.cache_dir) + + def get_currencies(self, h): + d = get_exchanges_by_ccy(h) + return sorted(d.keys()) + + def get_exchanges_by_ccy(self, ccy, h): + d = get_exchanges_by_ccy(h) + return d.get(ccy, []) + + def ccy_amount_str(self, amount, commas): + prec = CCY_PRECISIONS.get(self.ccy, 2) + fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) + try: + rounded_amount = round(amount, prec) + except decimal.InvalidOperation: + rounded_amount = amount + return fmt_str.format(rounded_amount) + + def run(self): + # This runs from the plugins thread which catches exceptions + if self.is_enabled(): + if self.timeout ==0 and self.show_history(): + self.exchange.get_historical_rates(self.ccy, self.cache_dir) + if self.timeout <= time.time(): + self.timeout = time.time() + 150 + self.exchange.update(self.ccy) + + def is_enabled(self): + return bool(self.config.get('use_exchange_rate')) + + def set_enabled(self, b): + return self.config.set_key('use_exchange_rate', bool(b)) + + def get_history_config(self): + return bool(self.config.get('history_rates')) + + def set_history_config(self, b): + self.config.set_key('history_rates', bool(b)) + + def get_history_capital_gains_config(self): + return bool(self.config.get('history_rates_capital_gains', False)) + + def set_history_capital_gains_config(self, b): + self.config.set_key('history_rates_capital_gains', bool(b)) + + def get_fiat_address_config(self): + return bool(self.config.get('fiat_address')) + + def set_fiat_address_config(self, b): + self.config.set_key('fiat_address', bool(b)) + + def get_currency(self): + '''Use when dynamic fetching is needed''' + return self.config.get("currency", "EUR") + + def config_exchange(self): + return self.config.get('use_exchange', 'BitcoinAverage') + + def show_history(self): + return self.is_enabled() and self.get_history_config() and self.ccy in self.exchange.history_ccys() + + def set_currency(self, ccy): + self.ccy = ccy + self.config.set_key('currency', ccy, True) + self.timeout = 0 # Because self.ccy changes + self.on_quotes() + + def set_exchange(self, name): + class_ = globals().get(name, BitcoinAverage) + self.print_error("using exchange", name) + if self.config_exchange() != name: + self.config.set_key('use_exchange', name, True) + self.exchange = class_(self.on_quotes, self.on_history) + # A new exchange means new fx quotes, initially empty. Force + # a quote refresh + self.timeout = 0 + self.exchange.read_historical_rates(self.ccy, self.cache_dir) + + def on_quotes(self): + if self.network: + self.network.trigger_callback('on_quotes') + + def on_history(self): + if self.network: + self.network.trigger_callback('on_history') + + def exchange_rate(self): + '''Returns None, or the exchange rate as a Decimal''' + rate = self.exchange.quotes.get(self.ccy) + if rate is None: + return Decimal('NaN') + return Decimal(rate) + + def format_amount(self, btc_balance): + rate = self.exchange_rate() + return '' if rate.is_nan() else "%s" % self.value_str(btc_balance, rate) + + def format_amount_and_units(self, btc_balance): + rate = self.exchange_rate() + return '' if rate.is_nan() else "%s %s" % (self.value_str(btc_balance, rate), self.ccy) + + def get_fiat_status_text(self, btc_balance, base_unit, decimal_point): + rate = self.exchange_rate() + return _(" (No FX rate available)") if rate.is_nan() else " 1 %s~%s %s" % (base_unit, + self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy) + + def fiat_value(self, satoshis, rate): + return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate) + + def value_str(self, satoshis, rate): + return self.format_fiat(self.fiat_value(satoshis, rate)) + + def format_fiat(self, value): + if value.is_nan(): + return _("No data") + return "%s" % (self.ccy_amount_str(value, True)) + + def history_rate(self, d_t): + if d_t is None: + return Decimal('NaN') + rate = self.exchange.historical_rate(self.ccy, d_t) + # Frequently there is no rate for today, until tomorrow :) + # Use spot quotes in that case + if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2: + rate = self.exchange.quotes.get(self.ccy, 'NaN') + self.history_used_spot = True + return Decimal(rate) + + def historical_value_str(self, satoshis, d_t): + return self.format_fiat(self.historical_value(satoshis, d_t)) + + def historical_value(self, satoshis, d_t): + return self.fiat_value(satoshis, self.history_rate(d_t)) + + def timestamp_rate(self, timestamp): + from .util import timestamp_to_datetime + date = timestamp_to_datetime(timestamp) + return self.history_rate(date) diff --git a/electrum/gui/__init__.py b/electrum/gui/__init__.py new file mode 100644 index 000000000..9974520ac --- /dev/null +++ b/electrum/gui/__init__.py @@ -0,0 +1,5 @@ +# To create a new GUI, please add its code to this directory. +# Three objects are passed to the ElectrumGui: config, daemon and plugins +# The Wallet object is instanciated by the GUI + +# Notifications about network events are sent to the GUI by using network.register_callback() diff --git a/electrum/gui/kivy/Makefile b/electrum/gui/kivy/Makefile new file mode 100644 index 000000000..7e87afa6b --- /dev/null +++ b/electrum/gui/kivy/Makefile @@ -0,0 +1,32 @@ +PYTHON = python3 + +# needs kivy installed or in PYTHONPATH + +.PHONY: theming apk clean + +theming: + $(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png +prepare: + # running pre build setup + @cp tools/buildozer.spec ../../../buildozer.spec + # copy electrum to main.py + @cp ../../../run_electrum ../../../main.py + @-if [ ! -d "../../.buildozer" ];then \ + cd ../../..; buildozer android debug;\ + cp -f electrum/gui/kivy/tools/blacklist.txt .buildozer/android/platform/python-for-android/src/blacklist.txt;\ + rm -rf ./.buildozer/android/platform/python-for-android/dist;\ + fi +apk: + @make prepare + @-cd ../../..; buildozer android debug deploy run + @make clean +release: + @make prepare + @-cd ../../..; buildozer android release + @make clean +clean: + # Cleaning up + # rename main.py to electrum + @-rm ../../../main.py + # remove buildozer.spec + @-rm ../../../buildozer.spec diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md new file mode 100644 index 000000000..4c151accc --- /dev/null +++ b/electrum/gui/kivy/Readme.md @@ -0,0 +1,130 @@ +# Kivy GUI + +The Kivy GUI is used with Electrum on Android devices. To generate an APK file, follow these instructions. + +## 1. Preliminaries + +Make sure the current user can write `/opt` (e.g. `sudo chown username: /opt`). + +We assume that you already got Electrum to run from source on this machine, +hence have e.g. `git`, `python3-pip` and `python3-setuptools`. + +## 2. Install kivy + +Install kivy for python3 as described [here](https://kivy.org/docs/installation/installation-linux.html). +So for example: +```sh +sudo add-apt-repository ppa:kivy-team/kivy +sudo apt-get install python3-kivy +``` + + +## 3. Install python-for-android (p4a) +p4a is used to package Electrum, Python, SDL and a bootstrap Java app into an APK file. +We patched p4a to add some functionality we need for Electrum. Until those changes are +merged into p4a, you need to merge them locally (into the master branch): + +3.1 [kivy/python-for-android#1217](https://github.com/kivy/python-for-android/pull/1217) + +Something like this should work: + +```sh +cd /opt +git clone https://github.com/kivy/python-for-android +cd python-for-android +git remote add agilewalker https://github.com/agilewalker/python-for-android +git remote add sombernight https://github.com/SomberNight/python-for-android +git fetch --all +git checkout 93759f36ba45c7bbe0456a4b3e6788622924cbac +git cherry-pick a2fb5ecbc09c4847adbcfd03c6b1ca62b3d09b8d # openssl-fix +git cherry-pick a0ef2007bc60ed642fbd8b61937995dbed0ddd24 # disable backups +``` + +## 4. Install buildozer +4.1 Buildozer is a frontend to p4a. Luckily we don't need to patch it: + +```sh +cd /opt +git clone https://github.com/kivy/buildozer +cd buildozer +sudo python3 setup.py install +``` + +4.2 Install additional dependencies: +```sh +sudo apt-get install python-pip +``` +and the ones listed +[here](https://buildozer.readthedocs.io/en/latest/installation.html#targeting-android). + +You will also need +```sh +python3 -m pip install colorama appdirs sh jinja2 +``` + + +4.3 Download the [Crystax NDK](https://www.crystax.net/en/download) manually. +Extract into `/opt/crystax-ndk-10.3.2` + + +## 5. Create the UI Atlas +In the `gui/kivy` directory of Electrum, run `make theming`. + +## 6. Download Electrum dependencies +```sh +sudo contrib/make_packages +``` + +## 7. Try building the APK and fail + +```sh +contrib/make_apk +``` + +During this build attempt, buildozer downloaded some tools, +e.g. those needed in the next step. + +## 8. Update the Android SDK build tools + +### Method 1: Using the GUI + + Start the Android SDK manager in GUI mode: + + ~/.buildozer/android/platform/android-sdk-20/tools/android + + Check the latest SDK available and install it + ("Android SDK Tools" and "Android SDK Platform-tools"). + Close the SDK manager. Repeat until there is no newer version. + + Reopen the SDK manager, and install the latest build tools + ("Android SDK Build-tools"), 27.0.3 at the time of writing. + + Install "Android Support Repository" from the SDK manager (under "Extras"). + +### Method 2: Using the command line: + + Repeat the following command until there is nothing to install: + + ~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t tools,platform-tools + + Install Build Tools, android API 19 and Android Support Library: + + ~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t build-tools-27.0.3,android-19,extra-android-m2repository + + +## 9. Build the APK + +```sh +contrib/make_apk +``` + +# FAQ +## Why do I get errors like `package me.dm7.barcodescanner.zxing does not exist` while compiling? +Update your Android build tools to version 27 like described above. + +## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling? +Make sure that your p4a installation includes commit a3cc78a6d1a107cd3b6bd28db8b80f89e3ecddd2. +Also make sure you have recent SDK tools and platform-tools + +## I changed something but I don't see any differences on the phone. What did I do wrong? +You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` diff --git a/electrum/gui/kivy/__init__.py b/electrum/gui/kivy/__init__.py new file mode 100644 index 000000000..7b9941281 --- /dev/null +++ b/electrum/gui/kivy/__init__.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2012 thomasv@gitorious +# +# 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. +# +# Kivy GUI + +import sys +import os + +try: + sys.argv = [''] + import kivy +except ImportError: + # This error ideally shouldn't be raised with pre-built packages + sys.exit("Error: Could not import kivy. Please install it using the" + \ + "instructions mentioned here `http://kivy.org/#download` .") + +# minimum required version for kivy +kivy.require('1.8.0') +from kivy.logger import Logger + + + + +class ElectrumGui: + + def __init__(self, config, daemon, plugins): + Logger.debug('ElectrumGUI: initialising') + self.daemon = daemon + self.network = daemon.network + self.config = config + self.plugins = plugins + + def main(self): + from .main_window import ElectrumWindow + self.config.open_last_wallet() + w = ElectrumWindow(config=self.config, + network=self.network, + plugins = self.plugins, + gui_object=self) + w.run() diff --git a/electrum/gui/kivy/data/background.png b/electrum/gui/kivy/data/background.png new file mode 100644 index 000000000..77f42ccdd Binary files /dev/null and b/electrum/gui/kivy/data/background.png differ diff --git a/electrum/gui/kivy/data/fonts/Roboto-Bold.ttf b/electrum/gui/kivy/data/fonts/Roboto-Bold.ttf new file mode 100644 index 000000000..87d3af3e1 Binary files /dev/null and b/electrum/gui/kivy/data/fonts/Roboto-Bold.ttf differ diff --git a/electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf b/electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf new file mode 100644 index 000000000..c38f7c881 Binary files /dev/null and b/electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf differ diff --git a/electrum/gui/kivy/data/fonts/Roboto-Medium.ttf b/electrum/gui/kivy/data/fonts/Roboto-Medium.ttf new file mode 100644 index 000000000..879834198 Binary files /dev/null and b/electrum/gui/kivy/data/fonts/Roboto-Medium.ttf differ diff --git a/electrum/gui/kivy/data/fonts/Roboto.ttf b/electrum/gui/kivy/data/fonts/Roboto.ttf new file mode 100644 index 000000000..153c60882 Binary files /dev/null and b/electrum/gui/kivy/data/fonts/Roboto.ttf differ diff --git a/electrum/gui/kivy/data/fonts/tron/License.txt b/electrum/gui/kivy/data/fonts/tron/License.txt new file mode 100644 index 000000000..fbc86ed93 --- /dev/null +++ b/electrum/gui/kivy/data/fonts/tron/License.txt @@ -0,0 +1,4 @@ +Copyright (c) 2010-2011, Jeff Bell [www.randombell.com] | [jeffbell@randombell.com]. +This font may be distributed freely however must retain this document as well as the Readme.txt file. +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is available with a FAQ at: http://scripts.sil.org/OFL \ No newline at end of file diff --git a/electrum/gui/kivy/data/fonts/tron/Readme.txt b/electrum/gui/kivy/data/fonts/tron/Readme.txt new file mode 100644 index 000000000..2e2fb69f6 --- /dev/null +++ b/electrum/gui/kivy/data/fonts/tron/Readme.txt @@ -0,0 +1,21 @@ +TR2N v1.3 + +ABOUT THE FONT: +A font based upon the poster text for TRON LEGACY. + +The font is different from the pre-existing TRON font currently on the web. Similar in minor aspects but different in most. Style based upon text from different region posters. + +UPDATE HISTORY: +3/7/11 - Adjusted the letter B (both lowercase and uppercase), capped off the ends of T, P and R, added a few more punctuation marks, as well as added the TR and TP ligature to allow for the solid bar connect as in the poster art. + +1/22/11 - Made minor corrections to all previous letters and punctuation. Corrected issue with number 8's top filling in. + +ABOUT THE AUTHOR: +Jeff Bell has produced fonts before, but this is the first one in over 10 years. His original 3 fonts were under the name DJ-JOHNNYRKA and include "CASPER", "BEVERLY HILLS COP", "THE GODFATHER" and "FIDDUMS FAMILY". + +For more information on Jeff Bell and his work can be found online: + +www.randombell.com +www.damovieman.deviantart.com +http://www.imdb.com/name/nm3983081/ +http://www.vimeo.com/user4004969/videos \ No newline at end of file diff --git a/electrum/gui/kivy/data/fonts/tron/Tr2n.ttf b/electrum/gui/kivy/data/fonts/tron/Tr2n.ttf new file mode 100644 index 000000000..8e8c0dec6 Binary files /dev/null and b/electrum/gui/kivy/data/fonts/tron/Tr2n.ttf differ diff --git a/electrum/gui/kivy/data/glsl/default.fs b/electrum/gui/kivy/data/glsl/default.fs new file mode 100644 index 000000000..19145d653 --- /dev/null +++ b/electrum/gui/kivy/data/glsl/default.fs @@ -0,0 +1,4 @@ +$HEADER$ +void main (void){ + gl_FragColor = frag_color * texture2D(texture0, tex_coord0); +} diff --git a/electrum/gui/kivy/data/glsl/default.png b/electrum/gui/kivy/data/glsl/default.png new file mode 100644 index 000000000..a14255e4d Binary files /dev/null and b/electrum/gui/kivy/data/glsl/default.png differ diff --git a/electrum/gui/kivy/data/glsl/default.vs b/electrum/gui/kivy/data/glsl/default.vs new file mode 100644 index 000000000..ac9ac4d6d --- /dev/null +++ b/electrum/gui/kivy/data/glsl/default.vs @@ -0,0 +1,6 @@ +$HEADER$ +void main (void) { + frag_color = color * vec4(1.0, 1.0, 1.0, opacity); + tex_coord0 = vTexCoords0; + gl_Position = projection_mat * modelview_mat * vec4(vPosition.xy, 0.0, 1.0); +} diff --git a/electrum/gui/kivy/data/glsl/header.fs b/electrum/gui/kivy/data/glsl/header.fs new file mode 100644 index 000000000..e9f887ba6 --- /dev/null +++ b/electrum/gui/kivy/data/glsl/header.fs @@ -0,0 +1,10 @@ +#ifdef GL_ES + precision highp float; +#endif + +/* Outputs from the vertex shader */ +varying vec4 frag_color; +varying vec2 tex_coord0; + +/* uniform texture samplers */ +uniform sampler2D texture0; diff --git a/electrum/gui/kivy/data/glsl/header.vs b/electrum/gui/kivy/data/glsl/header.vs new file mode 100644 index 000000000..a2638bffc --- /dev/null +++ b/electrum/gui/kivy/data/glsl/header.vs @@ -0,0 +1,17 @@ +#ifdef GL_ES + precision highp float; +#endif + +/* Outputs to the fragment shader */ +varying vec4 frag_color; +varying vec2 tex_coord0; + +/* vertex attributes */ +attribute vec2 vPosition; +attribute vec2 vTexCoords0; + +/* uniform variables */ +uniform mat4 modelview_mat; +uniform mat4 projection_mat; +uniform vec4 color; +uniform float opacity; diff --git a/electrum/gui/kivy/data/images/defaulttheme-0.png b/electrum/gui/kivy/data/images/defaulttheme-0.png new file mode 100644 index 000000000..8cfc82455 Binary files /dev/null and b/electrum/gui/kivy/data/images/defaulttheme-0.png differ diff --git a/electrum/gui/kivy/data/images/defaulttheme.atlas b/electrum/gui/kivy/data/images/defaulttheme.atlas new file mode 100644 index 000000000..26ed5d0da --- /dev/null +++ b/electrum/gui/kivy/data/images/defaulttheme.atlas @@ -0,0 +1 @@ +{"defaulttheme-0.png": {"progressbar_background": [391, 227, 24, 24], "tab_btn_disabled": [264, 137, 32, 32], "tab_btn_pressed": [366, 137, 32, 32], "image-missing": [152, 171, 48, 48], "splitter_h": [174, 123, 32, 7], "splitter_down": [501, 253, 7, 32], "splitter_disabled_down": [503, 291, 7, 32], "vkeyboard_key_down": [468, 137, 32, 32], "vkeyboard_disabled_key_down": [400, 137, 32, 32], "selector_right": [248, 223, 55, 62], "player-background": [2, 287, 103, 103], "selector_middle": [191, 223, 55, 62], "spinner": [235, 82, 29, 37], "tab_btn_disabled_pressed": [298, 137, 32, 32], "switch-button_disabled": [277, 291, 43, 32], "textinput_disabled_active": [372, 326, 64, 64], "splitter_grip": [36, 50, 12, 26], "vkeyboard_key_normal": [2, 44, 32, 32], "button_disabled": [80, 82, 29, 37], "media-playback-stop": [302, 171, 48, 48], "splitter": [501, 87, 7, 32], "splitter_down_h": [140, 123, 32, 7], "sliderh_background_disabled": [72, 132, 41, 37], "modalview-background": [464, 456, 45, 54], "button": [142, 82, 29, 37], "splitter_disabled": [502, 137, 7, 32], "checkbox_radio_disabled_on": [433, 87, 32, 32], "slider_cursor": [402, 171, 48, 48], "vkeyboard_disabled_background": [68, 221, 64, 64], "checkbox_disabled_on": [297, 87, 32, 32], "sliderv_background_disabled": [2, 78, 37, 41], "button_disabled_pressed": [111, 82, 29, 37], "audio-volume-muted": [102, 171, 48, 48], "close": [417, 231, 20, 20], "action_group_disabled": [452, 171, 33, 48], "vkeyboard_background": [2, 221, 64, 64], "checkbox_off": [331, 87, 32, 32], "tab_disabled": [305, 253, 96, 32], "sliderh_background": [115, 132, 41, 37], "switch-button": [322, 291, 43, 32], "tree_closed": [439, 231, 20, 20], "bubble_btn_pressed": [435, 291, 32, 32], "selector_left": [134, 223, 55, 62], "filechooser_file": [174, 326, 64, 64], "checkbox_radio_disabled_off": [399, 87, 32, 32], "checkbox_radio_on": [196, 137, 32, 32], "checkbox_on": [365, 87, 32, 32], "button_pressed": [173, 82, 29, 37], "audio-volume-high": [464, 406, 48, 48], "audio-volume-low": [2, 171, 48, 48], "progressbar": [305, 227, 32, 24], "previous_normal": [487, 187, 19, 32], "separator": [504, 342, 5, 48], "filechooser_folder": [240, 326, 64, 64], "checkbox_radio_off": [467, 87, 32, 32], "textinput_active": [306, 326, 64, 64], "textinput": [438, 326, 64, 64], "player-play-overlay": [122, 395, 117, 115], "media-playback-pause": [202, 171, 48, 48], "sliderv_background": [41, 78, 37, 41], "ring": [354, 402, 108, 108], "bubble_arrow": [487, 175, 16, 10], "slider_cursor_disabled": [352, 171, 48, 48], "checkbox_disabled_off": [469, 291, 32, 32], "action_group_down": [2, 121, 33, 48], "spinner_disabled": [204, 82, 29, 37], "splitter_disabled_h": [106, 123, 32, 7], "bubble": [107, 325, 65, 65], "media-playback-start": [252, 171, 48, 48], "vkeyboard_disabled_key_normal": [434, 137, 32, 32], "overflow": [230, 137, 32, 32], "tree_opened": [461, 231, 20, 20], "action_item": [339, 227, 24, 24], "bubble_btn": [401, 291, 32, 32], "audio-volume-medium": [52, 171, 48, 48], "action_group": [37, 121, 33, 48], "spinner_pressed": [266, 82, 29, 37], "filechooser_selected": [2, 392, 118, 118], "tab": [403, 253, 96, 32], "action_bar": [158, 133, 36, 36], "action_view": [365, 227, 24, 24], "tab_btn": [332, 137, 32, 32], "switch-background": [192, 291, 83, 32], "splitter_disabled_down_h": [72, 123, 32, 7], "action_item_down": [367, 291, 32, 32], "switch-background_disabled": [107, 291, 83, 32], "textinput_disabled": [241, 399, 111, 111], "splitter_grip_h": [483, 239, 26, 12]}} \ No newline at end of file diff --git a/electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java b/electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java new file mode 100644 index 000000000..8f4714628 --- /dev/null +++ b/electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java @@ -0,0 +1,48 @@ +package org.electrum.qr; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.content.Intent; + +import java.util.Arrays; + +import me.dm7.barcodescanner.zxing.ZXingScannerView; + +import com.google.zxing.Result; +import com.google.zxing.BarcodeFormat; + +public class SimpleScannerActivity extends Activity implements ZXingScannerView.ResultHandler { + private ZXingScannerView mScannerView; + final String TAG = "org.electrum.SimpleScannerActivity"; + + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view + mScannerView.setFormats(Arrays.asList(BarcodeFormat.QR_CODE)); + setContentView(mScannerView); // Set the scanner view as the content view + } + + @Override + public void onResume() { + super.onResume(); + mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results. + mScannerView.startCamera(); // Start camera on resume + } + + @Override + public void onPause() { + super.onPause(); + mScannerView.stopCamera(); // Stop camera on pause + } + + @Override + public void handleResult(Result rawResult) { + Intent resultIntent = new Intent(); + resultIntent.putExtra("text", rawResult.getText()); + resultIntent.putExtra("format", rawResult.getBarcodeFormat().toString()); + setResult(Activity.RESULT_OK, resultIntent); + this.finish(); + } +} diff --git a/electrum/gui/kivy/data/logo/kivy-icon-32.png b/electrum/gui/kivy/data/logo/kivy-icon-32.png new file mode 100644 index 000000000..455fa9763 Binary files /dev/null and b/electrum/gui/kivy/data/logo/kivy-icon-32.png differ diff --git a/electrum/gui/kivy/data/style.kv b/electrum/gui/kivy/data/style.kv new file mode 100644 index 000000000..1bbc60093 --- /dev/null +++ b/electrum/gui/kivy/data/style.kv @@ -0,0 +1,754 @@ +#:kivy 1.0 + +