diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fa082c7e6b6..91218688f0c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,201 +1,197 @@ # Contributing to Electrum ABC -## Main repository +The Electrum ABC project welcomes contributors! -The Electrum ABC source repository has been merged into the Bitcoin ABC repository, -and the development is now taking place at [reviews.bitcoinabc.org](https://reviews.bitcoinabc.org/). +This guide is intended to help developers and non-developers contribute effectively to the project. -Please read the main [CONTRIBUTING.md](https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/CONTRIBUTING.md) -document to familiarize yourself with the development philosophy and find out how to -set up the Bitcoin ABC repository. +The development philosophy and communication channels are identical to the ones +used by the Bitcoin-ABC project. Please read the relevant sections in +[Bitcoin ABC's CONTRIBUTING.md document](https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/CONTRIBUTING.md). -The original Electrum ABC github repository is maintained as a mirror of the `electrum/` -directory in the main repository. +The rest of this document provides information that is specific to +Electrum ABC. -The rest of this document provides instructions that are specific to Electrum ABC. +## Contributing to the development -## Contacting developers +### Getting set up with the Electrum ABC Repository -[Join the Electrum ABC telegram group](https://t.me/ElectrumABC) to get in contact -with developers or to get help from the community. +Electrum ABC is hosted on Github.com. To contribute to the repository, +you should start by creating an account on github.com. -## Installing dependencies +You will then need to clone the main repository. For that, navigate to +https://github.com/bitcoin-abc/ElectrumABC, and click the *Fork* button +that is on the top right of the window. This will create a new github +repository that is under your own management. -All commands in this document assume that your current working directory is the -`electrum/` directory that resides at the root of the Bitcoin ABC repository. +If your Github username is *your_username*, you will now have a copy of +the Electrum ABC repository at the address +`https://github.com/your_username/ElectrumABC` -### Python +Next, you must clone your github repository on your own computer, +so that you can actually edit the files. The simplest way +of doing this is to use the HTTPS link of your remote repository: -Python 3.7 or above is required to run Electrum ABC. +```shell +git clone https://github.com/your_username/ElectrumABC.git +``` -If your system lacks Python 3.7+, you can use `pyenv` to install newer versions, or -install an [alternative python distribution](https://www.python.org/download/alternatives/) -in addition to your system's version. +This has the drawback of requiring you to type your password every time +you want to use a git command to interact with your remote repository. +To avoid this, you can also connect to GitHub with SSH. This is a bit more +complicated to set up initially, but will save you time on the long run. +You can read more on this subject here: +[Connecting to GitHub with SSH](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/connecting-to-github-with-ssh) -### Python packages +Now you should have a local directory named `ElectrumABC`, with a single +remote repository named `origin`. This remote was set up automatically by +the git clone command. You can check this using the `git remote -v` command. -The simplest way to install all needed packages is to run the following command: ```shell -pip install .[all] +$ cd ElectrumABC +$ git remote -v +origin git@github.com:your_username/ElectrumABC.git (fetch) +origin git@github.com:your_username/ElectrumABC.git (push) ``` -This will install Electrum ABC and all its dependencies as a python package and application. +The next step is to also add the main repository to the available +remote repositories. Call it `upstream`. -This is equivalent to: ```shell -pip install . -pip install .[gui] -pip install .[hardware] +$ git remote add upstream https://github.com/Bitcoin-ABC/ElectrumABC.git +$ git remote -v +origin git@github.com:your_username/ElectrumABC.git (fetch) +origin git@github.com:your_username/ElectrumABC.git (push) +upstream https://github.com/Bitcoin-ABC/ElectrumABC.git (fetch) +upstream https://github.com/Bitcoin-ABC/ElectrumABC.git (push) ``` -If you do not want to install Electrum ABC as a package, you can install only the dependencies -using the following commands: -```shell -pip3 install -r contrib/requirements/requirements.txt -pip3 install -r contrib/requirements/requirements-binaries.txt -pip3 install -r contrib/requirements/requirements-hw.txt -``` +Before writing or editing code, you must set-up pre-commit hooks to enforce +a consistent code style. +This is done by installing the [`pre-commit` tool](https://pre-commit.com/), and +then running `pre-commit install` in the root directory of the project. -## Running Electrum ABC from source - -If you installed the application as a python package, you can run it from anywhere -using `electrum-abc` (assuming that your system has the python script directory in -its PATH). - -If you installed all dependencies, you can also start the application by invoking -the `./electrum-abc` script. See the following sections for additional instructions -and optional dependencies. - -### Running from source on old Linux - -If your Linux distribution has a version of python 3 lower than the minimum required -version, it is recommended to do a user dir install with -[pyenv](https://github.com/pyenv/pyenv-installer). This allows Electrum ABC -to run completely independently of your system configuration. - -1. Install `pyenv` in your user - account. Follow the printed instructions about updating your environment - variables and `.bashrc`, and restart your shell to ensure that they are - loaded. -2. Run `pyenv install 3.9.7`. This will download and compile that version of - python, storing it under `.pyenv` in your home directory. -3. `cd` into the Electrum ABC directory. Run `pyenv local 3.9.7` which inserts - a file `.python-version` into the current directory. -4. [Install Electrum ABC requirements](#python-packages) -5. [Compile libsecp256k1](#compiling-libsecp256k1) - -### Running from source on macOS - -You need to install **either** [MacPorts](https://www.macports.org) **or** -[HomeBrew](https://www.brew.sh). Follow the instructions on either site for -installing (Xcode from [Apple's developer site](https://developer.apple.com) -is required for either). - -1. After installing either HomeBrew or MacPorts, clone this repository and - switch to the directory: - `git clone https://github.com/Bitcoin-ABC/ElectrumABC && cd ElectrumABC` -2. Install python 3.7+. For brew: - `brew install python3` - or if using MacPorts: - `sudo port install python37` -3. Install PyQt5: `python3 -m pip install --user pyqt5` -4. [Install Electrum ABC requirements](#python-packages) -5. [Compile libsecp256k1](#compiling-libsecp256k1) - -## Running tests - -Running unit tests: ```shell -python3 test_runner.py +pip install pre-commit +pre-commit install ``` -This can also be run as a `ninja` target in the context of a Bitcoin ABC build: +You are now ready to contribute to Electrum ABC. + +### Development workflow + +This section is a summary of a typical development workflow. It assumes that +you are already familiar with *git*. If you aren't, start by reading a tutorial +on that topic, for instance: +[Starting with an Existing Project | Learn Version Control with Git](https://www.git-tower.com/learn/git/ebook/en/command-line/basics/working-on-your-project/#start) + +After the initial set up, your local repository should be in the same +state as the main repository. However, the remote repository will change +as other people contribute code. So before you start working on any +development, be sure to synchronize your local `master` branch with `upstream`. + ```shell -ninja check-electrum +git checkout master +git pull upstream master ``` -Functional tests can be run with the following command: +Now, create a local development branch with a descriptive name. For instance, +if you want to fix a typo in the `README` file, you could call it +`readme_typo_1` + ```shell -pytest electrumabc/tests/regtest +git checkout -b readme_typo_1 ``` -This requires `docker` and additional python dependencies: +The next step is to edit the source files in your local repository and +commit the changes as you go. It is advised to test your changes after +each commit, and to add a *test plan* in your commit message. +Each new commit should be strictly an improvement, and should not break +any existing feature. When you are finished, push your +branch to your own remote repository: + ```shell -pip3 install -r contrib/requirements/requirements-regtest.txt +git push origin readme_typo_1 ``` -## Compiling libsecp256k1 +Now go to GitHub. The new branch will show up with a green Pull Request button. +Make sure the title and message are clear, concise, and self-explanatory. +Then click the button to submit it. Your pull request will be reviewed by +other contributors. -Compiling libsecp256k1 is highly-recommended when running from source, to use fast -cryptographic algorithms instead of using fallback pure-python algos. +Address the reviewer's feedback by editing the local commits and pushing them +again to your repository with `git push -f`. Editing a past commit requires +more advanced git skills. See +[Rewriting history](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase). +If you don't feel confident enough to do this, you can fix the code by adding new +commits and the reviewer can take care of rebasing the changes. +But we encourage you to try first, and feel free to ask for help. -It is required when using CashFusion, as slow participants can cause a fusion round -to fail. +GitHub will automatically show your new changes in the pull request after you push +them to your remote repository, but the reviewer might not be aware of the changes. +So you should post a reply in the pull request's *Conversation* to notify him of +the changes. -On Debian or Ubuntu linux: -```shell -sudo apt-get install libtool automake -./contrib/make_secp -``` +### General guidelines -On MacOS: -```shell -brew install coreutils automake -./contrib/make_secp -``` +Electrum ABC adheres to standard Python style guidelines and good practices. +To ensure that your contributions respect those common guidelines, it is +recommended to use a recent IDE that includes a linter, such as PyCharm +or Sublime Text with the SublimeLinter plugin, and to not ignore any +codestyle violation warning. -or if using MacPorts: `sudo port install coreutils automake` +Alternatively, you can manually run a linter on your code such as +[`flake8`](https://pypi.org/project/flake8/). -## Compiling the icons file for Qt +The initial codebase does not strictly adhere to style guidelines, and we do +not plan to immediately fix this problem for the entire codebase, as this would +make backports from Electron Cash harder. But any new code, or existing code +that is modified, should be fixed. -If you change or add any icons to the `icons` subdirectory, you need to run the following -script before you can use them in the application: -```shell -contrib/gen_icons.sh -``` +Automatic formatting is achieved using pre-commit hooks that run the following +formatting tools: +- [`isort`](https://pycqa.github.io/isort/) +- [`black`](https://github.com/psf/black) -This requires the `pyrcc5` command which can be installed using your package manager. -For instance for Ubuntu or Debian, run: -``` -sudo apt-get install pyqt5-dev-tools -``` +The code should be documented and tested. -## Creating translations - -```shell -sudo apt-get install python-requests gettext -./contrib/make_locale -``` +## Other ways of contributing -## Plugin development +In addition to submitting pull requests to improve the software or its documentation, +there are a few other ways you can help. -For plugin development, see the [plugin documentation](electrumabc_plugins/README.md). +### Running an Electrum server -## Creating Binaries +Electrum ABC relies on a network of SPV servers. If you can run a full node and +a public Electrum server, this will improve the resiliency of the network by +providing on more server for redundancy. -See the *Building the release files* section in [contrib/release.md](contrib/release.md) +You will need to keep your node software updated, especially around hard fork dates. -## Backporting +If you run a such an Electrum server, you can contact us to have it added +to the [list of trusted servers](electrumabc/servers.json), or submit +a pull request to add it yourself. You can run such a server for the mainnet +or for the [testnet](electrumabc/servers.json). -Electrum or Electron Cash features, refactoring commits or bug fixes can be -backported into Electrum ABC. +See https://github.com/cculianu/Fulcrum for how to run a SPV server. -To do this, first add the remote repositories: -```shell -git remote add electrum https://github.com/spesmilo/electrum.git -git remote add electroncash https://github.com/Electron-Cash/Electron-Cash.git -``` +### Translations -Then fetch the remote git history: -```shell -git fetch electrum -git fetch electroncash -``` +The messages displayed in the graphical interface need to be translated in +as many languages as possible. At the moment, Electrum ABC is still using the +[Electron Cash translations](https://crowdin.com/project/electron-cash) from +the date of the fork. As the Electrum ABC graphical interface will slowly +diverge from Electron Cash, we will need to update the translations +accordingly. -This step must be repeated every time you want to backport a commit that is more -recent than the last time you fetched the history. +If you are interested in helping to set up and manage a separate translation +project for Electrum ABC, feel free to contact us on github. -Then you can cherry-pick the relevant commits: -```shell -git cherry-pick -Xsubtree=electrum -``` +### Reviewing pull requests + +Contributing code is great, but all new code must also be reviewed before being +added to the repository. The review process can be a bottleneck, when other +contributors are very busy. Feel free to review any open pull request, as +having multiple reviewers increase the chances of spotting bugs before they +can cause any damage. + +See [Linus's law](https://en.wikipedia.org/wiki/Linus%27s_law). diff --git a/MANIFEST.in b/MANIFEST.in index d23e4906a68d..3ad23ad271d1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,7 @@ # but for now we keep it to avoid hacks such as copying the file in # contrib/make_linux_sdist include LICENCE RELEASE-NOTES.md AUTHORS -include README.md +include README.rst include electrum-abc.desktop include *.py include electrum-abc diff --git a/README.rst b/README.rst new file mode 100644 index 000000000000..34dbc88faad9 --- /dev/null +++ b/README.rst @@ -0,0 +1,170 @@ +Electrum ABC - Lightweight eCash client +======================================= + +:: + + Licence: MIT Licence + Author: Electrum ABC Developers + Language: Python + Homepage: https://www.bitcoinabc.org/electrum + + +Getting started +=============== + +**Note: If running from source, Python 3.7 or above is required to run Electrum ABC.** +If your system lacks Python 3.7, you have other options, such as the +`AppImage / binary releases `_ +or running from source using `pyenv` (see section `Running from source on old Linux`_ below). + +**macOS:** It is recommended that macOS users run +`the binary .dmg `_ +as that's simpler to use and has everything included. Otherwise, if you +want to run from source, see section `Running from source on macOS`_ below. + +If you want to use the Qt interface, install the Qt dependencies:: + + sudo apt-get install python3-pyqt5 python3-pyqt5.qtsvg + +If you downloaded the official package (tar.gz), you can run +Electrum ABC from its root directory, without installing it on your +system; all the python dependencies are included in the 'packages' +directory. To run Electrum ABC from its root directory, just do:: + + ./electrum-abc + +You can also install Electrum ABC on your system, by running this command:: + + pip3 install . --user + +Compile the icons file for Qt (normally you can skip this step, run this command if icons are missing):: + + sudo apt-get install pyqt5-dev-tools + pyrrc5 icons.qrc -o electrumabc_gui/qt/icons.py + +This will download and install the Python dependencies used by +Electrum ABC, instead of using the 'packages' directory. + +If you cloned the git repository, you need to compile extra files +before you can run Electrum ABC. Read the next section, "Development +Version". + +Hardware Wallet +--------------- + +Electrum ABC natively supports Ledger, Trezor and Satochip hardware wallets. +You need additional dependencies. To install them, run this command:: + + pip3 install -r contrib/requirements/requirements-hw.txt + +If you still have problems connecting to your Nano S please have a look at this +`troubleshooting `_ section on Ledger website. + + +Development version +=================== + +Check your python version >= 3.7, and install pyqt5, as instructed above in the +`Getting started`_ section above or `Running from source on old Linux`_ section below. + +If you are on macOS, see the `Running from source on macOS`_ section below. + +Check out the code from Github:: + + git clone https://github.com/Bitcoin-ABC/ElectrumABC + cd ElectrumABC + +Install the python dependencies:: + + pip3 install -r contrib/requirements/requirements.txt --user + +Create translations (optional):: + + sudo apt-get install python-requests gettext + ./contrib/make_locale + +Compile libsecp256k1 (optional, yet highly recommended):: + + sudo apt-get install libtool automake + ./contrib/make_secp + +For plugin development, see the `plugin documentation `_. + +Running unit tests (optional):: + + python3 -m electrumabc.tests + + +Running from source on old Linux +================================ + +If your Linux distribution has a different version of python 3 (such as python +3.5 in Debian 9), it is recommended to do a user dir install with +`pyenv `_. This allows Electrum ABC +to run completely independently of your system configuration. + +1. Install `pyenv `_ in your user + account. Follow the printed instructions about updating your environment + variables and ``.bashrc``, and restart your shell to ensure that they are + loaded. +2. Run ``pyenv install 3.9.7``. This will download and compile that version of + python, storing it under ``.pyenv`` in your home directory. +3. ``cd`` into the Electrum ABC directory. Run ``pyenv local 3.9.7`` which inserts + a file ``.python-version`` into the current directory. +4. While still in this directory, run ``pip install pyqt5``. +5. If you are installing from the source file (.tar.gz or .zip) then you are + ready and you may run ``./electrum-abc``. If you are using the git version, + then continue by following the Development version instructions above. + +Running from source on macOS +============================ + +You need to install **either** `MacPorts `_ **or** +`HomeBrew `_. Follow the instructions on either site for +installing (Xcode from `Apple's developer site `_ +is required for either). + +1. After installing either HomeBrew or MacPorts, clone this repository and + switch to the directory: + ``git clone https://github.com/Bitcoin-ABC/ElectrumABC && cd ElectrumABC`` +2. Install python 3.7+. For brew: ``brew install python3`` + or if using MacPorts: ``sudo port install python37`` +3. Install PyQt5: ``python3 -m pip install --user pyqt5`` +4. Install Electrum ABC requirements: + ``python3 -m pip install --user -r contrib/requirements/requirements.txt`` +5. Compile libsecp256k1 (optional, yet highly recommended): + ``./contrib/make_secp``. + This requires GNU tools and automake, install with brew: + ``brew install coreutils automake`` + or if using MacPorts: ``sudo port install coreutils automake`` +6. At this point you should be able to just run the sources: ``./electrum-abc`` + + +Creating Binaries +================= + +Linux AppImage & Source Tarball +------------------------------- + +See `contrib/build-linux/README.md `_. + +Mac OS X / macOS +---------------- + +See `contrib/osx/ `_. + +Windows +------- + +See `contrib/build-wine/ `_. + +Verifying Release Binaries +========================== + +See `contrib/pubkeys/README.md `_ + +Contact developers +================== + +`Join the Electrum ABC telegram group `_ to get in contact +with developers or to get help from the community. diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 80d7451a793c..e4f8eb35c670 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,14 +1,10 @@ -# Release notes +Electrum ABC is a fork of the open source Electron Cash wallet for eCash. -## Release 5.2.5 +The Electrum ABC software is NOT affiliated, associated, or endorsed by +Electron Cash, electroncash.org, Electrum or electrum.org. -- Fix a bug breaking the application when installed with the Windows installer. -- Remove option to run the application from the command line on Windows with the - released binary when using the default Windows terminal. Users can still use the - command line on Windows with more advanced terminal application (e.g. Git Bash) - or by running the application from sources. -- Add a test_runner.py script. +# Release notes ## Release 5.2.4 @@ -278,7 +274,7 @@ - Decrease the default transaction fee from 80 satoshis/byte to 10 sats/byte - Add an option in the 'Pay to' context menu to scan the current screen for a QR code. -- Add a documentation page "Contributing to Electrum ABC". +- Add a documentation page [Contributing to Electrum ABC](CONTRIBUTING.md). - Remove the deprecated CashShuffle plugin. - Specify a default server for CashFusion. - Fix a bug introduced in 4.3.1 when starting the program from the source diff --git a/contrib/build-linux/README.md b/contrib/build-linux/README.md index 286b3bf58a6b..8841fd945f10 100644 --- a/contrib/build-linux/README.md +++ b/contrib/build-linux/README.md @@ -5,20 +5,20 @@ Source tarballs 1. To ensure no accidental local changes are included, run: - ```shell - contrib/make_clean + ``` + $ contrib/make_clean ``` 2. To create the source tarball (with the libsecp library included): - ```shell - contrib/make_linux_sdist + ``` + $ contrib/make_linux_sdist ``` Alternatively, you may use a docker with all required dependencies installed: - ```shell - contrib/build-linux/srcdist_docker/build.sh + ``` + $ contrib/build-linux/srcdist_docker/build.sh ``` 3. A `.tar.gz` and a `.zip` file of Electrum ABC will be placed in the `dist/` subdirectory. @@ -32,10 +32,10 @@ AppImage 1. To create a deterministic Linux AppImage (standalone bundle): - ```shell - contrib/make_clean - git checkout COMMIT_OR_TAG - contrib/build-linux/appimage/build.sh + ``` + $ contrib/make_clean + $ git checkout COMMIT_OR_TAG + $ contrib/build-linux/appimage/build.sh ``` Where `COMMIT_OR_TAG` is a git commit or branch or tag (eg `master`, `4.0.2`, etc). diff --git a/contrib/build-linux/appimage/README.md b/contrib/build-linux/appimage/README.md index de185fd75fe8..683f4bcaec0b 100644 --- a/contrib/build-linux/appimage/README.md +++ b/contrib/build-linux/appimage/README.md @@ -14,9 +14,9 @@ folder. 2. Build binary - ```shell - git checkout REVISION_TAG_OR_BRANCH_OR_COMMIT_TAG - contrib/build-linux/appimage/build.sh + ``` + $ git checkout REVISION_TAG_OR_BRANCH_OR_COMMIT_TAG + $ contrib/build-linux/appimage/build.sh ``` _Note:_ If you are using a MacOS host, run the above **without** `sudo`. diff --git a/contrib/build-linux/appimage/_build.sh b/contrib/build-linux/appimage/_build.sh index 94acfeda70ad..f523c0896261 100755 --- a/contrib/build-linux/appimage/_build.sh +++ b/contrib/build-linux/appimage/_build.sh @@ -168,7 +168,7 @@ rm -rf "$PYDIR"/{ctypes,sqlite3,tkinter,unittest}/test rm -rf "$PYDIR"/distutils/{command,tests} rm -rf "$PYDIR"/config-3.9m-x86_64-linux-gnu rm -rf "$PYDIR"/site-packages/Cryptodome/SelfTest -rm -rf "$PYDIR"/site-packages/qrcode/tests +rm -rf "$PYDIR"/site-packages/{psutil,qrcode}/tests for component in connectivity declarative location multimedia quickcontrols quickcontrols2 serialport webengine websockets xmlpatterns ; do rm -rf "$PYDIR"/site-packages/PyQt5/Qt/translations/qt${component}_* done diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt index 463e1bdb0184..acf777f79de1 100644 --- a/contrib/deterministic-build/requirements-binaries.txt +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -85,9 +85,58 @@ cryptography==39.0.1 \ --hash=sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106 \ --hash=sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a \ --hash=sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8 +psutil==5.9.4 \ + --hash=sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff \ + --hash=sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1 \ + --hash=sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62 \ + --hash=sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549 \ + --hash=sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08 \ + --hash=sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7 \ + --hash=sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e \ + --hash=sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe \ + --hash=sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24 \ + --hash=sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad \ + --hash=sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94 \ + --hash=sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8 \ + --hash=sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7 \ + --hash=sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4 pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 +pycryptodomex==3.17 \ + --hash=sha256:0af93aad8d62e810247beedef0261c148790c52f3cd33643791cc6396dd217c1 \ + --hash=sha256:12056c38e49d972f9c553a3d598425f8a1c1d35b2e4330f89d5ff1ffb70de041 \ + --hash=sha256:23d83b610bd97704f0cd3acc48d99b76a15c8c1540d8665c94d514a49905bad7 \ + --hash=sha256:2d4d395f109faba34067a08de36304e846c791808524614c731431ee048fe70a \ + --hash=sha256:32e764322e902bbfac49ca1446604d2839381bbbdd5a57920c9daaf2e0b778df \ + --hash=sha256:3c2516b42437ae6c7a29ef3ddc73c8d4714e7b6df995b76be4695bbe4b3b5cd2 \ + --hash=sha256:40e8a11f578bd0851b02719c862d55d3ee18d906c8b68a9c09f8c564d6bb5b92 \ + --hash=sha256:4b51e826f0a04d832eda0790bbd0665d9bfe73e5a4d8ea93b6a9b38beeebe935 \ + --hash=sha256:4c4674f4b040321055c596aac926d12f7f6859dfe98cd12f4d9453b43ab6adc8 \ + --hash=sha256:55eed98b4150a744920597c81b3965b632038781bab8a08a12ea1d004213c600 \ + --hash=sha256:599bb4ae4bbd614ca05f49bd4e672b7a250b80b13ae1238f05fd0f09d87ed80a \ + --hash=sha256:5c23482860302d0d9883404eaaa54b0615eefa5274f70529703e2c43cc571827 \ + --hash=sha256:64b876d57cb894b31056ad8dd6a6ae1099b117ae07a3d39707221133490e5715 \ + --hash=sha256:67a3648025e4ddb72d43addab764336ba2e670c8377dba5dd752e42285440d31 \ + --hash=sha256:6feedf4b0e36b395329b4186a805f60f900129cdf0170e120ecabbfcb763995d \ + --hash=sha256:78f0ddd4adc64baa39b416f3637aaf99f45acb0bcdc16706f0cc7ebfc6f10109 \ + --hash=sha256:7a6651a07f67c28b6e978d63aa3a3fccea0feefed9a8453af3f7421a758461b7 \ + --hash=sha256:7a8dc3ee7a99aae202a4db52de5a08aa4d01831eb403c4d21da04ec2f79810db \ + --hash=sha256:7cc28dd33f1f3662d6da28ead4f9891035f63f49d30267d3b41194c8778997c8 \ + --hash=sha256:7fa0b52df90343fafe319257b31d909be1d2e8852277fb0376ba89d26d2921db \ + --hash=sha256:88b0d5bb87eaf2a31e8a759302b89cf30c97f2f8ca7d83b8c9208abe8acb447a \ + --hash=sha256:a4fa037078e92c7cc49f6789a8bac3de06856740bb2038d05f2d9a2e4b165d59 \ + --hash=sha256:a57e3257bacd719769110f1f70dd901c5b6955e9596ad403af11a3e6e7e3311c \ + --hash=sha256:ab33c2d9f275e05e235dbca1063753b5346af4a5cac34a51fa0da0d4edfb21d7 \ + --hash=sha256:c84689c73358dfc23f9fdcff2cb9e7856e65e2ce3b5ed8ff630d4c9bdeb1867b \ + --hash=sha256:c92537b596bd5bffb82f8964cabb9fef1bca8a28a9e0a69ffd3ec92a4a7ad41b \ + --hash=sha256:caa937ff29d07a665dfcfd7a84f0d4207b2ebf483362fa9054041d67fdfacc20 \ + --hash=sha256:d38ab9e53b1c09608ba2d9b8b888f1e75d6f66e2787e437adb1fecbffec6b112 \ + --hash=sha256:d4cf0128da167562c49b0e034f09e9cedd733997354f2314837c2fa461c87bb1 \ + --hash=sha256:db23d7341e21b273d2440ec6faf6c8b1ca95c8894da612e165be0b89a8688340 \ + --hash=sha256:ee8bf4fdcad7d66beb744957db8717afc12d176e3fd9c5d106835133881a049b \ + --hash=sha256:f854c8476512cebe6a8681cc4789e4fcff6019c17baa0fd72b459155dc605ab4 \ + --hash=sha256:fd29d35ac80755e5c0a99d96b44fb9abbd7e871849581ea6a4cb826d24267537 PyQt5==5.15.2 \ --hash=sha256:29889845688a54d62820585ad5b2e0200a36b304ff3d7a555e95599f110ba4ce \ --hash=sha256:372b08dc9321d1201e4690182697c5e7ffb2e0770e6b4a45519025134b12e4fc \ diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index ac78c3dee7fc..8fe2ef4a6a35 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -3,9 +3,9 @@ attrs==21.4.0 \ --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd btchip-python==0.1.32 \ --hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55 -certifi==2023.5.7 \ - --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ - --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 cffi==1.15.1 \ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ @@ -334,9 +334,9 @@ pyscard==2.0.0 \ --hash=sha256:852a4e354bb82cc1f68afb204349ca68ea6c5332242644a80651a5c62bb1ab5f \ --hash=sha256:e162f9af64b49beb435e6543819f604e45534c822eb77fd100773f359fbcb6d8 \ --hash=sha256:b364d9d9186e793c1c4709eb72a4d29e09067d36ca463b2c2abd995bd1055779 -requests==2.31.0 \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f +requests==2.28.2 \ + --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \ + --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf simple-rlp==0.1.3 \ --hash=sha256:2df1d2b769f0a0177d26231ab8be16e65e3546b17bb0a9a490efd3517c082876 six==1.16.0 \ diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt index ba7ad73a39fe..9e5e4dc2140c 100644 --- a/contrib/deterministic-build/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -1,6 +1,6 @@ -certifi==2023.5.7 \ - --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ - --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 charset-normalizer==3.0.1 \ --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \ --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \ @@ -139,40 +139,16 @@ pyaes==1.6.1 \ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 -pycryptodomex==3.17 \ - --hash=sha256:0af93aad8d62e810247beedef0261c148790c52f3cd33643791cc6396dd217c1 \ - --hash=sha256:12056c38e49d972f9c553a3d598425f8a1c1d35b2e4330f89d5ff1ffb70de041 \ - --hash=sha256:23d83b610bd97704f0cd3acc48d99b76a15c8c1540d8665c94d514a49905bad7 \ - --hash=sha256:2d4d395f109faba34067a08de36304e846c791808524614c731431ee048fe70a \ - --hash=sha256:32e764322e902bbfac49ca1446604d2839381bbbdd5a57920c9daaf2e0b778df \ - --hash=sha256:3c2516b42437ae6c7a29ef3ddc73c8d4714e7b6df995b76be4695bbe4b3b5cd2 \ - --hash=sha256:40e8a11f578bd0851b02719c862d55d3ee18d906c8b68a9c09f8c564d6bb5b92 \ - --hash=sha256:4b51e826f0a04d832eda0790bbd0665d9bfe73e5a4d8ea93b6a9b38beeebe935 \ - --hash=sha256:4c4674f4b040321055c596aac926d12f7f6859dfe98cd12f4d9453b43ab6adc8 \ - --hash=sha256:55eed98b4150a744920597c81b3965b632038781bab8a08a12ea1d004213c600 \ - --hash=sha256:599bb4ae4bbd614ca05f49bd4e672b7a250b80b13ae1238f05fd0f09d87ed80a \ - --hash=sha256:5c23482860302d0d9883404eaaa54b0615eefa5274f70529703e2c43cc571827 \ - --hash=sha256:64b876d57cb894b31056ad8dd6a6ae1099b117ae07a3d39707221133490e5715 \ - --hash=sha256:67a3648025e4ddb72d43addab764336ba2e670c8377dba5dd752e42285440d31 \ - --hash=sha256:6feedf4b0e36b395329b4186a805f60f900129cdf0170e120ecabbfcb763995d \ - --hash=sha256:78f0ddd4adc64baa39b416f3637aaf99f45acb0bcdc16706f0cc7ebfc6f10109 \ - --hash=sha256:7a6651a07f67c28b6e978d63aa3a3fccea0feefed9a8453af3f7421a758461b7 \ - --hash=sha256:7a8dc3ee7a99aae202a4db52de5a08aa4d01831eb403c4d21da04ec2f79810db \ - --hash=sha256:7cc28dd33f1f3662d6da28ead4f9891035f63f49d30267d3b41194c8778997c8 \ - --hash=sha256:7fa0b52df90343fafe319257b31d909be1d2e8852277fb0376ba89d26d2921db \ - --hash=sha256:88b0d5bb87eaf2a31e8a759302b89cf30c97f2f8ca7d83b8c9208abe8acb447a \ - --hash=sha256:a4fa037078e92c7cc49f6789a8bac3de06856740bb2038d05f2d9a2e4b165d59 \ - --hash=sha256:a57e3257bacd719769110f1f70dd901c5b6955e9596ad403af11a3e6e7e3311c \ - --hash=sha256:ab33c2d9f275e05e235dbca1063753b5346af4a5cac34a51fa0da0d4edfb21d7 \ - --hash=sha256:c84689c73358dfc23f9fdcff2cb9e7856e65e2ce3b5ed8ff630d4c9bdeb1867b \ - --hash=sha256:c92537b596bd5bffb82f8964cabb9fef1bca8a28a9e0a69ffd3ec92a4a7ad41b \ - --hash=sha256:caa937ff29d07a665dfcfd7a84f0d4207b2ebf483362fa9054041d67fdfacc20 \ - --hash=sha256:d38ab9e53b1c09608ba2d9b8b888f1e75d6f66e2787e437adb1fecbffec6b112 \ - --hash=sha256:d4cf0128da167562c49b0e034f09e9cedd733997354f2314837c2fa461c87bb1 \ - --hash=sha256:db23d7341e21b273d2440ec6faf6c8b1ca95c8894da612e165be0b89a8688340 \ - --hash=sha256:ee8bf4fdcad7d66beb744957db8717afc12d176e3fd9c5d106835133881a049b \ - --hash=sha256:f854c8476512cebe6a8681cc4789e4fcff6019c17baa0fd72b459155dc605ab4 \ - --hash=sha256:fd29d35ac80755e5c0a99d96b44fb9abbd7e871849581ea6a4cb826d24267537 +pycryptodome==3.14.1 \ + --hash=sha256:e04e40a7f8c1669195536a37979dd87da2c32dbdc73d6fe35f0077b0c17c803b \ + --hash=sha256:f2772af1c3ef8025c85335f8b828d0193fa1e43256621f613280e2c81bfad423 \ + --hash=sha256:9ec761a35dbac4a99dcbc5cd557e6e57432ddf3e17af8c3c86b44af9da0189c0 \ + --hash=sha256:e64738207a02a83590df35f59d708bf1e7ea0d6adce712a777be2967e5f7043c \ + --hash=sha256:e24d4ec4b029611359566c52f31af45c5aecde7ef90bf8f31620fd44c438efe7 \ + --hash=sha256:8b5c28058102e2974b9868d72ae5144128485d466ba8739abd674b77971454cc \ + --hash=sha256:924b6aad5386fb54f2645f22658cb0398b1f25bc1e714a6d1522c75d527deaa5 \ + --hash=sha256:53dedbd2a6a0b02924718b520a723e88bcf22e37076191eb9b91b79934fb2192 \ + --hash=sha256:ea56a35fd0d13121417d39a83f291017551fa2c62d6daa6b04af6ece7ed30d84 pypng==0.20220715.0 \ --hash=sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c \ --hash=sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1 @@ -192,9 +168,9 @@ qrcode==7.4.2 \ QtPy==2.3.0 \ --hash=sha256:0603c9c83ccc035a4717a12908bf6bc6cb22509827ea2ec0e94c2da7c9ed57c5 \ --hash=sha256:8d6d544fc20facd27360ea189592e6135c614785f0dec0b4f083289de6beb408 -requests==2.31.0 \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f +requests==2.28.2 \ + --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \ + --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf setuptools==66.1.1 \ --hash=sha256:6f590d76b713d5de4e49fe4fbca24474469f53c83632d5d0fd056f7ff7e8112b \ --hash=sha256:ac4008d396bc9cd983ea483cb7139c0240a07bbc74ffb6232fceffedc6cf03a8 diff --git a/contrib/docker_notes.md b/contrib/docker_notes.md new file mode 100644 index 000000000000..3d43ea0b9d50 --- /dev/null +++ b/contrib/docker_notes.md @@ -0,0 +1,20 @@ +# Notes about using Docker in the build scripts + +- To install Docker: + + This assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another similar system. + + ``` + $ 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 + ``` + +- To communicate with the docker daemon, the build scripts either need to be called via sudo, + or the unix user on the host system (e.g. the user you run as) needs to be + part of the `docker` group. i.e.: + ``` + $ sudo usermod -aG docker ${USER} + ``` + (and then reboot or similar for it to take effect) diff --git a/contrib/osx/requirements-osx-build.txt b/contrib/osx/requirements-osx-build.txt index 6299bbef9c94..93bb9b138209 100644 --- a/contrib/osx/requirements-osx-build.txt +++ b/contrib/osx/requirements-osx-build.txt @@ -3,9 +3,9 @@ altgraph==0.17 \ --hash=sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa biplist==1.0.3 \ --hash=sha256:4c0549764c5fe50b28042ec21aa2e14fe1a2224e239a1dae77d9e7f3932aa4c6 -certifi==2023.5.7 \ - --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ - --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 +certifi==2021.10.8 \ + --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ + --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 @@ -16,8 +16,8 @@ dmgbuild==1.4.2 \ --hash=sha256:1b03eaa229128c03a4da71ff97a4efa3ac008f04ddbe70b51ef797a21b73857c ds_store==1.3.0 \ --hash=sha256:e52478f258626600c1f53fc18c1ddcd8542fa0bca41d4bd81d57c04c87aabf24 -future==0.18.3 \ - --hash=sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307 +future==0.18.2 \ + --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d idna==3.3 \ --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d @@ -34,16 +34,18 @@ pip==21.3.1 \ pyinstaller-hooks-contrib==2022.3 \ --hash=sha256:9765e68552803327d58f6c5eca970bb245b7cdf073e2f912a2a3cb50360bc2d8 \ --hash=sha256:9fa4ca03d058cba676c3cc16005076ce6a529f144c08b87c69998625fbd84e0a -requests==2.31.0 \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f -setuptools==65.5.1 \ - --hash=sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f +requests==2.27.1 \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d +setuptools==62.0.0 \ + --hash=sha256:a65e3802053e99fc64c6b3b29c11132943d5b8c8facbcc461157511546510967 \ + --hash=sha256:7999cbd87f1b6e1f33bf47efa368b224bed5e27b5ef2c4d46580186cbcb1a86a six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 urllib3==1.26.7 \ --hash=sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece \ --hash=sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844 -wheel==0.38.4 \ - --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac +wheel==0.36.2 \ + --hash=sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e \ + --hash=sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e diff --git a/contrib/package_plugin.py b/contrib/package_plugin.py index b21942b16991..0d19c96b7a4c 100755 --- a/contrib/package_plugin.py +++ b/contrib/package_plugin.py @@ -76,14 +76,14 @@ def write_plugin_archive(metadata, source_package_path, archive_file_path): hasher.update(f.read()) base_name = os.path.basename(plugin_path) - with open(plugin_path + ".sha256", "w", encoding="utf-8") as f: + with open(plugin_path + ".sha256", "w") as f: f.write("{0} *{1}".format(hasher.hexdigest(), base_name)) return hasher.hexdigest() def write_manifest(metadata, manifest_file_path): - with open(manifest_file_path, "w", encoding="utf-8") as f: + with open(manifest_file_path, "w") as f: json.dump(metadata, f, indent=4) diff --git a/contrib/requirements/requirements-binaries-elcapitan.txt b/contrib/requirements/requirements-binaries-elcapitan.txt new file mode 100644 index 000000000000..7d53c805100c --- /dev/null +++ b/contrib/requirements/requirements-binaries-elcapitan.txt @@ -0,0 +1,5 @@ +PyQt5<5.12 +pycryptodomex<3.7 +websocket-client +PyQt5-sip==4.19.13 +psutil==5.6.1 diff --git a/contrib/requirements/requirements-binaries.txt b/contrib/requirements/requirements-binaries.txt index cd6e7555c6d2..b7e84d8605e7 100644 --- a/contrib/requirements/requirements-binaries.txt +++ b/contrib/requirements/requirements-binaries.txt @@ -1,2 +1,4 @@ PyQt5>=5.12.3,<=5.15.2 +pycryptodomex +psutil cryptography>=3.3.2 diff --git a/contrib/requirements/requirements-osx-build.txt b/contrib/requirements/requirements-osx-build.txt index 15cee78a0b58..ea0260bd6be4 100644 --- a/contrib/requirements/requirements-osx-build.txt +++ b/contrib/requirements/requirements-osx-build.txt @@ -1,5 +1,5 @@ dmgbuild==1.3.2 -requests==2.31.0 +requests==2.21.0 macholib>=1.8 altgraph pefile>=2017.8.1 diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index 0477cf8463cb..101913ea178d 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -12,4 +12,3 @@ certifi mnemonic>=0.19 pathvalidate dnspython[DNSSEC]>=2.0 -pycryptodomex diff --git a/contrib/secp_HOWTO.md b/contrib/secp_HOWTO.md index ccb6fc33046a..c4c437382562 100644 --- a/contrib/secp_HOWTO.md +++ b/contrib/secp_HOWTO.md @@ -86,8 +86,8 @@ But for now, your package manager's `libsecp256k1-0` will be sufficient. mileage may vary.* Example Ubuntu command: -```shell -sudo apt install libsecp256k1-0 +``` +$ sudo apt install libsecp256k1-0 ``` #### The above all failed! What now?! diff --git a/contrib/update_checker/README.md b/contrib/update_checker/README.md index d845e0959769..376796802178 100644 --- a/contrib/update_checker/README.md +++ b/contrib/update_checker/README.md @@ -6,8 +6,7 @@ This directory contains the `releases.json` file that the Electrum ABC update ch #### Update Checker Overview There is an update-checking facility built-in to the Qt desktop app. The facility basically functions as follows: -1. When the user selects "Check for updates...", Electrum ABC connects to the URL hard-coded as `RELEASES_JSON_URL` - in `electrumabc/constants.py` (currently: https://raw.github.com/Bitcoin-ABC/bitcoin-abc/master/electrum/contrib/update_checker/releases.json) +1. When the user selects "Check for updates...", Electrum ABC connects to the URL hard-coded in `gui/qt/update_checker.py` (currently: https://raw.github.com/Bitcoin-ABC/bitcoin-abc/master/electrum/contrib/update_checker/releases.json) 2. It downloads `releases.json` (the file in this directory) 3. It checks the versions seen in `releases.json` -- if they are newer than the version in the running app, and if the signed message is valid and is signed with one of the addresses hard-coded in `update_checker.py`, it then informs the user that an update is available. @@ -21,23 +20,23 @@ It hopefully will decrease the number of users running very old versions of Elec You need to update `releases.json` in this directory whenever a new version is released, and push it to master. This file contains a dictionary of: -```json +``` { "version string" : { "bitcoin address" : "signed message" } } ``` - **"version string"** above is a version of the form MAJOR.MINOR.REV[variant], e.g. "3.3.5" or "3.3.5CS" (in the latter, 'CS' is the variant) - And empty/omitted variant means "Electrum ABC Regular" -- The variant must match the variant in `electrumabc/version.py`. +- The variant must match the variant in `lib/version.py`. #### How To Update `releases.json` - 1. Release Electrum ABC as normal, updating the version in `electrumabc/version.py`. + 1. Release Electrum ABC as normal, updating the version in `lib/version.py`. 2. After release, or in tandem with releasing, edit `releases.json` 3. Make sure to replace the entry for the old version with a new entry. 4. So for example if you were on version "3.3.4" before and you are now releasing "3.3.5", look for "3.3.4" in `releases.json`, and update it to "3.3.5" - 5. Sign the text "3.3.5" with one of the bitcoin addresses listed in `electrumabc_gui/qt/update_checker.py`. Paste this address and the signed message (replacing the old address and signed message) into the dictionary entry for "3.3.5" in `releases.json`. + 5. Sign the text "3.3.5" with one of the bitcoin addresses listed in `gui/qt/update_checker.py`. Paste this address and the signed message (replacing the old address and signed message) into the dictionary entry for "3.3.5" in `releases.json`. 6. Push the new commit with the updated `releases.json` to master. (Since currently the `update_checker.py` code looks for this file in master on github). ##### Example @@ -57,18 +56,7 @@ Notice how the version string is different, the signing address happened to rema ##### How Do I Sign? -- Make sure you control one of the bitcoin addresses listed in `electrumabc_gui/qt/update_checker.py`. (If you do not, modify this file before release to include one of your addresses!) +- Make sure you control one of the bitcoin addresses listed in `gui/qt/update_checker.py`. (If you do not, modify this file before release to include one of your addresses!) - Open up Electrum ABC and go to that address in the "Addresses" tab and right click on it, selecting **"Sign/Verify Message"** - The message to be signed is the version string you will put into the JSON, so for example the simple string `3.3.5` in the example above. - Hit **sign**, and paste the signature text into the JSON (and signing address, of course, if it's changed). - -##### How do I test the signature - -Before pushing the commit to the remote repository, it is possible to test locally that a signature is correct. -For this, run the application from sources with the `--test-release-notification` command line option. - -```shell -./electrum-abc -v --test-release-notification -``` - -To trigger the version check manually, go to the *Help* menu and click *Check for updates*. diff --git a/contrib/update_checker/releases.json b/contrib/update_checker/releases.json index 4f7b4252d80a..e976342c2595 100644 --- a/contrib/update_checker/releases.json +++ b/contrib/update_checker/releases.json @@ -1,5 +1,5 @@ { - "5.2.5": { - "ecash:qz5j83ez703wvlwpqh94j6t45f8dn2afjgtgurgua0": "ILnn1mBuM5s8reMfOole4dkBg6xd1cyoqE6mXxpQFOKzeSkAZq8hM2BqGoZR9kOgJrsa3lH/PZxgv2MEQ6wCRlI=" + "5.2.1": { + "ecash:qz5j83ez703wvlwpqh94j6t45f8dn2afjgtgurgua0": "HxTpo7VLwiwlkN26Rq9y16gbcm22z4trkQh7XOkO5sAiPFLe4xkiuW5IOAXnp0wyH0ae/yc3rV+8Iy389AzeXeY=" } } diff --git a/electrum-abc b/electrum-abc index afd5eaddbd7d..b3fd6802597c 100755 --- a/electrum-abc +++ b/electrum-abc @@ -56,8 +56,7 @@ from electrumabc.plugins import Plugins from electrumabc.printerror import print_msg, print_stderr, set_verbosity from electrumabc.simple_config import SimpleConfig from electrumabc.storage import WalletStorage -from electrumabc.util import InvalidPassword -from electrumabc.json_util import json_encode, json_decode +from electrumabc.util import InvalidPassword, json_decode, json_encode from electrumabc.wallet import ( AbstractWallet, ImportedAddressWallet, @@ -66,6 +65,9 @@ from electrumabc.wallet import ( create_new_wallet, ) +# Import ok on other platforms, won't be called. +from electrumabc.winconsole import create_or_attach_console + # Workaround for PyQt5 5.12.3 # see https://github.com/pyinstaller/pyinstaller/issues/4293 if sys.platform == "win32" and hasattr(sys, "frozen") and hasattr(sys, "_MEIPASS"): @@ -567,6 +569,23 @@ def main(): # The hook will only be used in the Qt GUI right now util.setup_thread_excepthook() + # On windows, allocate a console if needed + if sys.platform.startswith("win"): + require_console = "-v" in sys.argv or "--verbose" in sys.argv + console_title = ( + _(f"{PROJECT_NAME} - Verbose Output") if require_console else None + ) + # Attempt to attach to ancestor process console. If create=True we will + # create a new console window if no ancestor process console exists. + # (Presumably if user ran with -v, they expect console output). + # The below is required to be able to get verbose or console output + # if running from cmd.exe (see spesmilo#2592, Electron-Cash#1295). + # The below will be a no-op if the terminal was msys/mingw/cygwin, since + # there will already be a console attached in that case. + # Worst case: The below will silently ignore errors so that startup + # may proceed unimpeded. + create_or_attach_console(create=require_console, title=console_title) + # on osx, delete Process Serial Number arg generated for apps launched in Finder sys.argv = list(filter(lambda x: not x.startswith("-psn"), sys.argv)) diff --git a/electrumabc/base_wizard.py b/electrumabc/base_wizard.py index 1e6cce8348a8..06e97cbb0345 100644 --- a/electrumabc/base_wizard.py +++ b/electrumabc/base_wizard.py @@ -335,7 +335,7 @@ def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET, *, storage=None): except Exception: devmgr.print_error("error", name) continue - devices += [(name, device_info) for device_info in u] + devices += list(map(lambda x: (name, x), u)) extra_button = None if sys.platform in ("linux", "linux2", "linux3"): extra_button = (_("Hardware Wallet Support..."), self.on_hw_wallet_support) @@ -621,7 +621,7 @@ def on_keystore(self, k): self.show_error(_("Wrong key type") + " %s" % t1) self.run("choose_keystore") return - if k.xpub in (ks.xpub for ks in self.keystores): + if k.xpub in map(lambda x: x.xpub, self.keystores): self.show_error(_("Error: duplicate master public key")) self.run("choose_keystore") return diff --git a/electrumabc/bitcoin.py b/electrumabc/bitcoin.py index 672b13f7e6f6..3c8af85200bd 100644 --- a/electrumabc/bitcoin.py +++ b/electrumabc/bitcoin.py @@ -502,11 +502,16 @@ def hash_160(public_key: bytes) -> bytes: md.update(sha256_hash) return md.digest() except ValueError: - from Cryptodome.Hash import RIPEMD160 + from Crypto.Hash import RIPEMD160 md = RIPEMD160.new() md.update(sha256_hash) return md.digest() + except ImportError: + from . import ripemd + + md = ripemd.new(sha256_hash) + return md.digest() def hash160_to_b58_address(h160, addrtype): @@ -644,8 +649,8 @@ def base_decode(v, length, base): def EncodeBase58Check(vchIn): - h = Hash(vchIn) - return base_encode(vchIn + h[0:4], base=58) + hash = Hash(vchIn) + return base_encode(vchIn + hash[0:4], base=58) def DecodeBase58Check(psz): @@ -657,8 +662,8 @@ def DecodeBase58Check(psz): return None key = vchRet[0:-4] csum = vchRet[-4:] - h = Hash(key) - cs32 = h[0:4] + hash = Hash(key) + cs32 = hash[0:4] if cs32 != csum: return None else: @@ -1267,7 +1272,7 @@ def bip32_derivation(s): def is_bip32_derivation(x): try: - list(bip32_derivation(x)) + [i for i in bip32_derivation(x)] return True except Exception: return False @@ -1354,8 +1359,7 @@ def bip38_decrypt(enc_key, password, *, require_fast=True, net=None): try: return Bip38Key(enc_key, net=net).decrypt(password) except Bip38Key.PasswordError: - # Bad password result is an empty tuple - return () + return tuple() # Bad password result is an empty tuple except Bip38Key.Error as e: print_error("[bip38_decrypt] Error with key", enc_key, "error was:", repr(e)) return None diff --git a/electrumabc/blockchain.py b/electrumabc/blockchain.py index 4c595c8480af..bf4a678bd894 100644 --- a/electrumabc/blockchain.py +++ b/electrumabc/blockchain.py @@ -219,17 +219,17 @@ def verify_proven_chunk(chunk_base_height, chunk_data): # Copied from electrumx -def root_from_proof(hash_, branch, index): +def root_from_proof(hash, branch, index): hash_func = bitcoin.Hash for elt in branch: if index & 1: - hash_ = hash_func(elt + hash_) + hash = hash_func(elt + hash) else: - hash_ = hash_func(hash_ + elt) + hash = hash_func(hash + elt) index >>= 1 if index: raise ValueError("index out of range for branch") - return hash_ + return hash class HeaderChunk: @@ -309,10 +309,10 @@ def check_header(self, header): height = header.get("block_height") return header_hash == self.get_hash(height) - def fork(parent, header) -> Blockchain: + def fork(parent, header): base_height = header.get("block_height") self = Blockchain(parent.config, base_height, parent.base_height) - open(self.path(), "wb").close() + open(self.path(), "w+").close() self.save_header(header) return self diff --git a/electrumabc/caches.py b/electrumabc/caches.py index 75a1c75e5e5e..76893a5eb2ee 100644 --- a/electrumabc/caches.py +++ b/electrumabc/caches.py @@ -64,7 +64,7 @@ def __init__(self, *, maxlen=10000, name="An Unnamed Cache", timeout=None): ) self.maxlen = maxlen self.name = name - self.d = {} + self.d = dict() _ExpiringCacheMgr.add_cache(self) def get(self, key, default=None): diff --git a/electrumabc/commands.py b/electrumabc/commands.py index 9a8fbf322d1a..c2221e14103c 100644 --- a/electrumabc/commands.py +++ b/electrumabc/commands.py @@ -40,14 +40,13 @@ from .address import Address, AddressError from .bitcoin import CASH, TYPE_ADDRESS, hash_160 from .constants import PROJECT_NAME, SCRIPT_NAME, XEC -from .json_util import json_decode from .mnemo import MnemonicElectrum, make_bip39_words from .paymentrequest import PR_EXPIRED, PR_PAID, PR_UNCONFIRMED, PR_UNKNOWN, PR_UNPAID from .plugins import run_hook from .printerror import print_error from .simple_config import SimpleConfig from .transaction import OPReturn, Transaction, TxOutput, multisig_script, tx_from_str -from .util import format_satoshis, to_bytes +from .util import format_satoshis, json_decode, to_bytes from .version import PACKAGE_VERSION from .wallet import create_new_wallet, restore_wallet_from_text @@ -1296,11 +1295,14 @@ def add_network_options(parser): "-1", "--oneserver", action="store_true", + dest="oneserver", + default=False, 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)" @@ -1309,6 +1311,7 @@ def add_network_options(parser): parser.add_argument( "-p", "--proxy", + dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http", ) @@ -1332,6 +1335,8 @@ def add_global_options(parser): "-v", "--verbose", action="store_true", + dest="verbose", + default=False, help="Show debugging information", ) group.add_argument( @@ -1341,6 +1346,8 @@ def add_global_options(parser): "-P", "--portable", action="store_true", + dest="portable", + default=False, help="Use local 'electrum_abc_data' directory", ) group.add_argument( @@ -1357,23 +1364,23 @@ def add_global_options(parser): "--forgetconfig", action="store_true", dest="forget_config", + default=False, help="Forget config on exit", ) 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( - "--test-release-notification", - action="store_true", - help="fetch release notification data from current source tree", - ) def get_parser(): @@ -1395,6 +1402,7 @@ def get_parser(): parser_gui.add_argument( "-g", "--gui", + dest="gui", help="select graphical user interface", choices=["qt", "text", "stdio"], ) @@ -1402,12 +1410,15 @@ def get_parser(): "-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( @@ -1423,6 +1434,7 @@ def get_parser(): parser_gui.add_argument( "-O", "--qt_opengl", + dest="qt_opengl", default=None, help=( "(Windows only) If using Qt gui, override the QT_OPENGL env-var with" @@ -1435,6 +1447,7 @@ def get_parser(): parser_gui.add_argument( "--qt_disable_highdpi", action="store_true", + dest="qt_disable_highdpi", default=None, help="(Linux & Windows only) If using Qt gui, disable high DPI scaling", ) @@ -1466,24 +1479,29 @@ def get_parser(): "-o", "--offline", action="store_true", + dest="offline", + default=False, help="Run offline", ) for optname, default in zip(cmd.options, cmd.defaults): - short_option, help_ = command_options[optname] - long_option = "--" + optname + a, help = command_options[optname] + b = "--" + optname action = "store_true" if type(default) is bool else "store" - args = (short_option, long_option) if short_option else (long_option,) + 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_, + help=help, type=_type, ) else: - p.add_argument(*args, action=action, default=default, help=help_) + p.add_argument( + *args, dest=optname, action=action, default=default, help=help + ) for param in cmd.params: h = param_descriptions.get(param, "") diff --git a/electrumabc/consolidate.py b/electrumabc/consolidate.py index 9dd581bd012e..af3360621685 100644 --- a/electrumabc/consolidate.py +++ b/electrumabc/consolidate.py @@ -24,6 +24,7 @@ """ This module provides coin consolidation tools. """ +import copy from typing import Iterator, List, Optional, Tuple from . import wallet @@ -81,12 +82,52 @@ def __init__( and (max_height is None or utxo["height"] <= max_height) ) ] + self.wallet = wallet_instance + + # Cache data common to all coins + self.address = address + self.txin_type = wallet_instance.get_txin_type(address) + self.received = {} + for tx_hash, height in wallet_instance.get_address_history(address): + for n, v, is_cb in self.wallet.txo.get(tx_hash, {}).get(address, []): + self.received[tx_hash + f":{n}"] = (height, v, is_cb) + + if isinstance(self.wallet, wallet.ImportedAddressWallet): + sig_info = { + "x_pubkeys": ["fd" + address.to_script_hex()], + "signatures": [None], + } + elif isinstance(self.wallet, wallet.ImportedPrivkeyWallet): + pubkey = self.wallet.keystore.address_to_pubkey(address) + sig_info = { + "x_pubkeys": [pubkey.to_ui_string()], + "signatures": [None], + "num_sig": 1, + } + elif isinstance(self.wallet, wallet.MultisigWallet): + derivation = self.wallet.get_address_index(address) + sig_info = { + "x_pubkeys": [ + k.get_xpubkey(*derivation) for k in self.wallet.get_keystores() + ], + "signatures": [None] * self.wallet.n, + "num_sig": self.wallet.m, + "pubkeys": None, + } + else: + # Default case for wallet.Simple_Deterministic_Wallet and Mock wallet used + # in test + derivation = self.wallet.get_address_index(address) + x_pubkey = self.wallet.keystore.get_xpubkey(*derivation) + sig_info = { + "x_pubkeys": [x_pubkey], + "signatures": [None], + "num_sig": 1, + } # Add more metadata to coins - address_history = wallet_instance.get_address_history(address) - received = wallet_instance.get_address_unspent(address, address_history) - for coin in self._coins: - wallet_instance.add_input_info(coin, received) + for i, c in enumerate(self._coins): + self.add_input_info(c, sig_info) def get_unsigned_transactions(self) -> List[Transaction]: """ @@ -144,3 +185,15 @@ def try_adding_another_coin_to_transaction( [(TYPE_ADDRESS, self.output_address, next_amount - tx_size * FEERATE)] ) return tx_size + + def add_input_info(self, txin, siginfo: dict): + """Reimplemented from wallet.add_input_info to optimize for multiple calls + with same address and same history. + Caching the transaction history is the most significant optimization, + as the original function loads the history from disk (text file) for + every call.""" + txin["type"] = self.txin_type + item = self.received.get(txin["prevout_hash"] + f":{txin['prevout_n']}") + tx_height, value, is_cb = item + txin["value"] = value + txin.update(copy.deepcopy(siginfo)) diff --git a/electrumabc/contacts.py b/electrumabc/contacts.py index d0d3fa066a71..50e9d338792a 100644 --- a/electrumabc/contacts.py +++ b/electrumabc/contacts.py @@ -106,7 +106,7 @@ def _loadv1(storage) -> List[Contact]: 'get'. Note this also supports the pre-v1 format, as the old Contacts class did.""" assert callable(getattr(storage, "get", None)) - d = {} + d = dict() d2 = storage.get("contacts") try: d.update(d2) # catch type errors, etc by doing this diff --git a/electrumabc/daemon.py b/electrumabc/daemon.py index 858093ff91a8..a50fc8c21c3a 100644 --- a/electrumabc/daemon.py +++ b/electrumabc/daemon.py @@ -36,13 +36,12 @@ from .commands import Commands, known_commands from .constants import PROJECT_NAME, SCRIPT_NAME from .exchange_rate import FxThread -from .json_util import json_decode from .jsonrpc import VerifyingJSONRPCServer from .network import Network from .printerror import print_error, print_stderr from .simple_config import SimpleConfig from .storage import WalletStorage -from .util import DaemonThread, standardize_path, to_string +from .util import DaemonThread, json_decode, standardize_path, to_string from .wallet import Wallet if TYPE_CHECKING: @@ -109,7 +108,7 @@ def get_server(config: SimpleConfig, timeout=2.0) -> Optional[jsonrpclib.Server] while True: create_time = None try: - with open(lockfile, encoding="utf-8") as f: + with open(lockfile) as f: (host, port), tmp_create_time = ast.literal_eval(f.read()) create_time = float(tmp_create_time) del tmp_create_time # ensures create_time is float; raises if create_time is not-float-compatible @@ -320,8 +319,10 @@ def run_cmdline(self, config_options): } else: wallet = None - # json decoded arguments passed to function - args = [json_decode(config.get(x)) for x in cmd.params] + # 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: diff --git a/electrumabc/dnssec.py b/electrumabc/dnssec.py index f00afb6f8844..6a873d5e1f11 100644 --- a/electrumabc/dnssec.py +++ b/electrumabc/dnssec.py @@ -32,6 +32,9 @@ # https://github.com/rthalley/dnspython/blob/master/tests/test_dnssec.py +import struct +import time + import dns.dnssec import dns.message import dns.name @@ -52,6 +55,147 @@ import dns.rdtypes.IN.AAAA import dns.resolver +if not hasattr(dns, "version"): + # Do some monkey patching needed for versions of dnspython < 2 + + # Pure-Python version of dns.dnssec._validate_rsig + import hashlib + + import ecdsa + + from . import rsakey + + def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None): + from dns.dnssec import ( # pylint: disable=no-name-in-module + ECDSAP256SHA256, + ECDSAP384SHA384, + ValidationFailure, + _find_candidate_keys, + _is_ecdsa, + _is_rsa, + _make_algorithm_id, + _make_hash, + _to_rdata, + ) + + 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 + elif rrsig.algorithm == ECDSAP384SHA384: + curve = ecdsa.curves.NIST384p + key_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") + + class PyCryptodomexHashAlike: + def __init__(self, hashlib_func): + self._hash = hashlib_func + + def new(self): + return self._hash() + + # replace validate_rrsig + dns.dnssec._validate_rrsig = python_validate_rrsig + dns.dnssec.validate_rrsig = python_validate_rrsig + dns.dnssec.validate = dns.dnssec._validate + dns.dnssec._have_ecdsa = True + dns.dnssec.MD5 = PyCryptodomexHashAlike(hashlib.md5) + dns.dnssec.SHA1 = PyCryptodomexHashAlike(hashlib.sha1) + dns.dnssec.SHA256 = PyCryptodomexHashAlike(hashlib.sha256) + dns.dnssec.SHA384 = PyCryptodomexHashAlike(hashlib.sha384) + dns.dnssec.SHA512 = PyCryptodomexHashAlike(hashlib.sha512) + from .printerror import print_error # hard-coded trust anchors (root KSKs) diff --git a/electrumabc/ecc_fast.py b/electrumabc/ecc_fast.py index 2a67cd898685..241b581645d6 100644 --- a/electrumabc/ecc_fast.py +++ b/electrumabc/ecc_fast.py @@ -68,17 +68,17 @@ def mul(self: ecdsa.ellipticcurve.Point, other: int): 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): + 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) + return _patched_functions.orig_sign(self, hash, random_k) # might not be int but might rather be gmpy2 'mpz' type - maybe_mpz = type(hash_) + maybe_mpz = type(hash) secret_exponent = self.secret_multiplier nonce_function = None sig = create_string_buffer(64) - sig_hash_bytes = int(hash_).to_bytes(32, byteorder="big") + sig_hash_bytes = int(hash).to_bytes(32, byteorder="big") secp256k1.secp256k1.secp256k1_ecdsa_sign( secp256k1.secp256k1.ctx, sig, @@ -96,11 +96,11 @@ def sign(self: ecdsa.ecdsa.Private_key, hash_: int, random_k: int): return ecdsa.ecdsa.Signature(maybe_mpz(r), maybe_mpz(s)) def verify( - self: ecdsa.ecdsa.Public_key, hash_: int, signature: ecdsa.ecdsa.Signature + 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) + return _patched_functions.orig_verify(self, hash, signature) sig = create_string_buffer(64) input64 = int(signature.r).to_bytes(32, byteorder="big") + int( signature.s @@ -129,7 +129,7 @@ def verify( return 1 == secp256k1.secp256k1.secp256k1_ecdsa_verify( secp256k1.secp256k1.ctx, sig, - int(hash_).to_bytes(32, byteorder="big"), + int(hash).to_bytes(32, byteorder="big"), pubkey, ) diff --git a/electrumabc/exchange_rate.py b/electrumabc/exchange_rate.py index 461d578ef3b8..f2c78c4cc884 100644 --- a/electrumabc/exchange_rate.py +++ b/electrumabc/exchange_rate.py @@ -233,7 +233,7 @@ def get_rates(self, ccy): "/api/v3/coins/ecash?localization=False&sparkline=false", ) prices = json_data["market_data"]["current_price"] - return {a[0].upper(): PyDecimal(a[1]) for a in prices.items()} + return dict([(a[0].upper(), PyDecimal(a[1])) for a in prices.items()]) def history_ccys(self): return [ diff --git a/electrumabc/interface.py b/electrumabc/interface.py index cb3d2a52c621..1025dbfc8032 100644 --- a/electrumabc/interface.py +++ b/electrumabc/interface.py @@ -38,7 +38,6 @@ from pathvalidate import sanitize_filename from . import pem, util, x509 -from .json_util import JSONSocketPipe from .printerror import PrintError, is_verbose, print_error, print_msg from .utils import Event @@ -352,7 +351,7 @@ def __init__(self, server, socket, *, max_message_bytes=0, config=None): self.host, self.port, _ = server.rsplit(":", 2) self.socket = socket - self.pipe = JSONSocketPipe(socket, max_message_bytes=max_message_bytes) + self.pipe = util.JSONSocketPipe(socket, max_message_bytes=max_message_bytes) # Dump network messages. Set at runtime from the console. self.debug = False self.request_time = time.time() @@ -407,14 +406,12 @@ def get_req_throttle_params(cls, config): return tup @classmethod - def set_req_throttle_params( - cls, config, max_unanswered_requests=None, chunkSize=None - ): + def set_req_throttle_params(cls, config, max=None, chunkSize=None): if not config: return l_ = list(cls.get_req_throttle_params(config)) - if max_unanswered_requests is not None: - l_[0] = max_unanswered_requests + if max is not None: + l_[0] = max if chunkSize is not None: l_[1] = chunkSize config.set_key("network_unanswered_requests_throttle", l_) diff --git a/electrumabc/invoice.py b/electrumabc/invoice.py index 2823201ec9b7..1fdc300555f2 100644 --- a/electrumabc/invoice.py +++ b/electrumabc/invoice.py @@ -128,7 +128,7 @@ def from_dict(cls, data: dict) -> Invoice: @classmethod def from_file(cls, filename: str) -> Invoice: - with open(filename, "r", encoding="utf-8") as f: + with open(filename, "r") as f: data = json.load(f) return Invoice.from_dict(data) diff --git a/electrumabc/json_db.py b/electrumabc/json_db.py index 3301a1b1bdbc..6f2c6aba6b2a 100644 --- a/electrumabc/json_db.py +++ b/electrumabc/json_db.py @@ -28,9 +28,8 @@ import json import threading -from . import bitcoin +from . import bitcoin, util from .address import Address -from .json_util import MyEncoder from .keystore import bip44_derivation_btc from .printerror import PrintError from .util import WalletFileException, multisig_type, profiler @@ -54,6 +53,11 @@ def __init__(self, raw, *, manual_upgrades): else: self.put("seed_version", FINAL_SEED_VERSION) + self.output_pretty_json: bool = True + + def set_output_pretty_json(self, flag: bool): + self.output_pretty_json = flag + def set_modified(self, b): with self.lock: self._modified = b @@ -88,8 +92,8 @@ def get(self, key, default=None): @modifier def put(self, key, value) -> bool: try: - json.dumps(key, cls=MyEncoder) - json.dumps(value, cls=MyEncoder) + json.dumps(key, cls=util.MyEncoder) + json.dumps(value, cls=util.MyEncoder) except Exception: self.print_error(f"json error: cannot save {repr(key)} ({repr(value)})") return False @@ -109,11 +113,9 @@ def commit(self): def dump(self): return json.dumps( self.data, - indent=None, - sort_keys=False, - # no whitespace in separators - separators=(",", ":"), - cls=MyEncoder, + indent=4 if self.output_pretty_json else None, + sort_keys=self.output_pretty_json, + cls=util.MyEncoder, ) def load_data(self, s): @@ -146,7 +148,7 @@ def load_data(self, s): if self.requires_upgrade(): self.upgrade() - def requires_split(self) -> bool: + def requires_split(self): d = self.get("accounts", {}) return len(d) > 1 @@ -198,7 +200,7 @@ def split_accounts(self): ) return result - def requires_upgrade(self) -> bool: + def requires_upgrade(self): return self.get_seed_version() < FINAL_SEED_VERSION @profiler @@ -335,7 +337,7 @@ def convert_version_14(self): if self.get("wallet_type") == "imported": addresses = self.get("addresses") if type(addresses) is list: - addresses = {x: None for x in addresses} + addresses = dict([(x, None) for x in addresses]) self.put("addresses", addresses) elif self.get("wallet_type") == "standard": if self.get("keystore").get("type") == "imported": @@ -387,7 +389,7 @@ def remove_from_list(list_name): if self.get("wallet_type") == "imported": addresses = self.get("addresses") assert isinstance(addresses, dict) - addresses_new = {} + addresses_new = dict() for address, details in addresses.items(): if not Address.is_valid(address): remove_address(address) diff --git a/electrumabc/mnemo.py b/electrumabc/mnemo.py index cc2995d90638..789c44e78639 100644 --- a/electrumabc/mnemo.py +++ b/electrumabc/mnemo.py @@ -287,7 +287,7 @@ def __init__(self, lang=None): self.print_error("loading wordlist for:", lang) filename = filenames[lang] self.data.words = tuple(load_wordlist(filename)) - self.data.word_indices = {} + self.data.word_indices = dict() for i, word in enumerate(self.data.words): self.data.word_indices[word] = ( i # saves on O(N) lookups for words. The alternative is to call wordlist.index(w) for each word which is slow. diff --git a/electrumabc/network.py b/electrumabc/network.py index 102a995e4e9b..85f017c8f5c7 100644 --- a/electrumabc/network.py +++ b/electrumabc/network.py @@ -58,15 +58,6 @@ # start on the wrong chain. DEFAULT_WHITELIST_SERVERS_ONLY = True -# Rate-limit the get_merkle requests sent to fulcrum servers. -# This affects the speed of the initial verification of transactions in a wallet with -# a large transaction history. Values up to 10000 have been successfully tested for a -# single client, and the fulcrum developer thinks it is safe to raise it from its -# previous value (10). -# If no issues are encountered after this change has been released, it should be safe -# to increase it more (to 500 or 1000) in future releases. -MAX_QLEN_GET_MERKLE_REQUESTS = 100 - def parse_servers(result): """parse servers list into dict format""" @@ -140,7 +131,7 @@ def servers_to_hostmap(servers): """Takes an iterable of HOST:PORT:PROTOCOL strings and breaks them into a hostmap dict of host -> { protocol : port } suitable to be passed to pick_random_server() and get_eligible_servers() above.""" - ret = {} + ret = dict() for s in servers: try: host, port, protocol = deserialize_server(s) @@ -153,7 +144,7 @@ def servers_to_hostmap(servers): ) # deserialization error continue - m = ret.get(host, {}) + m = ret.get(host, dict()) need_add = len(m) == 0 m[protocol] = port if need_add: @@ -298,7 +289,7 @@ def __init__(self, config: Optional[SimpleConfig] = None): ) ) self.default_server = self.get_config_server() - self.bad_certificate_servers: Dict[str, str] = {} + self.bad_certificate_servers: Dict[str, str] = dict() self.server_list_updated = Event() self.tor_controller = TorController(self.config) @@ -692,7 +683,7 @@ def start_network(self, protocol, proxy): assert not self.interface and not self.interfaces assert not self.connecting and self.socket_queue.empty() self.print_error("starting network") - self.disconnected_servers = set() + self.disconnected_servers = set([]) self.protocol = protocol self.set_proxy(proxy) self.start_interfaces() @@ -785,11 +776,14 @@ def switch_lagging_interface(self): if self.server_is_lagging() and self.auto_connect: # switch to one that has the correct header (not height) header = self.blockchain().read_header(self.get_local_height()) - filtered = [ - server_key - for (server_key, interface) in self.interfaces.items() - if interface.tip_header == header - ] + filtered = list( + map( + lambda x: x[0], + filter( + lambda x: x[1].tip_header == header, self.interfaces.items() + ), + ) + ) if filtered: choice = random.choice(filtered) self.switch_to_interface(choice, self.SWITCH_LAGGING) @@ -1205,7 +1199,7 @@ def maintain_sockets(self): self.start_random_interface() if now - self.nodes_retry_time > self.NODES_RETRY_INTERVAL: self.print_error("network: retrying connections") - self.disconnected_servers = set() + self.disconnected_servers = set([]) self.nodes_retry_time = now # main interface @@ -2327,7 +2321,7 @@ def transmogrify_broadcast_response_for_gui(server_msg): return _("An error occurred broadcasting the transaction") # Used by the verifier job. - def get_merkle_for_transaction(self, tx_hash, tx_height, callback): + def get_merkle_for_transaction(self, tx_hash, tx_height, callback, max_qlen=10): """Asynchronously enqueue a request for a merkle proof for a tx. Note that the callback param is required. May return None if too many requests were enqueued (max_qlen) or @@ -2337,7 +2331,7 @@ def get_merkle_for_transaction(self, tx_hash, tx_height, callback): "blockchain.transaction.get_merkle", [tx_hash, tx_height], callback=callback, - max_qlen=MAX_QLEN_GET_MERKLE_REQUESTS, + max_qlen=max_qlen, ) def get_proxies(self): diff --git a/electrumabc/paymentrequest.py b/electrumabc/paymentrequest.py index 8fceeaf77f13..c5a35818a9b7 100644 --- a/electrumabc/paymentrequest.py +++ b/electrumabc/paymentrequest.py @@ -270,7 +270,7 @@ def get_expiration_date(self): return self.details.expires def get_amount(self): - return sum(x.value for x in self.outputs) + return sum(map(lambda x: x[2], self.outputs)) def get_address(self) -> str: o = self.outputs[0] diff --git a/electrumabc/pem.py b/electrumabc/pem.py index de27f157056c..0b118e00dbaa 100644 --- a/electrumabc/pem.py +++ b/electrumabc/pem.py @@ -146,17 +146,17 @@ def pemSniff(inStr, name): def parse_private_key(s): """Parse a string containing a PEM-encoded .""" if pemSniff(s, "PRIVATE KEY"): - key = dePem(s, "PRIVATE KEY") - return _parsePKCS8(key) + bytes = dePem(s, "PRIVATE KEY") + return _parsePKCS8(bytes) elif pemSniff(s, "RSA PRIVATE KEY"): - key = dePem(s, "RSA PRIVATE KEY") - return _parseSSLeay(key) + bytes = dePem(s, "RSA PRIVATE KEY") + return _parseSSLeay(bytes) else: raise SyntaxError("Not a PEM private key file") -def _parsePKCS8(key): - s = ASN1Node(key) +def _parsePKCS8(_bytes): + s = ASN1Node(_bytes) root = s.root() version_node = s.first_child(root) version = bytestr_to_int(s.get_value_of_type(version_node, "INTEGER")) @@ -172,8 +172,8 @@ def _parsePKCS8(key): return _parseASN1PrivateKey(value) -def _parseSSLeay(key): - return _parseASN1PrivateKey(ASN1Node(key)) +def _parseSSLeay(bytes): + return _parseASN1PrivateKey(ASN1Node(bytes)) def bytesToNumber(s): @@ -195,7 +195,9 @@ def _parseASN1PrivateKey(s): dP = s.next_node(q) dQ = s.next_node(dP) qInv = s.next_node(dQ) - return [ - bytesToNumber(s.get_value_of_type(x, "INTEGER")) - for x in [n, e, d, p, q, dP, dQ, qInv] - ] + return list( + map( + lambda x: bytesToNumber(s.get_value_of_type(x, "INTEGER")), + [n, e, d, p, q, dP, dQ, qInv], + ) + ) diff --git a/electrumabc/plugins.py b/electrumabc/plugins.py index 206fe0a5db4c..c1a8c3d069ea 100644 --- a/electrumabc/plugins.py +++ b/electrumabc/plugins.py @@ -731,11 +731,11 @@ def __init__(self, parent, config, name): self._hooks_i_registered.append((aname, func)) # collect names of all class attributes with ._is_daemon_command - self._daemon_commands = { + self._daemon_commands = set( attrname for attrname in dir(type(self)) if getattr(getattr(type(self), attrname), "_is_daemon_command", False) - } + ) # we don't allow conflicting definitions of daemon command (between different plugins) for c in self._daemon_commands.intersection(self.parent.daemon_commands): self._daemon_commands.discard(c) diff --git a/electrumabc/ripemd.py b/electrumabc/ripemd.py new file mode 100644 index 000000000000..000f0d986242 --- /dev/null +++ b/electrumabc/ripemd.py @@ -0,0 +1,418 @@ +# ripemd.py - pure Python implementation of the RIPEMD-160 algorithm. +# Bjorn Edstrom 16 december 2007. +# +# Copyrights +# ========== +# +# This code is a derived from an implementation by Markus Friedl which is +# subject to the following license. This Python implementation is not +# subject to any other license. +# +# +# Copyright (c) 2001 Markus Friedl. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# +# Preneel, Bosselaers, Dobbertin, "The Cryptographic Hash Function RIPEMD-160", +# RSA Laboratories, CryptoBytes, Volume 3, Number 2, Autumn 1997, +# ftp://ftp.rsasecurity.com/pub/cryptobytes/crypto3n2.pdf + +import struct +import sys + +# block_size = 1 +digest_size = 20 +digestsize = 20 + + +class RIPEMD160: + """Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed.""" + + def __init__(self, arg=None): + self.ctx = RMDContext() + if arg: + self.update(arg) + self.dig = None + + def update(self, arg): + """update(arg)""" + RMD160Update(self.ctx, arg, len(arg)) + self.dig = None + + def digest(self) -> bytes: + """digest()""" + if self.dig: + return self.dig + ctx = self.ctx.copy() + self.dig = RMD160Final(self.ctx) + self.ctx = ctx + return self.dig + + def hexdigest(self): + """hexdigest()""" + dig = self.digest() + hex_digest = "" + for d in dig: + hex_digest += "%02x" % d + return hex_digest + + def copy(self): + """copy()""" + import copy + + return copy.deepcopy(self) + + +def new(arg=None): + """Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed.""" + return RIPEMD160(arg) + + +# +# Private. +# + + +class RMDContext: + def __init__(self): + self.state = [ + 0x67452301, + 0xEFCDAB89, + 0x98BADCFE, + 0x10325476, + 0xC3D2E1F0, + ] # uint32 + self.count = 0 # uint64 + self.buffer = [0] * 64 # uchar + + def copy(self): + ctx = RMDContext() + ctx.state = self.state[:] + ctx.count = self.count + ctx.buffer = self.buffer[:] + return ctx + + +K0 = 0x00000000 +K1 = 0x5A827999 +K2 = 0x6ED9EBA1 +K3 = 0x8F1BBCDC +K4 = 0xA953FD4E + +KK0 = 0x50A28BE6 +KK1 = 0x5C4DD124 +KK2 = 0x6D703EF3 +KK3 = 0x7A6D76E9 +KK4 = 0x00000000 + + +def ROL(n, x): + return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n)) + + +def F0(x, y, z): + return x ^ y ^ z + + +def F1(x, y, z): + return (x & y) | (((~x) % 0x100000000) & z) + + +def F2(x, y, z): + return (x | ((~y) % 0x100000000)) ^ z + + +def F3(x, y, z): + return (x & z) | (((~z) % 0x100000000) & y) + + +def F4(x, y, z): + return x ^ (y | ((~z) % 0x100000000)) + + +def R(a, b, c, d, e, Fj, Kj, sj, rj, X): + a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e + c = ROL(10, c) + return a % 0x100000000, c + + +PADDING = [0x80] + [0] * 63 + + +def RMD160Transform(state, block): # uint32 state[5], uchar block[64] + x = [0] * 16 + if sys.byteorder == "little": + x = struct.unpack("<16L", bytes([x for x in block[0:64]])) + else: + raise RuntimeError("Error!!") + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + # /* Round 1 */ + a, c = R(a, b, c, d, e, F0, K0, 11, 0, x) + e, b = R(e, a, b, c, d, F0, K0, 14, 1, x) + d, a = R(d, e, a, b, c, F0, K0, 15, 2, x) + c, e = R(c, d, e, a, b, F0, K0, 12, 3, x) + b, d = R(b, c, d, e, a, F0, K0, 5, 4, x) + a, c = R(a, b, c, d, e, F0, K0, 8, 5, x) + e, b = R(e, a, b, c, d, F0, K0, 7, 6, x) + d, a = R(d, e, a, b, c, F0, K0, 9, 7, x) + c, e = R(c, d, e, a, b, F0, K0, 11, 8, x) + b, d = R(b, c, d, e, a, F0, K0, 13, 9, x) + a, c = R(a, b, c, d, e, F0, K0, 14, 10, x) + e, b = R(e, a, b, c, d, F0, K0, 15, 11, x) + d, a = R(d, e, a, b, c, F0, K0, 6, 12, x) + c, e = R(c, d, e, a, b, F0, K0, 7, 13, x) + b, d = R(b, c, d, e, a, F0, K0, 9, 14, x) + a, c = R(a, b, c, d, e, F0, K0, 8, 15, x) + # /* #15 */ + # /* Round 2 */ + e, b = R(e, a, b, c, d, F1, K1, 7, 7, x) + d, a = R(d, e, a, b, c, F1, K1, 6, 4, x) + c, e = R(c, d, e, a, b, F1, K1, 8, 13, x) + b, d = R(b, c, d, e, a, F1, K1, 13, 1, x) + a, c = R(a, b, c, d, e, F1, K1, 11, 10, x) + e, b = R(e, a, b, c, d, F1, K1, 9, 6, x) + d, a = R(d, e, a, b, c, F1, K1, 7, 15, x) + c, e = R(c, d, e, a, b, F1, K1, 15, 3, x) + b, d = R(b, c, d, e, a, F1, K1, 7, 12, x) + a, c = R(a, b, c, d, e, F1, K1, 12, 0, x) + e, b = R(e, a, b, c, d, F1, K1, 15, 9, x) + d, a = R(d, e, a, b, c, F1, K1, 9, 5, x) + c, e = R(c, d, e, a, b, F1, K1, 11, 2, x) + b, d = R(b, c, d, e, a, F1, K1, 7, 14, x) + a, c = R(a, b, c, d, e, F1, K1, 13, 11, x) + e, b = R(e, a, b, c, d, F1, K1, 12, 8, x) + # /* #31 */ + # /* Round 3 */ + d, a = R(d, e, a, b, c, F2, K2, 11, 3, x) + c, e = R(c, d, e, a, b, F2, K2, 13, 10, x) + b, d = R(b, c, d, e, a, F2, K2, 6, 14, x) + a, c = R(a, b, c, d, e, F2, K2, 7, 4, x) + e, b = R(e, a, b, c, d, F2, K2, 14, 9, x) + d, a = R(d, e, a, b, c, F2, K2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, K2, 13, 8, x) + b, d = R(b, c, d, e, a, F2, K2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, K2, 14, 2, x) + e, b = R(e, a, b, c, d, F2, K2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, K2, 13, 0, x) + c, e = R(c, d, e, a, b, F2, K2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, K2, 5, 13, x) + a, c = R(a, b, c, d, e, F2, K2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, K2, 7, 5, x) + d, a = R(d, e, a, b, c, F2, K2, 5, 12, x) + # /* #47 */ + # /* Round 4 */ + c, e = R(c, d, e, a, b, F3, K3, 11, 1, x) + b, d = R(b, c, d, e, a, F3, K3, 12, 9, x) + a, c = R(a, b, c, d, e, F3, K3, 14, 11, x) + e, b = R(e, a, b, c, d, F3, K3, 15, 10, x) + d, a = R(d, e, a, b, c, F3, K3, 14, 0, x) + c, e = R(c, d, e, a, b, F3, K3, 15, 8, x) + b, d = R(b, c, d, e, a, F3, K3, 9, 12, x) + a, c = R(a, b, c, d, e, F3, K3, 8, 4, x) + e, b = R(e, a, b, c, d, F3, K3, 9, 13, x) + d, a = R(d, e, a, b, c, F3, K3, 14, 3, x) + c, e = R(c, d, e, a, b, F3, K3, 5, 7, x) + b, d = R(b, c, d, e, a, F3, K3, 6, 15, x) + a, c = R(a, b, c, d, e, F3, K3, 8, 14, x) + e, b = R(e, a, b, c, d, F3, K3, 6, 5, x) + d, a = R(d, e, a, b, c, F3, K3, 5, 6, x) + c, e = R(c, d, e, a, b, F3, K3, 12, 2, x) + # /* #63 */ + # /* Round 5 */ + b, d = R(b, c, d, e, a, F4, K4, 9, 4, x) + a, c = R(a, b, c, d, e, F4, K4, 15, 0, x) + e, b = R(e, a, b, c, d, F4, K4, 5, 5, x) + d, a = R(d, e, a, b, c, F4, K4, 11, 9, x) + c, e = R(c, d, e, a, b, F4, K4, 6, 7, x) + b, d = R(b, c, d, e, a, F4, K4, 8, 12, x) + a, c = R(a, b, c, d, e, F4, K4, 13, 2, x) + e, b = R(e, a, b, c, d, F4, K4, 12, 10, x) + d, a = R(d, e, a, b, c, F4, K4, 5, 14, x) + c, e = R(c, d, e, a, b, F4, K4, 12, 1, x) + b, d = R(b, c, d, e, a, F4, K4, 13, 3, x) + a, c = R(a, b, c, d, e, F4, K4, 14, 8, x) + e, b = R(e, a, b, c, d, F4, K4, 11, 11, x) + d, a = R(d, e, a, b, c, F4, K4, 8, 6, x) + c, e = R(c, d, e, a, b, F4, K4, 5, 15, x) + b, d = R(b, c, d, e, a, F4, K4, 6, 13, x) + # /* #79 */ + + aa = a + bb = b + cc = c + dd = d + ee = e + + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + # /* Parallel round 1 */ + a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) + e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) + d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) + c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) + b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) + a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) + e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) + d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) + c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) + b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) + a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) + e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) + d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) + c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) + b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) + a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) # /* #15 */ + # /* Parallel round 2 */ + e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) + d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) + c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) + a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) + e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) + d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) + c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) + a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) + e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) + d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) + c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) + b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) + a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) + e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) # /* #31 */ + # /* Parallel round 3 */ + d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) + b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) + e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) + c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) + a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) + c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) + b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) + a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) + e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) # /* #47 */ + # /* Parallel round 4 */ + c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) + b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) + a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) + e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) + d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) + c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) + b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) + a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) + e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) + d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) + c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) + b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) + a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) + e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) + d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) + c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) # /* #63 */ + # /* Parallel round 5 */ + b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) + e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) + d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) + c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) + b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) + a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) + e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) + d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) + c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) + b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) + e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) + d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) + c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) + b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) # /* #79 */ + + t = (state[1] + cc + d) % 0x100000000 + state[1] = (state[2] + dd + e) % 0x100000000 + state[2] = (state[3] + ee + a) % 0x100000000 + state[3] = (state[4] + aa + b) % 0x100000000 + state[4] = (state[0] + bb + c) % 0x100000000 + state[0] = t % 0x100000000 + + pass + + +def RMD160Update(ctx, inp, inplen): + if type(inp) == str: + inp = [ord(i) & 0xFF for i in inp] + + have = (ctx.count // 8) % 64 + need = 64 - have + ctx.count += 8 * inplen + off = 0 + if inplen >= need: + if have: + for i in range(need): + ctx.buffer[have + i] = inp[i] + RMD160Transform(ctx.state, ctx.buffer) + off = need + have = 0 + while off + 64 <= inplen: + RMD160Transform(ctx.state, inp[off:]) # <--- + off += 64 + if off < inplen: + # memcpy(ctx->buffer + have, input+off, len-off); + for i in range(inplen - off): + ctx.buffer[have + i] = inp[off + i] + + +def RMD160Final(ctx: RMDContext) -> bytes: + size = struct.pack("= low and n < high: return n @@ -331,7 +331,24 @@ def __len__(self): def hasPrivateKey(self): return self.d != 0 - def hashAndVerify(self, sigBytes, data): + def hashAndSign(self, bytes): + """Hash and sign the passed-in bytes. + + This requires the key to have a private component. It performs + a PKCS1-SHA1 signature on the passed-in data. + + @type bytes: str or L{bytearray} of unsigned bytes + @param bytes: The value which will be hashed and signed. + + @rtype: L{bytearray} of unsigned bytes. + @return: A PKCS1-SHA1 signature on the passed-in data. + """ + hashBytes = SHA1(bytearray(bytes)) + prefixedHashBytes = self._addPKCS1SHA1Prefix(hashBytes) + sigBytes = self.sign(prefixedHashBytes) + return sigBytes + + def hashAndVerify(self, sigBytes, bytes): """Hash and verify the passed-in bytes with the signature. This verifies a PKCS1-SHA1 signature on the passed-in data. @@ -339,13 +356,13 @@ def hashAndVerify(self, sigBytes, data): @type sigBytes: L{bytearray} of unsigned bytes @param sigBytes: A PKCS1-SHA1 signature. - @type data: str or L{bytearray} of unsigned bytes - @param data: The value which will be hashed and verified. + @type bytes: str or L{bytearray} of unsigned bytes + @param bytes: The value which will be hashed and verified. @rtype: bool @return: Whether the signature matches the passed-in data. """ - hashBytes = SHA1(bytearray(data)) + hashBytes = SHA1(bytearray(bytes)) # Try it with/without the embedded NULL prefixedHashBytes1 = self._addPKCS1SHA1Prefix(hashBytes, False) @@ -354,21 +371,21 @@ def hashAndVerify(self, sigBytes, data): result2 = self.verify(sigBytes, prefixedHashBytes2) return result1 or result2 - def sign(self, data): + def sign(self, bytes): """Sign the passed-in bytes. This requires the key to have a private component. It performs a PKCS1 signature on the passed-in data. - @type data: L{bytearray} of unsigned bytes - @param data: The value which will be signed. + @type bytes: L{bytearray} of unsigned bytes + @param bytes: The value which will be signed. @rtype: L{bytearray} of unsigned bytes. @return: A PKCS1 signature on the passed-in data. """ if not self.hasPrivateKey(): raise AssertionError() - paddedBytes = self._addPKCS1Padding(data, 1) + paddedBytes = self._addPKCS1Padding(bytes, 1) m = bytesToNumber(paddedBytes) if m >= self.n: raise ValueError() @@ -376,7 +393,7 @@ def sign(self, data): sigBytes = numberToByteArray(c, numBytes(self.n)) return sigBytes - def verify(self, sigBytes, data): + def verify(self, sigBytes, bytes): """Verify the passed-in bytes with the signature. This verifies a PKCS1 signature on the passed-in data. @@ -384,15 +401,15 @@ def verify(self, sigBytes, data): @type sigBytes: L{bytearray} of unsigned bytes @param sigBytes: A PKCS1 signature. - @type data: L{bytearray} of unsigned bytes - @param data: The value which will be verified. + @type bytes: L{bytearray} of unsigned bytes + @param bytes: The value which will be verified. @rtype: bool @return: Whether the signature matches the passed-in data. """ if len(sigBytes) != numBytes(self.n): return False - paddedBytes = self._addPKCS1Padding(data, 1) + paddedBytes = self._addPKCS1Padding(bytes, 1) c = bytesToNumber(sigBytes) if c >= self.n: return False @@ -400,18 +417,18 @@ def verify(self, sigBytes, data): checkBytes = numberToByteArray(m, numBytes(self.n)) return checkBytes == paddedBytes - def encrypt(self, data): + def encrypt(self, bytes): """Encrypt the passed-in bytes. This performs PKCS1 encryption of the passed-in data. - @type data: L{bytearray} of unsigned bytes - @param data: The value which will be encrypted. + @type bytes: L{bytearray} of unsigned bytes + @param bytes: The value which will be encrypted. @rtype: L{bytearray} of unsigned bytes. @return: A PKCS1 encryption of the passed-in data. """ - paddedBytes = self._addPKCS1Padding(data, 2) + paddedBytes = self._addPKCS1Padding(bytes, 2) m = bytesToNumber(paddedBytes) if m >= self.n: raise ValueError() @@ -456,7 +473,7 @@ def decrypt(self, encBytes): # Helper Functions for RSA Keys # ************************************************************************** - def _addPKCS1SHA1Prefix(self, data, withNULL=True): + def _addPKCS1SHA1Prefix(self, bytes, withNULL=True): # There is a long history of confusion over whether the SHA1 # algorithmIdentifier should be encoded with a NULL parameter or # with the parameter omitted. While the original intention was @@ -504,10 +521,11 @@ def _addPKCS1SHA1Prefix(self, data, withNULL=True): 0x14, ] ) - return prefixBytes + data + prefixedBytes = prefixBytes + bytes + return prefixedBytes - def _addPKCS1Padding(self, data, blockType): - padLength = numBytes(self.n) - (len(data) + 3) + def _addPKCS1Padding(self, bytes, blockType): + padLength = numBytes(self.n) - (len(bytes) + 3) if blockType == 1: # Signature padding pad = [0xFF] * padLength elif blockType == 2: # Encryption padding @@ -520,7 +538,8 @@ def _addPKCS1Padding(self, data, blockType): raise AssertionError() padding = bytearray([0, blockType] + pad + [0]) - return padding + data + paddedBytes = padding + bytes + return paddedBytes def _rawPrivateKeyOp(self, m): # Create blinding values, on the first pass: diff --git a/electrumabc/slp/slp.py b/electrumabc/slp/slp.py index 2aec35c6d8b5..474ac02897d3 100644 --- a/electrumabc/slp/slp.py +++ b/electrumabc/slp/slp.py @@ -765,7 +765,7 @@ def load(self) -> bool: for k, v in data["token_quantities"].items() } # build the mapping of prevouthash:n (str) -> token_id_hex (str) from self.token_quantities - self.txo_token_id = {} + self.txo_token_id = dict() for token_id_hex, txo_dict in self.token_quantities.items(): for txo in txo_dict: self.txo_token_id[txo] = token_id_hex @@ -800,7 +800,7 @@ def save(self): data = { "validity": self.validity, "token_quantities": { - k: [[v0, v1] for v0, v1 in v.items()] + k: list([v0, v1] for v0, v1 in v.items()) for k, v in self.token_quantities.items() }, "txo_byaddr": { @@ -815,17 +815,17 @@ def clear(self): self.need_rebuild = False # txid -> int - self.validity = {} + self.validity = dict() # [address] -> set of "prevouthash:n" for that address - self.txo_byaddr = {} + self.txo_byaddr = dict() # [token_id_hex] -> dict of ["prevouthash:n"] -> qty (-1 for qty indicates # minting baton) - self.token_quantities = {} + self.token_quantities = dict() # ["prevouthash:n"] -> "token_id_hex" - self.txo_token_id = {} + self.txo_token_id = dict() def rebuild(self): """This takes wallet.lock""" @@ -949,7 +949,7 @@ def add_tx(self, txid, tx): def _add_token_qty(self, token_id_hex, txo_name, qty): """No checks are done for address, etc. qty is just faithfully added for a given token/txo_name combo.""" - d = self.token_quantities.get(token_id_hex, {}) + d = self.token_quantities.get(token_id_hex, dict()) need_insert = not d d[txo_name] = qty # NB: negative quantity indicates mint baton if need_insert: diff --git a/electrumabc/storage.py b/electrumabc/storage.py index 13aa60b82e75..6c46471ddfe2 100644 --- a/electrumabc/storage.py +++ b/electrumabc/storage.py @@ -194,7 +194,7 @@ def encrypt_before_writing(self, plaintext: str) -> str: s = plaintext if self.pubkey: s = bytes(s, "utf8") - c = zlib.compress(s, level=zlib.Z_BEST_SPEED) + c = zlib.compress(s) enc_magic = self._get_encryption_magic() s = bitcoin.encrypt_message(c, self.pubkey, enc_magic) s = s.decode("utf8") @@ -218,20 +218,26 @@ def set_password(self, password, enc_version=None): ec_key = self.get_key(password) self.pubkey = ec_key.get_public_key() self._encryption_version = enc_version + # Encrypted wallets are not human readable, so we can gain some performance + # by writing compact JSON. + self.db.set_output_pretty_json(False) else: self.pubkey = None self._encryption_version = STO_EV_PLAINTEXT + self.db.set_output_pretty_json(True) + # make sure next storage.write() saves changes + self.db.set_modified(True) - def requires_upgrade(self) -> bool: + def requires_upgrade(self): if not self.is_past_initial_decryption(): raise Exception("storage not yet decrypted!") - return self.db.requires_upgrade() + self.db.requires_upgrade() def upgrade(self): self.db.upgrade() self.write() - def requires_split(self) -> bool: + def requires_split(self): return self.db.requires_split() def split_accounts(self): diff --git a/electrumabc/synchronizer.py b/electrumabc/synchronizer.py index 0a62589fd96a..ed287c1ccb33 100644 --- a/electrumabc/synchronizer.py +++ b/electrumabc/synchronizer.py @@ -62,11 +62,11 @@ def __init__(self, wallet: AbstractWallet, network): self._need_release = False self.new_addresses: Set[Address] = set() - self.new_addresses_for_change: Dict[Address, type(None)] = {} + self.new_addresses_for_change: Dict[Address, type(None)] = dict() """Basically, an ordered set of Addresses (this assumes that dictionaries are ordered, so Python > 3.6) """ - self.requested_tx: Dict[str, int] = {} + self.requested_tx: Dict[str, int] = dict() """Mapping of tx_hash -> tx_height""" self.requested_tx_by_sh: DefaultDict[str, Set[str]] = defaultdict(set) """Mapping of scripthash -> set of requested tx_hashes""" @@ -303,8 +303,8 @@ def _on_address_history(self, response): self.print_error("receiving history {} {}".format(addr, len(result))) # Remove request; this allows up_to_date to be True server_status = self.requested_histories.pop(scripthash) - hashes = {item["tx_hash"] for item in result} - hist = [(item["tx_hash"], item["height"]) for item in result] + hashes = set(map(lambda item: item["tx_hash"], result)) + hist = list(map(lambda item: (item["tx_hash"], item["height"]), result)) # tx_fees tx_fees = [(item["tx_hash"], item.get("fee")) for item in result] tx_fees = dict(filter(lambda x: x[1] is not None, tx_fees)) @@ -438,7 +438,7 @@ def _pop_new_addresses(self) -> Tuple[Set[Address], Iterable[Address]]: if not self.limit_change_subs: # Pop all queued change addrs addresses_for_change = self.new_addresses_for_change.keys() - self.new_addresses_for_change = {} + self.new_addresses_for_change = dict() else: # Change address subs limiting in place, only grab first self.limit_change_subs new change addresses addresses_for_change = list(self.new_addresses_for_change.keys())[ diff --git a/electrumabc/tests/__main__.py b/electrumabc/tests/__main__.py new file mode 100644 index 000000000000..c4345d4b6cd9 --- /dev/null +++ b/electrumabc/tests/__main__.py @@ -0,0 +1,58 @@ +import unittest + +from .test_address import TestAddressFromString +from .test_asert import TestASERTDaa +from .test_avalanche import suite as test_avalanche_suite +from .test_bip44_derivation_path import TestBip44Derivations +from .test_bitcoin import suite as test_bitcoin_suite +from .test_blockchain import TestBlockchain +from .test_cashaddrenc import TestCashAddrAddress +from .test_commands import suite as test_commands_suite +from .test_consolidate import suite as test_consolidate_suite +from .test_dnssec import TestDnsSec +from .test_interface import TestInterface +from .test_invoice import TestInvoice +from .test_mnemonic import suite as test_mnemonic_suite +from .test_paymentrequests import TestPaymentRequests +from .test_schnorr import suite as test_schnorr_suite +from .test_simple_config import suite as test_simple_config_suite +from .test_slp import SLPTests +from .test_storage_upgrade import TestStorageUpgrade +from .test_transaction import suite as test_transaction_suite +from .test_uint256 import suite as test_uint256_suite +from .test_util import suite as test_util_suite +from .test_wallet import suite as test_wallet_suite +from .test_wallet_vertical import TestWalletKeystoreAddressIntegrity + + +def suite(): + test_suite = unittest.TestSuite() + loadTests = unittest.defaultTestLoader.loadTestsFromTestCase + test_suite.addTest(loadTests(TestAddressFromString)) + test_suite.addTest(loadTests(TestASERTDaa)) + test_suite.addTest(test_avalanche_suite()) + test_suite.addTest(loadTests(TestBip44Derivations)) + test_suite.addTest(test_bitcoin_suite()) + test_suite.addTest(loadTests(TestBlockchain)) + test_suite.addTest(loadTests(TestCashAddrAddress)) + test_suite.addTest(test_commands_suite()) + test_suite.addTest(test_consolidate_suite()) + test_suite.addTest(loadTests(TestDnsSec)) + test_suite.addTest(loadTests(TestInterface)) + test_suite.addTest(loadTests(TestInvoice)) + test_suite.addTest(test_mnemonic_suite()) + test_suite.addTest(loadTests(TestPaymentRequests)) + test_suite.addTest(test_schnorr_suite()) + test_suite.addTest(test_simple_config_suite()) + test_suite.addTest(loadTests(SLPTests)) + test_suite.addTest(loadTests(TestStorageUpgrade)) + test_suite.addTest(test_transaction_suite()) + test_suite.addTest(test_uint256_suite()) + test_suite.addTest(test_util_suite()) + test_suite.addTest(test_wallet_suite()) + test_suite.addTest(loadTests(TestWalletKeystoreAddressIntegrity)) + return test_suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/electrumabc/tests/regtest/docker-compose.yml b/electrumabc/tests/regtest/docker-compose.yml index cd528a06610d..9dc522a4bf2c 100644 --- a/electrumabc/tests/regtest/docker-compose.yml +++ b/electrumabc/tests/regtest/docker-compose.yml @@ -1,16 +1,16 @@ version: "3" services: bitcoind: - image: bitcoinabc/bitcoin-abc:latest + image: bitcoinabc/bitcoin-abc:0.27.3 command: "bitcoind -conf=/conf/bitcoind.conf" volumes: - - ./configs:/conf + - conf-files:/conf networks: - bitcoin ports: - 18333:18334 fulcrum: - image: cculianu/fulcrum:latest + image: cculianu/fulcrum:v1.9.0 command: "Fulcrum /conf/fulcrum.conf" networks: - bitcoin @@ -21,7 +21,15 @@ services: depends_on: - bitcoind volumes: - - ./configs:/conf + - conf-files:/conf networks: bitcoin: + +volumes: + conf-files: + driver: local + driver_opts: + o: bind + type: none + device: electrumabc/tests/regtest/configs diff --git a/electrumabc/tests/regtest/test_rpc_misc.py b/electrumabc/tests/regtest/test_rpc_misc.py index 2096141bd98b..c2c87d581c9c 100644 --- a/electrumabc/tests/regtest/test_rpc_misc.py +++ b/electrumabc/tests/regtest/test_rpc_misc.py @@ -4,13 +4,12 @@ from jsonrpcclient import request # See https://docs.pytest.org/en/7.1.x/how-to/fixtures.html -from .util import ( # noqa: F401 +from .util import docker_compose_file # noqa: F401 +from .util import fulcrum_service # noqa: F401 +from .util import ( EC_DAEMON_RPC_URL, SUPPORTED_PLATFORM, bitcoind_rpc_connection, - docker_compose_command, - docker_compose_file, - fulcrum_service, poll_for_answer, ) diff --git a/electrumabc/tests/regtest/test_rpc_payment_request.py b/electrumabc/tests/regtest/test_rpc_payment_request.py index 6ef1d615e644..e4c0870d7d98 100644 --- a/electrumabc/tests/regtest/test_rpc_payment_request.py +++ b/electrumabc/tests/regtest/test_rpc_payment_request.py @@ -4,13 +4,12 @@ from jsonrpcclient import request # See https://docs.pytest.org/en/7.1.x/how-to/fixtures.html -from .util import ( # noqa: F401 +from .util import docker_compose_file # noqa: F401 +from .util import fulcrum_service # noqa: F401 +from .util import ( EC_DAEMON_RPC_URL, SUPPORTED_PLATFORM, bitcoind_rpc_connection, - docker_compose_command, - docker_compose_file, - fulcrum_service, poll_for_answer, ) diff --git a/electrumabc/tests/regtest/util.py b/electrumabc/tests/regtest/util.py index 8682a36e12e2..9019ff5aeb6b 100644 --- a/electrumabc/tests/regtest/util.py +++ b/electrumabc/tests/regtest/util.py @@ -6,7 +6,7 @@ import subprocess import tempfile import time -from typing import Any, Generator, Optional +from typing import Any, Generator import pytest import requests @@ -15,6 +15,7 @@ from jsonrpcclient import parse as rpc_parse from jsonrpcclient import request +_datadir = None _bitcoind = None SUPPORTED_PLATFORM = ( @@ -25,16 +26,13 @@ FULCRUM_STATS_URL = "http://localhost:8081/stats" BITCOIND_RPC_URL = "http://user:pass@0.0.0.0:18333" -ELECTRUM_ROOT = os.path.join(os.path.dirname(__file__), "..", "..", "..") -ELECTRUMABC_COMMAND = os.path.join(ELECTRUM_ROOT, "electrum-abc") - def poll_for_answer( url: Any, - json_req: Optional[Any] = None, + json_req: Any | None = None, poll_interval: int = 1, poll_timeout: int = 10, - expected_answer: Optional[Any] = None, + expected_answer: Any | None = None, ) -> Any: """Poll an RPC method until timeout or an expected answer has been received""" start = current = time.time() @@ -87,20 +85,26 @@ def bitcoind_rpc_connection() -> AuthServiceProxy: # Creates a temp directory on disk for wallet storage # Starts a daemon, creates and loads a wallet -def start_ec_daemon(datadir: str) -> None: +def start_ec_daemon() -> None: """ Creates a temp directory on disk for wallet storage Starts a daemon, creates and loads a wallet """ - default_wallet = os.path.join(datadir, "default_wallet") + if _datadir is None: + assert False + os.mkdir(_datadir + "/regtest") + shutil.copyfile( + "electrumabc/tests/regtest/configs/electrum-abc-config", + _datadir + "/regtest/config", + ) subprocess.run( [ - ELECTRUMABC_COMMAND, + "./electrum-abc", "--regtest", "-D", - datadir, + _datadir, "-w", - default_wallet, + _datadir + "/default_wallet", "daemon", "start", ], @@ -112,7 +116,7 @@ def start_ec_daemon(datadir: str) -> None: assert result == PACKAGE_VERSION - r = request("create", params={"wallet_path": default_wallet}) + r = request("create", params={"wallet_path": _datadir + "/default_wallet"}) result = poll_for_answer(EC_DAEMON_RPC_URL, r) assert "seed" in result assert len(result["seed"].split(" ")) == 12 @@ -124,72 +128,42 @@ def start_ec_daemon(datadir: str) -> None: poll_for_answer( EC_DAEMON_RPC_URL, request("getinfo"), - expected_answer=(f'wallets["{default_wallet}"]', True), + expected_answer=('wallets["' + _datadir + '/default_wallet"]', True), ) -def stop_ec_daemon(datadir) -> None: - """Stops the daemon""" +def stop_ec_daemon() -> None: + """Stops the daemon and removes the wallet storage from disk""" subprocess.run( - [ELECTRUMABC_COMMAND, "--regtest", "-D", datadir, "daemon", "stop"], check=True + ["./electrum-abc", "--regtest", "-D", _datadir, "daemon", "stop"], check=True ) + if _datadir is None or _datadir.startswith("/tmp") is False: + assert False + shutil.rmtree(_datadir) @pytest.fixture(scope="session") def docker_compose_file(pytestconfig) -> str: """Needed since the docker-compose.yml is not in the root directory""" return os.path.join( - str(pytestconfig.rootdir), - "electrumabc", - "tests", - "regtest", - "docker-compose.yml", + str(pytestconfig.rootdir), "electrumabc/tests/regtest/docker-compose.yml" ) @pytest.fixture(scope="session") -def docker_compose_command() -> str: - """Use the docker-compose command rather than `docker compose`. This is no longer - the default since pytest-docker 2.0.0 was released, so we need to specify it. - The docker version installed on CI seems to be too old to be compatible with the - way pytest-docker calls the `docker compose` command. - """ - return "docker-compose" - - -def make_tmp_electrum_data_dir() -> str: - """Create a temporary directory with a regtest subdirectory, and copy the Electrum - config file into it. - The caller is responsible for deleting the temporary directory.""" - datadir = tempfile.mkdtemp() - os.mkdir(os.path.join(datadir, "regtest")) - shutil.copyfile( - os.path.join( - ELECTRUM_ROOT, - "electrumabc", - "tests", - "regtest", - "configs", - "electrum-abc-config", - ), - os.path.join(datadir, "regtest", "config"), - ) - return datadir - - -@pytest.fixture(scope="function") def fulcrum_service(docker_services: Any) -> Generator[None, None, None]: - """Makes sure all services (bitcoind, fulcrum and the electrum daemon) are up and - running, make a temporary data dir for Electrum ABC, delete it at the end of the - test session. - """ - electrum_datadir = make_tmp_electrum_data_dir() - bitcoind_rpc_connection() - poll_for_answer(FULCRUM_STATS_URL, expected_answer=("Controller.TxNum", 102)) - - try: - start_ec_daemon(electrum_datadir) + """Makes sure all services (bitcoind, fulcrum and the EC daemon) are up and running""" + global _datadir + global _bitcoind + if _datadir is not None: yield - stop_ec_daemon(electrum_datadir) - finally: - shutil.rmtree(electrum_datadir) + else: + _datadir = tempfile.mkdtemp() + _bitcoind = bitcoind_rpc_connection() + poll_for_answer(FULCRUM_STATS_URL, expected_answer=("Controller.TxNum", 102)) + + try: + start_ec_daemon() + yield + finally: + stop_ec_daemon() diff --git a/electrumabc/tests/test_commands.py b/electrumabc/tests/test_commands.py index cb11c8138139..de0a555234a5 100644 --- a/electrumabc/tests/test_commands.py +++ b/electrumabc/tests/test_commands.py @@ -74,41 +74,13 @@ def test_global_options(self): self.assertEqual(args_with_command.cmd, "history") # global options must be before any command or subparser - with open(os.devnull, "w", encoding="utf-8") as null, redirect_stderr(null): + with open(os.devnull, "w") as null, redirect_stderr(null): with self.assertRaises(SystemExit): self.parser.parse_args(["history", "-w", "/path/to/wallet"]) with self.assertRaises(SystemExit): self.parser.parse_args(["gui", "-w", "/path/to/wallet"]) - def test_default_values(self): - """Test a boolean argument with action store_true""" - args = self.parser.parse_args([]) - self.assertEqual(args.regtest, False) - - args = self.parser.parse_args(["--regtest"]) - self.assertEqual(args.regtest, True) - - def test_dest(self): - """Test that some arguments have the expected dest""" - path_to_wallet = os.path.abspath("/path/to/wallet") - args = self.parser.parse_args(["--testnet", "--wallet", path_to_wallet]) - - # Test arguments that have no explicit dest, so the dest is inferred from - # the long option. - self.assertTrue(hasattr(args, "testnet")) - # The argument is defined even if not on the command line. - self.assertTrue(hasattr(args, "test_release_notification")) - - # Test arguments with an explicit dest different from the long option - # --wallet - self.assertFalse(hasattr(args, "wallet")) - self.assertTrue(hasattr(args, "wallet_path")) - - # --dir - self.assertFalse(hasattr(args, "dir")) - self.assertTrue(hasattr(args, "data_path")) - def suite(): test_suite = unittest.TestSuite() diff --git a/electrumabc/tests/test_simple_config.py b/electrumabc/tests/test_simple_config.py index 76bd3bebacae..e77da4c7b681 100644 --- a/electrumabc/tests/test_simple_config.py +++ b/electrumabc/tests/test_simple_config.py @@ -144,9 +144,7 @@ def read_user_dir(): ) config.save_user_config() contents = None - with open( - os.path.join(self.electrum_dir, "config"), "r", encoding="utf-8" - ) as f: + with open(os.path.join(self.electrum_dir, "config"), "r") as f: contents = f.read() result = ast.literal_eval(contents) result.pop("config_version", None) @@ -182,7 +180,7 @@ class something: thefile = os.path.join(self.user_dir, "config") payload = something() - with open(thefile, "w", encoding="utf-8") as f: + with open(thefile, "w") as f: f.write(repr(payload)) result = read_user_config(self.user_dir) diff --git a/electrumabc/tests/test_storage_upgrade.py b/electrumabc/tests/test_storage_upgrade.py index 9bcde2d930b5..c3de528d080d 100644 --- a/electrumabc/tests/test_storage_upgrade.py +++ b/electrumabc/tests/test_storage_upgrade.py @@ -274,7 +274,6 @@ def tearDownClass(cls): def _upgrade_storage(self, wallet_json, accounts=1): storage = self._load_storage_from_json_string(wallet_json, manual_upgrades=True) - self.assertTrue(storage.requires_upgrade()) if accounts == 1: self.assertFalse(storage.requires_split()) @@ -295,7 +294,7 @@ def _sanity_check_upgraded_storage(self, storage): Wallet(storage) def _load_storage_from_json_string(self, wallet_json, manual_upgrades=True): - with open(self.wallet_path, "w", encoding="utf-8") as f: + with open(self.wallet_path, "w") as f: f.write(wallet_json) storage = WalletStorage(self.wallet_path, manual_upgrades=manual_upgrades) return storage diff --git a/electrumabc/tests/test_wallet.py b/electrumabc/tests/test_wallet.py index f8b9c37f8087..263641883604 100644 --- a/electrumabc/tests/test_wallet.py +++ b/electrumabc/tests/test_wallet.py @@ -49,7 +49,7 @@ class TestWalletStorage(WalletTestCase): def test_read_dictionary_from_file(self): some_dict = {"a": "b", "c": "d"} contents = json.dumps(some_dict) - with open(self.wallet_path, "w", encoding="utf-8") as f: + with open(self.wallet_path, "w") as f: contents = f.write(contents) storage = WalletStorage(self.wallet_path, manual_upgrades=True) @@ -66,7 +66,7 @@ def test_write_dictionary_to_file(self): storage.write() contents = "" - with open(self.wallet_path, "r", encoding="utf-8") as f: + with open(self.wallet_path, "r") as f: contents = f.read() self.assertEqual(some_dict, json.loads(contents)) diff --git a/electrumabc/transaction.py b/electrumabc/transaction.py index 638ae0768e99..d82803ca7c85 100644 --- a/electrumabc/transaction.py +++ b/electrumabc/transaction.py @@ -208,19 +208,32 @@ def write_compact_size(self, size): self.write(b"\xff") self._write_num("... + + Correctly handles SSL sockets and gives useful info for select loops. + """ + + class Closed(RuntimeError): + """Raised if socket is closed""" + + def __init__(self, socket, *, max_message_bytes=0): + """A max_message_bytes of <= 0 means unlimited, otherwise a positive + value indicates this many bytes to limit the message size by. This is + used by get(), which will raise MessageSizeExceeded if the message size + received is larger than max_message_bytes.""" + self.socket = socket + socket.settimeout(0) + self.recv_time = time.time() + self.max_message_bytes = max_message_bytes + self.recv_buf = bytearray() + self.send_buf = bytearray() + + def idle_time(self): + return time.time() - self.recv_time + + def get_selectloop_info(self): + """Returns tuple: + + read_pending - new data is available that may be unknown to select(), + so perform a get() regardless of select(). + write_pending - some send data is still buffered, so make sure to call + send_flush if writing becomes available. + """ + try: + # pending() only defined on SSL sockets. + has_pending = self.socket.pending() > 0 + except AttributeError: + has_pending = False + return has_pending, bool(self.send_buf) + + def get(self): + """Attempt to read out a message, possibly saving additional messages in + a receive buffer. + + If no message is currently available, this raises util.timeout and you + should retry once data becomes available to read. If connection is bad for + some known reason, raises .Closed; other errors will raise other exceptions. + """ + while True: + response, self.recv_buf = parse_json(self.recv_buf) + if response is not None: + return response + + try: + data = self.socket.recv(1024) + except (socket.timeout, BlockingIOError, ssl.SSLWantReadError): + raise timeout + except OSError as exc: + if exc.errno in (11, 35, 60, 10035): + # some OSes might give these ways of indicating a would-block error. + raise timeout + if exc.errno == 9: + # EBADF. Someone called close() locally so FD is bad. + raise self.Closed("closed by local") + raise self.Closed( + "closing due to {}: {}".format(type(exc).__name__, str(exc)) + ) + + if not data: + raise self.Closed("closed by remote") + + self.recv_buf.extend(data) + self.recv_time = time.time() + + if ( + self.max_message_bytes > 0 + and len(self.recv_buf) > self.max_message_bytes + ): + raise self.Closed( + f"Message limit is: {self.max_message_bytes}; receive buffer" + " exceeded this limit!" + ) + + def send(self, request): + out = json.dumps(request) + "\n" + out = out.encode("utf8") + self.send_buf.extend(out) + return self.send_flush() + + def send_all(self, requests): + out = b"".join(map(lambda x: (json.dumps(x) + "\n").encode("utf8"), requests)) + self.send_buf.extend(out) + return self.send_flush() + + def send_flush(self): + """Flush any unsent data from a prior call to send / send_all. + + Raises timeout if more data remains to be sent. + Raise .Closed in the event of a socket error that requires abandoning + this socket. + """ + send_buf = self.send_buf + while send_buf: + try: + sent = self.socket.send(send_buf) + except (socket.timeout, BlockingIOError, ssl.SSLWantWriteError): + raise timeout + except OSError as exc: + if exc.errno in (11, 35, 60, 10035): + # some OSes might give these ways of indicating a would-block error. + raise timeout + if exc.errno == 9: + # EBADF. Someone called close() locally so FD is bad. + raise self.Closed("closed by local") + raise self.Closed( + "closing due to {}: {}".format(type(exc).__name__, str(exc)) + ) + + if sent == 0: + # shouldn't happen, but just in case, we don't want to infinite + # loop. + raise timeout + del send_buf[:sent] + + def setup_thread_excepthook(): """ Workaround for `sys.excepthook` thread bug from: diff --git a/electrumabc/version.py b/electrumabc/version.py index f2f9a4c13446..165778938011 100644 --- a/electrumabc/version.py +++ b/electrumabc/version.py @@ -1,7 +1,7 @@ import re # version of the client package -VERSION_TUPLE = (5, 2, 5) +VERSION_TUPLE = (5, 2, 4) PACKAGE_VERSION = ".".join(map(str, VERSION_TUPLE)) # protocol version requested PROTOCOL_VERSION = "1.4" diff --git a/electrumabc/wallet.py b/electrumabc/wallet.py index 70e5eba3d0f0..6a62d0c9e3d5 100644 --- a/electrumabc/wallet.py +++ b/electrumabc/wallet.py @@ -44,8 +44,6 @@ from enum import Enum, auto from typing import ( TYPE_CHECKING, - Any, - Dict, ItemsView, List, Optional, @@ -118,18 +116,6 @@ DEFAULT_CONFIRMED_ONLY = False -HistoryItemType = Tuple[str, int] -"""(tx_hash, block_height)""" - -CoinsItemType = Tuple[int, int, bool] -"""(block_height, amount, is_coinbase)""" - -UnspentCoinsType = Dict[str, CoinsItemType] -"""{"tx_hash:prevout": (block_height, amount, is_coinbase), ...}""" - -SpendCoinsType = Dict[str, int] -"""{"tx_hash:prevout": block_height, ...}""" - class AddressNotFoundError(Exception): """Exception used for Address errors.""" @@ -286,16 +272,18 @@ def __init__(self, storage: WalletStorage): self.labels = storage.get("labels", {}) # Frozen addresses frozen_addresses = storage.get("frozen_addresses", []) - self.frozen_addresses = {Address.from_string(addr) for addr in frozen_addresses} + self.frozen_addresses = set( + Address.from_string(addr) for addr in frozen_addresses + ) # Frozen coins (UTXOs) -- note that we have 2 independent levels of "freezing": address-level and coin-level. # The two types of freezing are flagged independently of each other and 'spendable' is defined as a coin that satisfies # BOTH levels of freezing. self.frozen_coins = set(storage.get("frozen_coins", [])) self.frozen_coins_tmp = set() # in-memory only - self.change_reserved = { + self.change_reserved = set( Address.from_string(a) for a in storage.get("change_reserved", ()) - } + ) self.change_reserved_default = [ Address.from_string(a) for a in storage.get("change_reserved_default", ()) ] @@ -773,18 +761,23 @@ def get_tx_delta(self, tx_hash, address): delta += v return delta - WalletDelta = namedtuple( - "WalletDelta", "is_relevant, is_mine, v, fee, spends_coins_mine" + WalletDelta = namedtuple("WalletDelta", "is_relevant, is_mine, v, fee") + WalletDelta2 = namedtuple( + "WalletDelta2", WalletDelta._fields + ("spends_coins_mine",) ) def get_wallet_delta(self, tx) -> WalletDelta: + return self._get_wallet_delta(tx, ver=1) + + def _get_wallet_delta(self, tx, *, ver=1) -> Union[WalletDelta, WalletDelta2]: """Effect of tx on wallet""" + assert ver in (1, 2) is_relevant = False is_mine = False is_pruned = False is_partial = False v_in = v_out = v_out_mine = 0 - spends_coins_mine = [] + spends_coins_mine = list() for item in tx.inputs(): addr = item["address"] if self.is_mine(addr): @@ -796,7 +789,8 @@ def get_wallet_delta(self, tx) -> WalletDelta: for n, v, cb in d: if n == prevout_n: value = v - spends_coins_mine.append(f"{prevout_hash}:{prevout_n}") + if ver == 2: + spends_coins_mine.append(f"{prevout_hash}:{prevout_n}") break else: value = None @@ -831,13 +825,15 @@ def get_wallet_delta(self, tx) -> WalletDelta: fee = v_in - v_out if not is_mine: fee = None - return self.WalletDelta(is_relevant, is_mine, v, fee, spends_coins_mine) + if ver == 1: + return self.WalletDelta(is_relevant, is_mine, v, fee) + return self.WalletDelta2(is_relevant, is_mine, v, fee, spends_coins_mine) TxInfo = namedtuple( "TxInfo", ( "tx_hash, status, label, can_broadcast, amount, fee, height, conf," - " timestamp, exp_n, status_enum" + " timestamp, exp_n" ), ) @@ -849,19 +845,30 @@ class StatusEnum(Enum): Unsigned = auto() PartiallySigned = auto() - def get_tx_extended_info(self, tx) -> Tuple[WalletDelta, TxInfo]: + TxInfo2 = namedtuple("TxInfo2", TxInfo._fields + ("status_enum",)) + + def get_tx_info(self, tx) -> TxInfo: + """Return information for a transaction""" + return self._get_tx_info(tx, self.get_wallet_delta(tx), ver=1) + + def get_tx_extended_info(self, tx) -> Tuple[WalletDelta2, TxInfo2]: """Get extended information for a transaction, combined into 1 call (for performance)""" - delta = self.get_wallet_delta(tx) - info = self.get_tx_info(tx, delta) - return (delta, info) + delta2 = self._get_wallet_delta(tx, ver=2) + info2 = self._get_tx_info(tx, delta2, ver=2) + return (delta2, info2) - def get_tx_info(self, tx, delta) -> TxInfo: + def _get_tx_info(self, tx, delta, *, ver=1) -> Union[TxInfo, TxInfo2]: """get_tx_info implementation""" - is_relevant, is_mine, v, fee, __ = delta + assert ver in (1, 2) + if isinstance(delta, self.WalletDelta): + is_relevant, is_mine, v, fee = delta + else: + is_relevant, is_mine, v, fee, __ = delta exp_n = None can_broadcast = False label = "" height = conf = timestamp = None + status_enum = None tx_hash = tx.txid() if tx.is_complete(): if tx_hash in self.transactions: @@ -905,7 +912,21 @@ def get_tx_info(self, tx, delta) -> TxInfo: else: amount = None - return self.TxInfo( + if ver == 1: + return self.TxInfo( + tx_hash, + status, + label, + can_broadcast, + amount, + fee, + height, + conf, + timestamp, + exp_n, + ) + assert status_enum is not None + return self.TxInfo2( tx_hash, status, label, @@ -919,34 +940,21 @@ def get_tx_info(self, tx, delta) -> TxInfo: status_enum, ) - def get_address_unspent( - self, address: Address, address_history: List[HistoryItemType] - ) -> UnspentCoinsType: + def get_addr_io(self, address): + history = self.get_address_history(address) received = {} - for tx_hash, height in address_history: + sent = {} + for tx_hash, height in history: coins = self.txo.get(tx_hash, {}).get(address, []) for n, v, is_cb in coins: - received[f"{tx_hash}:{n}"] = (height, v, is_cb) - return received - - def get_address_spent( - self, address: Address, address_history: List[HistoryItemType] - ) -> SpendCoinsType: - sent = {} - for tx_hash, height in address_history: + received[tx_hash + ":%d" % n] = (height, v, is_cb) + for tx_hash, height in history: inputs = self.txi.get(tx_hash, {}).get(address, []) for txi, v in inputs: sent[txi] = height - return sent - - def get_addr_io(self, address: Address) -> Tuple[UnspentCoinsType, SpendCoinsType]: - history = self.get_address_history(address) - received = self.get_address_unspent(address, history) - sent = self.get_address_spent(address, history) return received, sent - def get_addr_utxo(self, address: Address) -> Dict[str, Dict[str, Any]]: - """Return a {"tx_hash:prevout_n": dict_of_info, ...} dict""" + def get_addr_utxo(self, address): coins, spent = self.get_addr_io(address) for txi in spent: coins.pop(txi) @@ -974,9 +982,12 @@ def get_addr_utxo(self, address: Address) -> Dict[str, Dict[str, Any]]: out[txo] = x return out - def get_addr_balance( - self, address: Address, exclude_frozen_coins=False - ) -> Tuple[int, int, int]: + # 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()]) + + def get_addr_balance(self, address, exclude_frozen_coins=False): """Returns the balance of a bitcoin address as a tuple of: (confirmed_matured, unconfirmed, unmatured) Note that 'exclude_frozen_coins = True' only checks for coin-level @@ -1161,8 +1172,7 @@ def get_balance( xx += x return cc, uu, xx - def get_address_history(self, address: Address) -> List[HistoryItemType]: - """Returns a list of (tx_hash, block_height) for an address""" + def get_address_history(self, address): assert isinstance(address, Address) return self._history.get(address, []) @@ -1615,10 +1625,10 @@ def receive_history_callback(self, addr, hist, tx_fees): def add_tx_to_history(self, txid): with self.lock: for addr in itertools.chain( - self.txi.get(txid, {}).keys(), self.txo.get(txid, {}).keys() + list(self.txi.get(txid, {}).keys()), list(self.txo.get(txid, {}).keys()) ): - cur_hist = self._history.get(addr, []) - if not any(x[0] == txid for x in cur_hist): + cur_hist = self._history.get(addr, list()) + if not any(True for x in cur_hist if x[0] == txid): cur_hist.append((txid, 0)) self._history[addr] = cur_hist @@ -2070,16 +2080,8 @@ def make_unsigned_transaction( if fixed_fee is None and config.fee_per_kb() is None: raise RuntimeError("Dynamic fee estimates not available") - # optimization for addresses with many coins: cache unspent coins - coins_for_address: Dict[str, UnspentCoinsType] = {} for item in inputs: - address = item["address"] - if address not in coins_for_address: - coins_for_address[address] = self.get_address_unspent( - address, self.get_address_history(address) - ) - - self.add_input_info(item, coins_for_address[address]) + self.add_input_info(item) # Fee estimator if fixed_fee is None: @@ -2152,7 +2154,7 @@ def fee_estimator(size): sign_schnorr=sign_schnorr, ) else: - sendable = sum(x["value"] for x in inputs) + sendable = sum(map(lambda x: x["value"], inputs)) outputs[i_max] = outputs[i_max]._replace(value=0) tx = Transaction.from_io(inputs, outputs, sign_schnorr=sign_schnorr) fee = fee_estimator(tx.estimated_size()) @@ -2441,11 +2443,7 @@ def cpfp(self, tx, fee, sign_schnorr=None, enable_current_block_locktime=True): item = coins.get(txid + ":%d" % i) if not item: return - - coins = self.get_address_unspent( - item["address"], self.get_address_history(item["address"]) - ) - self.add_input_info(item, coins) + self.add_input_info(item) inputs = [item] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, txo.destination, txo.value - fee)] locktime = 0 @@ -2456,11 +2454,12 @@ def cpfp(self, tx, fee, sign_schnorr=None, enable_current_block_locktime=True): inputs, outputs, locktime=locktime, sign_schnorr=sign_schnorr ) - def add_input_info(self, txin: Dict[str, Any], received: UnspentCoinsType): + def add_input_info(self, txin): address = txin["address"] if self.is_mine(address): txin["type"] = self.get_txin_type(address) # eCash needs value to sign + received, spent = self.get_addr_io(address) item = received.get(txin["prevout_hash"] + ":%d" % txin["prevout_n"]) tx_height, value, is_cb = item txin["value"] = value @@ -2509,8 +2508,14 @@ def add_input_values_to_tx(self, tx): def add_hw_info(self, tx): # add previous tx for hw wallets, if needed and not already there if any( - (isinstance(k, HardwareKeyStore) and k.can_sign(tx) and k.needs_prevtx()) - for k in self.get_keystores() + [ + ( + isinstance(k, HardwareKeyStore) + and k.can_sign(tx) + and k.needs_prevtx() + ) + for k in self.get_keystores() + ] ): for txin in tx.inputs(): if "prev_tx" not in txin: @@ -2549,8 +2554,10 @@ def sign_transaction(self, tx, password, *, use_cache=False): self.add_input_values_to_tx(tx) # hardware wallets require extra info if any( - (isinstance(k, HardwareKeyStore) and k.can_sign(tx)) - for k in self.get_keystores() + [ + (isinstance(k, HardwareKeyStore) and k.can_sign(tx)) + for k in self.get_keystores() + ] ): self.add_hw_info(tx) # sign @@ -2594,14 +2601,9 @@ def get_receiving_address(self, *, frozen_ok=True): if domain: return domain[0] - def get_payment_status( - self, address: Address, amount: int - ) -> Tuple[bool, Optional[int], List[str]]: - """Return (is_paid, num_confirmations, list_of_tx_hashes) - is_paid is True if the address received at least the specified amount. - """ + def get_payment_status(self, address, amount): local_height = self.get_local_height() - received = self.get_address_unspent(address, self.get_address_history(address)) + received, sent = self.get_addr_io(address) transactions = [] for txo, x in received.items(): h, v, is_cb = x @@ -2615,7 +2617,7 @@ def get_payment_status( transactions.append((conf, v, txid)) tx_hashes = [] vsum = 0 - for conf, v, tx_hash in sorted(transactions, reverse=True): + for conf, v, tx_hash in reversed(sorted(transactions)): vsum += v tx_hashes.append(tx_hash) if vsum >= amount: @@ -2825,7 +2827,9 @@ def remove_payment_request(self, addr, config, clear_address_label_if_no_tx=True return True def get_sorted_requests(self, config): - m = (self.get_payment_request(x, config) for x in self.receive_requests.keys()) + m = map( + lambda x: self.get_payment_request(x, config), self.receive_requests.keys() + ) try: def f(x): @@ -2862,7 +2866,7 @@ def is_multisig(self): return False def is_hardware(self): - return any(isinstance(k, HardwareKeyStore) for k in self.get_keystores()) + return any([isinstance(k, HardwareKeyStore) for k in self.get_keystores()]) def add_address(self, address, *, for_change=False): assert isinstance(address, Address) @@ -3438,7 +3442,7 @@ def synchronize_sequence(self, for_change): if len(addresses) < limit: self.create_new_address(for_change, save=False) continue - if all(not self.address_is_old(a) for a in addresses[-limit:]): + if all(map(lambda a: not self.address_is_old(a), addresses[-limit:])): break else: self.create_new_address(for_change, save=False) @@ -3597,7 +3601,7 @@ def get_keystores(self): return [self.keystores[i] for i in sorted(self.keystores.keys())] def can_have_keystore_encryption(self): - return any(k.may_have_password() for k in self.get_keystores()) + return any([k.may_have_password() for k in self.get_keystores()]) def _update_password_for_keystore(self, old_pw, new_pw): for name, keystore_ in self.keystores.items(): @@ -3619,7 +3623,7 @@ def has_seed(self): return self.keystore.has_seed() def is_watching_only(self): - return not any(not k.is_watching_only() for k in self.get_keystores()) + return not any([not k.is_watching_only() for k in self.get_keystores()]) def get_master_public_key(self): return self.keystore.get_master_public_key() diff --git a/electrumabc/websockets.py b/electrumabc/websockets.py index 5281e2ff7623..45d42ff2cb99 100644 --- a/electrumabc/websockets.py +++ b/electrumabc/websockets.py @@ -65,7 +65,7 @@ def __init__(self, config, network): self.config = config self.response_queue = queue.Queue() self.subscriptions = defaultdict(list) - self.sh2addr = {} + self.sh2addr = dict() def make_request(self, request_id): # read json file diff --git a/electrumabc/winconsole.py b/electrumabc/winconsole.py new file mode 100644 index 000000000000..981d44eb2a80 --- /dev/null +++ b/electrumabc/winconsole.py @@ -0,0 +1,147 @@ +# Electrum ABC - lightweight eCash client +# Copyright (C) 2020 The Electrum ABC Developers +# Copyright (C) 2019-2020 Axel Gembe +# +# 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. + +""" +This module is for to handling console attaching and / or creation in Windows +binaries that are built for the Windows subsystem and therefore do not +automatically allocate a console. +""" + +import atexit +import ctypes +import os +import sys +from typing import Iterator, Optional + +STD_OUTPUT_HANDLE = -11 +FILE_TYPE_DISK = 1 + + +def parent_process_pids() -> Iterator[int]: + """ + Returns all parent process PIDs, starting with the closest parent + """ + try: + import psutil + + pid = os.getpid() + while pid > 0: + pid = psutil.Process(pid).ppid() + yield pid + except psutil.NoSuchProcess: + # Parent process not found, likely terminated, nothing we can do + pass + + +def get_console_title() -> str: + """Return the current console title as a string. May return None on error.""" + b = bytes(1024) + b_ptr = ctypes.c_char_p(b) + title = None + # GetConsoleTitleW expects size in 2-byte chars + title_len = ctypes.windll.kernel32.GetConsoleTitleW(b_ptr, len(b) // 2) # type: ignore[attr-defined] + if title_len > 0: + title = b.decode("utf-16")[:title_len] + return title + + +def create_or_attach_console( + *, attach: bool = True, create: bool = False, title: Optional[str] = None +) -> bool: + """ + Workaround to the fact that cmd.exe based execution of this program means + it has no stdout handles and thus is always silent, thereby rendering + vernbose console output or command-line usage problematic. + + First, check if we have STD_OUTPUT_HANDLE (a console) and do nothing if + there is one, returning True. + + Otherwise, try to attach to the console of any ancestor process, and return + True. + + If not successful, optionally (create=True) create a new console. + + NB: Creating a new console results in a 'cmd.exe' console window to be + created on the Windows desktop, so only pass create=True if that's + acceptable. + + If a console was found or created, we redirect current output handles + (sys.stdout, sys.stderr) to this found and/or created console. + + Always return True on success or if there was a console already, + False on failure. + """ + std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + + has_console = std_out_handle > 0 + + if has_console: + # Output is being redirected to a file, or we have an msys console. + # do nothing + return True + + try: + if attach: + # Try to attach to a parent console + for pid in parent_process_pids(): + if ctypes.windll.kernel32.AttachConsole(pid): + has_console = True + break + except ImportError: + # User's system lacks psutil + return False + + created = False + + if not has_console and create: + # Try to allocate a new console + if ctypes.windll.kernel32.AllocConsole(): + has_console = True + created = True + + if not has_console: + # Indicate to caller no console is to be had. + return False + + try: + # Reopen Pythons console input and output handles + conout = open("CONOUT$", "w") + sys.stdout = conout + sys.stderr = conout + sys.stdin = open("CONIN$", "r") + except OSError: + # If we get here, we likely were in MinGW / MSYS where CONOUT$ / CONIN$ + # are not valid files or some other weirdness occurred. Give up. + return False + + if title: + # save the old title only if not created by us + old_title = get_console_title() if not created else None + # Set the console title, if specified + ctypes.windll.kernel32.SetConsoleTitleW(title) + if old_title is not None: + # undo the setting of the console title at app exit + atexit.register(ctypes.windll.kernel32.SetConsoleTitleW, old_title) + + return True diff --git a/electrumabc/x509.py b/electrumabc/x509.py index ec89f490a356..254ea2c205b5 100644 --- a/electrumabc/x509.py +++ b/electrumabc/x509.py @@ -231,7 +231,7 @@ def get_children(self, node): return nodes def get_sequence(self): - return [self.get_value(j) for j in self.get_children(self.root())] + return list(map(lambda j: self.get_value(j), self.get_children(self.root()))) def get_dict(self, node): p = {} @@ -411,6 +411,8 @@ def load_certificates(ca_path): x = X509(b) x.check_date() except Exception as e: + # with open('/tmp/tmp.txt', 'w') as f: + # f.write(pem.pem(b, 'CERTIFICATE').decode('ascii')) print_error("cert error:", e) continue diff --git a/electrumabc_gui/qt/__init__.py b/electrumabc_gui/qt/__init__.py index 29b711a93b4d..85ed60998748 100644 --- a/electrumabc_gui/qt/__init__.py +++ b/electrumabc_gui/qt/__init__.py @@ -248,7 +248,7 @@ def __init__(self, config: SimpleConfig, daemon: Daemon, plugins: Plugins): # also deletes itself) self._wallet_password_cache = Weak.KeyDictionary() # / - self.update_checker = UpdateChecker(self.config) + self.update_checker = UpdateChecker() self.update_checker_timer = QtCore.QTimer(self) self.update_checker_timer.timeout.connect(self.on_auto_update_timeout) self.update_checker_timer.setSingleShot(False) @@ -600,7 +600,7 @@ def toggle_tray_icon(self): def tray_activated(self, reason): if reason == QtWidgets.QSystemTrayIcon.DoubleClick: - if all(w.is_hidden() for w in self.windows): + if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: diff --git a/electrumabc_gui/qt/address_list.py b/electrumabc_gui/qt/address_list.py index c63474a75bb9..dab353e3ac79 100644 --- a/electrumabc_gui/qt/address_list.py +++ b/electrumabc_gui/qt/address_list.py @@ -44,10 +44,10 @@ from .consolidate_coins_dialog import ConsolidateCoinsWizard from .invoice_dialog import InvoiceDialog -from .tree_widget import MyTreeWidget from .util import ( MONOSPACE_FONT, ColorScheme, + MyTreeWidget, SortableTreeWidgetItem, rate_limited, webopen, @@ -73,6 +73,7 @@ class DataRoles(IntEnum): def __init__(self, main_window: ElectrumWindow, *, picker=False): super().__init__( + main_window, [], config=main_window.config, wallet=main_window.wallet, diff --git a/electrumabc_gui/qt/avalanche/delegation_editor.py b/electrumabc_gui/qt/avalanche/delegation_editor.py index e96df78fd1ff..62078b5fab69 100644 --- a/electrumabc_gui/qt/avalanche/delegation_editor.py +++ b/electrumabc_gui/qt/avalanche/delegation_editor.py @@ -111,7 +111,7 @@ def on_load_proof_clicked(self): ) if not fileName: return - with open(fileName, "r", encoding="utf-8") as f: + with open(fileName, "r") as f: proof_hex = f.read().strip() self.set_proof(proof_hex) self.tab_widget.setCurrentWidget(self.proof_edit) diff --git a/electrumabc_gui/qt/avalanche/proof_editor.py b/electrumabc_gui/qt/avalanche/proof_editor.py index 7bae701a59a3..ba87498ff944 100644 --- a/electrumabc_gui/qt/avalanche/proof_editor.py +++ b/electrumabc_gui/qt/avalanche/proof_editor.py @@ -574,7 +574,7 @@ def on_save_proof_clicked(self): ) if not fileName: return - with open(fileName, "w", encoding="utf-8") as f: + with open(fileName, "w") as f: f.write(proof.to_hex()) def update_master_pubkey(self, master_wif: str): @@ -736,7 +736,7 @@ def load_from_file(self) -> Optional[str]: ) if not fileName: return - with open(fileName, "r", encoding="utf-8") as f: + with open(fileName, "r") as f: proof_hex = f.read().strip() if self.try_to_decode_proof(proof_hex): self.accept() diff --git a/electrumabc_gui/qt/bip38_importer.py b/electrumabc_gui/qt/bip38_importer.py index f1f2e22187c2..b91fbf014397 100644 --- a/electrumabc_gui/qt/bip38_importer.py +++ b/electrumabc_gui/qt/bip38_importer.py @@ -78,7 +78,7 @@ def __init__( if not parent: self.setWindowModality(Qt.ApplicationModal) - self.decoded_keys = {} # results are placed here on success + self.decoded_keys = dict() # results are placed here on success self.success_cb, self.cancel_cb = on_success, on_cancel self.cur, self.decoded_wif, self.decoded_address = 0, None, None self.decrypter = None diff --git a/electrumabc_gui/qt/console.py b/electrumabc_gui/qt/console.py index 9fbcd5bdd7be..1a5b66452db5 100644 --- a/electrumabc_gui/qt/console.py +++ b/electrumabc_gui/qt/console.py @@ -9,7 +9,6 @@ from electrumabc import util from electrumabc.i18n import _ -from electrumabc.json_util import json_encode from electrumabc.printerror import print_msg from .util import MONOSPACE_FONT, ColorScheme @@ -336,7 +335,7 @@ def show_completions(self, completions): c = self.editor.textCursor() c.setPosition(self.completions_pos) - completions = (x.split(".")[-1] for x in completions) + completions = map(lambda x: x.split(".")[-1], completions) t = "\n" + " ".join(completions) if len(t) > 500: t = t[:500] + "..." @@ -446,7 +445,7 @@ def write(self, text): result = eval(command, self.namespace, self.namespace) if result is not None: if self.is_json: - print_msg(json_encode(result)) + print_msg(util.json_encode(result)) else: self.editor.appendPlainText(repr(result)) except SyntaxError: diff --git a/electrumabc_gui/qt/contact_list.py b/electrumabc_gui/qt/contact_list.py index 72c2904e36dd..4d4ae7a063c1 100644 --- a/electrumabc_gui/qt/contact_list.py +++ b/electrumabc_gui/qt/contact_list.py @@ -41,8 +41,7 @@ from electrumabc.plugins import run_hook from electrumabc.printerror import PrintError -from .tree_widget import MyTreeWidget -from .util import MONOSPACE_FONT, ColorScheme, rate_limited, webopen +from .util import MONOSPACE_FONT, ColorScheme, MyTreeWidget, rate_limited, webopen if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -60,6 +59,7 @@ class DataRoles(IntEnum): def __init__(self, main_window: ElectrumWindow): MyTreeWidget.__init__( self, + main_window, headers=["", _("Name"), _("Label"), _("Address"), _("Type")], config=main_window.config, wallet=main_window.wallet, @@ -305,7 +305,9 @@ def on_update(self): item = self.currentItem() current_contact = item.data(0, self.DataRoles.Contact) if item else None selected = self.selectedItems() or [] - selected_contacts = {item.data(0, self.DataRoles.Contact) for item in selected} + selected_contacts = set( + item.data(0, self.DataRoles.Contact) for item in selected + ) # must not hold a reference to a C++ object that will soon be deleted in # self.clear().. del item, selected diff --git a/electrumabc_gui/qt/history_list.py b/electrumabc_gui/qt/history_list.py index 59311eaddbb6..afc07eadde6f 100644 --- a/electrumabc_gui/qt/history_list.py +++ b/electrumabc_gui/qt/history_list.py @@ -37,8 +37,13 @@ from electrumabc.plugins import run_hook from electrumabc.util import Weak, profiler, timestamp_to_datetime -from .tree_widget import MyTreeWidget -from .util import MONOSPACE_FONT, SortableTreeWidgetItem, rate_limited, webopen +from .util import ( + MONOSPACE_FONT, + MyTreeWidget, + SortableTreeWidgetItem, + rate_limited, + webopen, +) if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -66,6 +71,7 @@ class HistoryList(MyTreeWidget): def __init__(self, main_window: ElectrumWindow): super().__init__( + main_window, [], config=main_window.config, wallet=main_window.wallet, diff --git a/electrumabc_gui/qt/invoice_dialog.py b/electrumabc_gui/qt/invoice_dialog.py index 1ec1254c5284..88742cf0aba3 100644 --- a/electrumabc_gui/qt/invoice_dialog.py +++ b/electrumabc_gui/qt/invoice_dialog.py @@ -146,7 +146,7 @@ def _on_save_clicked(self): if invoice is None: return - with open(filename, "w", encoding="utf-8") as f: + with open(filename, "w") as f: json.dump(invoice.to_dict(), f, indent=4) # hide dialog after saving self.accept() diff --git a/electrumabc_gui/qt/invoice_list.py b/electrumabc_gui/qt/invoice_list.py index 9029eba348da..7ed4b22129a8 100644 --- a/electrumabc_gui/qt/invoice_list.py +++ b/electrumabc_gui/qt/invoice_list.py @@ -33,11 +33,10 @@ from PyQt5.QtGui import QFont, QIcon from electrumabc.i18n import _ -from electrumabc.paymentrequest import PR_UNPAID, pr_tooltips +from electrumabc.paymentrequest import pr_tooltips from electrumabc.util import FileImportFailed, format_time -from .tree_widget import MyTreeWidget, pr_icons -from .util import MONOSPACE_FONT +from .util import MONOSPACE_FONT, PR_UNPAID, MyTreeWidget, pr_icons if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -49,6 +48,7 @@ class InvoiceList(MyTreeWidget): def __init__(self, main_window: ElectrumWindow): MyTreeWidget.__init__( self, + main_window, [_("Expires"), _("Requestor"), _("Description"), _("Amount"), _("Status")], config=main_window.config, wallet=main_window.wallet, diff --git a/electrumabc_gui/qt/main_window.py b/electrumabc_gui/qt/main_window.py index 5c7f9567de10..2a152bac4432 100644 --- a/electrumabc_gui/qt/main_window.py +++ b/electrumabc_gui/qt/main_window.py @@ -116,7 +116,6 @@ from .sign_verify_dialog import SignVerifyDialog from .statusbar import NetworkStatus, StatusBar from .transaction_dialog import show_transaction -from .tree_widget import MyTreeWidget from .util import ( MONOSPACE_FONT, Buttons, @@ -130,6 +129,7 @@ HelpButton, HelpLabel, MessageBoxMixin, + MyTreeWidget, OkButton, RateLimiter, TaskThread, @@ -1086,41 +1086,43 @@ def notify(self, message): self.gui_object.notify(message) # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user - def getOpenFileName(self, title, filter=""): # noqa: A002 + def getOpenFileName(self, title, filter=""): return __class__.static_getOpenFileName( - title=title, filtr=filter, config=self.config, parent=self + title=title, filter=filter, config=self.config, parent=self ) - def getSaveFileName(self, title, filename, filter=""): # noqa: A002 + def getSaveFileName(self, title, filename, filter=""): return __class__.static_getSaveFileName( title=title, filename=filename, - filtr=filter, + filter=filter, config=self.config, parent=self, ) @staticmethod - def static_getOpenFileName(*, title, parent=None, config=None, filtr=""): + def static_getOpenFileName(*, title, parent=None, config=None, filter=""): if not config: config = get_config() userdir = os.path.expanduser("~") directory = config.get("io_dir", userdir) if config else userdir fileName, __ = QtWidgets.QFileDialog.getOpenFileName( - parent, title, directory, filtr + parent, title, directory, filter ) if fileName and directory != os.path.dirname(fileName) and config: config.set_key("io_dir", os.path.dirname(fileName), True) return fileName @staticmethod - def static_getSaveFileName(*, title, filename, parent=None, config=None, filtr=""): + def static_getSaveFileName(*, title, filename, parent=None, config=None, filter=""): if not config: config = get_config() userdir = os.path.expanduser("~") directory = config.get("io_dir", userdir) if config else userdir path = os.path.join(directory, filename) - fileName, __ = QtWidgets.QFileDialog.getSaveFileName(parent, title, path, filtr) + fileName, __ = QtWidgets.QFileDialog.getSaveFileName( + parent, title, path, filter + ) if fileName and directory != os.path.dirname(fileName) and config: config.set_key("io_dir", os.path.dirname(fileName), True) return fileName @@ -1636,7 +1638,7 @@ def save_payment_request(self): self.show_error(_("No message or amount")) return False i = self.expires_combo.currentIndex() - expiration = expiration_values[i][1] + expiration = list(map(lambda x: x[1], expiration_values))[i] kwargs = {} opr = self.receive_opreturn_e.text().strip() if opr: @@ -1971,7 +1973,7 @@ def create_send_tab(self): self.from_label = QtWidgets.QLabel(_("&From")) grid.addWidget(self.from_label, 4, 0) - self.from_list = MyTreeWidget(["", ""], self.config, self.wallet) + self.from_list = MyTreeWidget(self, ["", ""], self.config, self.wallet) self.from_list.customContextMenuRequested.connect(self.from_list_menu) self.from_label.setBuddy(self.from_list) self.from_list.setHeaderHidden(True) @@ -2320,7 +2322,7 @@ def redraw_from_list(self, *, spendable=None): def name(x): return "{}:{}".format(x["prevout_hash"], x["prevout_n"]) - def format_outpoint_and_address(x): + def format(x): h = x["prevout_hash"] return "{}...{}:{:d}\t{}".format( h[0:10], h[-10:], x["prevout_n"], x["address"] @@ -2334,7 +2336,7 @@ def grayify(twi): def new(item, is_unremovable=False): ret = QtWidgets.QTreeWidgetItem( - [format_outpoint_and_address(item), self.format_amount(item["value"])] + [format(item), self.format_amount(item["value"])] ) ret.setData(0, Qt.UserRole, name(item)) ret.setData(0, Qt.UserRole + 1, is_unremovable) @@ -2615,7 +2617,7 @@ def do_send(self, preview=False): amount = ( tx.output_value() if self.max_button.isChecked() - else sum(x[2] for x in outputs) + else sum(map(lambda x: x[2], outputs)) ) fee = tx.get_fee() @@ -3299,8 +3301,13 @@ def show_pr_details(self, pr): grid.addWidget(QtWidgets.QLabel(pr.get_requestor()), 0, 1) grid.addWidget(QtWidgets.QLabel(_("Amount") + ":"), 1, 0) outputs_str = "\n".join( - self.format_amount(x[2]) + self.base_unit() + " @ " + x[1].to_ui_string() - for x in pr.get_outputs() + map( + lambda x: self.format_amount(x[2]) + + self.base_unit() + + " @ " + + x[1].to_ui_string(), + pr.get_outputs(), + ) ) grid.addWidget(QtWidgets.QLabel(outputs_str), 1, 1) expires = pr.get_expiration_date() @@ -5100,7 +5107,7 @@ def do_toggle(weakCb, name, i): "\n\n" + _("Requires") + ":\n" - + "\n".join(x[1] for x in descr.get("requires")) + + "\n".join(map(lambda x: x[1], descr.get("requires"))) ) grid.addWidget(HelpButton(msg), i, 2) except Exception: @@ -5502,8 +5509,10 @@ def process_notifs(self): n_ok, total_amount = 0, 0 for tx in txns: if tx: - delta = parent.wallet.get_wallet_delta(tx) - if not delta.is_relevant: + is_relevant, is_mine, v, fee = parent.wallet.get_wallet_delta( + tx + ) + if not is_relevant: continue - total_amount += delta.v + total_amount += v n_ok += 1 diff --git a/electrumabc_gui/qt/multi_transactions_dialog.py b/electrumabc_gui/qt/multi_transactions_dialog.py index 09a03bfc4926..27951b52f42c 100644 --- a/electrumabc_gui/qt/multi_transactions_dialog.py +++ b/electrumabc_gui/qt/multi_transactions_dialog.py @@ -162,22 +162,22 @@ def set_transactions(self, transactions: Sequence[transaction.Transaction]): self.fees_label.setToolTip(tooltip) def on_save_clicked(self): - directory = QtWidgets.QFileDialog.getExistingDirectory( + dir = QtWidgets.QFileDialog.getExistingDirectory( self, "Select output directory for transaction files", str(Path.home()) ) - if not directory: + if not dir: return for i, tx in enumerate(self.transactions): name = ( f"signed_{i:03d}.txn" if tx.is_complete() else f"unsigned_{i:03d}.txn" ) - path = Path(directory) / name + path = Path(dir) / name tx_dict = tx.as_dict() with open(path, "w+", encoding="utf-8") as f: f.write(json.dumps(tx_dict, indent=4) + "\n") QtWidgets.QMessageBox.information( - self, "Done saving", f"Saved {len(self.transactions)} files to {directory}" + self, "Done saving", f"Saved {len(self.transactions)} files to {dir}" ) def on_sign_clicked(self): diff --git a/electrumabc_gui/qt/network_dialog.py b/electrumabc_gui/qt/network_dialog.py index 30009047331e..ebfe47697a17 100644 --- a/electrumabc_gui/qt/network_dialog.py +++ b/electrumabc_gui/qt/network_dialog.py @@ -856,7 +856,7 @@ def hideEvent(slf, e): row += 1 def req_max_changed(val): - Interface.set_req_throttle_params(self.config, max_unanswered_requests=val) + Interface.set_req_throttle_params(self.config, max=val) def req_chunk_changed(val): Interface.set_req_throttle_params(self.config, chunkSize=val) @@ -864,7 +864,7 @@ def req_chunk_changed(val): def req_defaults(): p = Interface.req_throttle_default Interface.set_req_throttle_params( - self.config, max_unanswered_requests=p.max, chunkSize=p.chunkSize + self.config, max=p.max, chunkSize=p.chunkSize ) self.update() diff --git a/electrumabc_gui/qt/popup_widget.py b/electrumabc_gui/qt/popup_widget.py index 9b17833e32b9..915b8bd5e93b 100644 --- a/electrumabc_gui/qt/popup_widget.py +++ b/electrumabc_gui/qt/popup_widget.py @@ -366,7 +366,7 @@ def setPopupText(self, text): self.label.setText(text) -_extant_popups = {} +_extant_popups = dict() def ShowPopupLabel( diff --git a/electrumabc_gui/qt/request_list.py b/electrumabc_gui/qt/request_list.py index 7b3208d514c5..44aa1c4459fe 100644 --- a/electrumabc_gui/qt/request_list.py +++ b/electrumabc_gui/qt/request_list.py @@ -37,7 +37,7 @@ from electrumabc.plugins import run_hook from electrumabc.util import age, format_time -from .tree_widget import MyTreeWidget, pr_icons +from .util import MyTreeWidget, pr_icons if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -50,6 +50,7 @@ class RequestList(MyTreeWidget): def __init__(self, main_window: ElectrumWindow): MyTreeWidget.__init__( self, + main_window, [_("Date"), _("Address"), "", _("Description"), _("Amount"), _("Status")], config=main_window.config, wallet=main_window.wallet, diff --git a/electrumabc_gui/qt/udev_installer.py b/electrumabc_gui/qt/udev_installer.py index 9c1e71bd3940..089ffe18adae 100644 --- a/electrumabc_gui/qt/udev_installer.py +++ b/electrumabc_gui/qt/udev_installer.py @@ -135,7 +135,7 @@ def updateStatus(self): self.setStatus(_("Not installed"), True) return - with open(self.UDEV_RULES_FILE, "r", encoding="utf-8") as rules_file: + with open(self.UDEV_RULES_FILE, "r") as rules_file: rules_installed = rules_file.read() rules = self.generateRulesFile() diff --git a/electrumabc_gui/qt/update_checker.py b/electrumabc_gui/qt/update_checker.py index 92d7b30f3cca..8535e1e10287 100644 --- a/electrumabc_gui/qt/update_checker.py +++ b/electrumabc_gui/qt/update_checker.py @@ -27,8 +27,6 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import base64 -import json -import os.path import sys import threading import time @@ -42,7 +40,6 @@ from electrumabc.i18n import _ from electrumabc.networks import MainNet from electrumabc.printerror import PrintError, print_error -from electrumabc.simple_config import SimpleConfig from .util import Buttons @@ -92,9 +89,8 @@ class UpdateChecker(QtWidgets.QWidget, PrintError): ), ) - def __init__(self, config: SimpleConfig, parent=None): + def __init__(self, parent=None): super().__init__(parent) - self.is_test_run = config.get("test_release_notification", False) self.setWindowTitle(f"{PROJECT_NAME} - " + _("Update Checker")) self.content = QtWidgets.QVBoxLayout() self.content.setContentsMargins(*([10] * 4)) @@ -114,12 +110,9 @@ def __init__(self, config: SimpleConfig, parent=None): self.content.addWidget(self.pb) versions = QtWidgets.QHBoxLayout() - current_version_message = ( - _(f"Current version: {version.PACKAGE_VERSION}") - if not self.is_test_run - else "Test run" + versions.addWidget( + QtWidgets.QLabel(_(f"Current version: {version.PACKAGE_VERSION}")) ) - versions.addWidget(QtWidgets.QLabel(current_version_message)) self.latest_version_label = QtWidgets.QLabel(_(f"Latest version: {' '}")) versions.addWidget(self.latest_version_label) self.content.addLayout(versions) @@ -226,15 +219,13 @@ def _process_server_reply(self, signed_version_dict): return None return version.PACKAGE_VERSION - def _my_version(self): - if self.is_test_run: - # Return a lower version to always trigger the notification - return 0, 0, 0, "" - if getattr(self, "_my_version_parsed", None) is None: - self._my_version_parsed = version.parse_package_version( + @classmethod + def _my_version(cls): + if getattr(cls, "_my_version_parsed", None) is None: + cls._my_version_parsed = version.parse_package_version( version.PACKAGE_VERSION ) - return self._my_version_parsed + return cls._my_version_parsed @classmethod def _parse_version(cls, version_msg): @@ -248,9 +239,10 @@ def _parse_version(cls, version_msg): ) raise - def is_matching_variant(self, version_msg, *, return_parsed=False): - parsed_version = self._parse_version(version_msg) - me = self._my_version() + @classmethod + def is_matching_variant(cls, version_msg, *, return_parsed=False): + parsed_version = cls._parse_version(version_msg) + me = cls._my_version() # last element of tuple is always a string, the 'variant' # (may be '' for EC Regular) ret = me[-1] == parsed_version[-1] @@ -258,11 +250,12 @@ def is_matching_variant(self, version_msg, *, return_parsed=False): return ret, parsed_version return ret - def is_newer(self, version_msg): - yes, parsed = self.is_matching_variant(version_msg, return_parsed=True) + @classmethod + def is_newer(cls, version_msg): + yes, parsed = cls.is_matching_variant(version_msg, return_parsed=True) # make sure it's the same variant as us eg SLP, CS, '' regular, etc.. if yes: - v_me = self._my_version()[:-1] + v_me = cls._my_version()[:-1] v_server = parsed[:-1] return v_server > v_me return False @@ -343,7 +336,7 @@ def do_check(self, force=False): if not self.active_req: self.last_checked_ts = time.time() self._update_view() - self.active_req = _Req(self, self.is_test_run) + self.active_req = _Req(self) def did_check_recently(self, secs=10.0): return time.time() - self.last_checked_ts < secs @@ -395,21 +388,12 @@ class _Req(threading.Thread, PrintError): """ url = RELEASES_JSON_URL - local_path = os.path.join( - os.path.dirname(__file__), - "..", - "..", - "contrib", - "update_checker", - "releases.json", - ) - def __init__(self, checker, is_test_run: bool): + def __init__(self, checker): super().__init__(daemon=True) self.checker = checker self.aborted = False self.json = None - self.is_test_run = is_test_run self.start() def abort(self): @@ -421,8 +405,7 @@ def diagnostic_name(self): def run(self): self.checker._dl_prog.emit(self, 10) try: - source = self.url if not self.is_test_run else self.local_path - self.print_error("Requesting from", source, "...") + self.print_error("Requesting from", self.url, "...") self.json, self.url = self._do_request(self.url) self.checker._dl_prog.emit(self, 100) except Exception: @@ -434,18 +417,6 @@ def run(self): self.checker._req_finished.emit(self) def _do_request(self, url): - if self.is_test_run: - # Fetch the data in the local repository to test the signature - if not os.path.isfile(self.local_path): - raise RuntimeError( - f"{self.local_path} file not found. Did you not run the application" - " from sources?" - ) - with open(self.local_path, "r", encoding="utf-8") as f: - json_data = json.loads(f.read()) - self.print_msg(json_data) - return json_data, self.url - # will raise requests.exceptions.Timeout on timeout response = requests.get(url, allow_redirects=True, timeout=30.0) diff --git a/electrumabc_gui/qt/util.py b/electrumabc_gui/qt/util.py index 98109dafd779..f731cf94eb4f 100644 --- a/electrumabc_gui/qt/util.py +++ b/electrumabc_gui/qt/util.py @@ -25,8 +25,11 @@ QPixmap, ) +from electrumabc.paymentrequest import PR_EXPIRED, PR_PAID, PR_UNCONFIRMED, PR_UNPAID from electrumabc.printerror import PrintError, print_error +from electrumabc.simple_config import SimpleConfig from electrumabc.util import Weak, finalization_print_error +from electrumabc.wallet import AbstractWallet if platform.system() == "Windows": MONOSPACE_FONT = "Consolas" @@ -38,6 +41,13 @@ dialogs = [] +pr_icons = { + PR_UNPAID: ":icons/unpaid.svg", + PR_PAID: ":icons/confirmed.svg", + PR_EXPIRED: ":icons/expired.svg", + PR_UNCONFIRMED: ":icons/unconfirmed.svg", +} + # https://docs.python.org/3/library/gettext.html#deferred-translations def _(message): @@ -630,6 +640,284 @@ def set_csv(v): return gb, filename_e, b1 +class ElectrumItemDelegate(QtWidgets.QStyledItemDelegate): + def createEditor(self, parent, option, index): + return self.parent().createEditor(parent, option, index) + + +class MyTreeWidget(QtWidgets.QTreeWidget): + class SortSpec(namedtuple("SortSpec", "column, qt_sort_order")): + """Used to specify member: default_sort""" + + # Specify this in subclasses to apply a default sort order to the widget. + # If None, nothing is applied (items are presented in the order they are + # added). + default_sort: SortSpec = None + + # Specify this in subclasses to enable substring search/filtering (Ctrl+F) + # (if this and filter_data_columns are both empty, no search is applied) + filter_columns = [] + # Like the above but rather than search the item .text() field, it searches + # for *data* in columns, e.g. item.data(col, Qt.UserRole). The data must + # live in filter_data_role (Qt.UserRole by default) in the specified + # column(s) and be a str. Leave empty to disable this facility. Note that + # data matches for the Ctrl+F filter must be a full string match (no + # substring matching is done) -- this is in contrast to filter_columns + # matching above which does substring matching. This reason we match on full + # strings is this facility was initially added to allow for searching the + # history_list by txid. If this criterion doesn't suit your use-case when + # inheriting from this, you may always override this class's `filter` + # method. + filter_data_columns = [] + # the QTreeWidgetItem data role to use when searching data columns + filter_data_role: int = Qt.UserRole + + edited = pyqtSignal() + + def __init__( + self, + parent: QtWidgets.QWidget, + headers, + config: SimpleConfig, + wallet: AbstractWallet, + stretch_column=None, + editable_columns=None, + *, + deferred_updates=False, + save_sort_settings=False, + ): + QtWidgets.QTreeWidget.__init__(self, parent) + self.config = config + self.wallet = wallet + self.stretch_column = stretch_column + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.setUniformRowHeights(True) + # extend the syntax for consistency + self.addChild = self.addTopLevelItem + self.insertChild = self.insertTopLevelItem + self.deferred_updates = deferred_updates + self.deferred_update_ct, self._forced_update = 0, False + self._save_sort_settings = save_sort_settings + + # Control which columns are editable + self.editor = None + self.pending_update = False + if editable_columns is None: + editable_columns = [stretch_column] + self.editable_columns = editable_columns + self.setItemDelegate(ElectrumItemDelegate(self)) + self.itemDoubleClicked.connect(self.on_doubleclick) + self.update_headers(headers) + self.current_filter = "" + + self._setup_save_sort_mechanism() + + def _setup_save_sort_mechanism(self): + if self._save_sort_settings: + storage = self.wallet.storage + key = f"mytreewidget_default_sort_{type(self).__name__}" + default = (storage and storage.get(key, None)) or self.default_sort + if ( + default + and isinstance(default, (tuple, list)) + and len(default) >= 2 + and all(isinstance(i, int) for i in default) + ): + self.setSortingEnabled(True) + self.sortByColumn(default[0], default[1]) + if storage: + # Paranoia; hold a weak reference just in case subclass code + # does unusual things. + weakStorage = Weak.ref(storage) + + def save_sort(column, qt_sort_order): + storage = weakStorage() + if storage: + storage.put(key, [column, qt_sort_order]) + + self.header().sortIndicatorChanged.connect(save_sort) + elif self.default_sort: + self.setSortingEnabled(True) + self.sortByColumn(self.default_sort[0], self.default_sort[1]) + + def update_headers(self, headers): + self.setColumnCount(len(headers)) + self.setHeaderLabels(headers) + self.header().setStretchLastSection(False) + for col in range(len(headers)): + sm = ( + QtWidgets.QHeaderView.Stretch + if col == self.stretch_column + else QtWidgets.QHeaderView.ResizeToContents + ) + self.header().setSectionResizeMode(col, sm) + + def editItem(self, item, column): + if item and column in self.editable_columns: + self.editing_itemcol = (item, column, item.text(column)) + # Calling setFlags causes on_changed events for some reason + item.setFlags(item.flags() | Qt.ItemIsEditable) + super().editItem(item, column) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + + def keyPressEvent(self, event): + if event.key() in {Qt.Key_F2, Qt.Key_Return} and self.editor is None: + item, col = self.currentItem(), self.currentColumn() + if item and col > -1: + self.on_activated(item, col) + else: + super().keyPressEvent(event) + + def permit_edit(self, item, column): + return column in self.editable_columns and self.on_permit_edit(item, column) + + def on_permit_edit(self, item, column): + return True + + def on_doubleclick(self, item, column): + if self.permit_edit(item, column): + self.editItem(item, column) + + def on_activated(self, item, column): + # on 'enter' we show the menu + pt = self.visualItemRect(item).bottomLeft() + pt.setX(50) + self.customContextMenuRequested.emit(pt) + + def createEditor(self, parent, option, index): + self.editor = QtWidgets.QStyledItemDelegate.createEditor( + self.itemDelegate(), parent, option, index + ) + self.editor.editingFinished.connect(self.editing_finished) + return self.editor + + def editing_finished(self): + # Long-time QT bug - pressing Enter to finish editing signals + # editingFinished twice. If the item changed the sequence is + # Enter key: editingFinished, on_change, editingFinished + # Mouse: on_change, editingFinished + # This mess is the cleanest way to ensure we make the + # on_edited callback with the updated item + if self.editor: + (item, column, prior_text) = self.editing_itemcol + if self.editor.text() == prior_text: + self.editor = None # Unchanged - ignore any 2nd call + elif item.text(column) == prior_text: + pass # Buggy first call on Enter key, item not yet updated + else: + # What we want - the updated item + self.on_edited(*self.editing_itemcol) + self.editor = None + + # Now do any pending updates + if self.editor is None and self.pending_update: + self.pending_update = False + self.on_update() + self.deferred_update_ct = 0 + + def on_edited(self, item, column, prior): + """Called only when the text actually changes""" + key = item.data(0, Qt.UserRole) + text = item.text(column) + self.wallet.set_label(key, text) + self.edited.emit() + + def should_defer_update_incr(self): + ret = self.deferred_updates and not self.isVisible() and not self._forced_update + if ret: + self.deferred_update_ct += 1 + return ret + + def update(self): + # Defer updates if editing + if self.editor: + self.pending_update = True + else: + # Deferred update mode won't actually update the GUI if it's + # not on-screen, and will instead update it the next time it is + # shown. This has been found to radically speed up large wallets + # on initial synch or when new TX's arrive. + if self.should_defer_update_incr(): + return + self.setUpdatesEnabled(False) + scroll_pos_val = ( + self.verticalScrollBar().value() + ) # save previous scroll bar position + self.on_update() + self.deferred_update_ct = 0 + weakSelf = Weak.ref(self) + + def restoreScrollBar(): + slf = weakSelf() + if slf: + slf.updateGeometry() + slf.verticalScrollBar().setValue( + scroll_pos_val + ) # restore scroll bar to previous + slf.setUpdatesEnabled(True) + + QTimer.singleShot( + 0, restoreScrollBar + ) # need to do this from a timer some time later due to Qt quirks + if self.current_filter: + self.filter(self.current_filter) + + def on_update(self): + # Reimplemented in subclasses + pass + + def showEvent(self, e): + super().showEvent(e) + if e.isAccepted() and self.deferred_update_ct: + self._forced_update = True + self.update() + self._forced_update = False + # self.deferred_update_ct will be set right after on_update is called because some subclasses use @rate_limiter on the update() method + + def get_leaves(self, root=None): + if root is None: + root = self.invisibleRootItem() + child_count = root.childCount() + if child_count == 0: + if root is not self.invisibleRootItem(): + yield root + else: + return + for i in range(child_count): + item = root.child(i) + for x in self.get_leaves(item): + yield x + + def filter(self, p): + columns = self.__class__.filter_columns + data_columns = self.__class__.filter_data_columns + if not columns and not data_columns: + return + p = p.lower() + self.current_filter = p + bad_data_column = False + data_role = self.__class__.filter_data_role + for item in self.get_leaves(self.invisibleRootItem()): + no_match_text = all( + item.text(column).lower().find(p) == -1 for column in columns + ) + no_match_data = True + if no_match_text and not bad_data_column and data_columns: + try: + # data matching is different -- it must match exactly the + # specified search string. This was originally designed + # to allow for tx-hash searching of the history list. + no_match_data = all( + item.data(column, data_role).strip().lower() != p + for column in data_columns + ) + except (AttributeError, TypeError, ValueError): + # flag so we don't keep raising for each iteration of this + # loop. Programmer error here in subclass, silently ignore. + bad_data_column = True + item.setHidden(no_match_text and no_match_data) + + class OverlayControlMixin: STYLE_SHEET_COMMON = """ QPushButton { border-width: 1px; padding: 0px; margin: 0px; } @@ -933,7 +1221,7 @@ class RateLimiter(PrintError): # some defaults last_ts = 0.0 timer = None - saved_args = ((), {}) + saved_args = (tuple(), dict()) ctr = 0 def __init__(self, rate, ts_after, obj, func): @@ -1034,7 +1322,7 @@ def _pop_args(self): # grab the latest collated invocation's args. this attribute is always defined. args, kwargs = self.saved_args # clear saved args immediately - self.saved_args = ((), {}) + self.saved_args = (tuple(), dict()) return args, kwargs def _push_args(self, args, kwargs): @@ -1129,7 +1417,7 @@ def _push_args(self, args, kwargs): def _pop_args(self): weak_dict = self.saved_args self.saved_args = Weak.KeyDictionary() - return (weak_dict,), {} + return (weak_dict,), dict() def _call_func_for_all(self, weak_dict): for ref in weak_dict.keyrefs(): diff --git a/electrumabc_gui/qt/utxo_list.py b/electrumabc_gui/qt/utxo_list.py index b3f4a60c2cd8..f58b530a72c7 100644 --- a/electrumabc_gui/qt/utxo_list.py +++ b/electrumabc_gui/qt/utxo_list.py @@ -44,8 +44,13 @@ from .avalanche.proof_editor import AvaProofDialog from .consolidate_coins_dialog import ConsolidateCoinsWizard -from .tree_widget import MyTreeWidget -from .util import MONOSPACE_FONT, ColorScheme, SortableTreeWidgetItem, rate_limited +from .util import ( + MONOSPACE_FONT, + ColorScheme, + MyTreeWidget, + SortableTreeWidgetItem, + rate_limited, +) if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -108,6 +113,7 @@ def __init__(self, main_window: ElectrumWindow): ] MyTreeWidget.__init__( self, + main_window, columns, config=main_window.config, wallet=main_window.wallet, @@ -497,7 +503,7 @@ def dump_utxo(self, utxos: List[dict]): return if not fileName.endswith(".json") and not fileName.endswith(".JSON"): fileName += ".json" - with open(fileName, "w", encoding="utf-8") as outfile: + with open(fileName, "w") as outfile: json.dump(utxos_for_json, outfile) def _open_consolidate_coins_dialog(self, addr): diff --git a/electrumabc_gui/stdio.py b/electrumabc_gui/stdio.py index 633348546f86..ad34b2a6f6bd 100644 --- a/electrumabc_gui/stdio.py +++ b/electrumabc_gui/stdio.py @@ -180,11 +180,12 @@ def print_contact(contact): self.print_list(messages, "%-20s %-45s " % ("Name", "Address")) def print_addresses(self): - messages = ( - f'{str(addr):>30} {self.wallet.labels.get(addr, ""):>30} ' - for addr in self.wallet.get_addresses() + messages = map( + lambda addr: "%30s %30s " + % (addr, self.wallet.labels.get(addr, "")), + self.wallet.get_addresses(), ) - self.print_list(messages, " Address Label ") + self.print_list(messages, "%19s %25s " % ("Address", "Label")) def print_order(self): print( diff --git a/electrumabc_gui/text.py b/electrumabc_gui/text.py index 3fa6bfd07b6b..1e3fd9d51a07 100644 --- a/electrumabc_gui/text.py +++ b/electrumabc_gui/text.py @@ -220,14 +220,12 @@ def print_contact(contact): self.print_list(messages, "%-20s %-45s " % ("Name", "Address")) def print_addresses(self): - def fmt_address_line(address: str, label: str): - return f"{address:<45} {label:<30}" - - messages = [ - fmt_address_line(str(addr), self.wallet.labels.get(addr, "")) - for addr in self.wallet.get_addresses() - ] - self.print_list(messages, fmt_address_line("Address", "Label")) + fmt = "%-45s %-30s" + messages = map( + lambda addr: fmt % (addr, self.wallet.labels.get(addr, "")), + self.wallet.get_addresses(), + ) + self.print_list(messages, fmt % ("Address", "Label")) def print_edit_line(self, y, label, text, index, size): text += " " * (size - len(text)) @@ -478,7 +476,7 @@ def show_message(self, message, getchar=True): def run_popup(self, title, items): return self.run_dialog( title, - [{"type": "button", "label": x} for x in items], + list(map(lambda x: {"type": "button", "label": x}, items)), interval=1, y_pos=self.pos + 3, ) diff --git a/electrumabc_plugins/README.rst b/electrumabc_plugins/README.rst new file mode 100644 index 000000000000..60c7ccb48897 --- /dev/null +++ b/electrumabc_plugins/README.rst @@ -0,0 +1,196 @@ +Electrum ABC - Plugins +====================== + +The plugin system of Electrum ABC is designed to allow the development +of new features without increasing the core code of Electrum ABC. + +Electrum ABC is written in pure python. if you want to add a feature +that requires non-python libraries, then it must be submitted as a +plugin. If the feature you want to add requires communication with +a remote server (not an Electrum ABC server), then it should be a +plugin as well. If the feature you want to add introduces new +dependencies in the code, then it should probably be a plugin. + +There are two types of plugins supported by Electrum ABC. The first is the +internal plugin, currently offered under "Optional Features" in the Tools +menu of the QT client. The second is the external plugin, which the user +has to manually install, currently managed under "Installed Plugins" in the +Tools menu of the QT client. + +At this time, there is no documentation for the plugin API. What API there +is, mostly consists of some limited hooks which provide notification on events +and a base class which provides the basic plugin integration. + +**WARNING:** The plugin API will be upgraded at some point. Plugins will +be required to specify their release plugin API version, and those that +predate it will be assumed to be version 0. This will be used to first +deprecate, and then over time, remove the existing API as plugin +developers transition to any replacement API. Plenty of time and warning +will be given before any removal, allowing plugin developers to upgrade. +Given the extremely limited state of the Electrum plugin API we inherited, +this may even reduce maintenance requirements +as internals a plugin developer makes use of can easily get changed +in the course of normal development. + +Risks and Dangers +================= + +Plugins, like Electrum ABC, are written in pure Python, in the form of +PythonPackages_. This means they can access almost all of Electron +Cash's state, and change any behaviour, perhaps even in dishonest ways +you might not even notice at first. They might even use Python's file +system access, or other similar functionality, to damage whatever else +they can access. + +If you plan to install plugins, you should ensure that they are fully vetted +by someone trustworthy, and do so at your own risk, only installing them from +official locations. + +If you plan to develop a plugin, it is in your best interest to get it +reviewed by someone a plugin user knows and trusts, before releasing it, +in order to have provable safety for potential users as a feature. + +.. _PythonPackages: https://docs.python.org/3/tutorial/modules.html#packages + +Types of Plugin +=============== + +Optional features (internal plugins) are included with Electrum ABC, and are +available to all users of Electrum ABC to enable and disable as they wish. +They cannot be uninstalled, and no installation functionality is provided +either. + +User installable plugins (external plugins) are not included with Electron +Cash. The user must use the Plugin Manager to install these, through the +user interface. In the QT UI, this is accessed through the Tools menu. The +process of installation includes both warnings and required user confirmations +that they accept the risks installing them incurs. + +Internal Plugin Rules +===================== + +- We expect plugin developers to maintain their plugin code. However, + once a plugin is merged in Electrum ABC, we will have to maintain it + too, because changes in the Electrum ABC code often require updates in + the plugin code. Therefore, plugins have to be easy to maintain. If + we believe that a plugin will create too much maintenance work in + the future, it will be rejected. + +- Plugins should be compatible with Electrum ABC's conventions. If your + plugin does not fit with Electrum ABC's architecture, or if we believe + that it will create too much maintenance work, it will not be + accepted. In particular, do not duplicate existing Electrum ABC code in + your plugin. + +- We may decide to remove a plugin after it has been merged in + Electrum ABC. For this reason, a plugin must be easily removable, + without putting at risk the user's bitcoins. If we feel that a + plugin cannot be removed without threatening users who rely on it, + we will not merge it. + +External Plugins +================ + +At this time, external plugins must be developed in the same way as an +internal plugin. It might be that this can be done by placing a symbolic link +to your plugin's Python package directory, in the ``plugins`` directory within the +clone of the Electrum ABC source you are developing within. + +Please be sure that you test your plugin with the same recommended version of +Python for the version of Electrum ABC you intend to specify in your +plugin's minimum Electrum ABC version. Not doing so, will cause you pain +and potential users to avoid your plugin. + +Packaging The Hard Way +---------------------- + +Once your plugin is ready for use by other users, you can package it for them +so they can take advantage of the easy methods of plugin installation available +in the QT user interface (drag and drop onto the plugin manager, or click +``Add Plugin`` and select the plugin zip archive). + +An external plugin must be constructed in the form of a zip archive that is +acceptable to the Python ``zipimport`` module. Within this archive must be two +things: + +- The ``manifest.json`` file which provides plugin metadata. +- The Python package directory that contains your plugin code. + +It is recommended that your Python package directory contain precompiled +Python bytecode files. Python includes +the `compileall module ` +within it's standard library which can do this from the command line. This +is because ``zipimport`` does not support writing these back into the zip archive +which encapulates your packaged plugin. + +The ``manifest.json`` file has required fields: + +- ``display_name``: This is the name of your plugin. +- ``version``: This is the version of your plugin. Only numeric versions of the + form ``.`` (e.g. ``1.0``) or ``..`` + (e.g. ``1.0.1``) are supported. +- ``project_url``: This is the official URL of your project. +- ``description``: A longer form description of how your plugin upgrades + Electrum ABC. +- ``minimum_ec_version``: This is the earliest version of Electrum ABC + which your plugin is known to work with. This will not be ``3.2`` or lower + as the external plugin functionality only arrived after that version. +- ``package_name``: This is the name of the Python package directory at the + top level of the zip archive, which contains your plugin code. It is + necessary to formally specify this, to spare Electrum ABC confusion in + the case of advanced plugin packages which contain multiple Python + packages, or even looking around to distinguish between the one Python + package and other data directories. +- ``available_for``: This is a list of keywords which map to supported + Electrum ABC plugin interfaces. Valid values to include are ``qt`` and + ``cmdline``. + +If you do not include these fields in your manifest file, then the user will +see an error message when they try and install it. + +Example ``manifest.json`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: json + + { + "display_name": "Scheduled Payments", + "version": "1.0", + "project_url": "https://github.com/rt121212121/electron_cash_scheduled_payments_plugin", + "description": "This allows a user to specify recurring payments to a number of recipients.", + "minimum_ec_version": "3.2", + "package_name": "scheduled_payments", + "available_for": [ + "qt" + ] + } + +The Easy Way +------------ + +In the ``contrib`` directory of the Electrum ABC source tree, you can find a script +named ``package_plugin.py``. Execute this script with the command-line +``py -3 package_plugin.py``. You must have ``PyQT5`` installed, which you will have +if you are developing against a clone of the GIT repository. + +A window will be displayed with fields for all the required manifest fields, and +when they have valid values, will allow you to generate the package zip archive +automatically. This will create a zip archive with sha256 checksum which any +user can then drag into their Electrum ABC wallet's plugin manager, to +almost immediately install and run (sure they have to check a barrage of warnings +about the damage you could do to them). + +Advanced Python Packaging +------------------------- + +With a bit of thought a user can bundle additional supporting Python packages, +or even binary data like icons, into their plugin archive. + +It is not possible to import Python extension modules (.pyd, .dll, .so, etc) +from within a ``ziparchive`` "mounted zip archive". + +If you need to extract data from the archive, to make use of it, please contact +the Electrum ABC developers to work out a standard way to do so, so that if +a user uninstalls your plugin, the extracted data can also be removed. For this +initial external plugin feature release, this level of functionality is not +officially supported or recommended. diff --git a/electrumabc_plugins/audio_modem/qt.py b/electrumabc_plugins/audio_modem/qt.py index bfd4a101dfda..693291742826 100644 --- a/electrumabc_plugins/audio_modem/qt.py +++ b/electrumabc_plugins/audio_modem/qt.py @@ -63,7 +63,7 @@ def settings_dialog(self, window): layout = QtWidgets.QGridLayout(d) layout.addWidget(QtWidgets.QLabel(_("Bit rate [kbps]: ")), 0, 0) - bitrates = sorted(amodem.config.bitrates.keys()) + bitrates = list(sorted(amodem.config.bitrates.keys())) def _index_changed(index): bitrate = bitrates[index] diff --git a/electrumabc_plugins/cosigner_pool/qt.py b/electrumabc_plugins/cosigner_pool/qt.py index b1624fc22a32..42d8424ff257 100644 --- a/electrumabc_plugins/cosigner_pool/qt.py +++ b/electrumabc_plugins/cosigner_pool/qt.py @@ -330,7 +330,7 @@ def _find_window_and_state_for_wallet(self, wallet): def cosigner_can_sign(self, tx, cosigner_xpub): from electrumabc.keystore import is_xpubkey, parse_xpubkey - xpub_set = set() + xpub_set = set([]) for txin in tx.inputs(): for x_pubkey in txin["x_pubkeys"]: if is_xpubkey(x_pubkey): diff --git a/electrumabc_plugins/digitalbitbox/digitalbitbox.py b/electrumabc_plugins/digitalbitbox/digitalbitbox.py index 34dc7f8bfb8e..87e0a19e8185 100644 --- a/electrumabc_plugins/digitalbitbox/digitalbitbox.py +++ b/electrumabc_plugins/digitalbitbox/digitalbitbox.py @@ -313,7 +313,7 @@ def mobile_pairing_dialog(self): return try: - with open(os.path.join(dbb_user_dir, "config.dat"), encoding="utf-8") as f: + with open(os.path.join(dbb_user_dir, "config.dat")) as f: dbb_config = json.load(f) except (FileNotFoundError, json.JSONDecodeError): return diff --git a/electrumabc_plugins/fusion/covert.py b/electrumabc_plugins/fusion/covert.py index 30d0ec8fb888..e4db571c8afe 100644 --- a/electrumabc_plugins/fusion/covert.py +++ b/electrumabc_plugins/fusion/covert.py @@ -66,25 +66,26 @@ class Unrecoverable(FusionError): def is_tor_port(host, port): if not 0 <= port < 65536: return False - - if not hasattr(socket, "_socketobject"): + try: socketclass = socket.socket - else: - # socket.socket could be monkeypatched (see electrumabc/network.py), - # in which case we need to get the real one. - socketclass = socket._socketobject - - ret = False - with socketclass(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(0.1) try: - s.connect((host, port)) - # Tor responds uniquely to HTTP-like requests - s.send(b"GET\n") - ret = b"Tor is not an HTTP Proxy" in s.recv(1024) - except socket.error: + # socket.socket could be monkeypatched (see electrumabc/network.py), + # in which case we need to get the real one. + socketclass = socket._socketobject + except AttributeError: pass - return ret + s = socketclass(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.1) + s.connect((host, port)) + # Tor responds uniquely to HTTP-like requests + s.send(b"GET\n") + if b"Tor is not an HTTP Proxy" in s.recv(1024): + s.close() + return True + s.close() + except socket.error: + pass + return False class TorLimiter: @@ -214,12 +215,12 @@ def __init__( if tor_host is None or tor_port is None: self.proxy_opts = None else: - self.proxy_opts = { - "proxy_type": socks.SOCKS5, - "proxy_addr": tor_host, - "proxy_port": tor_port, - "proxy_rdns": True, - } + self.proxy_opts = dict( + proxy_type=socks.SOCKS5, + proxy_addr=tor_host, + proxy_port=tor_port, + proxy_rdns=True, + ) # The timespan (in s) used for randomizing action times on established # connections. Each connection chooses a delay between 0 and randspan, @@ -393,7 +394,7 @@ def run_connection(self, covconn, conn_time, rand_delay, connect_timeout): proxy_opts = None else: unique = f"CF{self.randtag}_{covconn.conn_number}" - proxy_opts = {"proxy_username": unique, "proxy_password": unique} + proxy_opts = dict(proxy_username=unique, proxy_password=unique) proxy_opts.update(self.proxy_opts) limiter.bump() try: diff --git a/electrumabc_plugins/fusion/fusion.py b/electrumabc_plugins/fusion/fusion.py index 3af5172bf8a6..8b0310ff2f68 100644 --- a/electrumabc_plugins/fusion/fusion.py +++ b/electrumabc_plugins/fusion/fusion.py @@ -314,8 +314,8 @@ def __init__( self.tor_host = tor_host self.tor_port = tor_port - self.coins = {} # full input info - self.keypairs = {} + self.coins = dict() # full input info + self.keypairs = dict() self.outputs = [] # for detecting spends (and finally unfreezing coins) we remember for each wallet: # - which coins we have from that wallet ("txid:n"), @@ -370,15 +370,13 @@ def add_coins_from_wallet(self, wallet, password, coins): )[:20] xpubkeys_set = set() for c in coins: - address_history = wallet.get_address_history(c["address"]) - received = wallet.get_address_unspent(c["address"], address_history) - wallet.add_input_info(c, received) + wallet.add_input_info(c) (xpubkey,) = c["x_pubkeys"] xpubkeys_set.add(xpubkey) # get private keys and convert x_pubkeys to real pubkeys - keypairs = {} - pubkeys = {} + keypairs = dict() + pubkeys = dict() for xpubkey in xpubkeys_set: derivation = wallet.keystore.get_pubkey_derivation(xpubkey) privkey = wallet.keystore.get_private_key(derivation, password) @@ -396,8 +394,8 @@ def add_coins_from_wallet(self, wallet, password, coins): } self.add_coins(coindict, keypairs) - coinstrs = {t + ":" + str(i) for t, i in coindict} - txids = {t for t, i in coindict} + coinstrs = set(t + ":" + str(i) for t, i in coindict) + txids = set(t for t, i in coindict) self.source_wallet_info[wallet][0].update(coinstrs) self.source_wallet_info[wallet][1].update(txids) wallet.set_frozen_coin_state(coinstrs, True, temporary=True) @@ -647,7 +645,7 @@ def allocate_outputs( # outputs. # Calculate the number of distinct inputs as the number of distinct pubkeys # (i.e. extra inputs from same address don't count as distinct) - num_distinct = len({pub for (_, _), (pub, _) in self.inputs}) + num_distinct = len(set(pub for (_, _), (pub, _) in self.inputs)) min_outputs = max(MIN_TX_COMPONENTS - num_distinct, 1) if max_outputs < min_outputs: raise FusionError( diff --git a/electrumabc_plugins/fusion/plugin.py b/electrumabc_plugins/fusion/plugin.py index 5cd1800b9bb8..f9feb8973077 100644 --- a/electrumabc_plugins/fusion/plugin.py +++ b/electrumabc_plugins/fusion/plugin.py @@ -503,9 +503,9 @@ def add_wallet(self, wallet, password=None): # fusions that were auto-started. wallet._fusions_auto = weakref.WeakSet() # caache: stores a map of txid -> fusion_depth (or False if txid is not a fuz tx) - wallet._cashfusion_is_fuz_txid_cache = {} + wallet._cashfusion_is_fuz_txid_cache = dict() # cache: stores a map of address -> fusion_depth if the address has fuz utxos - wallet._cashfusion_address_cache = {} + wallet._cashfusion_address_cache = dict() # all accesses to the above must be protected by wallet.lock if Conf(wallet).autofuse: @@ -1022,12 +1022,12 @@ def fusion_server_stop(self, daemon, config): def fusion_server_status(self, daemon, config): if not self.fusion_server: return "fusion server not running" - return { - "poolsizes": { + return dict( + poolsizes={ t: len(pool.pool) for t, pool in self.fusion_server.waiting_pools.items() } - } + ) @daemon_command def fusion_server_fuse(self, daemon, config): diff --git a/electrumabc_plugins/fusion/qt.py b/electrumabc_plugins/fusion/qt.py index 9d27d182d2cd..b35f1335baca 100644 --- a/electrumabc_plugins/fusion/qt.py +++ b/electrumabc_plugins/fusion/qt.py @@ -1337,15 +1337,14 @@ def torport_update(self, goodport): self.le_tor_port.setText(sport) if goodport is None: self.l_tor_status.setPixmap(self.pm_bad_proxy) - # TODO: switch from %-string to str.format (update also translation files) if autoport: self.l_tor_status.setToolTip( _("Cannot find a Tor proxy on ports %(ports)s.") - % {"ports": TOR_PORTS} + % dict(ports=TOR_PORTS) ) else: self.l_tor_status.setToolTip( - _("Cannot find a Tor proxy on port %(port)d.") % {"port": port} + _("Cannot find a Tor proxy on port %(port)d.") % dict(port=port) ) else: self.l_tor_status.setToolTip(_("Found a valid Tor proxy on this port.")) @@ -1441,8 +1440,8 @@ def __init__(self, parent, plugin, wallet): self.wallet = wallet self.conf = Conf(self.wallet) - self.idx2confkey = {} # int -> 'normal', 'consolidate', etc.. - self.confkey2idx = {} # str 'normal', 'consolidate', etc -> int + self.idx2confkey = dict() # int -> 'normal', 'consolidate', etc.. + self.confkey2idx = dict() # str 'normal', 'consolidate', etc -> int assert not hasattr(self.wallet, "_cashfusion_settings_window") main_window = self.wallet.weak_window() @@ -2041,7 +2040,7 @@ def __init__(self, plugin): def refresh(self): tree = self.t_active_fusions - reselect_fusions = {i.data(0, Qt.UserRole)() for i in tree.selectedItems()} + reselect_fusions = set(i.data(0, Qt.UserRole)() for i in tree.selectedItems()) reselect_fusions.discard(None) reselect_items = [] tree.clear() @@ -2063,9 +2062,9 @@ def create_menu_active_fusions(self, position): if not selected: return - fusions = {i.data(0, Qt.UserRole)() for i in selected} + fusions = set(i.data(0, Qt.UserRole)() for i in selected) fusions.discard(None) - statuses = {f.status[0] for f in fusions} + statuses = set(f.status[0] for f in fusions) selection_of_1_fusion = list(fusions)[0] if len(fusions) == 1 else None has_live = "running" in statuses or "waiting" in statuses diff --git a/electrumabc_plugins/fusion/server.py b/electrumabc_plugins/fusion/server.py index dc81174de7b8..5a74b49158e2 100644 --- a/electrumabc_plugins/fusion/server.py +++ b/electrumabc_plugins/fusion/server.py @@ -221,7 +221,7 @@ def __init__(self, fill_threshold, tag_max): self.pool = ( set() ) # clients who will be put into fusion round if started at this tier - self.queue = [] # clients who are waiting due to tags being full + self.queue = list() # clients who are waiting due to tags being full self.tags = defaultdict(TagStatus) # how are the various tags self.fill_threshold = fill_threshold # minimum number of pool clients to trigger setting fill_time self.fill_time = None # when did pool exceed fill_threshold @@ -508,7 +508,7 @@ def new_client_job(self, client): tnow = time.monotonic() # scan through tiers and collect statuses, also check start times. - statuses = {} + statuses = dict() tfill_thresh = tnow - Params.start_time_max for t, pool in mytierpools.items(): if client not in pool.pool: @@ -719,7 +719,7 @@ def client_start(c, collector): Params.num_components, ) - newhashes = {m.salted_component_hash for m in commit_messages} + newhashes = set(m.salted_component_hash for m in commit_messages) with lock: expected_len = len(seen_salthashes) + len(newhashes) seen_salthashes.update(newhashes) @@ -876,7 +876,7 @@ def client_start(c, collector): ) # mark all missing-signature components as bad. - bad_inputs = {i for i, sig in enumerate(signatures) if sig is None} + bad_inputs = set(i for i, sig in enumerate(signatures) if sig is None) # further, search for duplicated inputs (through matching the prevout and claimed pubkey). prevout_spenders = defaultdict(list) @@ -992,7 +992,7 @@ def client_get_proofs(client, collector): ) # Now, repackage the proofs according to destination. - proofs_to_relay = [[] for _ in self.clients] + proofs_to_relay = [list() for _ in self.clients] for src_client, relays in results: for proof, src_commitment_idx, dest_client_idx, dest_key_idx in relays: proofs_to_relay[dest_client_idx].append( @@ -1009,11 +1009,7 @@ def client_get_blames(client, myindex, proofs, collector): client.send( pb.TheirProofsList( proofs=[ - { - "encrypted_proof": x, - "src_commitment_idx": y, - "dst_key_idx": z, - } + dict(encrypted_proof=x, src_commitment_idx=y, dst_key_idx=z) for x, y, z, _ in proofs ] ) @@ -1028,7 +1024,9 @@ def client_get_blames(client, myindex, proofs, collector): # making us check many inputs against blockchain. if len(msg.blames) > len(proofs): client.error("too many blames") - if len({blame.which_proof for blame in msg.blames}) != len(msg.blames): + if len(set(blame.which_proof for blame in msg.blames)) != len( + msg.blames + ): client.error("multiple blames point to same proof") # Note, the rest of this function might run for a while if many @@ -1165,7 +1163,7 @@ def __init__(self, bindhost, port=0, upnp=None): self.round_pubkey = None def start_components(self, round_pubkey, feerate): - self.components = {} + self.components = dict() self.feerate = feerate self.round_pubkey = round_pubkey for c in self.spawned_clients: diff --git a/electrumabc_plugins/fusion/tests/__main__.py b/electrumabc_plugins/fusion/tests/__main__.py new file mode 100644 index 000000000000..395e3d9ee06d --- /dev/null +++ b/electrumabc_plugins/fusion/tests/__main__.py @@ -0,0 +1,16 @@ +import unittest + +from . import test_encrypt, test_pedersen + + +def suite(): + test_suite = unittest.TestSuite() + loadTests = unittest.defaultTestLoader.loadTestsFromTestCase + test_suite.addTest(loadTests(test_encrypt.TestNormal)) + test_suite.addTest(loadTests(test_pedersen.TestNormal)) + test_suite.addTest(loadTests(test_pedersen.TestBadSetup)) + return test_suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/electrumabc_plugins/fusion/util.py b/electrumabc_plugins/fusion/util.py index 11c899690f89..de4f3bd21a60 100644 --- a/electrumabc_plugins/fusion/util.py +++ b/electrumabc_plugins/fusion/util.py @@ -161,18 +161,18 @@ def tx_from_components(all_components, session_hash): if len(inp.prev_txid) != 32: raise FusionError("bad component prevout") inputs.append( - { - "address": Address.from_P2PKH_hash(hash_160(inp.pubkey)), - "prevout_hash": inp.prev_txid[::-1].hex(), - "prevout_n": inp.prev_index, - "num_sig": 1, - "signatures": [None], - "type": "p2pkh", - "x_pubkeys": [inp.pubkey.hex()], - "pubkeys": [inp.pubkey.hex()], - "sequence": 0xFFFFFFFF, - "value": inp.amount, - } + dict( + address=Address.from_P2PKH_hash(hash_160(inp.pubkey)), + prevout_hash=inp.prev_txid[::-1].hex(), + prevout_n=inp.prev_index, + num_sig=1, + signatures=[None], + type="p2pkh", + x_pubkeys=[inp.pubkey.hex()], + pubkeys=[inp.pubkey.hex()], + sequence=0xFFFFFFFF, + value=inp.amount, + ) ) input_indices.append(i) elif ctype == "output": diff --git a/electrumabc_plugins/hw_wallet/cmdline.py b/electrumabc_plugins/hw_wallet/cmdline.py index 695662455757..3bf7ed69f449 100644 --- a/electrumabc_plugins/hw_wallet/cmdline.py +++ b/electrumabc_plugins/hw_wallet/cmdline.py @@ -26,7 +26,7 @@ def get_pin(self, msg): print_msg(msg) print_msg("a b c\nd e f\ng h i\n-----") o = raw_input() - return "".join(t[x] for x in o) + return "".join(map(lambda x: t[x], o)) def prompt_auth(self, msg): import getpass diff --git a/electrumabc_plugins/keepkey/keepkey.py b/electrumabc_plugins/keepkey/keepkey.py index 96d4ec257cf5..4869be584ed6 100644 --- a/electrumabc_plugins/keepkey/keepkey.py +++ b/electrumabc_plugins/keepkey/keepkey.py @@ -456,9 +456,9 @@ def f(x_pubkey): pubkeys = list(map(f, x_pubkeys)) multisig = self.types.MultisigRedeemScriptType( pubkeys=pubkeys, - signatures=( - bytes.fromhex(x)[:-1] if x else b"" - for x in txin.get("signatures") + signatures=map( + lambda x: bfh(x)[:-1] if x else b"", + txin.get("signatures"), ), m=txin.get("num_sig"), ) diff --git a/scripts/block_headers b/scripts/block_headers index 6a9991c413d5..29761b753c70 100755 --- a/scripts/block_headers +++ b/scripts/block_headers @@ -8,7 +8,7 @@ import time from electrumabc.network import Network from electrumabc.printerror import print_msg from electrumabc.simple_config import SimpleConfig -from electrumabc.json_util import json_encode +from electrumabc.util import json_encode def callback(response): diff --git a/scripts/get_history b/scripts/get_history index 3eb82e65910a..6b101e26f6dc 100755 --- a/scripts/get_history +++ b/scripts/get_history @@ -5,7 +5,7 @@ import sys from electrumabc.address import Address from electrumabc.network import Network from electrumabc.printerror import print_msg -from electrumabc.json_util import json_encode +from electrumabc.util import json_encode try: addr = sys.argv[1] diff --git a/scripts/util.py b/scripts/util.py index 807cf30b4d1a..86287e3999bf 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -41,7 +41,7 @@ def wait_on_interfaces(interfaces, timeout=10): result = defaultdict(list) timeout = time.time() + timeout while len(result) < len(interfaces) and time.time() < timeout: - rin = list(interfaces.values()) + rin = [i for i in interfaces.values()] win = [i for i in interfaces.values() if i.unsent_requests] rout, wout, xout = select.select(rin, win, [], 1) for interface in wout: diff --git a/scripts/watch_address b/scripts/watch_address index 2564b693ec10..c3e7e8731e5e 100755 --- a/scripts/watch_address +++ b/scripts/watch_address @@ -7,7 +7,7 @@ from electrumabc.address import Address from electrumabc.network import Network from electrumabc.printerror import print_msg from electrumabc.simple_config import SimpleConfig -from electrumabc.json_util import json_encode +from electrumabc.util import json_encode def callback(response): diff --git a/setup.py b/setup.py index aa49de9d4ea7..1b0e84a3e2a9 100755 --- a/setup.py +++ b/setup.py @@ -11,16 +11,16 @@ import setuptools.command.sdist from setuptools import setup -with open("README.md", "r", encoding="utf-8") as f: +with open("README.rst", "r", encoding="utf-8") as f: long_description = f.read() -with open("contrib/requirements/requirements.txt", encoding="utf-8") as f: +with open("contrib/requirements/requirements.txt") as f: requirements = f.read().splitlines() -with open("contrib/requirements/requirements-hw.txt", encoding="utf-8") as f: +with open("contrib/requirements/requirements-hw.txt") as f: requirements_hw = f.read().splitlines() -with open("contrib/requirements/requirements-binaries.txt", encoding="utf-8") as f: +with open("contrib/requirements/requirements-binaries.txt") as f: requirements_binaries = f.read().splitlines() # We use this convoluted way of importing version.py and constants.py @@ -53,7 +53,7 @@ def get_constants(): if platform.system() in ["Linux", "FreeBSD", "DragonFly"]: parser = argparse.ArgumentParser(add_help=False) - parser.add_argument("--user", dest="is_user", action="store_true") + parser.add_argument("--user", dest="is_user", action="store_true", default=False) parser.add_argument("--system", dest="is_user", action="store_false", default=False) parser.add_argument("--root=", dest="root_path", metavar="dir", default="/") parser.add_argument(