├── .gitignore ├── .gitlab-ci.yml ├── .pre-commit-config.yaml ├── AUTHORS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── doc ├── argos.md ├── build_with_pyinstaller.md ├── how-to_automate_transactions_and_multi-output.md ├── install_build.md ├── install_pip.md ├── install_poetry.md └── test_and_coverage.md ├── licence-G1 ├── .gitignore ├── .gitlab-ci.yml ├── README.md ├── license │ ├── license_g1-en.rst │ └── license_g1-fr-FR.rst └── package.json ├── logo ├── silkaj_logo.png └── silkaj_logo.svg ├── poetry.lock ├── pyproject.toml ├── release.sh ├── release_notes ├── images │ ├── poetry-logo.svg │ └── silkaj_pipeline.png └── v0.8.md ├── silkaj ├── __init__.py ├── auth.py ├── blockchain_tools.py ├── blocks.py ├── cert.py ├── checksum.py ├── cli.py ├── cli_tools.py ├── commands.py ├── constants.py ├── crypto_tools.py ├── license.py ├── membership.py ├── money.py ├── net.py ├── network_tools.py ├── tools.py ├── tui.py ├── tx.py ├── tx_history.py └── wot.py ├── tests ├── patched │ ├── auth.py │ ├── blockchain_tools.py │ ├── money.py │ ├── test_constants.py │ ├── tools.py │ ├── tx.py │ ├── tx_history.py │ └── wot.py ├── test_auth.py ├── test_checksum.py ├── test_cli.py ├── test_crypto_tools.py ├── test_end_to_end.py ├── test_membership.py ├── test_money.py ├── test_network_tools.py ├── test_tui.py ├── test_tx.py ├── test_tx_history.py ├── test_unit_cert.py ├── test_unit_tx.py ├── test_verify_blocks.py └── test_wot.py └── update_copyright_year.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Python 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # Idea 63 | .idea 64 | 65 | # Vim swap files 66 | *~ 67 | *.swp 68 | *.swo 69 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - checks 3 | - tests 4 | - publish 5 | - coverage 6 | 7 | variables: 8 | DOCKER_IMAGE: "registry.duniter.org/docker/python3/poetry" 9 | PYTHON_VERSION: "3.7" 10 | 11 | image: $DOCKER_IMAGE/$PYTHON_VERSION:latest 12 | 13 | .code_changes: 14 | rules: 15 | - changes: 16 | - silkaj/*.py 17 | - tests/*.py 18 | 19 | .changes: 20 | rules: 21 | - changes: 22 | - silkaj/*.py 23 | - tests/*.py 24 | - .gitlab-ci.yml 25 | - pyproject.toml 26 | - poetry.lock 27 | 28 | build: 29 | extends: .changes 30 | stage: checks 31 | script: 32 | - poetry build 33 | 34 | format: 35 | extends: .code_changes 36 | stage: checks 37 | image: $DOCKER_IMAGE/3.8:latest 38 | script: 39 | - black --check silkaj tests 40 | 41 | .tests: 42 | extends: .changes 43 | stage: tests 44 | image: $DOCKER_IMAGE/$PYTHON_VERSION:latest 45 | script: 46 | - poetry install 47 | - poetry run pytest 48 | 49 | tests-3.6: 50 | extends: .tests 51 | tags: [mille] 52 | variables: 53 | PYTHON_VERSION: "3.6" 54 | 55 | tests-3.7-coverage: 56 | extends: .tests 57 | tags: [mille] 58 | script: 59 | - poetry install 60 | - poetry run pytest --cov silkaj --cov-report html:cov_html 61 | - poetry run coverage-badge -o cov_html/coverage.svg 62 | artifacts: 63 | paths: 64 | - cov_html 65 | expire_in: 2 days 66 | 67 | tests-3.8: 68 | extends: .tests 69 | tags: [redshift] 70 | variables: 71 | PYTHON_VERSION: "3.8" 72 | 73 | tests-3.9: 74 | extends: .tests 75 | tags: [redshift] 76 | variables: 77 | PYTHON_VERSION: "3.9" 78 | 79 | pypi_test: 80 | stage: publish 81 | rules: 82 | - if: $CI_COMMIT_TAG 83 | when: manual 84 | script: 85 | - poetry config repositories.pypi_test https://test.pypi.org/legacy/ 86 | - poetry publish --build --username $PYPI_TEST_LOGIN --password $PYPI_TEST_PASSWORD --repository pypi_test 87 | 88 | pypi: 89 | stage: publish 90 | rules: 91 | - if: $CI_COMMIT_TAG 92 | when: manual 93 | script: 94 | - poetry publish --build --username $PYPI_LOGIN --password $PYPI_PASSWORD 95 | 96 | pages: 97 | extends: .changes 98 | needs: [tests-3.7-coverage] 99 | rules: 100 | - if: $CI_COMMIT_BRANCH == "dev" 101 | stage: coverage 102 | script: mv cov_html/ public/ 103 | artifacts: 104 | paths: 105 | - public 106 | expire_in: 2 days 107 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors and contributors 2 | 3 | - Moul, Maël Azimi 4 | - Tortue 5 | - cebash, Sébastien DA ROCHA 6 | - Matograine 7 | - atrax, 8 | - cgeek, Cédric Moreau 9 | - jytou, Jean-Yves Toumit 10 | - Bernard 11 | - ManUtopiK, Emmanuel Salomon 12 | - mmuman, François Revol 13 | - MrNem, Pi Nguyen 14 | - vtexier, Vincent Texier 15 | - vincentux, 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Goals 4 | Part of the Duniter project running the Ğ1 currency, Silkaj project is aiming to create a generic tool to manage his account and wallets, and to monitor the currency. 5 | 6 | ## Install the development environment 7 | We are using [Poetry](https://poetry.eustace.io/) as a development environment solution. Start [installing Poetry](doc/install_poetry.md). 8 | This will install a sandboxed Python environment. 9 | Dependencies will be installed in it in order to have Silkaj running and to have pre-installed developement tools. 10 | 11 | ## Workflow 12 | - We use branches for merge requests 13 | - We prefer fast-forward and rebase method than having a merge commit. This in order to have a clean history. 14 | 15 | ## Branches 16 | - `master` branch as stable 17 | - maintainance branches, to maintain a stable version while developing future version with breaking changes. For instance: `0.7` 18 | - `dev` branch 19 | 20 | ## Developing with DuniterPy 21 | [DuniterPy](https://git.duniter.org/clients/python/duniterpy) is a Python library for Duniter clients. 22 | It implements a client with multiple APIs, the handling for document signing. 23 | As it is coupled with Silkaj, it is oftenly needed to develop in both repositories. 24 | 25 | ### How to use DuniterPy as editable with Poetry 26 | Clone DuniterPy locally alongside of `silkaj` repository: 27 | 28 | ```bash 29 | silkaj> cd .. 30 | git clone https://git.duniter.org/clients/python/duniterpy 31 | ``` 32 | 33 | Use DuniterPy as a [path dependency](https://poetry.eustace.io/docs/versions/#path-dependencies): 34 | ```bash 35 | poetry add ../duniterpy 36 | ``` 37 | 38 | ## Formatting 39 | We are using [Black](https://github.com/python/black) formatter tool. 40 | To have Black installed in your Poetry virtualenv, you will need Python v3.6 or greater. 41 | Run Black on a Python file to format it: 42 | ```bash 43 | poetry run black silkaj/cli.py 44 | ``` 45 | 46 | ### Pre-commit 47 | Then, you can use the `pre-commit` tool to check staged changes before committing. 48 | To do so, you need to run `pre-commit install` to install the git hook. 49 | Black is called on staged files, so commit should fail in case black made changes. 50 | You will have to add Black changes in order to commit your changes. 51 | 52 | ## Tests 53 | We are using [Pytest](https://pytest.org) as a tests framework. To know more about how Silkaj implement it read the [project test documentation](doc/test_and_coverage.md). 54 | 55 | To run tests, within `silkaj` repository: 56 | ```bash 57 | poetry run pytest 58 | ``` 59 | 60 | ### How to test a single file 61 | Specifiy the path of the test: 62 | ```bash 63 | poetry run pytest tests/test_end_to_end.py 64 | ``` 65 | 66 | ## Version update 67 | We are using the [Semantic Versioning](https://semver.org). 68 | 69 | To create a release, we use following script which will update the version in different files, and will make a commit and a tag out of it. 70 | ```bash 71 | ./release.sh 0.8.1 72 | ``` 73 | 74 | Then, a `git push --tags` is necessary to publish the tag. Git could be configured to publish tags with a simple `push`. 75 | 76 | ## PyPI and PyPI test distributions 77 | Silkaj is distributed to PyPI, the Python Package Index, for further `pip` installation. 78 | Silkaj can be published to [PyPI](https://pypi.org/project/silkaj) or to [PyPI test](https://test.pypi.org/project/silkaj/) for testing purposes. 79 | Publishing to PyPI or PyPI test can be directly done from the continuous delivery or from Poetry it-self. 80 | The CD jobs does appear on a tag and have to be triggered manually. 81 | Only the project maintainers have the rights to publish tags. 82 | 83 | ### PyPI 84 | Publishing to PyPI from Poetry: 85 | ```bash 86 | poetry publish --build 87 | ``` 88 | ### PyPI test 89 | Publishing to PyPI test from Poetry: 90 | ```bash 91 | poetry config repositories.pypi_test https://test.pypi.org/legacy/ 92 | poetry publish --build --repository pypi_test 93 | ``` 94 | 95 | To install this package: 96 | ```bash 97 | pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.python.org/simple/ silkaj 98 | ``` 99 | 100 | The `--extra-index-url` is used to retrieve dependencies packages from the official PyPI not to get issues with missing or testing dependencies comming from PyPI test repositories. 101 | 102 | ## Continuous integration and delivery 103 | ### Own built Docker images 104 | - https://git.duniter.org/docker/python3/poetry 105 | - Python images based on Debian Buster 106 | - Poetry installed on top 107 | - Black installed on v3.8 108 | 109 | ### Jobs 110 | - Checks: 111 | - Format 112 | - Build 113 | - Tests on supported Python versions: 114 | - Installation 115 | - Pytest for v3.6, 3.7, and 3.8 116 | - PyPI distribution 117 | - test 118 | - stable 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silkaj 2 | [![Version](https://img.shields.io/pypi/v/silkaj.svg)](https://pypi.python.org/pypi/silkaj) [![License](https://img.shields.io/pypi/l/silkaj.svg)](https://pypi.python.org/pypi/silkaj) [![Python versions](https://img.shields.io/pypi/pyversions/silkaj.svg)](https://pypi.python.org/pypi/silkaj) 3 | 4 | - CLI Duniter client written with Python 3. 5 | - [Website](https://silkaj.duniter.org) 6 | 7 | ## Install 8 | ```bash 9 | pip3 install silkaj --user 10 | ``` 11 | 12 | - [Install with Pip](doc/install_pip.md) 13 | - [Install the Development environment](doc/install_poetry.md) 14 | - [Install with the build](doc/install_build.md) 15 | - [Build an executable with Pyinstaller](doc/build_with_pyinstaller.md) 16 | 17 | ## Usage 18 | - Get help usage with `-h` or `--help` options, then run: 19 | ```bash 20 | silkaj 21 | ``` 22 | 23 | - Will automatically request and post data on `duniter.org 443` main Ğ1 node. 24 | 25 | - Specify a custom node with `-p` option: 26 | ```bash 27 | silkaj -p
: 28 | ``` 29 | 30 | ## Features 31 | ### Currency information 32 | - Currency information 33 | - Display the current Proof of Work difficulty level to generate the next block 34 | - Check the current network 35 | - Explore the blockchain block by block 36 | 37 | ### Money management 38 | - Send transaction 39 | - Consult the wallet balance 40 | 41 | ### Web-of-Trust management 42 | - Check sent and received certifications and consult the membership status of any given identity in the Web of Trust 43 | - Check the present currency information stand 44 | - Send certification 45 | 46 | ### Authentication 47 | - Three authentication methods: Scrypt, file, and (E)WIF 48 | 49 | ## Wrappers 50 | - [Install as a drop-down for GNOME Shell with Argos](doc/argos.md) 51 | - [How-to: automate transactions and multi-output](doc/how-to_automate_transactions_and_multi-output.md) 52 | - [Transaction generator written in Shell](https://gitlab.com/jytou/tgen) 53 | - [Ğ1Cotis](https://git.duniter.org/matograine/g1-cotis) 54 | - [G1pourboire](https://git.duniter.org/matograine/g1pourboire) 55 | - [Ğ1SMS](https://git.duniter.org/clients/G1SMS/) 56 | - [Ğmixer](https://git.duniter.org/tuxmain/gmixer-py/) 57 | 58 | ### Dependencies 59 | Silkaj is based on Python dependencies: 60 | 61 | - [Click](https://click.palletsprojects.com/): Command Line Interface Creation Kit. 62 | - [DuniterPy](https://git.duniter.org/clients/python/duniterpy/): Python APIs library to implement duniter clients softwares. 63 | - [Tabulate](https://bitbucket.org/astanin/python-tabulate/overview): to display charts. 64 | 65 | ### Names 66 | I wanted to call that program: 67 | - bamiyan 68 | - margouillat 69 | - lsociety 70 | - cashmere 71 | 72 | I finally called it `Silkaj` as `Silk` in esperanto. 73 | 74 | ### Website 75 | - [Silkaj website sources](https://git.duniter.org/websites/silkaj_website/) 76 | 77 | ## Packaging status 78 | [![Packaging status](https://repology.org/badge/vertical-allrepos/silkaj.svg)](https://repology.org/project/silkaj/versions) 79 | -------------------------------------------------------------------------------- /doc/argos.md: -------------------------------------------------------------------------------- 1 | ## Install as a drop-down for GNOME Shell with Argos 2 | Under GNOME Shell, with [Argos](https://github.com/p-e-w/argos) extension: 3 | 4 | - [Install Argos](https://github.com/p-e-w/argos#installation) 5 | - Inside `~/.config/argos/silkaj.30s.sh` put: 6 | 7 | ```bash 8 | #!/usr/bin/env bash 9 | /path/to/silkaj/silkaj argos 10 | ``` 11 | 12 | Add execution permission: 13 | ```bash 14 | chmod u+x ~/.config/argos/silkaj.30s.sh 15 | ``` 16 | 17 | Argos will run the script every 30 seconds. 18 | -------------------------------------------------------------------------------- /doc/build_with_pyinstaller.md: -------------------------------------------------------------------------------- 1 | # Build with Pyinstaller 2 | 3 | ## Install Pyinstaller 4 | ```bash 5 | pip install pyinstaller 6 | ``` 7 | 8 | If you are using Pyenv, don’t forget to save pyinstaller install: 9 | ```bash 10 | pyenv rehash 11 | ``` 12 | 13 | ## Build 14 | ```bash 15 | pyinstaller bin/silkaj --hidden-import=_cffi_backend --hidden-import=_scrypt --onefile 16 | ``` 17 | 18 | You will found the exetuable file on `dist` folder. 19 | -------------------------------------------------------------------------------- /doc/how-to_automate_transactions_and_multi-output.md: -------------------------------------------------------------------------------- 1 | # How-to: automate transactions and multi-output 2 | 3 | Once silkaj installed. We want to be able to send a transaction to many recipients and to automate that process. 4 | 5 | Tutoriel based on this [forum post (fr)](https://forum.duniter.org/t/silkaj-installation-virements-automatiques-et-multi-destinataires/4836). 6 | 7 | ### Create a recipient file 8 | You have to create a list of the public keys addresses to the recipients you wants to send money. 9 | 10 | > Example: here, we want to send money to Duniter’s pubkeys contributors. 11 | 12 | Create a `recipients.txt` file containing the list of the recipients keys. You can gather them with `silkaj id `. 13 | 14 | > Example: 15 | > 16 | >```txt 17 | >2ny7YAdmzReQxAayyJZsyVYwYhVyax2thKcGknmQy5nQ 18 | >FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J 19 | >D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx 20 | >HbTqJ1Ts3RhJ8Rx4XkNyh1oSKmoZL1kY5U7t9mKTSjAB 21 | >38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE 22 | >5cnvo5bmR8QbtyNVnkDXWq6n5My6oNLd1o6auJApGCsv 23 | >GfKERHnJTYzKhKUma5h1uWhetbA8yHKymhVH2raf2aCP 24 | >7F6oyFQywURCACWZZGtG97Girh9EL1kg2WBwftEZxDoJ 25 | >CRBxCJrTA6tmHsgt9cQh9SHcCc8w8q95YTp38CPHx2Uk 26 | >2sZF6j2PkxBDNAqUde7Dgo5x3crkerZpQ4rBqqJGn8QT 27 | >4FgeWzpWDQ2Vp38wJa2PfShLLKXyFGRLwAHA44koEhQj 28 | >55oM6F9ZE2MGi642GGjhCzHhdDdWwU6KchTjPzW7g3bp 29 | >BH8ZqCsp4sbHeDPPHpto53ukLLA4oMy4fXC5JpLZtB2f 30 | >``` 31 | 32 | ### Create an authentication file 33 | To process automated transactions, Silkaj needs to have an authentication file allowing to spent money. 34 | 35 | In order to create this file, with your wallet’s secret credentials, run following command: 36 | 37 | ```bash 38 | silkaj authfile 39 | Please enter your Scrypt Salt (Secret identifier): 40 | Please enter your Scrypt password (masked): 41 | Using default values. Scrypt parameters not specified or wrong format 42 | Scrypt parameters used: N: 4096, r: 16, p: 1 43 | Authentication file 'authfile' generated and stored in current folder for following public key: 44 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 45 | ``` 46 | 47 | ### Run the transaction 48 | Finally, you just have to run following command: 49 | 50 | ```bash 51 | silkaj --auth-file tx --amountUD 20 --output `cat recipients.txt | tr '\n' ':' | sed -e 's/:*$//'` 52 | ``` 53 | 54 | Note: Each pubkey will receive 20 UD. 55 | 56 | ### Automated transaction 57 | If you want to automate a transaction on each first day of the month, you can set a `crontab` on your machine (preferably a server running 7/24): 58 | 59 | ```bash 60 | 0 0 1 * * silkaj --auth-file tx --yes --amountUD 20 --output `cat recipients.txt | tr '\n' ':' | sed -e 's/:*$//'` 61 | ``` 62 | 63 | Note: the `--yes` option won’t prompt a confirmation. 64 | -------------------------------------------------------------------------------- /doc/install_build.md: -------------------------------------------------------------------------------- 1 | ## Install with the realease bundle 2 | 3 | Build are built on Fedora, and only works on Fedora for now. 4 | 5 | ### Download 6 | Download [last release](https://git.duniter.org/clients/python/silkaj/tags) with `wget`. 7 | 8 | 9 | ### Integrity check 10 | Check it's integrity comparing it to `silkaj_sha256sum`: 11 | ```bash 12 | sha256sum silkaj 13 | ``` 14 | 15 | ### Add executable permissions: 16 | ```bash 17 | chmod a+x silkaj 18 | ``` 19 | -------------------------------------------------------------------------------- /doc/install_pip.md: -------------------------------------------------------------------------------- 1 | # Install Silkaj with Pip 2 | 3 | You have to use a shell on GNU/Linux or a command tool (`cmd.exe`) on Windows. 4 | 5 | ## GNU/Linux 6 | The system must use UTF-8 locales… 7 | 8 | ### Install libsodium 9 | 10 | ```bash 11 | sudo apt install libsodium23 # Debian Buster 12 | sudo apt install libsodium18 # Debian Stretch 13 | sudo dnf install libsodium # Fedora 14 | ``` 15 | 16 | ### Install dependencies before installing 17 | 18 | ```bash 19 | sudo apt install python3-pip libssl-dev 20 | ``` 21 | 22 | On Ubuntu (14.04 and 16.04) and Debian 8, you need this package too: 23 | ```bash 24 | sudo apt install libffi-dev 25 | ``` 26 | 27 | Raspbian and Linux Mint are reported to require the installation of this package too: 28 | ```bash 29 | sudo apt install python3-dev 30 | ``` 31 | 32 | ### Completing `PATH` 33 | 34 | After intallation, if you get a `bash: silkaj: command not found` error, you should add `~/.local/bin` to your `PATH`: 35 | ```bash 36 | echo "export PATH=$PATH:$HOME/.local/bin" >> $HOME/.bashrc 37 | source $HOME/.bashrc 38 | ``` 39 | 40 | ## macOS 41 | To install Python, run the following command: 42 | ```bash 43 | brew install python3 44 | ``` 45 | 46 | `pip3` will automatically be installed with `python3` installation. 47 | 48 | Then, install Silkaj with: 49 | ```bash 50 | pip3 install silkaj --user 51 | ``` 52 | 53 | ## Windows 54 | 55 | ### Administrator rights 56 | Please note that the administrator rights might be mandatory for some of these operations. 57 | 58 | ### The `PATH` variable 59 | 60 | The main issue on Windows is about finding where are the requested files. 61 | 62 | Python must be installed (version 3.5 minimum). For instance https://sourceforge.net/projects/winpython/ 63 | 64 | You can test that Python is available by opening a command tool (cmd.exe) and running: 65 | ```bash 66 | C:\>python --version 67 | Python 3.6.7 68 | ``` 69 | 70 | When installing Python, take care to specify the good folder (for instance: `C:\WPy-3670`) 71 | 72 | After the installation, you commonly have to add by yourself this folder in the `PATH` environment variable: 73 | 74 | To make it by command tool (cmd.exe): 75 | ```bash 76 | set PATH=%PATH%;C:\WPy-3670\ 77 | ``` 78 | Then you have to exit the cmd tool so that `PATH` variable can be updated internally. 79 | 80 | You may right click on computer, then go to Advanced System Parameters and use in the bottom the button Environment Variables. 81 | 82 | In order to be able to use silkaj and specifically the OpenSSL binaries, you also have to add the following folder to the `PATH` variable: 83 | C:\WPy-3670\python-3.6.7.amd64\Lib\site-packages\PyQt5\Qt\bin\ 84 | 85 | ```bash 86 | set PATH=%PATH%;C:\WPy-3670\python-3.6.7.amd64\Lib\site-packages\PyQt5\Qt\bin\ 87 | ``` 88 | 89 | ### Creating the command file `silkaj.bat` 90 | 91 | In order to be able to launch silkaj as a Windows command, you have to create a file `silkaj.bat` in the following folder: 92 | `C:\WPy-3670\python-3.6.7.amd64\Scripts\silkaj.bat` 93 | 94 | containing: 95 | ```bash 96 | rem @echo off 97 | python "%~dpn0" %* 98 | ``` 99 | 100 | and then to add this folder into the `PATH` variable: 101 | ```bash 102 | set PATH=%PATH%;C:\WPy-3670\python-3.6.7.amd64\Scripts\ 103 | ``` 104 | 105 | ## Install directly from internet (implicitely uses binaries from website Pypi) 106 | 107 | Assuming that Python v3 and pip version 3 are installed and available. You can check with: 108 | ```bash 109 | pip3 --version 110 | ``` 111 | 112 | ### Install for all users 113 | 114 | ```bash 115 | pip3 install silkaj 116 | ``` 117 | 118 | ### Install for current user only 119 | 120 | ```bash 121 | pip3 install silkaj --user 122 | ``` 123 | 124 | ### Upgrade 125 | 126 | ```bash 127 | pip3 install silkaj --user --upgrade 128 | ``` 129 | 130 | ### Uninstall (useful to see the real paths) 131 | 132 | ```bash 133 | pip3 uninstall silkaj --user 134 | ``` 135 | 136 | ### Check silkaj is working 137 | 138 | ```bash 139 | silkaj info 140 | ``` 141 | 142 | --- 143 | 144 | ## Install from original sources from the forge 145 | 146 | ### Retrieve silkaj sources on linux 147 | ```bash 148 | sudo apt install git 149 | git clone https://git.duniter.org/clients/python/silkaj.git 150 | cd silkaj 151 | ``` 152 | 153 | ### Retrieve silkaj sources on Windows 154 | 155 | First, you have to install the git tool from https://git-scm.com/download/win 156 | 157 | Then change directory to where you want to download the sources, for instance: 158 | ```bash 159 | cd ~/appdata/Roaming/ 160 | ``` 161 | 162 | Then download Silkaj sources (`dev` branch) with git tool 163 | which will create a folder silkaj 164 | 165 | ```bash 166 | git clone https://git.duniter.org/clients/python/silkaj.git 167 | ``` 168 | 169 | Then change directory to the downloaded folder 170 | ```bash 171 | cd ~/appdata/Roaming/silkaj 172 | ``` 173 | 174 | ### Install 175 | 176 | After being sure you have changed directory to the downloaded folder 177 | ```bash 178 | pip3 install . 179 | ``` 180 | 181 | Or install it as "editable", for development: 182 | ```bash 183 | pip3 install -e . 184 | ``` 185 | -------------------------------------------------------------------------------- /doc/install_poetry.md: -------------------------------------------------------------------------------- 1 | ## Install Silkaj in a development environement with Poetry 2 | 3 | ### Install libsodium 4 | 5 | ```bash 6 | sudo apt install libsodium23 # Debian Buster 7 | sudo apt install libsodium18 # Debian Stretch 8 | sudo dnf install libsodium # Fedora 9 | ``` 10 | 11 | ### Install Poetry 12 | - [Installation documentation](https://poetry.eustace.io/docs/#installation) 13 | 14 | ### On Debian Buster 15 | ```bash 16 | sudo apt install python3-pip python3-venv 17 | sudo apt install libffi-dev # Required on ARMbian 18 | pip3 install poetry --user 19 | ``` 20 | 21 | ### Install dependencies and the Python environment 22 | ```bash 23 | git clone https://git.duniter.org/clients/python/silkaj 24 | cd silkaj 25 | poetry install 26 | ``` 27 | 28 | ### Run Silkaj 29 | Within `silkaj` repository run Silkaj: 30 | ```bash 31 | poetry run silkaj 32 | ``` 33 | 34 | You might need to enter Poetry shell to access development tools such as `pytest` or `black`. 35 | 36 | ### Make Silkaj accessible from everywhere 37 | 38 | Add following alias to your shell configuration: 39 | ```bash 40 | alias silkaj="cd /path/to/silkaj/silkaj && poetry run silkaj" 41 | ``` 42 | -------------------------------------------------------------------------------- /doc/test_and_coverage.md: -------------------------------------------------------------------------------- 1 | ## Test and coverage 2 | 3 | ### Install tests dependencies 4 | 5 | Using pipenv: 6 | ``` 7 | pipenv install --dev 8 | ``` 9 | 10 | ### Runing tests: 11 | 12 | Simply run: 13 | ``` 14 | pytest 15 | ``` 16 | 17 | To have a coverage report: 18 | ``` 19 | pytest --cov silkaj --cov-report html:cov_html 20 | ``` 21 | 22 | Where: 23 | * `--cov silkaj` option generates coverage data on the silkaj package 24 | * `--cov-report html:cov_html` generates a browsable report of coverage in cov\_html dir. You can omit this if you just want coverage data to be generated 25 | 26 | See [pytest documentation](https://docs.pytest.org/en/latest/usage.html) for more information 27 | 28 | 29 | ### Writing tests 30 | 31 | There should be 3 kinds of test: 32 | * end to end test: uses the real data and the real blockchain. Obviously don't presume the data value as it can change. These test are written in tests/test\_end\_to\_end.py. 33 | * integration test: mock some of the input and/or output classes and shouldn't use the actual blockchain, you should use this when mocking a class (used by your code) is too complicated. 34 | * unit test: for functions that don't need mock or mock can me done easily (you should prefer this to integration tests). Are written in tests/test\_unit\_*package*.py 35 | 36 | You should try to write an end to end test first, then if your coverage too bad add some unit tests. If it's still too bad, write an integration test. 37 | 38 | A better strategy (TDD) is to write first the End to end test. When it fails, before writing the code, you should implement the unit tests. When this one fails too, you can write your code to make your test pass. It's better but takes longer and the code is tested at least twice. So the previous strategy is a better compromise 39 | 40 | ### Tips 41 | 42 | Test an Exception is raised: https://docs.pytest.org/en/latest/assert.html#assertions-about-expected-exceptions 43 | 44 | Test a function with several values: You can use pytest.mark.parametrize as done in tests/test\_unit\_tx.py 45 | 46 | To mock a user input: 47 | 48 | ```python 49 | from unittest.mock import patch 50 | 51 | from silkaj.cert import certification_confirmation 52 | 53 | 54 | # this will add a mock_input parameter that will be used whenever the code tries to get input from user 55 | @patch('builtins.input') 56 | def test_certification_confirmation(mock_input): 57 | id_to_certify = {"pubkey": "pubkeyid to certify"} 58 | main_id_to_certify = {"uid": "id to certify"} 59 | 60 | # the input will return "yes" to the tested function (certification_confirmation) 61 | mock_input.return_value = "yes" 62 | 63 | # ensure the tested function returns something 64 | assert certification_confirmation( 65 | "certifier id", 66 | "certifier pubkey", 67 | id_to_certify, 68 | main_id_to_certify) 69 | 70 | # ensure that input is called once 71 | mock_input.assert_called_once() 72 | ``` 73 | -------------------------------------------------------------------------------- /licence-G1/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | Thumbs.db 3 | -------------------------------------------------------------------------------- /licence-G1/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # inspiré de : https://forum.duniter.org/t/doppler-gitlab/3183/30 2 | # GITHUB_URL_AND_KEY should look like https://duniter-gitlab:TOKEN@github.com/chemin/vers/ton/depot/github 3 | mirror_to_github: 4 | script: 5 | - git remote add github $GITHUB_URL_AND_KEY 6 | - git config --global user.email "contact@duniter.org" 7 | - git config --global user.name "Duniter" 8 | - git push --force --mirror github 9 | npm: 10 | script: 11 | - echo TODO 12 | only: 13 | tags 14 | -------------------------------------------------------------------------------- /licence-G1/README.md: -------------------------------------------------------------------------------- 1 | # Licence Ǧ1 ([fr](license/license_g1-fr-FR.rst)) ([en](license/license_g1-en.rst)) 2 | 3 | Dernière version : (consulter la licence) 4 | 5 | // TODO : import depuis npm, version npm publié automatiquement selon les tags 6 | -------------------------------------------------------------------------------- /licence-G1/license/license_g1-en.rst: -------------------------------------------------------------------------------- 1 | License Ğ1 - v0.2.5 2 | =================== 3 | 4 | :date: 2017-08-21 16:59 5 | :modified: 2018-01-24 19:20 6 | 7 | **Money licensing and liability commitment.** 8 | 9 | Any certification operation of a new member of Ğ1 must first be accompanied by the transmission of this license of the currency Ğ1 whose certifier must ensure that it has been studied, understood and accepted by the person who will be certified. 10 | 11 | Money Ğ1 12 | -------- 13 | 14 | Ğ1 occurs via a Universal Dividend (DU) for any human member, which is of the form: 15 | 16 | * 1 DU per person per day 17 | 18 | The amount of DU is identical each day until the next equinox, where the DU will then be reevaluated according to the formula: 19 | 20 | * DU day (the following equinox) = DU (equinox) + c² (M / N) (equinox) / (15778800 seconds) 21 | 22 | With as parameters: 23 | 24 | * c = 4.88% / equinox 25 | * UD (0) = 10.00 Ğ1 26 | 27 | And as variables: 28 | 29 | * _M_ the total monetary mass at the equinox 30 | * _N_ the number of members at the equinox 31 | 32 | Web of Trust Ğ1 (WoT Ğ1) 33 | ------------------------ 34 | 35 | **Warning:** Certifying is not just about making sure you've met the person, it's ensuring that the community Ğ1 knows the certified person well enough and Duplicate account made by a person certified by you, or other types of problems (disappearance ...), by cross-checking that will reveal the problem if necessary. 36 | 37 | When you are a member of Ğ1 and you are about to certify a new account: 38 | 39 | **You are assured:** 40 | 41 | 1°) The person who declares to manage this public key (new account) and to have personally checked with him that this is the public key is sufficiently well known (not only to know this person visually) that you are about to certify. 42 | 43 | 2a°) To meet her physically to make sure that it is this person you know who manages this public key. 44 | 45 | 2b°) Remotely verify the public person / key link by contacting the person via several different means of communication, such as social network + forum + mail + video conference + phone (acknowledge voice). 46 | 47 | Because if you can hack an email account or a forum account, it will be much harder to imagine hacking four distinct means of communication, and mimic the appearance (video) as well as the voice of the person . 48 | 49 | However, the 2 °) is preferable to 3 °, whereas the 1 °) is always indispensable in all cases. 50 | 51 | 3 °) To have verified with the person concerned that he has indeed generated his Duniter account revocation document, which will enable him, if necessary, to cancel his account (in case of account theft, ID, an incorrectly created account, etc.). 52 | 53 | **Abbreviated WoT rules:** 54 | 55 | Each member has a stock of 100 possible certifications, which can only be issued at the rate of 1 certification / 5 days. 56 | 57 | Valid for 2 months, certification for a new member is definitively adopted only if the certified has at least 4 other certifications after these 2 months, otherwise the entry process will have to be relaunched. 58 | 59 | To become a new member of WoT Ğ1 therefore 5 certifications must be obtained at a distance < 5 of 80% of the WoT sentinels. 60 | 61 | A member of the TdC Ğ1 is sentinel when he has received and issued at least Y [N] certifications where N is the number of members of the TdC and Y [N] = ceiling N ^ (1/5). Examples: 62 | 63 | * For 1024 < N ≤ 3125 we have Y [N] = 5 64 | * For 7776 < N ≤ 16807 we have Y [N] = 7 65 | * For 59049 < N ≤ 100 000 we have Y [N] = 10 66 | 67 | Once the new member is part of the WoT Ğ1 his certifications remain valid for 2 years. 68 | 69 | To remain a member, you must renew your agreement regularly with your private key (every 12 months) and make sure you have at least 5 certifications valid after 2 years. 70 | 71 | Software Ğ1 and license Ğ1 72 | -------------------------- 73 | 74 | The software Ğ1 allowing users to manage their use of Ğ1 must transmit this license with the software and all the technical parameters of the currency Ğ1 and TdC Ğ1 which are entered in block 0 of Ğ1. 75 | 76 | For more details in the technical details it is possible to consult directly the code of Duniter which is a free software and also the data of the blockchain Ğ1 by retrieving it via a Duniter instance or node Ğ1. 77 | 78 | More information on the Duniter Team website [https://www.duniter.org](https://www.duniter.org) 79 | -------------------------------------------------------------------------------- /licence-G1/license/license_g1-fr-FR.rst: -------------------------------------------------------------------------------- 1 | Licence Ğ1 - v0.2.5 2 | =================== 3 | 4 | :date: 2017-04-04 12:59 5 | :modified: 2018-01-24 19:20 6 | 7 | **Licence de la monnaie et engagement de responsabilité.** 8 | 9 | Toute opération de certification d'un nouveau membre de Ğ1 doit préalablement s'accompagner de la transmission de cette licence de la monnaie Ğ1 dont le certificateur doit s'assurer qu'elle a été étudiée, comprise et acceptée par la personne qui sera certifiée. 10 | 11 | Tout événement de rencontre concernant Ğ1 devrait s'accompagner de la transmission de cette licence, qui peut être lue à haute voix, et transmise par tout moyen. 12 | 13 | Toile de confiance Ğ1 (TdC Ğ1) 14 | ------------------------------ 15 | 16 | **Avertissement :** Certifier n'est pas uniquement s'assurer que vous avez rencontré la personne, c'est assurer à la communauté Ğ1 que vous connaissez suffisamment bien la personne certifiée et que vous saurez ainsi la contacter facilement, et être en mesure de repérer un double compte effectué par une personne certifiée par vous-même, ou d'autres types de problèmes (disparition...), en effectuant des recoupements qui permettront de révéler le problème le cas échéant. 17 | 18 | **Conseils fortement recommandés** 19 | 20 | Ne certifiez jamais seul, mais accompagné d'au moins un autre membre de la TdC Ğ1 afin d'éviter toute erreur de manipulation. En cas d'erreur, prévenez immédiatement d'autres membres de la TdC Ğ1. 21 | 22 | Avant toute certification, assurez vous de vérifier si son compte (qu'il soit en cours de validation ou déjà membre) a déjà reçu une ou plusieurs certifications. Le cas échéant demandez des informations pour entrer en contact avec ces autres certifieurs afin de vérifier ensemble que vous connaissez bien la personne concernée par la création du nouveau compte, ainsi que la clé publique correspondante. 23 | 24 | Vérifiez que vos contacts ont bien étudié et compris la licence Ğ1 à jour. 25 | 26 | Si vous vous rendez compte qu'un certifieur effectif ou potentiel du compte concerné ne connaît pas la personne concernée, alertez immédiatement des experts du sujet au sein de vos connaissance de la TdC Ğ1, afin que la procédure de validation soit vérifiée par la TdC Ğ1. 27 | 28 | Lorsque vous êtes membre de la TdC Ğ1 et que vous vous apprêtez à certifier un nouveau compte : 29 | 30 | **Vous êtes vous assuré :** 31 | 32 | 1°) D'avoir bien vérifié avec la personne concernée qu'elle a bien généré son document Duniter de révocation de compte, qui lui permettra le cas échéant de pouvoir annuler son compte (cas d'un vol de compte, d'un changement de ID, d'un compte créé à tort etc.). 33 | 34 | 2°) De suffisamment bien connaître (pas seulement de la connaître "de visu") la personne qui déclare gérer cette clé publique (nouveau compte) et d'avoir personnellement vérifié avec elle qu'il s'agit bien de cette clé publique que vous vous apprêtez à certifier (voir les conseils fortement recommandés ci-dessus pour s'assurer de "bien connaître"). 35 | 36 | 3a°) De la rencontrer physiquement pour vous assurer que c'est bien cette personne que vous connaissez qui gère cette clé publique. 37 | 38 | 3b°) Ou bien de vérifer à distance le lien personne / clé publique en contactant la personne par plusieurs moyens de communication différents, comme réseau social + forum + mail + vidéo conférence + téléphone (reconnaître la voix). Car si l'on peut pirater un compte mail ou un compte forum, il sera bien plus difficile d'imaginer pirater quatre moyens de communication distincts, et imiter l'apparence (vidéo) ainsi que la voix de la personne en plus. 39 | 40 | Le 3a°) restant toutefois préférable au 3b°), tandis que le 1°) et 2°) est toujours indispensable dans tous les cas. 41 | 42 | **Règles abrégées de la TdC :** 43 | 44 | Chaque membre a un stock de 100 certifications possibles, qu'il ne peut émettre qu'au rythme de 1 certification / 5 jours. 45 | 46 | Valable 2 mois, une certification pour un nouveau membre n'est définitivement adoptée que si le certifié possède au moins 4 autres certifications au bout de ces 2 mois, sinon le processus d'entrée devra être relancé. 47 | 48 | Pour devenir un nouveau membre de la TdC Ğ1 il faut donc obtenir 5 certifications et ne pas se trouver à une distance < 5 de 80% des membres référents de la TdC. 49 | 50 | Un membre de la TdC Ğ1 est membre référent lorsqu'il a reçu et émis au moins Y[N] certifications où N est le nombre de membres de la TdC et Y[N] = plafond N^(1/5). Exemples : 51 | 52 | * Pour 1024 < N ≤ 3125 on a Y[N] = 5 53 | * Pour 7776 < N ≤ 16807 on a Y[N] = 7 54 | * pour 59049 < N ≤ 100 000 on a Y[N] = 10 55 | 56 | Une fois que le nouveau membre est partie prenante de la TdC Ğ1 ses certifications restent valables 2 ans. 57 | 58 | Pour rester membre il faut renouveler son accord régulièrement avec sa clé privée (tous les 12 mois) et s'assurer d'avoir toujours au moins 5 certifications valides au delà des 2 ans. 59 | 60 | Monnaie Ğ1 61 | ---------- 62 | 63 | Ğ1 se produit via un Dividende Universel (DU) pour tout être humain membre de la Toile de Confiance Ğ1, qui est de la forme : 64 | 65 | * 1 DU par personne et par jour 66 | 67 | **Code de la monnaie Ğ1** 68 | 69 | Le montant en Ğ1 du DU est identique chaque jour jusqu'au prochain équinoxe où le DU sera alors réévalué selon la formule (avec 1 jour = 86 400 secondes) : 70 | 71 | * DUjour(équinoxe suivant) = DUjour(équinoxe) + c² (M/N)(équinoxe) / (182,625 jours) 72 | 73 | Avec comme paramètres : 74 | 75 | * c = 4,88% / équinoxe 76 | * DU(0) = 10,00 Ğ1 77 | 78 | Et comme variables : 79 | 80 | * *M* la masse monétaire totale à l'équinoxe 81 | * *N* le nombre de membres à l'équinoxe 82 | 83 | Logiciels Ğ1 et licence Ğ1 84 | -------------------------- 85 | 86 | Les logiciels Ğ1 permettant aux utilisateurs de gérer leur utilisation de Ğ1 doivent transmettre cette licence avec le logiciel ainsi que l'ensemble des paramètres techniques de la monnaie Ğ1 et de la TdC Ğ1 qui sont inscrits dans le bloc 0 de Ğ1. 87 | 88 | Pour plus de précisions dans les détails techniques il est possible de consulter directement le code de Duniter qui est un logiciel libre ansi que les données de la blockchain Ğ1 en la récupérant via une instance (ou noeud) Duniter Ğ1. 89 | 90 | Plus d'informations sur le site de l'équipe Duniter https://www.duniter.org 91 | -------------------------------------------------------------------------------- /licence-G1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "licence-g1", 3 | "version": "1.0.0", 4 | "description": "Licence de la Ǧ1", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@git.duniter.org:communication/licence-G1.git" 12 | }, 13 | "keywords": [ 14 | "licence", 15 | "crypto-currency", 16 | "monnaie", 17 | "libre", 18 | "June", 19 | "G1", 20 | "Ǧ1" 21 | ], 22 | "author": "Duniter community", 23 | "license": "ISC" 24 | } 25 | -------------------------------------------------------------------------------- /logo/silkaj_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duniter/silkaj/b1635e06142373ac7f00e0f801118689a1dd77b2/logo/silkaj_logo.png -------------------------------------------------------------------------------- /logo/silkaj_logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "silkaj" 3 | version = "0.10.0dev" 4 | description = "Powerfull, lightweight, and multi-platform command line client written with Python for Duniter’s currencies: Ğ1 and Ğ1-Test." 5 | authors = ["Moul "] 6 | maintainers = ["Moul "] 7 | readme = "README.md" 8 | license = "AGPL-3.0-or-later" 9 | homepage = "https://silkaj.duniter.org" 10 | repository = "https://git.duniter.org/clients/python/silkaj" 11 | documentation = "https://git.duniter.org/clients/python/silkaj/tree/dev/doc" 12 | keywords = ["g1", "duniter", "cryptocurrency", "librecurrency", "RTM"] 13 | 14 | [tool.poetry.dependencies] 15 | python = "^3.6.8" 16 | duniterpy = "0.62.0" 17 | click = "^7.1.2" 18 | tabulate = "^0.8.7" 19 | texttable = "^1.6.3" 20 | pendulum = "^2.1.2" 21 | 22 | [tool.poetry.dev-dependencies] 23 | black = "^20.8b1" 24 | pre-commit = "^2.10.1" 25 | pytest = "^6.0" 26 | pytest-cov = "^2.7" 27 | pytest-asyncio = "^0.14.0" 28 | asynctest = "^0.13.0" 29 | coverage-badge = "^1.0" 30 | pytest-sugar = "^0.9.2" 31 | 32 | [tool.poetry.scripts] 33 | silkaj = "silkaj.cli:cli" 34 | 35 | [build-system] 36 | requires = ["poetry-core>=1.0.0"] 37 | build-backend = "poetry.core.masonry.api" 38 | 39 | classifiers = [ 40 | "Development Status :: 5 - Production/Stable", 41 | "License :: OSI Approved :: GNU Affero General Public License v3", 42 | "Operating System :: OS Independent", 43 | "Environment :: Console", 44 | "Intended Audience :: End Users/Desktop", 45 | "Intended Audience :: Developers", 46 | "Natural Language :: English" 47 | ] 48 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$1 4 | 5 | check_argument_specified() { 6 | if [[ -z $VERSION ]]; then 7 | error_message "You should specify a version number as argument" 8 | fi 9 | } 10 | 11 | check_version_format() { 12 | if [[ ! $VERSION =~ ^[0-9]+.[0-9]+.[0-9]+[0-9A-Za-z]*$ ]]; then 13 | error_message "Wrong format version" 14 | fi 15 | } 16 | 17 | check_branch() { 18 | branch=`git rev-parse --abbrev-ref HEAD` 19 | if [[ "$branch" != "master" ]]; then 20 | error_message "Current branch should be 'master'" 21 | fi 22 | } 23 | 24 | update_version() { 25 | sed -i "s/SILKAJ_VERSION = \".*\"/SILKAJ_VERSION = \"$VERSION\"/" silkaj/constants.py 26 | poetry version "$VERSION" 27 | git diff 28 | } 29 | 30 | commit_tag() { 31 | git commit silkaj/constants.py pyproject.toml -m "v$VERSION" 32 | git tag "v$VERSION" -a -m "$VERSION" 33 | } 34 | 35 | build() { 36 | if [[ -z $VIRTUAL_ENV ]]; then 37 | error_message "Activate silkaj-env" 38 | fi 39 | exec_installed pyinstaller 40 | pyinstaller bin/silkaj --hidden-import=_cffi_backend --hidden-import=_scrypt --onefile 41 | } 42 | 43 | checksum() { 44 | # Generate sha256 checksum file 45 | exec_installed sha256sum 46 | cd dist 47 | sha256sum silkaj > silkaj_sha256sum 48 | } 49 | 50 | exec_installed() { 51 | if [[ ! `command -v $1` ]]; then 52 | error_message "'$1' is not install on your machine" 53 | fi 54 | } 55 | 56 | error_message() { 57 | echo $1 58 | exit 59 | } 60 | 61 | check_argument_specified 62 | check_version_format 63 | #check_branch 64 | update_version 65 | commit_tag 66 | #build 67 | #checksum 68 | #error_message "Build and checksum can be found in 'dist' folder" 69 | -------------------------------------------------------------------------------- /release_notes/images/poetry-logo.svg: -------------------------------------------------------------------------------- 1 | logo-origami -------------------------------------------------------------------------------- /release_notes/images/silkaj_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duniter/silkaj/b1635e06142373ac7f00e0f801118689a1dd77b2/release_notes/images/silkaj_pipeline.png -------------------------------------------------------------------------------- /silkaj/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | name = "silkaj" 19 | -------------------------------------------------------------------------------- /silkaj/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import re 19 | from click import command, option, pass_context, confirm 20 | from getpass import getpass 21 | from pathlib import Path 22 | 23 | from duniterpy.key import SigningKey 24 | from duniterpy.key.scrypt_params import ScryptParams 25 | 26 | # had to import display_pubkey_and_checksum from wot to avoid loop dependency. 27 | from silkaj.wot import display_pubkey_and_checksum 28 | from silkaj.tools import message_exit 29 | from silkaj.constants import PUBKEY_PATTERN 30 | 31 | 32 | SEED_HEX_PATTERN = "^[0-9a-fA-F]{64}$" 33 | PUBSEC_PUBKEY_PATTERN = "pub: ({0})".format(PUBKEY_PATTERN) 34 | PUBSEC_SIGNKEY_PATTERN = "sec: ([1-9A-HJ-NP-Za-km-z]{87,90})" 35 | 36 | 37 | @pass_context 38 | def auth_method(ctx): 39 | if ctx.obj["AUTH_SEED"]: 40 | return auth_by_seed() 41 | if ctx.obj["AUTH_FILE"]: 42 | return auth_by_auth_file() 43 | if ctx.obj["AUTH_WIF"]: 44 | return auth_by_wif() 45 | else: 46 | return auth_by_scrypt() 47 | 48 | 49 | @pass_context 50 | def has_auth_method(ctx): 51 | return ( 52 | ctx.obj["AUTH_SCRYPT"] 53 | or ctx.obj["AUTH_FILE"] 54 | or ctx.obj["AUTH_SEED"] 55 | or ctx.obj["AUTH_WIF"] 56 | ) 57 | 58 | 59 | @command("authfile", help="Generate authentication file") 60 | @option("--file", default="authfile", show_default=True, help="Path file") 61 | def generate_auth_file(file): 62 | key = auth_method() 63 | authfile = Path(file) 64 | pubkey_cksum = display_pubkey_and_checksum(key.pubkey) 65 | if authfile.is_file(): 66 | confirm( 67 | "Would you like to erase " 68 | + file 69 | + " by an authfile corresponding to following pubkey `" 70 | + pubkey_cksum 71 | + "`?", 72 | abort=True, 73 | ) 74 | key.save_seedhex_file(file) 75 | print( 76 | "Authentication file 'authfile' generated and stored in current\ 77 | folder for following public key:", 78 | pubkey_cksum, 79 | ) 80 | 81 | 82 | @pass_context 83 | def auth_by_auth_file(ctx): 84 | """ 85 | Uses an authentication file to generate the key 86 | Authfile can either be: 87 | * A seed in hexadecimal encoding 88 | * PubSec format with public and private key in base58 encoding 89 | """ 90 | file = ctx.obj["AUTH_FILE_PATH"] 91 | authfile = Path(file) 92 | if not authfile.is_file(): 93 | message_exit('Error: the file "' + file + '" does not exist') 94 | filetxt = authfile.open("r").read() 95 | 96 | # two regural expressions for the PubSec format 97 | regex_pubkey = re.compile(PUBSEC_PUBKEY_PATTERN, re.MULTILINE) 98 | regex_signkey = re.compile(PUBSEC_SIGNKEY_PATTERN, re.MULTILINE) 99 | 100 | # Seed hexadecimal format 101 | if re.search(re.compile(SEED_HEX_PATTERN), filetxt): 102 | return SigningKey.from_seedhex_file(file) 103 | # PubSec format 104 | elif re.search(regex_pubkey, filetxt) and re.search(regex_signkey, filetxt): 105 | return SigningKey.from_pubsec_file(file) 106 | else: 107 | message_exit("Error: the format of the file is invalid") 108 | 109 | 110 | def auth_by_seed(): 111 | seedhex = getpass("Please enter your seed on hex format: ") 112 | try: 113 | return SigningKey.from_seedhex(seedhex) 114 | except Exception as error: 115 | message_exit(error) 116 | 117 | 118 | @pass_context 119 | def auth_by_scrypt(ctx): 120 | salt = getpass("Please enter your Scrypt Salt (Secret identifier): ") 121 | password = getpass("Please enter your Scrypt password (masked): ") 122 | 123 | if ctx.obj["AUTH_SCRYPT_PARAMS"]: 124 | n, r, p = ctx.obj["AUTH_SCRYPT_PARAMS"].split(",") 125 | 126 | if n.isnumeric() and r.isnumeric() and p.isnumeric(): 127 | n, r, p = int(n), int(r), int(p) 128 | if n <= 0 or n > 65536 or r <= 0 or r > 512 or p <= 0 or p > 32: 129 | message_exit("Error: the values of Scrypt parameters are not good") 130 | scrypt_params = ScryptParams(n, r, p) 131 | else: 132 | message_exit("one of n, r or p is not a number") 133 | else: 134 | scrypt_params = None 135 | 136 | try: 137 | return SigningKey.from_credentials(salt, password, scrypt_params) 138 | except ValueError as error: 139 | message_exit(error) 140 | 141 | 142 | def auth_by_wif(): 143 | wif_hex = getpass("Enter your WIF or Encrypted WIF address (masked): ") 144 | password = getpass( 145 | "(Leave empty in case WIF format) Enter the Encrypted WIF password (masked): " 146 | ) 147 | try: 148 | return SigningKey.from_wif_or_ewif_hex(wif_hex, password) 149 | except Exception as error: 150 | message_exit(error) 151 | -------------------------------------------------------------------------------- /silkaj/blockchain_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from silkaj.network_tools import ClientInstance 19 | from duniterpy.api.bma import blockchain 20 | 21 | 22 | class BlockchainParams(object): 23 | __instance = None 24 | 25 | def __new__(cls): 26 | if BlockchainParams.__instance is None: 27 | BlockchainParams.__instance = object.__new__(cls) 28 | return BlockchainParams.__instance 29 | 30 | def __init__(self): 31 | self.params = self.get_params() 32 | 33 | async def get_params(self): 34 | client = ClientInstance().client 35 | return await client(blockchain.parameters) 36 | 37 | 38 | class HeadBlock(object): 39 | __instance = None 40 | 41 | def __new__(cls): 42 | if HeadBlock.__instance is None: 43 | HeadBlock.__instance = object.__new__(cls) 44 | return HeadBlock.__instance 45 | 46 | def __init__(self): 47 | self.head_block = self.get_head() 48 | 49 | async def get_head(self): 50 | client = ClientInstance().client 51 | return await client(blockchain.current) 52 | -------------------------------------------------------------------------------- /silkaj/blocks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import logging 19 | from asyncio import sleep 20 | from click import command, argument, INT, progressbar 21 | from aiohttp.client_exceptions import ServerDisconnectedError 22 | 23 | from duniterpy.api import bma 24 | from duniterpy.api.client import Client 25 | from duniterpy.api.errors import DuniterError 26 | from duniterpy.documents import Block 27 | from duniterpy.key.verifying_key import VerifyingKey 28 | 29 | from silkaj.tools import message_exit, coroutine 30 | from silkaj.network_tools import EndPoint 31 | from silkaj.constants import BMA_MAX_BLOCKS_CHUNK_SIZE 32 | 33 | 34 | @command( 35 | "verify", 36 | help="Verify blocks’ signatures. \ 37 | If only FROM_BLOCK is specified, it verifies from this block to the last block. \ 38 | If nothing specified, the whole blockchain gets verified.", 39 | ) 40 | @argument("from_block", default=0, type=INT) 41 | @argument("to_block", default=0, type=INT) 42 | @coroutine 43 | async def verify_blocks_signatures(from_block, to_block): 44 | client = Client(EndPoint().BMA_ENDPOINT) 45 | to_block = await check_passed_blocks_range(client, from_block, to_block) 46 | invalid_blocks_signatures = list() 47 | chunks_from = range(from_block, to_block + 1, BMA_MAX_BLOCKS_CHUNK_SIZE) 48 | with progressbar(chunks_from, label="Processing blocks verification") as bar: 49 | for chunk_from in bar: 50 | chunk_size = get_chunk_size(from_block, to_block, chunks_from, chunk_from) 51 | logging.info( 52 | "Processing chunk from block {} to {}".format( 53 | chunk_from, chunk_from + chunk_size 54 | ) 55 | ) 56 | chunk = await get_chunk(client, chunk_size, chunk_from) 57 | 58 | for block in chunk: 59 | block = Block.from_signed_raw(block["raw"] + block["signature"] + "\n") 60 | verify_block_signature(invalid_blocks_signatures, block) 61 | 62 | await client.close() 63 | display_result(from_block, to_block, invalid_blocks_signatures) 64 | 65 | 66 | async def check_passed_blocks_range(client, from_block, to_block): 67 | head_number = (await client(bma.blockchain.current))["number"] 68 | if to_block == 0: 69 | to_block = head_number 70 | if to_block > head_number: 71 | await client.close() 72 | message_exit( 73 | "Passed TO_BLOCK argument is bigger than the head block: " 74 | + str(head_number) 75 | ) 76 | if from_block > to_block: 77 | await client.close() 78 | message_exit("TO_BLOCK should be bigger or equal to FROM_BLOCK") 79 | return to_block 80 | 81 | 82 | def get_chunk_size(from_block, to_block, chunks_from, chunk_from): 83 | """If not last chunk, take the maximum size 84 | Otherwise, calculate the size for the last chunk""" 85 | if chunk_from != chunks_from[-1]: 86 | return BMA_MAX_BLOCKS_CHUNK_SIZE 87 | else: 88 | return (to_block + 1 - from_block) % BMA_MAX_BLOCKS_CHUNK_SIZE 89 | 90 | 91 | async def get_chunk(client, chunk_size, chunk_from): 92 | try: 93 | return await client(bma.blockchain.blocks, chunk_size, chunk_from) 94 | except ServerDisconnectedError: 95 | logging.info("Reach BMA anti-spam protection. Waiting two seconds") 96 | await sleep(2) 97 | return await client(bma.blockchain.blocks, chunk_size, chunk_from) 98 | except DuniterError as error: 99 | logging.error(error) 100 | 101 | 102 | def verify_block_signature(invalid_blocks_signatures, block): 103 | key = VerifyingKey(block.issuer) 104 | if not key.verify_document(block): 105 | invalid_blocks_signatures.append(block.number) 106 | 107 | 108 | def display_result(from_block, to_block, invalid_blocks_signatures): 109 | result = "Within {0}-{1} range, ".format(from_block, to_block) 110 | if invalid_blocks_signatures: 111 | result += "blocks with a wrong signature: " 112 | result += " ".join(str(n) for n in invalid_blocks_signatures) 113 | else: 114 | result += "no blocks with a wrong signature." 115 | print(result) 116 | -------------------------------------------------------------------------------- /silkaj/cert.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import sys 19 | import click 20 | from time import time 21 | from tabulate import tabulate 22 | from duniterpy.api import bma 23 | from duniterpy.documents import BlockUID, block_uid, Identity, Certification 24 | 25 | from silkaj.auth import auth_method 26 | from silkaj.tools import message_exit, coroutine 27 | from silkaj.network_tools import ClientInstance 28 | from silkaj.blockchain_tools import BlockchainParams, HeadBlock 29 | from silkaj.license import license_approval 30 | from silkaj import wot, tui 31 | from silkaj.constants import SUCCESS_EXIT_STATUS 32 | from silkaj.crypto_tools import is_pubkey_and_check 33 | 34 | 35 | @click.command("cert", help="Send certification") 36 | @click.argument("uid_pubkey_to_certify") 37 | @click.pass_context 38 | @coroutine 39 | async def send_certification(ctx, uid_pubkey_to_certify): 40 | client = ClientInstance().client 41 | 42 | checked_pubkey = is_pubkey_and_check(uid_pubkey_to_certify) 43 | if checked_pubkey: 44 | uid_pubkey_to_certify = checked_pubkey 45 | 46 | idty_to_certify, pubkey_to_certify, send_certs = await wot.choose_identity( 47 | uid_pubkey_to_certify 48 | ) 49 | 50 | # Authentication 51 | key = auth_method() 52 | 53 | issuer_pubkey = key.pubkey 54 | issuer = await pre_checks(client, issuer_pubkey, pubkey_to_certify) 55 | 56 | # Display license and ask for confirmation 57 | head = await HeadBlock().head_block 58 | currency = head["currency"] 59 | license_approval(currency) 60 | 61 | # Certification confirmation 62 | await certification_confirmation( 63 | ctx, issuer, issuer_pubkey, pubkey_to_certify, idty_to_certify 64 | ) 65 | 66 | certification = docs_generation( 67 | currency, 68 | pubkey_to_certify, 69 | idty_to_certify, 70 | issuer_pubkey, 71 | head, 72 | ) 73 | 74 | if ctx.obj["DISPLAY_DOCUMENT"]: 75 | click.echo(certification.signed_raw(), nl=False) 76 | await tui.send_doc_confirmation("certification") 77 | 78 | # Sign document 79 | certification.sign([key]) 80 | 81 | # Send certification document 82 | response = await client(bma.wot.certify, certification.signed_raw()) 83 | 84 | if response.status == 200: 85 | print("Certification successfully sent.") 86 | else: 87 | print("Error while publishing certification: {0}".format(await response.text())) 88 | 89 | await client.close() 90 | 91 | 92 | async def pre_checks(client, issuer_pubkey, pubkey_to_certify): 93 | # Check whether current user is member 94 | issuer = await wot.is_member(issuer_pubkey) 95 | if not issuer: 96 | message_exit("Current identity is not member.") 97 | 98 | if issuer_pubkey == pubkey_to_certify: 99 | message_exit("You can’t certify yourself!") 100 | 101 | # Check if the certification can be renewed 102 | req = await client(bma.wot.requirements, pubkey_to_certify) 103 | req = req["identities"][0] 104 | for cert in req["certifications"]: 105 | if cert["from"] == issuer_pubkey: 106 | params = await BlockchainParams().params 107 | # Ğ1: 0<–>2y - 2y + 2m 108 | # ĞT: 0<–>4.8m - 4.8m + 12.5d 109 | renewable = cert["expiresIn"] - params["sigValidity"] + params["sigReplay"] 110 | if renewable > 0: 111 | renewable_date = tui.convert_time(time() + renewable, "date") 112 | message_exit("Certification renewable the " + renewable_date) 113 | 114 | # Check if the certification is already in the pending certifications 115 | for pending_cert in req["pendingCerts"]: 116 | if pending_cert["from"] == issuer_pubkey: 117 | message_exit("Certification is currently been processed") 118 | return issuer 119 | 120 | 121 | async def certification_confirmation( 122 | ctx, issuer, issuer_pubkey, pubkey_to_certify, idty_to_certify 123 | ): 124 | cert = list() 125 | cert.append(["Cert", "Issuer", "–>", "Recipient: Published: #block-hash date"]) 126 | client = ClientInstance().client 127 | idty_timestamp = idty_to_certify["meta"]["timestamp"] 128 | block_uid_idty = block_uid(idty_timestamp) 129 | block = await client(bma.blockchain.block, block_uid_idty.number) 130 | block_uid_date = ( 131 | ": #" + idty_timestamp[:15] + "… " + tui.convert_time(block["time"], "all") 132 | ) 133 | cert.append(["ID", issuer["uid"], "–>", idty_to_certify["uid"] + block_uid_date]) 134 | cert.append( 135 | [ 136 | "Pubkey", 137 | tui.display_pubkey_and_checksum(issuer_pubkey), 138 | "–>", 139 | tui.display_pubkey_and_checksum(pubkey_to_certify), 140 | ] 141 | ) 142 | params = await BlockchainParams().params 143 | cert_begins = tui.convert_time(time(), "date") 144 | cert_ends = tui.convert_time(time() + params["sigValidity"], "date") 145 | cert.append(["Valid", cert_begins, "—>", cert_ends]) 146 | click.echo(tabulate(cert, tablefmt="fancy_grid")) 147 | if not ctx.obj["DISPLAY_DOCUMENT"]: 148 | await tui.send_doc_confirmation("certification") 149 | 150 | 151 | def docs_generation(currency, pubkey_to_certify, idty_to_certify, issuer_pubkey, head): 152 | identity = Identity( 153 | version=10, 154 | currency=currency, 155 | pubkey=pubkey_to_certify, 156 | uid=idty_to_certify["uid"], 157 | ts=block_uid(idty_to_certify["meta"]["timestamp"]), 158 | signature=idty_to_certify["self"], 159 | ) 160 | 161 | return Certification( 162 | version=10, 163 | currency=currency, 164 | pubkey_from=issuer_pubkey, 165 | identity=identity, 166 | timestamp=BlockUID(head["number"], head["hash"]), 167 | signature="", 168 | ) 169 | -------------------------------------------------------------------------------- /silkaj/checksum.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import re 19 | import click 20 | 21 | from silkaj.auth import auth_method, has_auth_method 22 | from silkaj.crypto_tools import ( 23 | gen_checksum, 24 | PUBKEY_DELIMITED_PATTERN, 25 | PUBKEY_CHECKSUM_PATTERN, 26 | ) 27 | from silkaj.tools import message_exit 28 | from silkaj.tui import display_pubkey_and_checksum 29 | 30 | 31 | MESSAGE = "You should specify a pubkey or an authentication method" 32 | 33 | 34 | @click.command( 35 | "checksum", 36 | help="Generate checksum out of a passed pubkey or an authentication method.\ 37 | Can also check if the checksum is valid", 38 | ) 39 | @click.argument("pubkey_checksum", nargs=-1) 40 | def checksum_command(pubkey_checksum): 41 | if has_auth_method(): 42 | key = auth_method() 43 | click.echo(display_pubkey_and_checksum(key.pubkey)) 44 | else: 45 | if not pubkey_checksum: 46 | message_exit(MESSAGE) 47 | elif re.search(re.compile(PUBKEY_DELIMITED_PATTERN), pubkey_checksum[0]): 48 | click.echo(display_pubkey_and_checksum(pubkey_checksum[0])) 49 | elif re.search(re.compile(PUBKEY_CHECKSUM_PATTERN), pubkey_checksum[0]): 50 | pubkey, checksum = pubkey_checksum[0].split(":") 51 | if checksum == gen_checksum(pubkey): 52 | click.echo("The checksum is valid") 53 | else: 54 | click.echo("The checksum is invalid") 55 | else: 56 | message_exit("Error: Wrong public key format") 57 | -------------------------------------------------------------------------------- /silkaj/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | # -*- coding: utf-8 -*- 19 | 20 | import sys 21 | from click import group, help_option, version_option, option, pass_context 22 | 23 | from silkaj.tx import send_transaction 24 | from silkaj.tx_history import transaction_history 25 | from silkaj.money import cmd_amount 26 | from silkaj.cert import send_certification 27 | from silkaj.checksum import checksum_command 28 | from silkaj.commands import ( 29 | currency_info, 30 | difficulties, 31 | argos_info, 32 | list_blocks, 33 | ) 34 | 35 | # from silkaj.net import network_info 36 | from silkaj.wot import received_sent_certifications, id_pubkey_correspondence 37 | from silkaj.auth import generate_auth_file 38 | from silkaj.license import license_command 39 | from silkaj.membership import send_membership 40 | from silkaj.blocks import verify_blocks_signatures 41 | from silkaj.constants import ( 42 | SILKAJ_VERSION, 43 | G1_DEFAULT_ENDPOINT, 44 | G1_TEST_DEFAULT_ENDPOINT, 45 | ) 46 | 47 | 48 | @group() 49 | @help_option("-h", "--help") 50 | @version_option(SILKAJ_VERSION, "-v", "--version") 51 | @option( 52 | "--peer", 53 | "-p", 54 | help="Default endpoint to reach Ğ1 currency by its official node {}\ 55 | This option allows to specify a custom endpoint as follow: :.\ 56 | In case no port is specified, it defaults to 443.".format( 57 | ":".join(G1_DEFAULT_ENDPOINT) 58 | ), 59 | ) 60 | @option( 61 | "--gtest", 62 | "-gt", 63 | is_flag=True, 64 | help="Default endpoint to reach ĞTest currency by its official node: {}\ 65 | ".format( 66 | ":".join(G1_TEST_DEFAULT_ENDPOINT) 67 | ), 68 | ) 69 | @option( 70 | "--auth-scrypt", 71 | "--scrypt", 72 | is_flag=True, 73 | help="Scrypt authentication: default method", 74 | ) 75 | @option("--nrp", help='Scrypt parameters: defaults N,r,p: "4096,16,1"') 76 | @option( 77 | "--auth-file", 78 | "-af", 79 | is_flag=True, 80 | help="Authentication file. Defaults to: './authfile'", 81 | ) 82 | @option( 83 | "--file", 84 | default="authfile", 85 | show_default=True, 86 | help="Path file specification with '--auth-file'", 87 | ) 88 | @option("--auth-seed", "--seed", is_flag=True, help="Seed hexadecimal authentication") 89 | @option("--auth-wif", "--wif", is_flag=True, help="WIF and EWIF authentication methods") 90 | @option( 91 | "--display", 92 | "-d", 93 | is_flag=True, 94 | help="Display the generated document before sending it", 95 | ) 96 | @option( 97 | "--dry-run", 98 | "-n", 99 | is_flag=True, 100 | help="By-pass licence, confirmation. \ 101 | Do not send the document, but display it instead", 102 | ) 103 | @pass_context 104 | def cli( 105 | ctx, 106 | peer, 107 | gtest, 108 | auth_scrypt, 109 | nrp, 110 | auth_file, 111 | file, 112 | auth_seed, 113 | auth_wif, 114 | display, 115 | dry_run, 116 | ): 117 | if display and dry_run: 118 | sys.exit("ERROR: display and dry-run options can not be used together") 119 | 120 | ctx.obj = dict() 121 | ctx.ensure_object(dict) 122 | ctx.obj["PEER"] = peer 123 | ctx.obj["GTEST"] = gtest 124 | ctx.obj["AUTH_SCRYPT"] = auth_scrypt 125 | ctx.obj["AUTH_SCRYPT_PARAMS"] = nrp 126 | ctx.obj["AUTH_FILE"] = auth_file 127 | ctx.obj["AUTH_FILE_PATH"] = file 128 | ctx.obj["AUTH_SEED"] = auth_seed 129 | ctx.obj["AUTH_WIF"] = auth_wif 130 | ctx.obj["DISPLAY_DOCUMENT"] = display 131 | ctx.obj["DRY_RUN"] = dry_run 132 | 133 | 134 | cli.add_command(argos_info) 135 | cli.add_command(generate_auth_file) 136 | cli.add_command(cmd_amount) 137 | cli.add_command(list_blocks) 138 | cli.add_command(send_certification) 139 | cli.add_command(checksum_command) 140 | cli.add_command(difficulties) 141 | cli.add_command(transaction_history) 142 | cli.add_command(id_pubkey_correspondence) 143 | cli.add_command(currency_info) 144 | cli.add_command(license_command) 145 | cli.add_command(send_membership) 146 | # cli.add_command(network_info) 147 | cli.add_command(send_transaction) 148 | cli.add_command(verify_blocks_signatures) 149 | cli.add_command(received_sent_certifications) 150 | 151 | 152 | @cli.command("about", help="Display program information") 153 | def about(): 154 | print( 155 | "\ 156 | \n @@@@@@@@@@@@@\ 157 | \n @@@ @ @@@\ 158 | \n @@@ @@ @@@@@@ @@. Silkaj", 159 | SILKAJ_VERSION, 160 | "\ 161 | \n @@ @@@ @@@@@@@@@@@ @@,\ 162 | \n @@ @@@ &@@@@@@@@@@@@@ @@@ Powerfull and lightweight command line client\ 163 | \n @@ @@@ @@@@@@@@@# @@@@ @@(\ 164 | \n @@ @@@@ @@@@@@@@@ @@@ @@ Built in Python for Duniter’s currencies: Ğ1 and Ğ1-Test\ 165 | \n @@ @@@ @@@@@@@@ @ @@@ @@\ 166 | \n @@ @@@ @@@@@@ @@@@ @@ @@ Authors: see AUTHORS.md file\ 167 | \n @@ @@@@ @@@ @@@@@@@ @@ @@\ 168 | \n @@ @@@@* @@@@@@@@@ @# @@ Website: https://silkaj.duniter.org\ 169 | \n @@ @@@@@ @@@@@@@@@@ @ ,@@\ 170 | \n @@ @@@@@ @@@@@@@@@@ @ ,@@ Repository: https://git.duniter.org/clients/python/silkaj\ 171 | \n @@@ @@@@@@@@@@@@ @ @@*\ 172 | \n @@@ @@@@@@@@ @ @@@ License: GNU AGPLv3\ 173 | \n @@@@ @@ @@@,\ 174 | \n @@@@@@@@@@@@@@@\n", 175 | ) 176 | -------------------------------------------------------------------------------- /silkaj/cli_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from click import Option, UsageError 19 | 20 | 21 | class MutuallyExclusiveOption(Option): 22 | def __init__(self, *args, **kwargs): 23 | self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", [])) 24 | help = kwargs.get("help", "") 25 | if self.mutually_exclusive: 26 | ex_str = ", ".join(self.mutually_exclusive) 27 | kwargs["help"] = help + ( 28 | " NOTE: This argument is mutually exclusive with " 29 | " arguments: [" + ex_str + "]." 30 | ) 31 | super(MutuallyExclusiveOption, self).__init__(*args, **kwargs) 32 | 33 | def handle_parse_result(self, ctx, opts, args): 34 | if self.mutually_exclusive.intersection(opts) and self.name in opts: 35 | raise UsageError( 36 | "Usage: `{}` is mutually exclusive with " 37 | "arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive)) 38 | ) 39 | 40 | return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args) 41 | -------------------------------------------------------------------------------- /silkaj/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from click import command, option, argument, IntRange 19 | from os import system 20 | from collections import OrderedDict 21 | from tabulate import tabulate 22 | from operator import itemgetter 23 | from asyncio import sleep 24 | import aiohttp 25 | from _socket import gaierror 26 | import jsonschema 27 | 28 | from duniterpy.api import bma 29 | 30 | from silkaj.tools import coroutine 31 | from silkaj.wot import identity_of 32 | from silkaj.network_tools import ( 33 | best_endpoint_address, 34 | EndPoint, 35 | ClientInstance, 36 | ) 37 | from silkaj.blockchain_tools import HeadBlock 38 | from silkaj.tools import CurrencySymbol 39 | from silkaj.tui import convert_time 40 | from silkaj.constants import ASYNC_SLEEP 41 | 42 | 43 | @command("info", help="Display information about currency") 44 | @coroutine 45 | async def currency_info(): 46 | head_block = await HeadBlock().head_block 47 | ep = EndPoint().ep 48 | print( 49 | "Connected to node:", 50 | ep[best_endpoint_address(ep, False)], 51 | ep["port"], 52 | "\nCurrent block number:", 53 | head_block["number"], 54 | "\nCurrency name:", 55 | await CurrencySymbol().symbol, 56 | "\nNumber of members:", 57 | head_block["membersCount"], 58 | "\nMinimal Proof-of-Work:", 59 | head_block["powMin"], 60 | "\nCurrent time:", 61 | convert_time(head_block["time"], "all"), 62 | "\nMedian time:", 63 | convert_time(head_block["medianTime"], "all"), 64 | "\nDifference time:", 65 | convert_time(head_block["time"] - head_block["medianTime"], "hour"), 66 | ) 67 | client = ClientInstance().client 68 | await client.close() 69 | 70 | 71 | def match_pattern(pow, match="", p=1): 72 | while pow > 0: 73 | if pow >= 16: 74 | match += "0" 75 | pow -= 16 76 | p *= 16 77 | else: 78 | match += "[0-" + hex(15 - pow)[2:].upper() + "]" 79 | p *= pow 80 | pow = 0 81 | return match + "*", p 82 | 83 | 84 | def power(nbr, pow=0): 85 | while nbr >= 10: 86 | nbr /= 10 87 | pow += 1 88 | return "{0:.1f} × 10^{1}".format(nbr, pow) 89 | 90 | 91 | @command( 92 | "diffi", 93 | help="Display the current Proof of Work difficulty level to generate the next block", 94 | ) 95 | @coroutine 96 | async def difficulties(): 97 | client = ClientInstance().client 98 | try: 99 | ws = await client(bma.ws.block) 100 | while True: 101 | current = await ws.receive_json() 102 | jsonschema.validate(current, bma.ws.WS_BLOCK_SCHEMA) 103 | diffi = await client(bma.blockchain.difficulties) 104 | display_diffi(current, diffi) 105 | await client.close() 106 | 107 | except (aiohttp.WSServerHandshakeError, ValueError) as e: 108 | print("Websocket block {0} : {1}".format(type(e).__name__, str(e))) 109 | except (aiohttp.ClientError, gaierror, TimeoutError) as e: 110 | print("{0} : {1}".format(str(e), BMAS_ENDPOINT)) 111 | except jsonschema.ValidationError as e: 112 | print("{:}:{:}".format(str(e.__class__.__name__), str(e))) 113 | 114 | 115 | def display_diffi(current, diffi): 116 | levels = [OrderedDict((i, d[i]) for i in ("uid", "level")) for d in diffi["levels"]] 117 | diffi["levels"] = levels 118 | issuers = 0 119 | sorted_diffi = sorted(diffi["levels"], key=itemgetter("level"), reverse=True) 120 | for d in diffi["levels"]: 121 | if d["level"] / 2 < current["powMin"]: 122 | issuers += 1 123 | d["match"] = match_pattern(d["level"])[0][:20] 124 | d["Π diffi"] = power(match_pattern(d["level"])[1]) 125 | d["Σ diffi"] = d.pop("level") 126 | system("cls||clear") 127 | print( 128 | "Current block: n°{0}, generated on the {1}\n\ 129 | Generation of next block n°{2} possible by at least {3}/{4} members\n\ 130 | Common Proof-of-Work difficulty level: {5}, hash starting with `{6}`\n{7}".format( 131 | current["number"], 132 | convert_time(current["time"], "all"), 133 | diffi["block"], 134 | issuers, 135 | len(diffi["levels"]), 136 | current["powMin"], 137 | match_pattern(int(current["powMin"]))[0], 138 | tabulate( 139 | sorted_diffi, headers="keys", tablefmt="orgtbl", stralign="center" 140 | ), 141 | ) 142 | ) 143 | 144 | 145 | @command("blocks", help="Display blocks: default: 0 for current window size") 146 | @argument("number", default=0, type=IntRange(0, 5000)) 147 | @option( 148 | "--detailed", 149 | "-d", 150 | is_flag=True, 151 | help="Force detailed view. Compact view happen over 30 blocks", 152 | ) 153 | @coroutine 154 | async def list_blocks(number, detailed): 155 | head_block = await HeadBlock().head_block 156 | current_nbr = head_block["number"] 157 | if number == 0: 158 | number = head_block["issuersFrame"] 159 | client = ClientInstance().client 160 | blocks = await client(bma.blockchain.blocks, number, current_nbr - number + 1) 161 | issuers = list() 162 | issuers_dict = dict() 163 | for block in blocks: 164 | issuer = OrderedDict() 165 | issuer["pubkey"] = block["issuer"] 166 | if detailed or number <= 30: 167 | issuer["block"] = block["number"] 168 | issuer["gentime"] = convert_time(block["time"], "all") 169 | issuer["mediantime"] = convert_time(block["medianTime"], "all") 170 | issuer["hash"] = block["hash"][:10] 171 | issuer["powMin"] = block["powMin"] 172 | issuers_dict[issuer["pubkey"]] = issuer 173 | issuers.append(issuer) 174 | for pubkey in issuers_dict.keys(): 175 | issuer = issuers_dict[pubkey] 176 | idty = await identity_of(issuer["pubkey"]) 177 | for issuer2 in issuers: 178 | if ( 179 | issuer2.get("pubkey") is not None 180 | and issuer.get("pubkey") is not None 181 | and issuer2["pubkey"] == issuer["pubkey"] 182 | ): 183 | issuer2["uid"] = idty["uid"] 184 | issuer2.pop("pubkey") 185 | await sleep(ASYNC_SLEEP) 186 | await client.close() 187 | print( 188 | "Last {0} blocks from n°{1} to n°{2}".format( 189 | number, current_nbr - number + 1, current_nbr 190 | ), 191 | end=" ", 192 | ) 193 | if detailed or number <= 30: 194 | sorted_list = sorted(issuers, key=itemgetter("block"), reverse=True) 195 | print( 196 | "\n" 197 | + tabulate( 198 | sorted_list, headers="keys", tablefmt="orgtbl", stralign="center" 199 | ) 200 | ) 201 | else: 202 | list_issued = list() 203 | for issuer in issuers: 204 | found = False 205 | for issued in list_issued: 206 | if issued.get("uid") is not None and issued["uid"] == issuer["uid"]: 207 | issued["blocks"] += 1 208 | found = True 209 | break 210 | if not found: 211 | issued = OrderedDict() 212 | issued["uid"] = issuer["uid"] 213 | issued["blocks"] = 1 214 | list_issued.append(issued) 215 | for issued in list_issued: 216 | issued["percent"] = issued["blocks"] / number * 100 217 | sorted_list = sorted(list_issued, key=itemgetter("blocks"), reverse=True) 218 | print( 219 | "from {0} issuers\n{1}".format( 220 | len(list_issued), 221 | tabulate( 222 | sorted_list, 223 | headers="keys", 224 | tablefmt="orgtbl", 225 | floatfmt=".1f", 226 | stralign="center", 227 | ), 228 | ) 229 | ) 230 | 231 | 232 | @command("argos", help="Display currency information formatted for Argos or BitBar") 233 | @coroutine 234 | async def argos_info(): 235 | head_block = await HeadBlock().head_block 236 | currency_symbol = await CurrencySymbol().symbol 237 | print(currency_symbol, "|") 238 | print("---") 239 | ep = EndPoint().ep 240 | endpoint_address = ep[best_endpoint_address(ep, False)] 241 | if ep["port"] == "443": 242 | href = "href=https://%s/" % (endpoint_address) 243 | else: 244 | href = "href=http://%s:%s/" % (endpoint_address, ep["port"]) 245 | print( 246 | "Connected to node:", 247 | ep[best_endpoint_address(ep, False)], 248 | ep["port"], 249 | "|", 250 | href, 251 | "\nCurrent block number:", 252 | head_block["number"], 253 | "\nCurrency name:", 254 | currency_symbol, 255 | "\nNumber of members:", 256 | head_block["membersCount"], 257 | "\nMinimal Proof-of-Work:", 258 | head_block["powMin"], 259 | "\nCurrent time:", 260 | convert_time(head_block["time"], "all"), 261 | "\nMedian time:", 262 | convert_time(head_block["medianTime"], "all"), 263 | "\nDifference time:", 264 | convert_time(head_block["time"] - head_block["medianTime"], "hour"), 265 | ) 266 | client = ClientInstance().client 267 | await client.close() 268 | -------------------------------------------------------------------------------- /silkaj/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | SILKAJ_VERSION = "0.10.0dev" 19 | G1_SYMBOL = "Ğ1" 20 | GTEST_SYMBOL = "ĞTest" 21 | G1_DEFAULT_ENDPOINT = "g1.duniter.org", "443" 22 | G1_TEST_DEFAULT_ENDPOINT = "g1-test.duniter.org", "443" 23 | CONNECTION_TIMEOUT = 10 24 | ASYNC_SLEEP = 0.15 25 | SUCCESS_EXIT_STATUS = 0 26 | FAILURE_EXIT_STATUS = 1 27 | BMA_MAX_BLOCKS_CHUNK_SIZE = 5000 28 | PUBKEY_MIN_LENGTH = 43 29 | PUBKEY_MAX_LENGTH = 44 30 | PUBKEY_PATTERN = f"[1-9A-HJ-NP-Za-km-z]{{{PUBKEY_MIN_LENGTH},{PUBKEY_MAX_LENGTH}}}" 31 | MINIMAL_ABSOLUTE_TX_AMOUNT = 0.01 32 | MINIMAL_RELATIVE_TX_AMOUNT = 1e-6 33 | CENT_MULT_TO_UNIT = 100 34 | SHORT_PUBKEY_SIZE = 8 35 | -------------------------------------------------------------------------------- /silkaj/crypto_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import re 19 | import hashlib 20 | 21 | import base58 22 | 23 | from silkaj.constants import PUBKEY_PATTERN 24 | from silkaj.tools import message_exit 25 | 26 | PUBKEY_DELIMITED_PATTERN = "^{0}$".format(PUBKEY_PATTERN) 27 | CHECKSUM_SIZE = 3 28 | CHECKSUM_PATTERN = f"[1-9A-HJ-NP-Za-km-z]{{{CHECKSUM_SIZE}}}" 29 | PUBKEY_CHECKSUM_PATTERN = "^{0}:{1}$".format(PUBKEY_PATTERN, CHECKSUM_PATTERN) 30 | 31 | 32 | def is_pubkey_and_check(pubkey): 33 | """ 34 | Checks if the given argument contains a pubkey. 35 | If so, verifies the checksum if needed and returns the pubkey. 36 | Exits if the checksum is wrong. 37 | Else, return False 38 | """ 39 | if re.search(re.compile(PUBKEY_PATTERN), pubkey): 40 | if check_pubkey_format(pubkey, True): 41 | return validate_checksum(pubkey) 42 | return pubkey 43 | return False 44 | 45 | 46 | def check_pubkey_format(pubkey, display_error=True): 47 | """ 48 | Checks if a pubkey has a checksum. 49 | Exits if the pubkey is invalid. 50 | """ 51 | if re.search(re.compile(PUBKEY_DELIMITED_PATTERN), pubkey): 52 | return False 53 | elif re.search(re.compile(PUBKEY_CHECKSUM_PATTERN), pubkey): 54 | return True 55 | elif display_error: 56 | message_exit("Error: bad format for following public key: " + pubkey) 57 | return 58 | 59 | 60 | def validate_checksum(pubkey_checksum): 61 | """ 62 | Check pubkey checksum after the pubkey, delimited by ":". 63 | If check pass: return pubkey 64 | Else: exit. 65 | """ 66 | pubkey, checksum = pubkey_checksum.split(":") 67 | if checksum == gen_checksum(pubkey): 68 | return pubkey 69 | message_exit( 70 | "Error: public key '" 71 | + pubkey 72 | + "' does not match checksum '" 73 | + checksum 74 | + "'.\nPlease verify the public key." 75 | ) 76 | 77 | 78 | def gen_checksum(pubkey): 79 | """ 80 | Returns the checksum of the input pubkey (encoded in b58) 81 | """ 82 | pubkey_byte = base58.b58decode(pubkey) 83 | hash = hashlib.sha256(hashlib.sha256(pubkey_byte).digest()).digest() 84 | return base58.b58encode(hash)[:3].decode("utf-8") 85 | -------------------------------------------------------------------------------- /silkaj/license.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import webbrowser 19 | from click import command, echo_via_pager, confirm 20 | 21 | 22 | def license_approval(currency): 23 | if currency != "g1": 24 | return 25 | if confirm( 26 | "You will be asked to approve Ğ1 license. Would you like to display it?" 27 | ): 28 | display_license() 29 | confirm("Do you approve Ğ1 license?", abort=True) 30 | 31 | 32 | @command("license", help="Display Ğ1 license") 33 | def license_command(): 34 | display_license() 35 | 36 | 37 | def display_license(): 38 | language = input("In which language would you like to display Ğ1 license [en/fr]? ") 39 | if language == "en": 40 | if not webbrowser.open("https://duniter.org/en/wiki/g1-license/"): 41 | echo_via_pager(open("licence-G1/license/license_g1-en.rst").read()) 42 | else: 43 | if not webbrowser.open("https://duniter.org/fr/wiki/licence-g1/"): 44 | echo_via_pager(open("licence-G1/license/license_g1-fr-FR.rst").read()) 45 | -------------------------------------------------------------------------------- /silkaj/membership.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import sys 19 | import logging 20 | 21 | import pendulum 22 | import click 23 | from tabulate import tabulate 24 | 25 | from duniterpy.api import bma 26 | from duniterpy.documents import BlockUID, block_uid, Membership 27 | 28 | from silkaj import auth, wot, tui 29 | from silkaj.tools import coroutine 30 | from silkaj.network_tools import ClientInstance 31 | from silkaj.blockchain_tools import BlockchainParams, HeadBlock 32 | from silkaj.license import license_approval 33 | from silkaj.constants import SUCCESS_EXIT_STATUS 34 | 35 | 36 | @click.command( 37 | "membership", 38 | help="Send and sign membership document: \n\ 39 | for first emission and for renewal", 40 | ) 41 | @click.pass_context 42 | @coroutine 43 | async def send_membership(ctx): 44 | dry_run = ctx.obj["DRY_RUN"] 45 | 46 | # Authentication 47 | key = auth.auth_method() 48 | 49 | # Get the identity information 50 | head_block = await HeadBlock().head_block 51 | membership_timestamp = BlockUID(head_block["number"], head_block["hash"]) 52 | identity = (await wot.choose_identity(key.pubkey))[0] 53 | identity_uid = identity["uid"] 54 | identity_timestamp = block_uid(identity["meta"]["timestamp"]) 55 | 56 | # Display license and ask for confirmation 57 | currency = head_block["currency"] 58 | if not dry_run: 59 | license_approval(currency) 60 | 61 | # Confirmation 62 | client = ClientInstance().client 63 | await display_confirmation_table(identity_uid, key.pubkey, identity_timestamp) 64 | if not dry_run and not ctx.obj["DISPLAY_DOCUMENT"]: 65 | await tui.send_doc_confirmation("membership document for this identity") 66 | 67 | membership = generate_membership_document( 68 | currency, 69 | key.pubkey, 70 | membership_timestamp, 71 | identity_uid, 72 | identity_timestamp, 73 | ) 74 | 75 | # Sign document 76 | membership.sign([key]) 77 | 78 | logging.debug(membership.signed_raw()) 79 | 80 | if dry_run: 81 | click.echo(membership.signed_raw()) 82 | await client.close() 83 | sys.exit(SUCCESS_EXIT_STATUS) 84 | 85 | if ctx.obj["DISPLAY_DOCUMENT"]: 86 | click.echo(membership.signed_raw()) 87 | await tui.send_doc_confirmation("membership document for this identity") 88 | 89 | # Send the membership signed raw document to the node 90 | response = await client(bma.blockchain.membership, membership.signed_raw()) 91 | 92 | if response.status == 200: 93 | print("Membership successfully sent") 94 | else: 95 | print("Error while publishing membership: {0}".format(await response.text())) 96 | logging.debug(await response.text()) 97 | await client.close() 98 | 99 | 100 | async def display_confirmation_table(identity_uid, pubkey, identity_timestamp): 101 | """ 102 | Check whether there is pending memberships already in the mempool 103 | Display their expiration date 104 | 105 | Actually, it works sending a membership document even if the time 106 | between two renewals is not awaited as for the certification 107 | """ 108 | 109 | client = ClientInstance().client 110 | 111 | identities_requirements = await client(bma.wot.requirements, pubkey) 112 | for identity_requirements in identities_requirements["identities"]: 113 | if identity_requirements["uid"] == identity_uid: 114 | membership_expires = identity_requirements["membershipExpiresIn"] 115 | pending_expires = identity_requirements["membershipPendingExpiresIn"] 116 | pending_memberships = identity_requirements["pendingMemberships"] 117 | break 118 | 119 | table = list() 120 | if membership_expires: 121 | expires = pendulum.now().add(seconds=membership_expires).diff_for_humans() 122 | table.append(["Expiration date of current membership", expires]) 123 | 124 | if pending_memberships: 125 | line = [ 126 | "Number of pending membership(s) in the mempool", 127 | len(pending_memberships), 128 | ] 129 | table.append(line) 130 | 131 | expiration = pendulum.now().add(seconds=pending_expires).diff_for_humans() 132 | table.append(["Pending membership documents will expire", expiration]) 133 | 134 | table.append(["User Identifier (UID)", identity_uid]) 135 | table.append(["Public Key", tui.display_pubkey_and_checksum(pubkey)]) 136 | 137 | table.append(["Block Identity", str(identity_timestamp)[:45] + "…"]) 138 | 139 | block = await client(bma.blockchain.block, identity_timestamp.number) 140 | table.append( 141 | ["Identity published", pendulum.from_timestamp(block["time"]).format("LL")] 142 | ) 143 | 144 | params = await BlockchainParams().params 145 | membership_validity = ( 146 | pendulum.now().add(seconds=params["msValidity"]).diff_for_humans() 147 | ) 148 | table.append(["Expiration date of new membership", membership_validity]) 149 | 150 | membership_mempool = ( 151 | pendulum.now().add(seconds=params["msPeriod"]).diff_for_humans() 152 | ) 153 | table.append( 154 | ["Expiration date of new membership from the mempool", membership_mempool] 155 | ) 156 | 157 | click.echo(tabulate(table, tablefmt="fancy_grid")) 158 | 159 | 160 | def generate_membership_document( 161 | currency, 162 | pubkey, 163 | membership_timestamp, 164 | identity_uid, 165 | identity_timestamp, 166 | ): 167 | return Membership( 168 | version=10, 169 | currency=currency, 170 | issuer=pubkey, 171 | membership_ts=membership_timestamp, 172 | membership_type="IN", 173 | uid=identity_uid, 174 | identity_ts=identity_timestamp, 175 | ) 176 | -------------------------------------------------------------------------------- /silkaj/money.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from click import command, argument, pass_context, echo 19 | from tabulate import tabulate 20 | 21 | from silkaj.network_tools import ClientInstance 22 | from silkaj.blockchain_tools import HeadBlock 23 | from silkaj.tools import CurrencySymbol, message_exit, coroutine 24 | from silkaj.auth import auth_method, has_auth_method 25 | 26 | # had to import wot to prevent loop dependency. No use here. 27 | from silkaj import wot 28 | from silkaj.crypto_tools import ( 29 | is_pubkey_and_check, 30 | check_pubkey_format, 31 | validate_checksum, 32 | ) 33 | from silkaj.tui import display_amount, display_pubkey_and_checksum 34 | 35 | from duniterpy.api.bma import tx, blockchain 36 | from duniterpy.documents.transaction import InputSource 37 | 38 | 39 | @command("balance", help="Get wallet balance") 40 | @argument("pubkeys", nargs=-1) 41 | @pass_context 42 | @coroutine 43 | async def cmd_amount(ctx, pubkeys): 44 | client = ClientInstance().client 45 | if not has_auth_method(): 46 | 47 | # check input pubkeys 48 | if not pubkeys: 49 | message_exit("You should specify one or many pubkeys") 50 | pubkeys_list = list() 51 | wrong_pubkeys = False 52 | for inputPubkey in pubkeys: 53 | pubkey = is_pubkey_and_check(inputPubkey) 54 | if not pubkey: 55 | wrong_pubkeys = True 56 | print(f"ERROR: pubkey {inputPubkey} has a wrong format") 57 | elif pubkey in pubkeys_list: 58 | message_exit( 59 | f"ERROR: pubkey {display_pubkey_and_checksum(pubkey)} was specified many times" 60 | ) 61 | pubkeys_list.append(pubkey) 62 | if wrong_pubkeys: 63 | message_exit("Please check the pubkeys format.") 64 | 65 | total = [0, 0] 66 | for pubkey in pubkeys_list: 67 | inputs_balance = await get_amount_from_pubkey(pubkey) 68 | await show_amount_from_pubkey(pubkey, inputs_balance) 69 | total[0] += inputs_balance[0] 70 | total[1] += inputs_balance[1] 71 | if len(pubkeys_list) > 1: 72 | await show_amount_from_pubkey("Total", total) 73 | else: 74 | key = auth_method() 75 | pubkey = key.pubkey 76 | await show_amount_from_pubkey(pubkey, await get_amount_from_pubkey(pubkey)) 77 | await client.close() 78 | 79 | 80 | async def show_amount_from_pubkey(label, inputs_balance): 81 | """ 82 | Shows the balance of a pubkey. 83 | `label` can be either a pubkey or "Total". 84 | """ 85 | totalAmountInput = inputs_balance[0] 86 | balance = inputs_balance[1] 87 | currency_symbol = await CurrencySymbol().symbol 88 | ud_value = await UDValue().ud_value 89 | average, monetary_mass = await get_average() 90 | member = False 91 | 92 | # if `pubkey` is a pubkey, get pubkey:checksum and uid 93 | if label != "Total": 94 | member = await wot.is_member(label) 95 | pubkey_and_ck = display_pubkey_and_checksum(label) 96 | # display balance table 97 | display = list() 98 | display.append(["Balance of pubkey", pubkey_and_ck]) 99 | 100 | if member: 101 | display.append(["User identifier", member["uid"]]) 102 | 103 | if totalAmountInput - balance != 0: 104 | display_amount(display, "Blockchain", balance, ud_value, currency_symbol) 105 | display_amount( 106 | display, 107 | "Pending transaction", 108 | (totalAmountInput - balance), 109 | ud_value, 110 | currency_symbol, 111 | ) 112 | display_amount(display, "Total amount", totalAmountInput, ud_value, currency_symbol) 113 | display.append( 114 | [ 115 | "Total relative to M/N", 116 | "{0} x M/N".format(round(totalAmountInput / average, 2)), 117 | ] 118 | ) 119 | echo(tabulate(display, tablefmt="fancy_grid")) 120 | 121 | 122 | async def get_average(): 123 | head = await HeadBlock().head_block 124 | monetary_mass = head["monetaryMass"] 125 | members_count = head["membersCount"] 126 | average = monetary_mass / members_count 127 | return average, monetary_mass 128 | 129 | 130 | async def get_amount_from_pubkey(pubkey): 131 | listinput, amount = await get_sources(pubkey) 132 | 133 | totalAmountInput = 0 134 | for input in listinput: 135 | totalAmountInput += amount_in_current_base(input) 136 | return totalAmountInput, amount 137 | 138 | 139 | async def get_sources(pubkey): 140 | client = ClientInstance().client 141 | # Sources written into the blockchain 142 | sources = await client(tx.sources, pubkey) 143 | 144 | listinput = list() 145 | amount = 0 146 | for source in sources["sources"]: 147 | if source["conditions"] == "SIG(" + pubkey + ")": 148 | listinput.append( 149 | InputSource( 150 | amount=source["amount"], 151 | base=source["base"], 152 | source=source["type"], 153 | origin_id=source["identifier"], 154 | index=source["noffset"], 155 | ) 156 | ) 157 | amount += amount_in_current_base(listinput[-1]) 158 | 159 | # pending source 160 | history = await client(tx.pending, pubkey) 161 | history = history["history"] 162 | pendings = history["sending"] + history["receiving"] + history["pending"] 163 | 164 | # add pending output 165 | pending_sources = list() 166 | for pending in pendings: 167 | identifier = pending["hash"] 168 | for i, output in enumerate(pending["outputs"]): 169 | outputsplited = output.split(":") 170 | if outputsplited[2] == "SIG(" + pubkey + ")": 171 | inputgenerated = InputSource( 172 | amount=int(outputsplited[0]), 173 | base=int(outputsplited[1]), 174 | source="T", 175 | origin_id=identifier, 176 | index=i, 177 | ) 178 | if inputgenerated not in listinput: 179 | # add pendings before blockchain sources for change txs 180 | listinput.insert(0, inputgenerated) 181 | 182 | for input in pending["inputs"]: 183 | pending_sources.append(InputSource.from_inline(input)) 184 | 185 | # remove input already used 186 | for input in pending_sources: 187 | if input in listinput: 188 | listinput.remove(input) 189 | 190 | return listinput, amount 191 | 192 | 193 | class UDValue(object): 194 | __instance = None 195 | 196 | def __new__(cls): 197 | if UDValue.__instance is None: 198 | UDValue.__instance = object.__new__(cls) 199 | return UDValue.__instance 200 | 201 | def __init__(self): 202 | self.ud_value = self.get_ud_value() 203 | 204 | async def get_ud_value(self): 205 | client = ClientInstance().client 206 | blockswithud = await client(blockchain.ud) 207 | NBlastUDblock = blockswithud["result"]["blocks"][-1] 208 | lastUDblock = await client(blockchain.block, NBlastUDblock) 209 | return lastUDblock["dividend"] * 10 ** lastUDblock["unitbase"] 210 | 211 | 212 | def amount_in_current_base(source): 213 | """ 214 | Get amount in current base from input or output source 215 | """ 216 | return source.amount * 10 ** source.base 217 | -------------------------------------------------------------------------------- /silkaj/net.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | """ 19 | from click import command, option, get_terminal_size 20 | from datetime import datetime 21 | from os import system 22 | from collections import OrderedDict 23 | from tabulate import tabulate 24 | from asyncio import sleep 25 | 26 | from duniterpy.api.client import Client 27 | from duniterpy.api import bma 28 | 29 | from silkaj.tools import coroutine 30 | from silkaj.wot import identity_of 31 | from silkaj.network_tools import ( 32 | discover_peers, 33 | best_endpoint_address, 34 | ClientInstance, 35 | ) 36 | from silkaj.tools import message_exit 37 | from silkaj.tui import convert_time 38 | from silkaj.constants import ASYNC_SLEEP 39 | 40 | 41 | def get_network_sort_key(endpoint): 42 | t = list() 43 | for akey in network_sort_keys: 44 | if akey == "diffi" or akey == "block" or akey == "port": 45 | t.append(int(endpoint[akey]) if akey in endpoint else 0) 46 | else: 47 | t.append(str(endpoint[akey]) if akey in endpoint else "") 48 | return tuple(t) 49 | 50 | 51 | @command("net", help="Display network view") 52 | @option( 53 | "--discover", "-d", is_flag=True, help="Discover the network (could take a while)" 54 | ) 55 | @option( 56 | "--sort", 57 | "-s", 58 | default="block,member,diffi,uid", 59 | show_default=True, 60 | help="Sort column names comma-separated", 61 | ) 62 | @coroutine 63 | async def network_info(discover, sort): 64 | global network_sort_keys 65 | network_sort_keys = sort.split(",") 66 | width = get_terminal_size()[0] 67 | if width < 146: 68 | message_exit( 69 | "Wide screen need to be larger than 146. Current width: " + str(width) 70 | ) 71 | # discover peers 72 | # and make sure fields are always ordered the same 73 | infos = [ 74 | OrderedDict( 75 | (i, p.get(i, None)) for i in ("domain", "port", "ip4", "ip6", "pubkey") 76 | ) 77 | for p in await discover_peers(discover) 78 | ] 79 | client = ClientInstance().client 80 | diffi = await client(bma.blockchain.difficulties) 81 | members = 0 82 | print("Getting informations about nodes:") 83 | for i, info in enumerate(infos): 84 | ep = info 85 | api = "BASIC_MERKLED_API " if ep["port"] != "443" else "BMAS " 86 | api += ep.get("domain") + " " if ep["domain"] else "" 87 | api += ep.get("ip4") + " " if ep["ip4"] else "" 88 | api += ep.get("ip6") + " " if ep["ip6"] else "" 89 | api += ep.get("port") 90 | print("{0:.0f}%".format(i / len(infos) * 100, 1), end=" ") 91 | best_ep = best_endpoint_address(info, False) 92 | print(best_ep if best_ep is None else info[best_ep], end=" ") 93 | print(info["port"]) 94 | await sleep(ASYNC_SLEEP) 95 | try: 96 | info["uid"] = await identity_of(info["pubkey"]) 97 | info["uid"] = info["uid"]["uid"] 98 | info["member"] = "yes" 99 | members += 1 100 | except: 101 | info["uid"] = None 102 | info["member"] = "no" 103 | info["pubkey"] = info["pubkey"][:5] + "…" 104 | for d in diffi["levels"]: 105 | if info.get("uid") is not None: 106 | if info["uid"] == d["uid"]: 107 | info["diffi"] = d["level"] 108 | if len(info["uid"]) > 10: 109 | info["uid"] = info["uid"][:9] + "…" 110 | sub_client = Client(api) 111 | current_blk = await sub_client(bma.blockchain.current) 112 | if current_blk is not None: 113 | info["gen_time"] = convert_time(current_blk["time"], "hour") 114 | if width > 171: 115 | info["mediantime"] = convert_time(current_blk["medianTime"], "hour") 116 | if width > 185: 117 | info["difftime"] = convert_time( 118 | current_blk["time"] - current_blk["medianTime"], "hour" 119 | ) 120 | info["block"] = current_blk["number"] 121 | info["hash"] = current_blk["hash"][:10] + "…" 122 | summary = await sub_client(bma.node.summary) 123 | info["version"] = summary["duniter"]["version"] 124 | await sub_client.close() 125 | if info.get("domain") is not None and len(info["domain"]) > 20: 126 | info["domain"] = "…" + info["domain"][-20:] 127 | if info.get("ip6") is not None: 128 | if width < 156: 129 | info.pop("ip6") 130 | else: 131 | info["ip6"] = info["ip6"][:8] + "…" 132 | await client.close() 133 | print( 134 | len(infos), 135 | "peers ups, with", 136 | members, 137 | "members and", 138 | len(infos) - members, 139 | "non-members at", 140 | datetime.now().strftime("%H:%M:%S"), 141 | ) 142 | infos = sorted(infos, key=get_network_sort_key) 143 | print(tabulate(infos, headers="keys", tablefmt="orgtbl", stralign="center")) 144 | 145 | """ 146 | -------------------------------------------------------------------------------- /silkaj/network_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from __future__ import unicode_literals 19 | import re 20 | import socket 21 | import logging 22 | from sys import exit, stderr 23 | from asyncio import sleep 24 | from duniterpy.api.client import Client 25 | from duniterpy.api.bma import network 26 | from duniterpy.constants import IPV4_REGEX, IPV6_REGEX 27 | 28 | from silkaj.constants import ( 29 | G1_DEFAULT_ENDPOINT, 30 | G1_TEST_DEFAULT_ENDPOINT, 31 | CONNECTION_TIMEOUT, 32 | ASYNC_SLEEP, 33 | FAILURE_EXIT_STATUS, 34 | ) 35 | 36 | 37 | async def discover_peers(discover): 38 | """ 39 | From first node, discover his known nodes. 40 | Remove from know nodes if nodes are down. 41 | If discover option: scan all network to know all nodes. 42 | display percentage discovering. 43 | """ 44 | client = ClientInstance().client 45 | endpoints = await get_peers_among_leaves(client) 46 | if discover: 47 | print("Discovering network") 48 | for i, endpoint in enumerate(endpoints): 49 | if discover: 50 | print("{0:.0f}%".format(i / len(endpoints) * 100)) 51 | if best_endpoint_address(endpoint, False) is None: 52 | endpoints.remove(endpoint) 53 | elif discover: 54 | endpoints = await recursive_discovering(endpoints, endpoint) 55 | return endpoints 56 | 57 | 58 | async def recursive_discovering(endpoints, endpoint): 59 | """ 60 | Discover recursively new nodes. 61 | If new node found add it and try to found new node from his known nodes. 62 | """ 63 | api = generate_duniterpy_endpoint_format(endpoint) 64 | sub_client = Client(api) 65 | news = await get_peers_among_leaves(sub_client) 66 | await sub_client.close() 67 | for new in news: 68 | if best_endpoint_address(new, False) is not None and new not in endpoints: 69 | endpoints.append(new) 70 | await recursive_discovering(endpoints, new) 71 | return endpoints 72 | 73 | 74 | async def get_peers_among_leaves(client): 75 | """ 76 | Browse among leaves of peers to retrieve the other peers’ endpoints 77 | """ 78 | leaves = await client(network.peers, leaves=True) 79 | peers = list() 80 | for leaf in leaves["leaves"]: 81 | await sleep(ASYNC_SLEEP + 0.05) 82 | leaf_response = await client(network.peers, leaf=leaf) 83 | peers.append(leaf_response["leaf"]["value"]) 84 | return parse_endpoints(peers) 85 | 86 | 87 | def parse_endpoints(rep): 88 | """ 89 | endpoints[]{"domain", "ip4", "ip6", "pubkey"} 90 | list of dictionaries 91 | reps: raw endpoints 92 | """ 93 | i, j, endpoints = 0, 0, [] 94 | while i < len(rep): 95 | if rep[i]["status"] == "UP": 96 | while j < len(rep[i]["endpoints"]): 97 | ep = parse_endpoint(rep[i]["endpoints"][j]) 98 | j += 1 99 | if ep is None: 100 | break 101 | ep["pubkey"] = rep[i]["pubkey"] 102 | endpoints.append(ep) 103 | i += 1 104 | j = 0 105 | return endpoints 106 | 107 | 108 | def generate_duniterpy_endpoint_format(ep): 109 | api = "BASIC_MERKLED_API " if ep["port"] != "443" else "BMAS " 110 | api += ep.get("domain") + " " if "domain" in ep else "" 111 | api += ep.get("ip4") + " " if "ip4" in ep else "" 112 | api += ep.get("ip6") + " " if "ip6" in ep else "" 113 | api += ep.get("port") 114 | return api 115 | 116 | 117 | def singleton(class_): 118 | instances = {} 119 | 120 | def getinstance(*args, **kwargs): 121 | if class_ not in instances: 122 | instances[class_] = class_(*args, **kwargs) 123 | return instances[class_] 124 | 125 | return getinstance 126 | 127 | 128 | @singleton 129 | class EndPoint(object): 130 | def __init__(self): 131 | ep = dict() 132 | try: 133 | from click.globals import get_current_context 134 | 135 | ctx = get_current_context() 136 | peer = ctx.obj["PEER"] 137 | gtest = ctx.obj["GTEST"] 138 | # To be activated when dropping Python 3.5 139 | # except (ModuleNotFoundError, RuntimeError): 140 | except: 141 | peer, gtest = None, None 142 | if peer: 143 | if ":" in peer: 144 | ep["domain"], ep["port"] = peer.rsplit(":", 1) 145 | else: 146 | ep["domain"], ep["port"] = peer, "443" 147 | else: 148 | ep["domain"], ep["port"] = ( 149 | G1_TEST_DEFAULT_ENDPOINT if gtest else G1_DEFAULT_ENDPOINT 150 | ) 151 | if ep["domain"].startswith("[") and ep["domain"].endswith("]"): 152 | ep["domain"] = ep["domain"][1:-1] 153 | self.ep = ep 154 | api = "BMAS" if ep["port"] == "443" else "BASIC_MERKLED_API" 155 | self.BMA_ENDPOINT = " ".join([api, ep["domain"], ep["port"]]) 156 | 157 | 158 | @singleton 159 | class ClientInstance(object): 160 | def __init__(self): 161 | self.client = Client(EndPoint().BMA_ENDPOINT) 162 | 163 | 164 | def parse_endpoint(rep): 165 | """ 166 | rep: raw endpoint, sep: split endpoint 167 | domain, ip4 or ip6 could miss on raw endpoint 168 | """ 169 | ep, sep = {}, rep.split(" ") 170 | if sep[0] == "BASIC_MERKLED_API": 171 | if check_port(sep[-1]): 172 | ep["port"] = sep[-1] 173 | if ( 174 | len(sep) == 5 175 | and check_ip(sep[1]) == 0 176 | and check_ip(sep[2]) == 4 177 | and check_ip(sep[3]) == 6 178 | ): 179 | ep["domain"], ep["ip4"], ep["ip6"] = sep[1], sep[2], sep[3] 180 | if len(sep) == 4: 181 | ep = endpoint_type(sep[1], ep) 182 | ep = endpoint_type(sep[2], ep) 183 | if len(sep) == 3: 184 | ep = endpoint_type(sep[1], ep) 185 | return ep 186 | else: 187 | return None 188 | 189 | 190 | def endpoint_type(sep, ep): 191 | typ = check_ip(sep) 192 | if typ == 0: 193 | ep["domain"] = sep 194 | elif typ == 4: 195 | ep["ip4"] = sep 196 | elif typ == 6: 197 | ep["ip6"] = sep 198 | return ep 199 | 200 | 201 | def check_ip(address): 202 | if re.match(IPV4_REGEX, address) != None: 203 | return 4 204 | elif re.match(IPV6_REGEX, address) != None: 205 | return 6 206 | return 0 207 | 208 | 209 | def best_endpoint_address(ep, main): 210 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 211 | s.settimeout(CONNECTION_TIMEOUT) 212 | addresses, port = ("domain", "ip6", "ip4"), int(ep["port"]) 213 | for address in addresses: 214 | if address in ep: 215 | try: 216 | s.connect((ep[address], port)) 217 | s.close() 218 | return address 219 | except Exception as e: 220 | logging.debug( 221 | "Connection to endpoint %s (%s) failled (%s)" % (ep, address, e) 222 | ) 223 | if main: 224 | print("Wrong node given as argument", file=stderr) 225 | exit(FAILURE_EXIT_STATUS) 226 | return None 227 | 228 | 229 | def check_port(port): 230 | try: 231 | port = int(port) 232 | except: 233 | print("Port must be an integer", file=stderr) 234 | return False 235 | if port < 0 or port > 65536: 236 | print("Wrong port number", file=stderr) 237 | return False 238 | return True 239 | -------------------------------------------------------------------------------- /silkaj/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from sys import exit 19 | from asyncio import get_event_loop 20 | from functools import update_wrapper 21 | 22 | from silkaj.constants import G1_SYMBOL, GTEST_SYMBOL, FAILURE_EXIT_STATUS 23 | from silkaj.blockchain_tools import BlockchainParams 24 | 25 | 26 | class CurrencySymbol(object): 27 | __instance = None 28 | 29 | def __new__(cls): 30 | if CurrencySymbol.__instance is None: 31 | CurrencySymbol.__instance = object.__new__(cls) 32 | return CurrencySymbol.__instance 33 | 34 | def __init__(self): 35 | self.symbol = self.get_symbol() 36 | 37 | async def get_symbol(self): 38 | params = await BlockchainParams().params 39 | if params["currency"] == "g1": 40 | return G1_SYMBOL 41 | elif params["currency"] == "g1-test": 42 | return GTEST_SYMBOL 43 | 44 | 45 | def message_exit(message): 46 | print(message) 47 | exit(FAILURE_EXIT_STATUS) 48 | 49 | 50 | def coroutine(f): 51 | def wrapper(*args, **kwargs): 52 | loop = get_event_loop() 53 | return loop.run_until_complete(f(*args, **kwargs)) 54 | 55 | return update_wrapper(wrapper, f) 56 | -------------------------------------------------------------------------------- /silkaj/tui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import sys 19 | import click 20 | from datetime import datetime 21 | 22 | from silkaj import wot, network_tools, constants 23 | from silkaj import crypto_tools as ct 24 | 25 | 26 | def display_amount(tx, message, amount, ud_value, currency_symbol): 27 | """ 28 | Displays an amount in unit and relative reference. 29 | """ 30 | amount_UD = round((amount / ud_value), 2) 31 | tx.append( 32 | [ 33 | message + " (unit|relative)", 34 | "{unit_amount} {currency_symbol} | {UD_amount} UD {currency_symbol}".format( 35 | unit_amount=str(amount / 100), 36 | currency_symbol=currency_symbol, 37 | UD_amount=str(amount_UD), 38 | ), 39 | ] 40 | ) 41 | 42 | 43 | async def display_pubkey(tx, message, pubkey): 44 | """ 45 | Displays a pubkey and the eventually associated id. 46 | """ 47 | tx.append([message + " (pubkey:checksum)", display_pubkey_and_checksum(pubkey)]) 48 | id = await wot.is_member(pubkey) 49 | if id: 50 | tx.append([message + " (id)", id["uid"]]) 51 | 52 | 53 | def display_pubkey_and_checksum( 54 | pubkey, short=False, length=constants.SHORT_PUBKEY_SIZE 55 | ): 56 | """ 57 | Returns ":" in full form. 58 | returns `length` first chars of pubkey and checksum in short form. 59 | `length` defaults to SHORT_PUBKEY_SIZE. 60 | """ 61 | short_pubkey = pubkey[:length] + "…" if short else pubkey 62 | return short_pubkey + ":" + ct.gen_checksum(pubkey) 63 | 64 | 65 | async def send_doc_confirmation(document_name): 66 | if not click.confirm(f"Do you confirm sending this {document_name}?"): 67 | client = network_tools.ClientInstance().client 68 | await client.close() 69 | sys.exit(constants.SUCCESS_EXIT_STATUS) 70 | 71 | 72 | def convert_time(timestamp, kind): 73 | ts = int(timestamp) 74 | date = "%Y-%m-%d" 75 | hour = "%H:%M" 76 | second = ":%S" 77 | if kind == "all": 78 | pattern = date + " " + hour + second 79 | elif kind == "date": 80 | pattern = date 81 | elif kind == "hour": 82 | pattern = hour 83 | if ts >= 3600: 84 | pattern += second 85 | return datetime.fromtimestamp(ts).strftime(pattern) 86 | -------------------------------------------------------------------------------- /silkaj/tx_history.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from click import command, argument, option, echo_via_pager, get_terminal_size 19 | from texttable import Texttable 20 | from operator import itemgetter, neg, eq, ne 21 | from time import time 22 | 23 | from duniterpy.api.bma.tx import history 24 | from duniterpy.documents.transaction import Transaction 25 | 26 | from silkaj.network_tools import ClientInstance 27 | from silkaj.tools import coroutine 28 | from silkaj.tui import convert_time, display_pubkey_and_checksum 29 | from silkaj.crypto_tools import validate_checksum, check_pubkey_format 30 | from silkaj import wot 31 | from silkaj.money import get_amount_from_pubkey, amount_in_current_base, UDValue 32 | from silkaj.tools import CurrencySymbol 33 | 34 | 35 | @command("history", help="Display transaction history") 36 | @argument("pubkey") 37 | @option("--uids", "-u", is_flag=True, help="Display uids") 38 | @option("--full-pubkey", "-f", is_flag=True, help="Display full-length pubkeys") 39 | @coroutine 40 | async def transaction_history(pubkey, uids, full_pubkey): 41 | if check_pubkey_format(pubkey): 42 | pubkey = validate_checksum(pubkey) 43 | 44 | client = ClientInstance().client 45 | ud_value = await UDValue().ud_value 46 | currency_symbol = await CurrencySymbol().symbol 47 | 48 | header = await generate_header(pubkey, currency_symbol, ud_value) 49 | received_txs, sent_txs = list(), list() 50 | await get_transactions_history(client, pubkey, received_txs, sent_txs) 51 | remove_duplicate_txs(received_txs, sent_txs) 52 | txs_list = await generate_table( 53 | received_txs, sent_txs, pubkey, ud_value, currency_symbol, uids, full_pubkey 54 | ) 55 | table = Texttable(max_width=get_terminal_size()[0]) 56 | table.add_rows(txs_list) 57 | await client.close() 58 | echo_via_pager(header + table.draw()) 59 | 60 | 61 | async def generate_header(pubkey, currency_symbol, ud_value): 62 | try: 63 | idty = await wot.identity_of(pubkey) 64 | except: 65 | idty = dict([("uid", "")]) 66 | balance = await get_amount_from_pubkey(pubkey) 67 | return "Transactions history from: {uid} {pubkey}\n\ 68 | Current balance: {balance} {currency}, {balance_ud} UD {currency} on the {date}\n\ 69 | ".format( 70 | uid=idty["uid"], 71 | pubkey=display_pubkey_and_checksum(pubkey), 72 | currency=currency_symbol, 73 | balance=balance[1] / 100, 74 | balance_ud=round(balance[1] / ud_value, 2), 75 | date=convert_time(time(), "all"), 76 | ) 77 | 78 | 79 | async def get_transactions_history(client, pubkey, received_txs, sent_txs): 80 | """ 81 | Get transaction history 82 | Store txs in Transaction object 83 | """ 84 | tx_history = await client(history, pubkey) 85 | currency = tx_history["currency"] 86 | 87 | for received in tx_history["history"]["received"]: 88 | received_txs.append(Transaction.from_bma_history(currency, received)) 89 | for sent in tx_history["history"]["sent"]: 90 | sent_txs.append(Transaction.from_bma_history(currency, sent)) 91 | 92 | 93 | def remove_duplicate_txs(received_txs, sent_txs): 94 | """ 95 | Remove duplicate transactions from history 96 | Remove received tx which contains output back return 97 | that we don’t want to displayed 98 | A copy of received_txs is necessary to remove elements 99 | """ 100 | for received_tx in list(received_txs): 101 | if received_tx in sent_txs: 102 | received_txs.remove(received_tx) 103 | 104 | 105 | async def generate_table( 106 | received_txs, sent_txs, pubkey, ud_value, currency_symbol, uids, full_pubkey 107 | ): 108 | """ 109 | Generate information in a list of lists for texttabe 110 | Merge received and sent txs 111 | Insert table header at the end not to disturb its generation 112 | Sort txs temporarily 113 | """ 114 | 115 | received_txs_table, sent_txs_table = list(), list() 116 | await parse_received_tx( 117 | received_txs_table, received_txs, pubkey, ud_value, uids, full_pubkey 118 | ) 119 | await parse_sent_tx(sent_txs_table, sent_txs, pubkey, ud_value, uids, full_pubkey) 120 | txs_table = received_txs_table + sent_txs_table 121 | 122 | table_titles = [ 123 | "Date", 124 | "Issuers/Recipients", 125 | "Amounts {}".format(currency_symbol), 126 | "Amounts UD{}".format(currency_symbol), 127 | "Comment", 128 | ] 129 | txs_table.insert(0, table_titles) 130 | 131 | txs_table.sort(key=itemgetter(0), reverse=True) 132 | return txs_table 133 | 134 | 135 | async def parse_received_tx( 136 | received_txs_table, received_txs, pubkey, ud_value, uids, full_pubkey 137 | ): 138 | """ 139 | Extract issuers’ pubkeys 140 | Get identities from pubkeys 141 | Convert time into human format 142 | Assign identities 143 | Get amounts and assign amounts and amounts_ud 144 | Append comment 145 | """ 146 | issuers = list() 147 | for received_tx in received_txs: 148 | for issuer in received_tx.issuers: 149 | issuers.append(issuer) 150 | identities = await wot.identities_from_pubkeys(issuers, uids) 151 | for received_tx in received_txs: 152 | tx_list = list() 153 | tx_list.append(convert_time(received_tx.time, "all")) 154 | tx_list.append(str()) 155 | for i, issuer in enumerate(received_tx.issuers): 156 | tx_list[1] += prefix(None, None, i) + assign_idty_from_pubkey( 157 | issuer, identities, full_pubkey 158 | ) 159 | amounts = tx_amount(received_tx, pubkey, received_func)[0] 160 | tx_list.append(amounts / 100) 161 | tx_list.append(amounts / ud_value) 162 | tx_list.append(received_tx.comment) 163 | received_txs_table.append(tx_list) 164 | 165 | 166 | async def parse_sent_tx(sent_txs_table, sent_txs, pubkey, ud_value, uids, full_pubkey): 167 | """ 168 | Extract recipients’ pubkeys from outputs 169 | Get identities from pubkeys 170 | Convert time into human format 171 | Store "Total" and total amounts according to the number of outputs 172 | If not output back return: 173 | Assign amounts, amounts_ud, identities, and comment 174 | """ 175 | pubkeys = list() 176 | for sent_tx in sent_txs: 177 | outputs = tx_amount(sent_tx, pubkey, sent_func)[1] 178 | for output in outputs: 179 | if output_available(output.condition, ne, pubkey): 180 | pubkeys.append(output.condition.left.pubkey) 181 | 182 | identities = await wot.identities_from_pubkeys(pubkeys, uids) 183 | for sent_tx in sent_txs: 184 | tx_list = list() 185 | tx_list.append(convert_time(sent_tx.time, "all")) 186 | 187 | total_amount, outputs = tx_amount(sent_tx, pubkey, sent_func) 188 | if len(outputs) > 1: 189 | tx_list.append("Total") 190 | amounts = str(total_amount / 100) 191 | amounts_ud = str(round(total_amount / ud_value, 2)) 192 | else: 193 | tx_list.append(str()) 194 | amounts = str() 195 | amounts_ud = str() 196 | 197 | for i, output in enumerate(outputs): 198 | if output_available(output.condition, ne, pubkey): 199 | amounts += prefix(None, outputs, i) + str( 200 | neg(amount_in_current_base(output)) / 100 201 | ) 202 | amounts_ud += prefix(None, outputs, i) + str( 203 | round(neg(amount_in_current_base(output)) / ud_value, 2) 204 | ) 205 | tx_list[1] += prefix(tx_list[1], outputs, 0) + assign_idty_from_pubkey( 206 | output.condition.left.pubkey, identities, full_pubkey 207 | ) 208 | tx_list.append(amounts) 209 | tx_list.append(amounts_ud) 210 | tx_list.append(sent_tx.comment) 211 | sent_txs_table.append(tx_list) 212 | 213 | 214 | def tx_amount(tx, pubkey, function): 215 | """ 216 | Determine transaction amount from output sources 217 | """ 218 | amount = 0 219 | outputs = list() 220 | for output in tx.outputs: 221 | if output_available(output.condition, ne, pubkey): 222 | outputs.append(output) 223 | amount += function(output, pubkey) 224 | return amount, outputs 225 | 226 | 227 | def received_func(output, pubkey): 228 | if output_available(output.condition, eq, pubkey): 229 | return amount_in_current_base(output) 230 | return 0 231 | 232 | 233 | def sent_func(output, pubkey): 234 | if output_available(output.condition, ne, pubkey): 235 | return neg(amount_in_current_base(output)) 236 | return 0 237 | 238 | 239 | def output_available(condition, comparison, value): 240 | """ 241 | Check if output source is available 242 | Currently only handle simple SIG condition 243 | XHX, CLTV, CSV should be handled when present in the blockchain 244 | """ 245 | if hasattr(condition.left, "pubkey"): 246 | return comparison(condition.left.pubkey, value) 247 | else: 248 | return False 249 | 250 | 251 | def assign_idty_from_pubkey(pubkey, identities, full_pubkey): 252 | idty = display_pubkey_and_checksum(pubkey, short=not full_pubkey) 253 | for identity in identities: 254 | if pubkey == identity["pubkey"]: 255 | idty = "{0} - {1}".format( 256 | identity["uid"], 257 | display_pubkey_and_checksum(pubkey, short=not full_pubkey), 258 | ) 259 | return idty 260 | 261 | 262 | def prefix(tx_addresses, outputs, occurence): 263 | """ 264 | Pretty print with texttable 265 | Break line when several values in a cell 266 | 267 | Received tx case, 'outputs' is not defined, then add a breakline 268 | between the pubkeys except for the first occurence for multi-sig support 269 | 270 | Sent tx case, handle "Total" line in case of multi-output txs 271 | In case of multiple outputs, there is a "Total" on the top, 272 | where there must be a breakline 273 | """ 274 | 275 | if not outputs: 276 | return "\n" if occurence > 0 else "" 277 | 278 | if tx_addresses == "Total": 279 | return "\n" 280 | return "\n" if len(outputs) > 1 else "" 281 | -------------------------------------------------------------------------------- /tests/patched/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | # This file contains patches for auth functions. 19 | 20 | from duniterpy.key import SigningKey 21 | 22 | 23 | def patched_auth_method(uid): 24 | """ 25 | insecure way to test keys 26 | """ 27 | return SigningKey.from_credentials(uid, uid) 28 | 29 | 30 | def patched_auth_by_seed(): 31 | return "call_auth_by_seed" 32 | 33 | 34 | def patched_auth_by_wif(): 35 | return "call_auth_by_wif" 36 | 37 | 38 | def patched_auth_by_auth_file(): 39 | return "call_auth_by_auth_file" 40 | 41 | 42 | def patched_auth_by_scrypt(): 43 | return "call_auth_by_scrypt" 44 | -------------------------------------------------------------------------------- /tests/patched/blockchain_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | # This file contains fake values for testing purposes 19 | 20 | from duniterpy.documents.block_uid import BlockUID 21 | 22 | currency = "g1" 23 | 24 | mocked_block = { 25 | "number": 48000, 26 | "time": 1592243760, 27 | "unitbase": 0, 28 | "currency": currency, 29 | "hash": "0000010D30B1284D34123E036B7BE0A449AE9F2B928A77D7D20E3BDEAC7EE14C", 30 | } 31 | 32 | fake_block_uid = BlockUID( 33 | 48000, "0000010D30B1284D34123E036B7BE0A449AE9F2B928A77D7D20E3BDEAC7EE14C" 34 | ) 35 | 36 | 37 | async def patched_params(self): 38 | return { 39 | "msValidity": 31557600, 40 | "msPeriod": 5259600, 41 | } 42 | 43 | 44 | async def patched_block(self, number): 45 | return mocked_block 46 | 47 | 48 | ## mock head_block() 49 | async def patched_head_block(self): 50 | return mocked_block 51 | -------------------------------------------------------------------------------- /tests/patched/money.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2020 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | # This file contains patched functions for testing purposes. 19 | 20 | from silkaj.constants import G1_SYMBOL 21 | from silkaj.money import amount_in_current_base 22 | from silkaj.tx import MAX_INPUTS_PER_TX 23 | from duniterpy.documents.transaction import InputSource 24 | from patched.test_constants import mock_ud_value 25 | 26 | 27 | async def patched_ud_value(self): 28 | return mock_ud_value 29 | 30 | 31 | # mock get_sources() 32 | async def patched_get_sources(pubkey): 33 | """ 34 | Returns transaction sources. 35 | This function doesn't cover all possibilities : only SIG() unlock condition. 36 | 37 | Can be called many times (depending on pubkey). 38 | If so, it will mock intermediary tx for the first 40 inputs. 39 | Tests using this function should reset the counter at the begining of each test case. 40 | See source_dict.py for inputs lists. 41 | 42 | all UTXO have the same amount : 100 43 | all UD have the same amount : 314 44 | 45 | for pubkey CtM5RZHopnSRAAoWNgTWrUhDEmspcCAxn6fuCEWDWudp : 3 TX, balance = 300 46 | for pubkey HcRgKh4LwbQVYuAc3xAdCynYXpKoiPE6qdxCMa8JeHat : 53 TX, balance = 5300 47 | for pubkey 2sq4w8yYVDWNxVWZqGWWDriFf5z7dn7iLahDCvEEotuY : 10 UD, balance = 3140 48 | for pubkey 9cwBBgXcSVMT74xiKYygX6FM5yTdwd3NABj1CfHbbAmp : 50 UD and 20 TX, balance = 17700 49 | else : 0 sources, balance = 0 50 | Same hash for each TX for convenience. This may change for other testing purposes. 51 | """ 52 | 53 | def listinput_UD(listinput, balance, pubkey, max_ud): 54 | a = 0 55 | while a < max_ud: 56 | listinput.append( 57 | InputSource( 58 | amount=mock_ud_value, 59 | base=0, 60 | source="D", 61 | origin_id=pubkey, 62 | index=a, 63 | ) 64 | ) 65 | balance += amount_in_current_base(listinput[-1]) 66 | a += 1 67 | return balance 68 | 69 | def listinput_TX(listinput, balance, max_tx): 70 | a = 0 71 | while a < max_tx: 72 | listinput.append( 73 | InputSource( 74 | amount=100, 75 | base=0, 76 | source="T", 77 | origin_id="1F3059ABF35D78DFB5AFFB3DEAB4F76878B04DB6A14757BBD6B600B1C19157E7", 78 | index=a, 79 | ) 80 | ) 81 | balance += amount_in_current_base(listinput[-1]) 82 | a += 1 83 | return balance 84 | 85 | listinput, n = list(), 0 86 | balance = 0 87 | if pubkey == "CtM5RZHopnSRAAoWNgTWrUhDEmspcCAxn6fuCEWDWudp": 88 | max_ud = 0 89 | max_tx = 3 90 | elif pubkey == "HcRgKh4LwbQVYuAc3xAdCynYXpKoiPE6qdxCMa8JeHat": 91 | if patched_get_sources.counter == 0: 92 | max_ud = 0 93 | max_tx = 53 94 | elif patched_get_sources.counter == 1: 95 | listinput.append( 96 | InputSource( 97 | amount=100 * MAX_INPUTS_PER_TX, # 100 * 46 = 4600 98 | base=0, 99 | source="T", 100 | origin_id="1F3059ABF35D78DFB5AFFB3DEAB4F76878B04DB6A14757BBD6B600B1C19157E7", 101 | index=93, 102 | ) 103 | ) 104 | max_ud = 0 105 | max_tx = 6 106 | elif pubkey == "2sq4w8yYVDWNxVWZqGWWDriFf5z7dn7iLahDCvEEotuY": 107 | max_ud = 10 108 | max_tx = 0 109 | elif pubkey == "9cwBBgXcSVMT74xiKYygX6FM5yTdwd3NABj1CfHbbAmp": 110 | if patched_get_sources.counter == 0: 111 | max_ud = 50 112 | max_tx = 20 113 | elif patched_get_sources.counter == 1: 114 | listinput.append( 115 | InputSource( 116 | amount=mock_ud_value * MAX_INPUTS_PER_TX, # 46 UD = 46*314 = 1444 117 | base=0, 118 | source="T", 119 | origin_id="1F3059ABF35D78DFB5AFFB3DEAB4F76878B04DB6A14757BBD6B600B1C19157E7", 120 | index=93, 121 | ) 122 | ) 123 | max_ud = 4 124 | max_tx = 20 125 | elif pubkey == "BdanxHdwRRzCXZpiqvTVTX4gyyh6qFTYjeCWCkLwDifx": 126 | listinput.append( 127 | InputSource( 128 | amount=9600, 129 | base=0, 130 | source="T", 131 | origin_id="1F3059ABF35D78DFB5AFFB3DEAB4F76878B04DB6A14757BBD6B600B1C19157E7", 132 | index=0, 133 | ) 134 | ) 135 | max_ud = 0 136 | max_tx = 0 137 | else: 138 | max_ud = 0 139 | max_tx = 0 140 | 141 | balance = listinput_UD(listinput, balance, pubkey, max_ud) 142 | balance = listinput_TX(listinput, balance, max_tx) 143 | 144 | patched_get_sources.counter += 1 145 | return listinput, balance 146 | 147 | 148 | patched_get_sources.counter = 0 149 | -------------------------------------------------------------------------------- /tests/patched/test_constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | # this file contains only constant values for testing (no function) to prevent circular dependencies 19 | 20 | mock_ud_value = 314 21 | -------------------------------------------------------------------------------- /tests/patched/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from silkaj.constants import G1_SYMBOL 19 | 20 | # mock CurrencySymbol().symbol 21 | async def patched_currency_symbol(self): 22 | return G1_SYMBOL 23 | -------------------------------------------------------------------------------- /tests/patched/tx.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from duniterpy.key import SigningKey 19 | from silkaj.tools import message_exit 20 | 21 | 22 | async def patched_gen_confirmation_table( 23 | issuer_pubkey, 24 | pubkey_amount, 25 | tx_amounts, 26 | outputAddresses, 27 | outputBackChange, 28 | comment, 29 | ): 30 | if not ( 31 | ( 32 | isinstance(issuer_pubkey, str) 33 | and isinstance(pubkey_amount, int) 34 | and isinstance(tx_amounts, list) 35 | and isinstance(outputAddresses, list) 36 | and isinstance(comment, str) 37 | and isinstance(outputBackchange, str) 38 | ) 39 | and len(tx_amounts) == len(outputAddresses) 40 | and sum(tx_amounts) <= pubkey_amount 41 | ): 42 | message_exit( 43 | "Test error : patched_transaction_confirmation() : Parameters are not coherent" 44 | ) 45 | 46 | 47 | async def patched_handle_intermediaries_transactions( 48 | key, 49 | issuers, 50 | tx_amounts, 51 | outputAddresses, 52 | Comment="", 53 | OutputbackChange=None, 54 | ): 55 | if not ( 56 | ( 57 | isinstance(key, SigningKey) 58 | and isinstance(issuers, str) 59 | and isinstance(tx_amounts, list) 60 | and isinstance(outputAddresses, list) 61 | and isinstance(Comment, str) 62 | and (isinstance(OutputBackchange, str) or OutputbackChange == None) 63 | ) 64 | and len(tx_amounts) == len(outputAddresses) 65 | and key.pubkey() == issuers 66 | ): 67 | message_exit( 68 | "Test error : patched_handle_intermediaries_transactions() : Parameters are not coherent" 69 | ) 70 | 71 | 72 | async def patched_generate_and_send_transaction( 73 | key, 74 | issuers, 75 | tx_amounts, 76 | listinput_and_amount, 77 | outputAddresses, 78 | Comment, 79 | OutputbackChange, 80 | ): 81 | if not ( 82 | ( 83 | isinstance(key, SigningKey) 84 | and isinstance(issuers, str) 85 | and isinstance(tx_amounts, list) 86 | and isinstance(listinput_and_amount, tuple) 87 | and isinstance(outputAddresses, list) 88 | and isinstance(Comment, str) 89 | and isinstance(OutputBackchange, str) 90 | ) 91 | and len(tx_amounts) == len(outputAddresses) 92 | and sum(tx_amounts) <= listinput_and_amount[2] 93 | and key.pubkey() == issuers 94 | ): 95 | message_exit( 96 | "Test error : patched_generate_and_send_transaction() : Parameters are not coherent" 97 | ) 98 | pass 99 | -------------------------------------------------------------------------------- /tests/patched/tx_history.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from duniterpy.documents.transaction import Transaction 19 | 20 | from patched.wot import pubkey_list 21 | from patched.blockchain_tools import currency 22 | 23 | fake_received_tx_hist = [ 24 | { 25 | "version": 10, 26 | "locktime": 0, 27 | "blockstamp": "150574-000003D64BB3E107EEEF90922A60DB56A6DF14FB4415A3F4A852A87F889E1C31", 28 | "blockstampTime": 1535913486, 29 | "issuers": ["CvrMiUhAJpNyX5sdAyZqPE6yEFfSsf6j9EpMmeKvMCWW"], 30 | "inputs": [ 31 | "7014:0:T:00DFBACFA3434057F6B2DAA8249324C64A658E40BC85CFD40E15FADDD88BACE3:1" 32 | ], 33 | "outputs": [ 34 | "100:0:SIG(d88fPFbDdJXJANHH7hedFMaRyGcnVZj9c5cDaE76LRN)", 35 | "6914:0:SIG(CvrMiUhAJpNyX5sdAyZqPE6yEFfSsf6j9EpMmeKvMCWW)", 36 | ], 37 | "unlocks": ["0:SIG(0)"], 38 | "signatures": [ 39 | "xz/l3o9GbUclrYDNKiRaVTrBP7cppDmrjDgE2rFNLJsnpu1e/AE2bHylftU09NYEDqzCUbehv19oF6zIRVwTDw==" 40 | ], 41 | "comment": "initialisation", 42 | "hash": "D2271075F2308C4092B1F57B3F1BE12AB684FAFCA62BA8EFE9F7F4D7A4D8D69F", 43 | "time": 111111114, 44 | "block_number": 150576, 45 | "received": None, 46 | }, 47 | { 48 | "version": 10, 49 | "locktime": 0, 50 | "blockstamp": "400498-0000000EE3E7C41160E5638B7DB3F76A82068D6D3D1CC2332EE7A39AF43A9EA6", 51 | "blockstampTime": 1613798963, 52 | "issuers": ["CmFKubyqbmJWbhyH2eEPVSSs4H4NeXGDfrETzEnRFtPd"], 53 | "inputs": ["1023:0:D:CmFKubyqbmJWbhyH2eEPVSSs4H4NeXGDfrETzEnRFtPd:396949"], 54 | "outputs": [ 55 | "100:0:SIG(d88fPFbDdJXJANHH7hedFMaRyGcnVZj9c5cDaE76LRN)", 56 | "923:0:SIG(CmFKubyqbmJWbhyH2eEPVSSs4H4NeXGDfrETzEnRFtPd)", 57 | ], 58 | "unlocks": ["0:SIG(0)"], 59 | "signatures": [ 60 | "pYSOTCrl1QbsKrgjgNWnUfD3wJnpbalv9EwjAbZozTbTOSzYoj+UInzKS8/OiSdyVqFVDLdpewTD+FOHRENDAA==" 61 | ], 62 | "comment": "", 63 | "hash": "F1F2E6D6CF123AB78B98B662FE3AFDD2577B8F6CEBC245660B2E67BC9C2026F6", 64 | "time": 111111113, 65 | "block_number": 400500, 66 | "received": None, 67 | }, 68 | ] 69 | 70 | 71 | fake_sent_tx_hist = [ 72 | { 73 | "version": 10, 74 | "locktime": 0, 75 | "blockstamp": "400503-0000000A7F3B6F4C5654D9CCFEA41E4726E02B08BB94EE30BD9A50908D28636D", 76 | "blockstampTime": 1613801234, 77 | "issuers": ["d88fPFbDdJXJANHH7hedFMaRyGcnVZj9c5cDaE76LRN"], 78 | "inputs": [ 79 | "100:0:T:F1F2E6D6CF123AB78B98B662FE3AFDD2577B8F6CEBC245660B2E67BC9C2026F6:0" 80 | ], 81 | "outputs": ["100:0:SIG(CvrMiUhAJpNyX5sdAyZqPE6yEFfSsf6j9EpMmeKvMCWW)"], 82 | "unlocks": ["0:SIG(0)"], 83 | "signatures": [ 84 | "cMNp7FF5yT/6LJT9CnNzkE08h+APEAYYwdFIROGxUZ9JGqbfPR1NRbcruq5Fl9BnBcJkuMNJbOwuYV8bPCmICw==" 85 | ], 86 | "comment": "", 87 | "hash": "580715ECD6743590F7A99A6C97E63511BC94B0293CB0037C6A3C96482F8DC7D2", 88 | "time": 111111112, 89 | "block_number": 400505, 90 | "received": None, 91 | }, 92 | { 93 | "version": 10, 94 | "locktime": 0, 95 | "blockstamp": "400503-0000000A7F3B6F4C5654D9CCFEA41E4726E02B08BB94EE30BD9A50908D28636D", 96 | "blockstampTime": 1613801235, 97 | "issuers": ["d88fPFbDdJXJANHH7hedFMaRyGcnVZj9c5cDaE76LRN"], 98 | "inputs": [ 99 | "100:0:T:D2271075F2308C4092B1F57B3F1BE12AB684FAFCA62BA8EFE9F7F4D7A4D8D69F:0" 100 | ], 101 | "outputs": ["100:0:SIG(CmFKubyqbmJWbhyH2eEPVSSs4H4NeXGDfrETzEnRFtPd)"], 102 | "unlocks": ["0:SIG(0)"], 103 | "signatures": [ 104 | "WL3dRX4XUenWlDYYhRmEOUgL5+Tc08LlOJWHNjmTlxqtsdHhGn7MuQ3lK+3Xv7PV6VFEEdc3vlJ52pWCLKN5BA==" 105 | ], 106 | "comment": "", 107 | "hash": "E874CDAC01D9F291DC1E03F8B0ADB6C19259DE5A11FB73A16318BA1AD59B9EDC", 108 | "time": 111111111, 109 | "block_number": 400505, 110 | "received": None, 111 | }, 112 | ] 113 | 114 | 115 | async def patched_get_transactions_history(client, pubkey, received_txs, sent_txs): 116 | for received in fake_received_tx_hist: 117 | received_txs.append(Transaction.from_bma_history(currency, received)) 118 | for sent in fake_sent_tx_hist: 119 | sent_txs.append(Transaction.from_bma_history(currency, sent)) 120 | -------------------------------------------------------------------------------- /tests/patched/wot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | pubkey_list = [ 19 | {"pubkey": "9cwBBgXcSVMT74xiKYygX6FM5yTdwd3NABj1CfHbbAmp", "uid": ""}, 20 | {"pubkey": "BUhLyJT17bzDVXW66xxfk1F7947vytmwJVadTaWb8sJS", "uid": ""}, 21 | {"pubkey": "CtM5RZHopnSRAAoWNgTWrUhDEmspcCAxn6fuCEWDWudp", "uid": "riri"}, 22 | {"pubkey": "HcRgKh4LwbQVYuAc3xAdCynYXpKoiPE6qdxCMa8JeHat", "uid": "fifi"}, 23 | {"pubkey": "2sq4w8yYVDWNxVWZqGWWDriFf5z7dn7iLahDCvEEotuY", "uid": "loulou"}, 24 | { 25 | "pubkey": "CvrMiUhAJpNyX5sdAyZqPE6yEFfSsf6j9EpMmeKvMCWW", 26 | "uid": "mato", 27 | }, 28 | ] 29 | 30 | 31 | # mock is_member 32 | async def patched_is_member(pubkey): 33 | for account in pubkey_list: 34 | if account["pubkey"] == pubkey: 35 | if account["uid"]: 36 | return account 37 | return False 38 | 39 | 40 | # patch wot requirements 41 | async def patched_wot_requirements_one_pending(pubkey, identity_uid): 42 | return { 43 | "identities": [ 44 | { 45 | "uid": "toto", 46 | "pendingMemberships": [ 47 | { 48 | "membership": "IN", 49 | "issuer": "5B8iMAzq1dNmFe3ZxFTBQkqhq4fsztg1gZvxHXCk1XYH", 50 | "number": 613206, 51 | "blockNumber": 613206, 52 | "userid": "moul-test", 53 | "expires_on": 1598624404, 54 | "type": "IN", 55 | } 56 | ], 57 | "membershipPendingExpiresIn": 6311520, 58 | "membershipExpiresIn": 2603791, 59 | }, 60 | ], 61 | } 62 | 63 | 64 | async def patched_wot_requirements_no_pending(pubkey, identity_uid): 65 | return { 66 | "identities": [ 67 | { 68 | "uid": "toto", 69 | "pendingMemberships": [], 70 | "membershipPendingExpiresIn": 0, 71 | "membershipExpiresIn": 3724115, 72 | } 73 | ] 74 | } 75 | 76 | 77 | # for history 78 | async def patched_identities_from_pubkeys(pubkeys, uids): 79 | if not uids: 80 | return list() 81 | uniq_pubkeys = list(filter(None, set(pubkeys))) 82 | identities = list() 83 | 84 | for pubkey in uniq_pubkeys: 85 | for id in pubkey_list: 86 | if id.get("pubkey", False) == pubkey: 87 | identities.append(id) 88 | return identities 89 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import click 3 | 4 | from silkaj import auth 5 | 6 | from patched.auth import ( 7 | patched_auth_by_seed, 8 | patched_auth_by_wif, 9 | patched_auth_by_auth_file, 10 | patched_auth_by_scrypt, 11 | ) 12 | 13 | # test auth_method 14 | @pytest.mark.parametrize( 15 | "seed, file, wif, auth_method_called", 16 | [ 17 | (True, False, False, "call_auth_by_seed"), 18 | (False, True, False, "call_auth_by_auth_file"), 19 | (False, False, True, "call_auth_by_wif"), 20 | (False, False, False, "call_auth_by_scrypt"), 21 | ], 22 | ) 23 | def test_auth_method(seed, file, wif, auth_method_called, monkeypatch): 24 | monkeypatch.setattr("silkaj.auth.auth_by_seed", patched_auth_by_seed) 25 | monkeypatch.setattr("silkaj.auth.auth_by_wif", patched_auth_by_wif) 26 | monkeypatch.setattr("silkaj.auth.auth_by_auth_file", patched_auth_by_auth_file) 27 | monkeypatch.setattr("silkaj.auth.auth_by_scrypt", patched_auth_by_scrypt) 28 | ctx = click.Context( 29 | click.Command(""), obj={"AUTH_SEED": seed, "AUTH_FILE": file, "AUTH_WIF": wif} 30 | ) 31 | with ctx: 32 | assert auth_method_called == auth.auth_method() 33 | -------------------------------------------------------------------------------- /tests/test_checksum.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import pytest 19 | from click.testing import CliRunner 20 | 21 | from silkaj.cli import cli 22 | from silkaj.checksum import MESSAGE 23 | 24 | 25 | pubkey = "3rp7ahDGeXqffBQTnENiXEFXYS7BRjYmS33NbgfCuDc8" 26 | checksum = "DFQ" 27 | pubkey_checksum = pubkey + ":" + checksum 28 | pubkey_seedhex_authfile = ( 29 | "3bc6f2484e441e40562155235cdbd8ce04c25e7df35bf5f87c067bf239db8511" 30 | ) 31 | 32 | 33 | @pytest.mark.parametrize( 34 | "command, excepted_output", 35 | [ 36 | (["checksum", pubkey_checksum], "The checksum is valid"), 37 | (["checksum", pubkey + ":vAK"], "The checksum is invalid"), 38 | (["checksum", pubkey], pubkey_checksum), 39 | (["checksum", "uid"], "Error: Wrong public key format"), 40 | (["checksum"], MESSAGE), 41 | (["--auth-file", "checksum"], pubkey_checksum), 42 | (["--auth-file", "checksum", "pubkey"], pubkey_checksum), 43 | ], 44 | ) 45 | def test_checksum_command(command, excepted_output): 46 | with CliRunner().isolated_filesystem(): 47 | with open("authfile", "w") as f: 48 | f.write(pubkey_seedhex_authfile) 49 | result = CliRunner().invoke(cli, args=command) 50 | assert result.output == excepted_output + "\n" 51 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from click.testing import CliRunner 19 | 20 | from silkaj.cli import cli 21 | from silkaj.constants import FAILURE_EXIT_STATUS 22 | 23 | 24 | def test_cli_dry_run_display_options_passed_together(): 25 | # Run command with dry_run and display options 26 | command = ["--dry-run", "--display", "membership"] 27 | result = CliRunner().invoke(cli, args=command) 28 | 29 | error_msg = "ERROR: display and dry-run options can not be used together\n" 30 | assert error_msg == result.output 31 | assert result.exit_code == FAILURE_EXIT_STATUS 32 | -------------------------------------------------------------------------------- /tests/test_crypto_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import pytest 19 | 20 | from silkaj import crypto_tools 21 | 22 | # test gen_checksum 23 | @pytest.mark.parametrize( 24 | "pubkey, checksum", 25 | [ 26 | ("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", "KAv"), 27 | ], 28 | ) 29 | def test_gen_checksum(pubkey, checksum): 30 | assert checksum == crypto_tools.gen_checksum(pubkey) 31 | 32 | 33 | # test validate_checksum 34 | @pytest.mark.parametrize( 35 | "pubkey, checksum, expected", 36 | [ 37 | ("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", "KAv", None), 38 | ( 39 | "J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", 40 | "KA", 41 | "Error: public key 'J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX' does not match checksum 'KA'.\nPlease verify the public key.\n", 42 | ), 43 | ], 44 | ) 45 | def test_validate_checksum(pubkey, checksum, expected, capsys): 46 | pubkey_with_ck = str(pubkey + ":" + checksum) 47 | if expected == None: 48 | assert pubkey == crypto_tools.validate_checksum(pubkey_with_ck) 49 | else: 50 | with pytest.raises(SystemExit) as pytest_exit: 51 | test = crypto_tools.validate_checksum(pubkey_with_ck) 52 | assert capsys.readouterr().out == expected 53 | assert pytest_exit.type == SystemExit 54 | 55 | 56 | # test check_pubkey_format 57 | @pytest.mark.parametrize( 58 | "pubkey, display_error, expected", 59 | [ 60 | ("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KAv", True, True), 61 | ("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", True, False), 62 | ("Youpi", False, None), 63 | ("Youpi", True, "Error: bad format for following public key: Youpi\n"), 64 | ], 65 | ) 66 | def test_check_pubkey_format(pubkey, display_error, expected, capsys): 67 | if isinstance(expected, str): 68 | with pytest.raises(SystemExit) as pytest_exit: 69 | test = crypto_tools.check_pubkey_format(pubkey, display_error) 70 | assert capsys.readouterr().out == expected 71 | assert pytest_exit.type == SystemExit 72 | else: 73 | assert expected == crypto_tools.check_pubkey_format(pubkey, display_error) 74 | 75 | 76 | # test is_pubkey_and_check 77 | @pytest.mark.parametrize( 78 | "uid_pubkey, expected", 79 | [ 80 | ( 81 | "J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KAv", 82 | "J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", 83 | ), 84 | ( 85 | "J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", 86 | "J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", 87 | ), 88 | ("Youpi", False), 89 | ], 90 | ) 91 | def test_is_pubkey_and_check(uid_pubkey, expected): 92 | assert expected == crypto_tools.is_pubkey_and_check(uid_pubkey) 93 | 94 | 95 | # test is_pubkey_and_check errors 96 | @pytest.mark.parametrize( 97 | "uid_pubkey, expected", 98 | [ 99 | ( 100 | "J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KA", 101 | "Error: bad format for following public key: J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KA", 102 | ), 103 | ( 104 | "J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX:KAt", 105 | "Error: Wrong checksum for following public key: J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", 106 | ), 107 | ], 108 | ) 109 | def test_is_pubkey_and_check_errors(uid_pubkey, expected, capsys): 110 | with pytest.raises(SystemExit) as pytest_exit: 111 | test = crypto_tools.is_pubkey_and_check(uid_pubkey) 112 | assert capsys.readouterr() == expected 113 | assert pytest_exit.type == SystemExit 114 | -------------------------------------------------------------------------------- /tests/test_end_to_end.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from subprocess import check_output 19 | 20 | silkaj = ["poetry", "run", "silkaj"] 21 | 22 | 23 | def test_info(): 24 | """tests 'silkaj info' returns a number of members""" 25 | 26 | output = check_output(silkaj + ["info"]) 27 | assert "Number of members" in output.decode() 28 | 29 | 30 | def test_wot(): 31 | """tests 'silkaj wot' returns a number of members""" 32 | 33 | output = check_output(silkaj + ["wot", "Matograine"]).decode() 34 | assert "Matograine (CmFKubyq…:CQ5) from block #106433-00000340…" in output 35 | assert "received_expire" in output 36 | assert "received" in output 37 | assert "sent" in output 38 | assert "sent_expire" in output 39 | 40 | 41 | def test_id(): 42 | """tests 'silkaj id' certification on gtest""" 43 | 44 | output = check_output(silkaj + ["--gtest", "id", "elois"]).decode() 45 | assert "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C" in output 46 | 47 | 48 | def test_balance(): 49 | """tests 'silkaj amount' command on gtest""" 50 | 51 | output = check_output( 52 | silkaj + ["--gtest", "balance", "3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj"] 53 | ).decode() 54 | assert ( 55 | "│ Balance of pubkey │ 3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:EyF │" 56 | in output 57 | ) 58 | assert "│ Total amount (unit|relative) │" in output 59 | assert "UD ĞTest" in output 60 | assert "Total relative to M/N" in output 61 | -------------------------------------------------------------------------------- /tests/test_membership.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import sys 19 | from unittest.mock import Mock 20 | import pytest 21 | from click.testing import CliRunner 22 | 23 | from tabulate import tabulate 24 | import pendulum 25 | 26 | from duniterpy.documents import Membership, block_uid 27 | from duniterpy.api import bma 28 | from duniterpy.key import SigningKey 29 | 30 | from patched.blockchain_tools import ( 31 | currency, 32 | patched_params, 33 | patched_block, 34 | patched_head_block, 35 | fake_block_uid, 36 | ) 37 | from patched.wot import ( 38 | patched_wot_requirements_one_pending, 39 | patched_wot_requirements_no_pending, 40 | ) 41 | 42 | from silkaj import auth, wot 43 | from silkaj.cli import cli 44 | from silkaj.network_tools import ClientInstance 45 | from silkaj import membership 46 | from silkaj.blockchain_tools import BlockchainParams, HeadBlock 47 | from silkaj.constants import ( 48 | SUCCESS_EXIT_STATUS, 49 | FAILURE_EXIT_STATUS, 50 | ) 51 | from silkaj.tui import display_pubkey_and_checksum 52 | 53 | # AsyncMock available from Python 3.8. asynctest is used for Py < 3.8 54 | if sys.version_info[1] > 7: 55 | from unittest.mock import AsyncMock 56 | else: 57 | from asynctest.mock import CoroutineMock as AsyncMock 58 | 59 | 60 | # Values and patches 61 | pubkey = "EA7Dsw39ShZg4SpURsrgMaMqrweJPUFPYHwZA8e92e3D" 62 | identity_timestamp = block_uid( 63 | "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855" 64 | ) 65 | identity_uid = "toto" 66 | membership_timestamp = fake_block_uid 67 | 68 | 69 | def patched_auth_method(): 70 | return SigningKey.from_credentials(identity_uid, identity_uid) 71 | 72 | 73 | async def patched_choose_identity(pubkey): 74 | return ( 75 | {"uid": identity_uid, "meta": {"timestamp": identity_timestamp}}, 76 | pubkey, 77 | None, 78 | ) 79 | 80 | 81 | @pytest.mark.parametrize( 82 | "dry_run, display, confirmation, exit_code", 83 | [ 84 | (True, False, False, SUCCESS_EXIT_STATUS), 85 | (False, True, False, SUCCESS_EXIT_STATUS), 86 | (False, True, True, FAILURE_EXIT_STATUS), 87 | (False, False, True, FAILURE_EXIT_STATUS), 88 | ], 89 | ) 90 | def test_membership_cmd(dry_run, display, confirmation, exit_code, monkeypatch): 91 | # Monkeypatch and Mock 92 | monkeypatch.setattr(auth, "auth_method", patched_auth_method) 93 | monkeypatch.setattr(HeadBlock, "get_head", patched_head_block) 94 | monkeypatch.setattr(wot, "choose_identity", patched_choose_identity) 95 | 96 | patched_display_confirmation_table = AsyncMock() 97 | monkeypatch.setattr( 98 | membership, 99 | "display_confirmation_table", 100 | patched_display_confirmation_table, 101 | ) 102 | if not dry_run and not display: 103 | patched_generate_membership_document = Mock() 104 | monkeypatch.setattr( 105 | membership, 106 | "generate_membership_document", 107 | patched_generate_membership_document, 108 | ) 109 | 110 | # Run membership command 111 | command = [] 112 | if dry_run: 113 | command += ["--dry-run"] 114 | if display: 115 | command += ["--display"] 116 | command += ["membership"] 117 | pass_license = "No\nYes\n" 118 | confirmations = pass_license + ("Yes" if confirmation else "No") 119 | result = CliRunner().invoke(cli, args=command, input=confirmations) 120 | 121 | # Assert functions are called 122 | patched_display_confirmation_table.assert_awaited_once_with( 123 | identity_uid, 124 | pubkey, 125 | identity_timestamp, 126 | ) 127 | if dry_run or display: 128 | assert "Type: Membership" in result.output 129 | else: 130 | patched_generate_membership_document.assert_called_with( 131 | currency, 132 | pubkey, 133 | membership_timestamp, 134 | identity_uid, 135 | identity_timestamp, 136 | ) 137 | 138 | assert result.exit_code == exit_code 139 | 140 | 141 | @pytest.mark.parametrize( 142 | "patched_wot_requirements", 143 | [patched_wot_requirements_no_pending, patched_wot_requirements_one_pending], 144 | ) 145 | @pytest.mark.asyncio 146 | async def test_display_confirmation_table( 147 | patched_wot_requirements, monkeypatch, capsys 148 | ): 149 | monkeypatch.setattr(bma.wot, "requirements", patched_wot_requirements) 150 | monkeypatch.setattr(bma.blockchain, "parameters", patched_params) 151 | monkeypatch.setattr(bma.blockchain, "block", patched_block) 152 | 153 | client = ClientInstance().client 154 | identities_requirements = await client(bma.wot.requirements, pubkey) 155 | for identity_requirements in identities_requirements["identities"]: 156 | if identity_requirements["uid"] == identity_uid: 157 | membership_expires = identity_requirements["membershipExpiresIn"] 158 | pending_expires = identity_requirements["membershipPendingExpiresIn"] 159 | pending_memberships = identity_requirements["pendingMemberships"] 160 | break 161 | 162 | table = list() 163 | if membership_expires: 164 | expires = pendulum.now().add(seconds=membership_expires).diff_for_humans() 165 | table.append(["Expiration date of current membership", expires]) 166 | 167 | if pending_memberships: 168 | line = [ 169 | "Number of pending membership(s) in the mempool", 170 | len(pending_memberships), 171 | ] 172 | table.append(line) 173 | expiration = pendulum.now().add(seconds=pending_expires).diff_for_humans() 174 | table.append(["Pending membership documents will expire", expiration]) 175 | 176 | table.append(["User Identifier (UID)", identity_uid]) 177 | table.append(["Public Key", display_pubkey_and_checksum(pubkey)]) 178 | 179 | table.append(["Block Identity", str(identity_timestamp)[:45] + "…"]) 180 | 181 | block = await client(bma.blockchain.block, identity_timestamp.number) 182 | table.append( 183 | ["Identity published", pendulum.from_timestamp(block["time"]).format("LL")], 184 | ) 185 | 186 | params = await BlockchainParams().params 187 | membership_validity = ( 188 | pendulum.now().add(seconds=params["msValidity"]).diff_for_humans() 189 | ) 190 | table.append(["Expiration date of new membership", membership_validity]) 191 | 192 | membership_mempool = ( 193 | pendulum.now().add(seconds=params["msPeriod"]).diff_for_humans() 194 | ) 195 | table.append( 196 | ["Expiration date of new membership from the mempool", membership_mempool] 197 | ) 198 | 199 | expected = tabulate(table, tablefmt="fancy_grid") + "\n" 200 | 201 | await membership.display_confirmation_table( 202 | identity_uid, pubkey, identity_timestamp 203 | ) 204 | captured = capsys.readouterr() 205 | assert expected == captured.out 206 | 207 | 208 | def test_generate_membership_document(): 209 | generated_membership = membership.generate_membership_document( 210 | currency, 211 | pubkey, 212 | membership_timestamp, 213 | identity_uid, 214 | identity_timestamp, 215 | ) 216 | expected = Membership( 217 | version=10, 218 | currency=currency, 219 | issuer=pubkey, 220 | membership_ts=membership_timestamp, 221 | membership_type="IN", 222 | uid=identity_uid, 223 | identity_ts=identity_timestamp, 224 | ) 225 | # Direct equality check can be done without raw() once Membership.__eq__() is implemented 226 | assert expected.raw() == generated_membership.raw() 227 | -------------------------------------------------------------------------------- /tests/test_money.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import pytest 19 | from click.testing import CliRunner 20 | 21 | import duniterpy.api.bma.tx as bma_tx 22 | 23 | # had to import from wot to prevent loop dependencies 24 | from silkaj.wot import display_pubkey_and_checksum 25 | from silkaj.cli import cli 26 | from silkaj.constants import FAILURE_EXIT_STATUS 27 | from silkaj.money import get_sources 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_get_sources(monkeypatch): 32 | """ 33 | test that get_source() will : 34 | * only use simple SIG txs 35 | * only use blockchain sources to compute balance 36 | * return pending txs in first positions of the sources list 37 | """ 38 | 39 | async def patched_tx_sources(self, pubkey): 40 | return { 41 | "currency": "g1-test", 42 | "pubkey": "AhRMHUxMPXSeG7qXZrE6qCdjwK9p2bu5Eqei7xAWVEDK", 43 | "sources": [ 44 | # this source will be returned in inputlist, and its amount used. 45 | { 46 | "type": "T", 47 | "noffset": 2, 48 | "identifier": "0310F56D22F4CEF5E41B9D5CACB6E21F224B79D9398D53A4E754866435710242", 49 | "amount": 10, 50 | "base": 3, 51 | "conditions": "SIG(AhRMHUxMPXSeG7qXZrE6qCdjwK9p2bu5Eqei7xAWVEDK)", 52 | }, 53 | # this source will not be returned (complex unlock condition) 54 | { 55 | "type": "T", 56 | "noffset": 3, 57 | "identifier": "0D6A29451E64F468C0DB19F70D0D17F65BDCC98F3A16DD55B3755BE124B3DD6C", 58 | "amount": 30, 59 | "base": 3, 60 | "conditions": "(SIG(2VgEZnrGQ5hEgwoNrcXZnD9c8o5jL63LPBmJdvMyFhGe) || (SIG(AhRMHUxMPXSeG7qXZrE6qCdjwK9p2bu5Eqei7xAWVEDK) && CSV(864)))", 61 | }, 62 | ], 63 | } 64 | 65 | async def patched_tx_pending(self, pubkey): 66 | return { 67 | "currency": "g1-test", 68 | "pubkey": "AhRMHUxMPXSeG7qXZrE6qCdjwK9p2bu5Eqei7xAWVEDK", 69 | "history": { 70 | "sending": [], 71 | "received": [], 72 | "receiving": [], 73 | "sent": [], 74 | "pending": [ 75 | { 76 | "version": 10, 77 | "locktime": 0, 78 | "blockstamp": "671977-000008B6DE75715D3D83450A957CD75F781DA8E3E8E966D42A02F59049209533", 79 | "blockstampTime": 1607363853, 80 | "issuers": ["6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o"], 81 | "inputs": [ 82 | "2739:3:D:6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o:664106" 83 | ], 84 | "outputs": [ 85 | "60:3:SIG(AhRMHUxMPXSeG7qXZrE6qCdjwK9p2bu5Eqei7xAWVEDK)", 86 | "2679:3:SIG(6upqFiJ66WV6N3bPc8x8y7rXT3syqKRmwnVyunCtEj7o)", 87 | ], 88 | "unlocks": ["0:SIG(0)"], 89 | "signatures": [ 90 | "lrmzr/RkecJBOczlmkp3BNCiXejBzTnHdqmNzxQJyJDIx0UHON4jYkqVKeD77+nrOl8jVtonLt3ZYqd1fhi1Cw==" 91 | ], 92 | "comment": "DEMAIN DES L_AUBE", 93 | "hash": "D5A1A1AAA43FAA242CC2B19763619DA625092BB7FD23397AD362215375A920C8", 94 | "time": None, 95 | "block_number": None, 96 | "received": None, 97 | } 98 | ], 99 | }, 100 | } 101 | 102 | monkeypatch.setattr(bma_tx, "sources", patched_tx_sources) 103 | monkeypatch.setattr(bma_tx, "pending", patched_tx_pending) 104 | 105 | listinput, balance = await get_sources( 106 | "AhRMHUxMPXSeG7qXZrE6qCdjwK9p2bu5Eqei7xAWVEDK" 107 | ) 108 | assert len(listinput) == 2 109 | # test SIG() only source is used 110 | assert balance == 10000 # 10 in unitbase 3 111 | assert ( 112 | listinput[0].origin_id 113 | == "D5A1A1AAA43FAA242CC2B19763619DA625092BB7FD23397AD362215375A920C8" 114 | ) 115 | 116 | 117 | def test_balance_errors(): 118 | """ 119 | test balance command errors 120 | """ 121 | 122 | # twice the same pubkey 123 | result = CliRunner().invoke( 124 | cli, 125 | [ 126 | "balance", 127 | "BFb5yv8z1fowR6Z8mBXTALy5z7gHfMU976WtXhmRsUMh", 128 | "BFb5yv8z1fowR6Z8mBXTALy5z7gHfMU976WtXhmRsUMh", 129 | ], 130 | ) 131 | pubkeyCk = display_pubkey_and_checksum( 132 | "BFb5yv8z1fowR6Z8mBXTALy5z7gHfMU976WtXhmRsUMh" 133 | ) 134 | assert f"ERROR: pubkey {pubkeyCk} was specified many times" in result.output 135 | assert result.exit_code == FAILURE_EXIT_STATUS 136 | 137 | # wrong pubkey 138 | result = CliRunner().invoke( 139 | cli, 140 | [ 141 | "balance", 142 | "B", 143 | ], 144 | ) 145 | assert "ERROR: pubkey B has a wrong format" in result.output 146 | assert result.exit_code == FAILURE_EXIT_STATUS 147 | 148 | # no pubkey 149 | result = CliRunner().invoke(cli, ["balance"]) 150 | assert "You should specify one or many pubkeys" in result.output 151 | assert result.exit_code == FAILURE_EXIT_STATUS 152 | -------------------------------------------------------------------------------- /tests/test_network_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import pytest 19 | from silkaj import network_tools 20 | 21 | 22 | @pytest.mark.parametrize( 23 | "address,type", 24 | [ 25 | ("test.domain.com", 0), 26 | ("8.8.8.8", 4), 27 | ("2001:0db8:85a3:0000:0000:8a2e:0370:7334", 6), 28 | ("2001:db8::1:0", 6), 29 | ("2001:0db8:0t00:0000:0000:ff00:0042:8329", 0), 30 | ], 31 | ) 32 | def test_check_ip(address, type): 33 | assert network_tools.check_ip(address) == type 34 | -------------------------------------------------------------------------------- /tests/test_tui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import pytest 19 | from silkaj.tui import display_pubkey, display_amount, display_pubkey_and_checksum 20 | from silkaj.constants import G1_SYMBOL, SHORT_PUBKEY_SIZE 21 | 22 | from patched.wot import patched_is_member 23 | from patched.test_constants import mock_ud_value 24 | 25 | # display_amount() 26 | @pytest.mark.parametrize( 27 | "message, amount, currency_symbol", [("Total", 1000, G1_SYMBOL)] 28 | ) 29 | def test_display_amount(message, amount, currency_symbol): 30 | amount_UD = round(amount / mock_ud_value, 2) 31 | expected = [ 32 | [ 33 | message + " (unit|relative)", 34 | str(amount / 100) 35 | + " " 36 | + currency_symbol 37 | + " | " 38 | + str(amount_UD) 39 | + " UD " 40 | + currency_symbol, 41 | ] 42 | ] 43 | tx = list() 44 | display_amount(tx, message, amount, mock_ud_value, currency_symbol) 45 | assert tx == expected 46 | 47 | 48 | # display_pubkey() 49 | @pytest.mark.parametrize( 50 | "message, pubkey, id", 51 | [ 52 | ("From", "CtM5RZHopnSRAAoWNgTWrUhDEmspcCAxn6fuCEWDWudp", "riri"), 53 | ("To", "DBM6F5ChMJzpmkUdL5zD9UXKExmZGfQ1AgPDQy4MxSBw", ""), 54 | ], 55 | ) 56 | @pytest.mark.asyncio 57 | async def test_display_pubkey(message, pubkey, id, monkeypatch): 58 | monkeypatch.setattr("silkaj.wot.is_member", patched_is_member) 59 | 60 | expected = [[message + " (pubkey:checksum)", display_pubkey_and_checksum(pubkey)]] 61 | if id: 62 | expected.append([message + " (id)", id]) 63 | tx = list() 64 | await display_pubkey(tx, message, pubkey) 65 | assert tx == expected 66 | 67 | 68 | # display_pubkey_and_checksum 69 | @pytest.mark.parametrize( 70 | "pubkey, checksum", 71 | [ 72 | ("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX", "KAv"), 73 | ], 74 | ) 75 | def test_display_pubkey_and_checksum(pubkey, checksum): 76 | assert pubkey + ":" + checksum == display_pubkey_and_checksum(pubkey) 77 | assert pubkey[:SHORT_PUBKEY_SIZE] + "…:" + checksum == display_pubkey_and_checksum( 78 | pubkey, short=True 79 | ) 80 | assert pubkey[:14] + "…:" + checksum == display_pubkey_and_checksum( 81 | pubkey, short=True, length=14 82 | ) 83 | -------------------------------------------------------------------------------- /tests/test_tx.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import sys 19 | import pytest 20 | from click.testing import CliRunner 21 | from click import pass_context 22 | 23 | from silkaj import tx, auth, money 24 | from silkaj.cli import cli 25 | from silkaj.constants import ( 26 | MINIMAL_ABSOLUTE_TX_AMOUNT, 27 | MINIMAL_RELATIVE_TX_AMOUNT, 28 | FAILURE_EXIT_STATUS, 29 | CENT_MULT_TO_UNIT, 30 | PUBKEY_MIN_LENGTH, 31 | ) 32 | 33 | from patched.money import patched_ud_value, patched_get_sources 34 | from patched.test_constants import mock_ud_value 35 | from patched.auth import patched_auth_method 36 | from patched.tx import patched_gen_confirmation_table 37 | 38 | # AsyncMock available from Python 3.8. asynctest is used for Py < 3.8 39 | if sys.version_info[1] > 7: 40 | from unittest.mock import AsyncMock 41 | else: 42 | from asynctest.mock import CoroutineMock as AsyncMock 43 | 44 | 45 | # create test auths 46 | @pass_context 47 | def patched_auth_method_truc(ctx): 48 | return patched_auth_method("truc") 49 | 50 | 51 | @pass_context 52 | def patched_auth_method_riri(ctx): 53 | return patched_auth_method("riri") 54 | 55 | 56 | @pytest.mark.asyncio 57 | async def test_transaction_amount(monkeypatch): 58 | """test passed amounts passed tx command 59 | float ≠ 100 does not give the exact value""" 60 | 61 | monkeypatch.setattr(money.UDValue, "get_ud_value", patched_ud_value) 62 | trials = ( 63 | # tests for --amount (unit) 64 | ([141.89], None, ["A"], [14189]), 65 | ([141.99], None, ["A"], [14199]), 66 | ([141.01], None, ["A"], [14101]), 67 | ([141.89], None, ["A", "B"], [14189, 14189]), 68 | ([141.89, 141.99], None, ["A", "B"], [14189, 14199]), 69 | # tests for --amount_UD 70 | (None, [1.1], ["A"], [round(1.1 * mock_ud_value)]), 71 | ( 72 | None, 73 | [1.9], 74 | [ 75 | "A", 76 | "B", 77 | ], 78 | [round(1.9 * mock_ud_value), round(1.9 * mock_ud_value)], 79 | ), 80 | (None, [1.0001], ["A"], [round(1.0001 * mock_ud_value)]), 81 | (None, [9.9999], ["A"], [round(9.9999 * mock_ud_value)]), 82 | ( 83 | None, 84 | [1.9, 2.3], 85 | ["A", "B"], 86 | [round(1.9 * mock_ud_value), round(2.3 * mock_ud_value)], 87 | ), 88 | ) 89 | 90 | for trial in trials: 91 | assert trial[3] == await tx.transaction_amount(trial[0], trial[1], trial[2]) 92 | 93 | 94 | # transaction_amount errors() 95 | @pytest.mark.parametrize( 96 | "amounts, UDs_amounts, outputAddresses, expected", 97 | [ 98 | ( 99 | None, 100 | [0.00002], 101 | ["DBM6F5ChMJzpmkUdL5zD9UXKExmZGfQ1AgPDQy4MxSBw"], 102 | "Error: amount 0.00002 is too low.", 103 | ), 104 | ( 105 | [10, 56], 106 | None, 107 | ["DBM6F5ChMJzpmkUdL5zD9UXKExmZGfQ1AgPDQy4MxSBw"], 108 | "Error: The number of passed recipients is not the same as the passed amounts.", 109 | ), 110 | ( 111 | None, 112 | [1, 45], 113 | ["DBM6F5ChMJzpmkUdL5zD9UXKExmZGfQ1AgPDQy4MxSBw"], 114 | "Error: The number of passed recipients is not the same as the passed amounts.", 115 | ), 116 | ], 117 | ) 118 | @pytest.mark.asyncio 119 | async def test_transaction_amount_errors( 120 | amounts, UDs_amounts, outputAddresses, expected, capsys, monkeypatch 121 | ): 122 | # patched functions 123 | monkeypatch.setattr(money.UDValue, "get_ud_value", patched_ud_value) 124 | 125 | def too_little_amount(amounts, multiplicator): 126 | for amount in amounts: 127 | if amount * multiplicator < MINIMAL_ABSOLUTE_TX_AMOUNT * CENT_MULT_TO_UNIT: 128 | return True 129 | return False 130 | 131 | # run tests 132 | if amounts: 133 | given_amounts = amounts 134 | if UDs_amounts: 135 | given_amounts = UDs_amounts 136 | # check program exit on error 137 | with pytest.raises(SystemExit) as pytest_exit: 138 | # read output to check error. 139 | await tx.transaction_amount(amounts, UDs_amounts, outputAddresses) 140 | assert expected == capsys.readouterr() 141 | assert pytest_exit.type == SystemExit 142 | 143 | 144 | def test_tx_passed_amount_cli(): 145 | """One option""" 146 | result = CliRunner().invoke(cli, ["tx", "--amount", "1"]) 147 | assert "Error: Missing option" in result.output 148 | assert result.exit_code == 2 149 | 150 | result = CliRunner().invoke(cli, ["tx", "--amountUD", "1"]) 151 | assert "Error: Missing option" in result.output 152 | assert result.exit_code == 2 153 | 154 | result = CliRunner().invoke(cli, ["tx", "--allSources"]) 155 | assert "Error: Missing option" in result.output 156 | assert result.exit_code == 2 157 | 158 | """Multiple options""" 159 | result = CliRunner().invoke(cli, ["tx", "--amount", 1, "--amountUD", 1]) 160 | assert "Error: Usage" in result.output 161 | assert result.exit_code == 2 162 | 163 | result = CliRunner().invoke(cli, ["tx", "--amount", 1, "--allSources"]) 164 | assert "Error: Usage" in result.output 165 | assert result.exit_code == 2 166 | 167 | result = CliRunner().invoke(cli, ["tx", "--amountUD", 1, "--allSources"]) 168 | assert "Error: Usage" in result.output 169 | assert result.exit_code == 2 170 | 171 | result = CliRunner().invoke( 172 | cli, ["tx", "--amount", 1, "--amountUD", 1, "--allSources"] 173 | ) 174 | assert "Error: Usage" in result.output 175 | assert result.exit_code == 2 176 | 177 | result = CliRunner().invoke(cli, ["tx", "-r", "A"]) 178 | assert "Error: amount, amountUD or allSources is not set." in result.output 179 | assert result.exit_code == FAILURE_EXIT_STATUS 180 | 181 | result = CliRunner().invoke(cli, ["tx", "-r", "A", "-r", "B", "--allSources"]) 182 | assert ( 183 | "Error: the --allSources option can only be used with one recipient." 184 | in result.output 185 | ) 186 | assert result.exit_code == FAILURE_EXIT_STATUS 187 | 188 | result = CliRunner().invoke( 189 | cli, ["tx", "-r", "A", "-a", MINIMAL_ABSOLUTE_TX_AMOUNT - 0.001] 190 | ) 191 | assert "Error: Invalid value for '--amount'" in result.output 192 | assert result.exit_code == 2 193 | 194 | result = CliRunner().invoke( 195 | cli, ["tx", "-r", "A", "-d", MINIMAL_RELATIVE_TX_AMOUNT - 0.0000001] 196 | ) 197 | assert "Error: Invalid value for '--amountUD'" in result.output 198 | assert result.exit_code == 2 199 | 200 | result = CliRunner().invoke(cli, ["tx", "-r", "A", "-a", 1, "-a", 2]) 201 | assert ( 202 | "Error: The number of passed recipients is not the same as the passed amounts." 203 | in result.output 204 | ) 205 | assert result.exit_code == FAILURE_EXIT_STATUS 206 | 207 | result = CliRunner().invoke( 208 | cli, ["tx", "-r", "A", "-r", "B", "-r", "C", "-a", 1, "-a", 2] 209 | ) 210 | assert ( 211 | "Error: The number of passed recipients is not the same as the passed amounts." 212 | in result.output 213 | ) 214 | assert result.exit_code == FAILURE_EXIT_STATUS 215 | 216 | 217 | @pytest.mark.parametrize( 218 | "arguments, auth_method, is_account_filled", 219 | [ 220 | ( 221 | ["tx", "--allSources", "-r", "A" * PUBKEY_MIN_LENGTH], 222 | patched_auth_method_truc, 223 | False, 224 | ), 225 | ( 226 | ["tx", "--allSources", "-r", "A" * PUBKEY_MIN_LENGTH], 227 | patched_auth_method_riri, 228 | True, 229 | ), 230 | ], 231 | ) 232 | def test_tx_passed_all_sources_empty( 233 | arguments, auth_method, is_account_filled, monkeypatch 234 | ): 235 | """test that --allSources on an empty pubkey returns an error""" 236 | 237 | # patch functions 238 | monkeypatch.setattr(auth, "auth_method", auth_method) 239 | monkeypatch.setattr(money, "get_sources", patched_get_sources) 240 | patched_gen_confirmation_table = AsyncMock() 241 | monkeypatch.setattr(tx, "gen_confirmation_table", patched_gen_confirmation_table) 242 | 243 | result = CliRunner().invoke(cli, args=arguments) 244 | # test error 245 | if not is_account_filled: 246 | assert ( 247 | "Error: Issuer pubkey FA4uAQ92rmxidQPgtMopaLfNNzhxu7wLgUsUkqKkSwPr:4E7 is empty. No transaction sent." 248 | in result.output 249 | ) 250 | assert result.exit_code == FAILURE_EXIT_STATUS 251 | 252 | # test that error don't occur when issuer balance > 0 253 | else: 254 | tx.gen_confirmation_table.assert_called_once() 255 | -------------------------------------------------------------------------------- /tests/test_tx_history.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | import pytest 18 | 19 | from silkaj import tx_history, wot 20 | from silkaj.constants import ( 21 | G1_DEFAULT_ENDPOINT, 22 | SHORT_PUBKEY_SIZE, 23 | PUBKEY_MIN_LENGTH, 24 | PUBKEY_MAX_LENGTH, 25 | ) 26 | from silkaj.crypto_tools import CHECKSUM_SIZE 27 | 28 | from patched.tx_history import patched_get_transactions_history 29 | from patched.wot import patched_identities_from_pubkeys 30 | from patched.blockchain_tools import currency 31 | 32 | SHORT_PUBKEY_LENGTH_WITH_CHECKSUM = ( 33 | SHORT_PUBKEY_SIZE + CHECKSUM_SIZE + 2 34 | ) # 2 chars "…:" ==> 8 + 3 + 2 = 13 35 | 36 | MIN_FULL_PUBKEY_LENGTH_WITH_CHECKSUM = ( 37 | PUBKEY_MIN_LENGTH + CHECKSUM_SIZE + 1 38 | ) # char `:` ==> 43 + 3 + 1 = 47 39 | 40 | MAX_FULL_PUBKEY_LENGTH_WITH_CHECKSUM = ( 41 | PUBKEY_MAX_LENGTH + CHECKSUM_SIZE + 1 42 | ) # char `:` ==> 44 + 3 + 1 = 48 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_tx_history_generate_table_and_pubkey_uid_display(monkeypatch): 47 | def min_pubkey_length_with_uid(pubkey): 48 | # uid is at least one char : " - " adds min 4 chars to 49 | return pubkey + 4 50 | 51 | monkeypatch.setattr(wot, "identities_from_pubkeys", patched_identities_from_pubkeys) 52 | 53 | client = "whatever" 54 | ud_value = 10.07 55 | table_columns = 5 56 | pubkey = "d88fPFbDdJXJANHH7hedFMaRyGcnVZj9c5cDaE76LRN" 57 | 58 | received_txs, sent_txs = list(), list() 59 | await patched_get_transactions_history(client, pubkey, received_txs, sent_txs) 60 | 61 | # simple table 62 | txs_list = await tx_history.generate_table( 63 | received_txs, 64 | sent_txs, 65 | pubkey, 66 | ud_value, 67 | currency, 68 | uids=False, 69 | full_pubkey=False, 70 | ) 71 | for tx_list in txs_list: 72 | assert len(tx_list) == table_columns 73 | if tx_list != txs_list[0]: 74 | assert "…:" in tx_list[1] 75 | assert len(tx_list[1]) == SHORT_PUBKEY_LENGTH_WITH_CHECKSUM 76 | 77 | # with uids 78 | txs_list_uids = await tx_history.generate_table( 79 | received_txs, 80 | sent_txs, 81 | pubkey, 82 | ud_value, 83 | currency, 84 | uids=True, 85 | full_pubkey=False, 86 | ) 87 | for tx_list in txs_list_uids: 88 | assert len(tx_list) == table_columns 89 | if tx_list != txs_list[0]: 90 | assert "…:" in tx_list[1] 91 | # check all lines 92 | assert len(txs_list_uids[1][1]) >= min_pubkey_length_with_uid( 93 | SHORT_PUBKEY_LENGTH_WITH_CHECKSUM 94 | ) 95 | assert len(txs_list_uids[2][1]) == SHORT_PUBKEY_LENGTH_WITH_CHECKSUM 96 | assert len(txs_list_uids[3][1]) >= min_pubkey_length_with_uid( 97 | SHORT_PUBKEY_LENGTH_WITH_CHECKSUM 98 | ) 99 | assert len(txs_list_uids[4][1]) == SHORT_PUBKEY_LENGTH_WITH_CHECKSUM 100 | 101 | # with full pubkeys 102 | txs_list_full = await tx_history.generate_table( 103 | received_txs, 104 | sent_txs, 105 | pubkey, 106 | ud_value, 107 | currency, 108 | uids=False, 109 | full_pubkey=True, 110 | ) 111 | for tx_list in txs_list_full: 112 | assert len(tx_list) == table_columns 113 | if tx_list != txs_list_full[0]: 114 | assert not "…:" in tx_list[1] 115 | assert ":" in tx_list[1] 116 | # this length is not true for multisig txs, which are very unlikely for now. 117 | assert ( 118 | len(tx_list[1]) == MIN_FULL_PUBKEY_LENGTH_WITH_CHECKSUM 119 | or len(tx_list[1]) == MAX_FULL_PUBKEY_LENGTH_WITH_CHECKSUM 120 | ) 121 | 122 | # with full pubkeys and uids 123 | txs_list_uids_full = await tx_history.generate_table( 124 | received_txs, 125 | sent_txs, 126 | pubkey, 127 | ud_value, 128 | currency, 129 | uids=True, 130 | full_pubkey=True, 131 | ) 132 | for tx_list in txs_list_uids_full: 133 | assert len(tx_list) == table_columns 134 | if tx_list != txs_list_uids_full[0]: 135 | assert not "…:" in tx_list[1] 136 | assert ":" in tx_list[1] 137 | # check all lines 138 | assert len(txs_list_uids_full[1][1]) >= min_pubkey_length_with_uid( 139 | MIN_FULL_PUBKEY_LENGTH_WITH_CHECKSUM 140 | ) 141 | assert ( 142 | len(txs_list_uids_full[2][1]) == MIN_FULL_PUBKEY_LENGTH_WITH_CHECKSUM 143 | or len(txs_list_uids_full[2][1]) == MAX_FULL_PUBKEY_LENGTH_WITH_CHECKSUM 144 | ) 145 | assert len(txs_list_uids_full[3][1]) >= min_pubkey_length_with_uid( 146 | MIN_FULL_PUBKEY_LENGTH_WITH_CHECKSUM 147 | ) 148 | assert ( 149 | len(txs_list_uids_full[4][1]) == MIN_FULL_PUBKEY_LENGTH_WITH_CHECKSUM 150 | or len(txs_list_uids_full[4][1]) == MAX_FULL_PUBKEY_LENGTH_WITH_CHECKSUM 151 | ) 152 | 153 | 154 | @pytest.mark.parametrize( 155 | "tx_addresses, outputs, occurence, return_value", 156 | [ 157 | (None, None, 0, ""), 158 | (None, None, 1, "\n"), 159 | (None, ["output1"], 0, ""), 160 | (None, ["output1"], 1, ""), 161 | (None, ["output1", "output2"], 0, "\n"), 162 | (None, ["output1", "output2"], 1, "\n"), 163 | ("pubkey", None, 0, ""), 164 | ("pubkey", None, 1, "\n"), 165 | ("pubkey", ["output1"], 0, ""), 166 | ("pubkey", ["output1"], 1, ""), 167 | ("Total", ["output1", "output2"], 0, "\n"), 168 | ("pubkey", ["output1", "output2"], 0, "\n"), 169 | ("pubkey", ["output1", "output2"], 1, "\n"), 170 | ], 171 | ) 172 | def test_prefix(tx_addresses, outputs, occurence, return_value): 173 | assert tx_history.prefix(tx_addresses, outputs, occurence) == return_value 174 | -------------------------------------------------------------------------------- /tests/test_unit_cert.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | from unittest.mock import patch 19 | 20 | from silkaj.cert import certification_confirmation 21 | 22 | 23 | # @patch('builtins.input') 24 | # def test_certification_confirmation(mock_input): 25 | # id_to_certify = {"pubkey": "pubkeyid to certify"} 26 | # main_id_to_certify = {"uid": "id to certify"} 27 | # mock_input.return_value = "yes" 28 | # 29 | # assert certification_confirmation( 30 | # "certifier id", 31 | # "certifier pubkey", 32 | # id_to_certify, 33 | # main_id_to_certify) 34 | # 35 | # mock_input.assert_called_once() 36 | # 37 | # 38 | # @patch('builtins.input') 39 | # def test_certification_confirmation_no(mock_input): 40 | # id_to_certify = {"pubkey": "pubkeyid to certify"} 41 | # main_id_to_certify = {"uid": "id to certify"} 42 | # mock_input.return_value = "no" 43 | # 44 | # assert certification_confirmation( 45 | # "certifier id", 46 | # "certifier pubkey", 47 | # id_to_certify, 48 | # main_id_to_certify) is None 49 | # 50 | # mock_input.assert_called_once() 51 | -------------------------------------------------------------------------------- /tests/test_verify_blocks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import pytest 19 | from click.testing import CliRunner 20 | 21 | from duniterpy.documents import Block 22 | from duniterpy.api.client import Client 23 | from duniterpy.api import bma 24 | 25 | from silkaj.network_tools import EndPoint 26 | from silkaj import cli 27 | from silkaj.blocks import ( 28 | check_passed_blocks_range, 29 | get_chunk_size, 30 | get_chunk, 31 | verify_block_signature, 32 | display_result, 33 | ) 34 | from silkaj.constants import ( 35 | SUCCESS_EXIT_STATUS, 36 | FAILURE_EXIT_STATUS, 37 | BMA_MAX_BLOCKS_CHUNK_SIZE, 38 | ) 39 | 40 | 41 | G1_INVALID_BLOCK_SIG = 15144 42 | HEAD_BLOCK = 48000 43 | 44 | 45 | async def current(self): 46 | return {"number": HEAD_BLOCK} 47 | 48 | 49 | @pytest.mark.parametrize( 50 | "from_block, to_block", [(2, 1), (20000, 15000), (0, HEAD_BLOCK + 1), (300000, 0)] 51 | ) 52 | @pytest.mark.asyncio 53 | async def test_check_passed_blocks_range(from_block, to_block, capsys, monkeypatch): 54 | monkeypatch.setattr(bma.blockchain, "current", current) 55 | client = Client(EndPoint().BMA_ENDPOINT) 56 | # https://medium.com/python-pandemonium/testing-sys-exit-with-pytest-10c6e5f7726f 57 | with pytest.raises(SystemExit) as pytest_wrapped_e: 58 | await check_passed_blocks_range(client, from_block, to_block) 59 | assert pytest_wrapped_e.type == SystemExit 60 | assert pytest_wrapped_e.value.code == FAILURE_EXIT_STATUS 61 | captured = capsys.readouterr() 62 | if to_block == HEAD_BLOCK + 1: 63 | expected = "Passed TO_BLOCK argument is bigger than the head block: " + str( 64 | HEAD_BLOCK 65 | ) 66 | else: 67 | expected = "TO_BLOCK should be bigger or equal to FROM_BLOCK\n" 68 | assert expected in captured.out 69 | 70 | 71 | @pytest.mark.parametrize( 72 | "from_block, to_block", 73 | [ 74 | (G1_INVALID_BLOCK_SIG, G1_INVALID_BLOCK_SIG), 75 | (G1_INVALID_BLOCK_SIG - 5, G1_INVALID_BLOCK_SIG + 5), 76 | (1, 10), 77 | (HEAD_BLOCK - 1, 0), 78 | ], 79 | ) 80 | def test_verify_blocks_signatures(from_block, to_block, monkeypatch): 81 | monkeypatch.setattr(bma.blockchain, "current", current) 82 | result = CliRunner().invoke(cli.cli, ["verify", str(from_block), str(to_block)]) 83 | assert result.exit_code == SUCCESS_EXIT_STATUS 84 | if to_block == 0: 85 | to_block = HEAD_BLOCK 86 | expected = "Within {0}-{1} range, ".format(from_block, to_block) 87 | if from_block == 1 or from_block == HEAD_BLOCK - 1: 88 | expected += "no blocks with a wrong signature." 89 | else: 90 | expected += "blocks with a wrong signature: " + str(G1_INVALID_BLOCK_SIG) 91 | assert expected + "\n" in result.output 92 | 93 | 94 | @pytest.mark.parametrize( 95 | "from_block, to_block, chunks_from, chunk_from", 96 | [ 97 | (140, 15150, [140, 5140, 10140, 15140], 140), 98 | (140, 15150, [140, 5140, 10140, 15140], 15140), 99 | (0, 2, [0], 0), 100 | ], 101 | ) 102 | def test_get_chunk_size(from_block, to_block, chunks_from, chunk_from): 103 | chunk_size = get_chunk_size(from_block, to_block, chunks_from, chunk_from) 104 | if chunk_from != chunks_from[-1]: 105 | assert chunk_size == BMA_MAX_BLOCKS_CHUNK_SIZE 106 | else: 107 | assert chunk_size == to_block + 1 - chunk_from 108 | 109 | 110 | @pytest.mark.parametrize("chunk_size, chunk_from", [(2, 1), (5, 10)]) 111 | @pytest.mark.asyncio 112 | async def test_get_chunks(chunk_size, chunk_from): 113 | client = Client(EndPoint().BMA_ENDPOINT) 114 | chunk = await get_chunk(client, chunk_size, chunk_from) 115 | assert chunk[0]["number"] + chunk_size - 1 == chunk[-1]["number"] 116 | await client.close() 117 | 118 | 119 | invalid_signature = "fJusVDRJA8akPse/sv4uK8ekUuvTGj1OoKYVdMQQAACs7OawDfpsV6cEMPcXxrQTCTRMrTN/rRrl20hN5zC9DQ==" 120 | invalid_block_raw = "Version: 10\nType: Block\nCurrency: g1\nNumber: 15144\nPoWMin: 80\n\ 121 | Time: 1493683741\nMedianTime: 1493681008\nUnitBase: 0\n\ 122 | Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx\nIssuersFrame: 106\n\ 123 | IssuersFrameVar: 0\nDifferentIssuersCount: 21\n\ 124 | PreviousHash: 0000033D8562368F1B099E924A4A83119BDA0452FAB2A8A4F1B1BA11F5450597\n\ 125 | PreviousIssuer: 5WD4WSHE96ySreSwQFXPqaKaKcwboRNApiPHjPWB6V9C\nMembersCount: 98\n\ 126 | Identities:\nJoiners:\nActives:\nLeavers:\nRevoked:\nExcluded:\nCertifications:\n\ 127 | Transactions:\nInnerHash: 8B194B5C38CF0A38D16256405AC3E5FA5C2ABD26BE4DCC0C7ED5CC9824E6155B\n\ 128 | Nonce: 30400000119992\n" 129 | 130 | valid_signature = "qhXtFtl6A/ZL7JMb7guSDlxiISGsHkQ4hTz5mhhdZO0KCLqD2TmvjcGpUFETBSdRYVacvFYOvUANyevlcfx6Ag==" 131 | valid_block_raw = "Version: 11\nType: Block\nCurrency: g1-test\nNumber: 509002\n\ 132 | PoWMin: 60\nTime: 1580293955\nMedianTime: 1580292050\nUnitBase: 2\n\ 133 | Issuer: 5B8iMAzq1dNmFe3ZxFTBQkqhq4fsztg1gZvxHXCk1XYH\nIssuersFrame: 26\n\ 134 | IssuersFrameVar: 0\nDifferentIssuersCount: 5\n\ 135 | PreviousHash: 0000EC4030E92E85F22F32663F5ABE137BA01CE59AF2A96050877320174C4A90\n\ 136 | PreviousIssuer: Dz37iRAXeg4nUsfVH82m61b39HK5fqm6Bu7mM2ujLYz1\nMembersCount: 11\n\ 137 | Identities:\nJoiners:\nActives:\nLeavers:\nRevoked:\nExcluded:\nCertifications:\n\ 138 | Transactions:\nInnerHash: 19A53ABFA19EC77B6360E38EA98BE10154CB92307F4909AE49E786CA7149F8C6\n\ 139 | Nonce: 10099900003511\n" 140 | 141 | 142 | @pytest.mark.parametrize( 143 | "signature, block_raw", 144 | [(invalid_signature, invalid_block_raw), (valid_signature, valid_block_raw)], 145 | ) 146 | def test_verify_block_signature(signature, block_raw): 147 | # Check with valid and non-valid signatures block 148 | invalid_signatures_blocks = [] 149 | block = Block.from_signed_raw(block_raw + signature + "\n") 150 | verify_block_signature(invalid_signatures_blocks, block) 151 | if block.number == G1_INVALID_BLOCK_SIG: 152 | assert invalid_signatures_blocks == [block.number] 153 | else: 154 | assert invalid_signatures_blocks == [] 155 | 156 | 157 | @pytest.mark.parametrize( 158 | "from_block, to_block, invalid_blocks_signatures", 159 | [(0, 5, []), (100, 500, [53]), (470, 2341, [243, 453])], 160 | ) 161 | def test_display_result(from_block, to_block, invalid_blocks_signatures, capsys): 162 | expected = "Within {0}-{1} range, ".format(from_block, to_block) 163 | if invalid_blocks_signatures: 164 | expected += "blocks with a wrong signature: " 165 | expected += " ".join(str(n) for n in invalid_blocks_signatures) 166 | else: 167 | expected += "no blocks with a wrong signature." 168 | display_result(from_block, to_block, invalid_blocks_signatures) 169 | captured = capsys.readouterr() 170 | assert expected + "\n" == captured.out 171 | -------------------------------------------------------------------------------- /tests/test_wot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2021 Maël Azimi 3 | 4 | Silkaj is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Silkaj is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with Silkaj. If not, see . 16 | """ 17 | 18 | import pytest 19 | import click 20 | 21 | from silkaj import wot 22 | 23 | 24 | pubkey_titi_tata = "B4RoF48cTxzmsQDB3UjodKdZ2cVymKSKzgiPVRoMeA88" 25 | pubkey_toto_tutu = "totoF48cTxzmsQDB3UjodKdZ2cVymKSKzgiPVRoMeA88" 26 | 27 | pubkey_titi_tata_checksum = "B4RoF48cTxzmsQDB3UjodKdZ2cVymKSKzgiPVRoMeA88:9iP" 28 | 29 | 30 | def identity_card(uid, timestamp): 31 | return { 32 | "uid": uid, 33 | "meta": {"timestamp": timestamp}, 34 | } 35 | 36 | 37 | titi = identity_card( 38 | "titi", 39 | "590358-000156C5620946D1D63DAF82BF3AA735CE0B3518D59274171C88A7DBA4C906BC", 40 | ) 41 | tata = identity_card( 42 | "tata", 43 | "210842-000000E7AAC79A07F487B33A48B3217F8A1F0A31CDB42C5DFC5220A20665B6B1", 44 | ) 45 | toto = identity_card( 46 | "toto", 47 | "189601-0000011405B5C96EA69C1273370E956ED7887FA56A75E3EFDF81E866A2C49FD9", 48 | ) 49 | tutu = identity_card( 50 | "tutu", 51 | "389601-0000023405B5C96EA69C1273370E956ED7887FA56A75E3EFDF81E866A2C49FD9", 52 | ) 53 | 54 | 55 | async def patched_lookup_one(pubkey_uid): 56 | return [ 57 | { 58 | "pubkey": pubkey_titi_tata, 59 | "uids": [titi], 60 | "signed": [], 61 | } 62 | ] 63 | 64 | 65 | async def patched_lookup_two(pubkey_uid): 66 | return [ 67 | { 68 | "pubkey": pubkey_titi_tata, 69 | "uids": [titi, tata], 70 | "signed": [], 71 | } 72 | ] 73 | 74 | 75 | async def patched_lookup_three(pubkey_uid): 76 | return [ 77 | { 78 | "pubkey": pubkey_titi_tata, 79 | "uids": [titi, tata], 80 | "signed": [], 81 | }, 82 | { 83 | "pubkey": pubkey_toto_tutu, 84 | "uids": [toto], 85 | "signed": [], 86 | }, 87 | ] 88 | 89 | 90 | async def patched_lookup_four(pubkey_uid): 91 | return [ 92 | { 93 | "pubkey": pubkey_titi_tata, 94 | "uids": [titi, tata], 95 | "signed": [], 96 | }, 97 | { 98 | "pubkey": pubkey_toto_tutu, 99 | "uids": [toto, tutu], 100 | "signed": [], 101 | }, 102 | ] 103 | 104 | 105 | async def patched_lookup_five(pubkey_uid): 106 | return [ 107 | { 108 | "pubkey": pubkey_titi_tata, 109 | "uids": [titi], 110 | "signed": [], 111 | }, 112 | { 113 | "pubkey": pubkey_toto_tutu, 114 | "uids": [titi], 115 | "signed": [], 116 | }, 117 | ] 118 | 119 | 120 | def patched_prompt_titi(message): 121 | return "00" 122 | 123 | 124 | def patched_prompt_tata(message): 125 | return "01" 126 | 127 | 128 | def patched_prompt_toto(message): 129 | return "10" 130 | 131 | 132 | def patched_prompt_tutu(message): 133 | return "11" 134 | 135 | 136 | @pytest.mark.parametrize( 137 | "selected_uid, pubkey, patched_prompt, patched_lookup", 138 | [ 139 | ("titi", pubkey_titi_tata, patched_prompt_titi, patched_lookup_one), 140 | ("titi", pubkey_titi_tata_checksum, patched_prompt_titi, patched_lookup_one), 141 | ("tata", pubkey_titi_tata, patched_prompt_tata, patched_lookup_two), 142 | ("toto", pubkey_toto_tutu, patched_prompt_toto, patched_lookup_three), 143 | ("tutu", pubkey_toto_tutu, patched_prompt_tutu, patched_lookup_four), 144 | ("titi", pubkey_toto_tutu, patched_prompt_toto, patched_lookup_five), 145 | ], 146 | ) 147 | @pytest.mark.asyncio 148 | async def test_choose_identity( 149 | selected_uid, pubkey, patched_prompt, patched_lookup, capsys, monkeypatch 150 | ): 151 | monkeypatch.setattr(wot, "wot_lookup", patched_lookup) 152 | monkeypatch.setattr(click, "prompt", patched_prompt) 153 | identity_card, get_pubkey, signed = await wot.choose_identity(pubkey) 154 | expected_pubkey = pubkey.split(":")[0] 155 | assert expected_pubkey == get_pubkey 156 | assert selected_uid == identity_card["uid"] 157 | 158 | # Check whether the table is not displayed in case of one identity 159 | # Check it is displayed for more than one identity 160 | # Check the uids and ids are in 161 | captured = capsys.readouterr() 162 | lookups = await patched_lookup("") 163 | 164 | # only one pubkey and one uid on this pubkey 165 | if len(lookups) == 1 and len(lookups[0]["uids"]) == 1: 166 | assert not captured.out 167 | 168 | # many pubkeys or many uid on one pubkey 169 | else: 170 | # if more than one pubkey, there should be a "10" numbering 171 | if len(lookups) > 1: 172 | assert " 10 " in captured.out 173 | for lookup in lookups: 174 | if len(lookup["uids"]) > 1: 175 | assert " 01 " in captured.out 176 | for uid in lookup["uids"]: 177 | assert uid["uid"] in captured.out 178 | -------------------------------------------------------------------------------- /update_copyright_year.sh: -------------------------------------------------------------------------------- 1 | #!/bin/fish 2 | 3 | # Script to update the copyright year in header files 4 | 5 | set NEW_YEAR (date +"%Y") 6 | set OLD_YEAR (math $NEW_YEAR - 1) 7 | sed -i "s/Copyright 2016-$OLD_YEAR M/Copyright 2016-$NEW_YEAR M/g" silkaj/*.py tests/*.py 8 | --------------------------------------------------------------------------------