├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.rst ├── certificates └── root │ ├── certs │ ├── AppiaDeveloperCA.crt │ └── test.crt │ └── keys │ └── test.key ├── docs ├── Makefile ├── api.rst ├── api │ ├── manta.payproc.PayProc.rst │ ├── manta.payproc.TXStorage.rst │ ├── manta.payproc.TXStorageMemory.rst │ ├── manta.payproc.TransactionState.rst │ ├── manta.payproc.rst │ ├── manta.store.rst │ └── manta.wallet.rst ├── conf.py ├── fonts │ └── Lato-Regular.ttf ├── images │ ├── manta-protocol-full.svg │ └── manta-protocol.svg ├── index.rst ├── make.bat ├── protocol.rst └── protocol │ ├── flow.rst │ ├── messages.rst │ ├── parameters.rst │ └── topics.rst ├── manta ├── __init__.py ├── base.py ├── dispatcher.py ├── messages.py ├── payproc.py ├── store.py ├── testing │ ├── __init__.py │ ├── __main__.py │ ├── broker.py │ ├── config.py │ ├── payproc.py │ ├── runner.py │ ├── store.py │ └── wallet.py └── wallet.py ├── mosquitto.conf ├── mypy.ini ├── notebooks ├── .ipynb_checkpoints │ └── Bip32-checkpoint.ipynb ├── Bip32.ipynb ├── Messages.ipynb └── RSAFUN.ipynb ├── requirements-dev.txt ├── requirements-docs.txt ├── requirements-tests.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── shell.nix └── tests ├── __init__.py ├── certificates ├── conftest.py ├── dummyconfig.yaml ├── integration ├── test_store_mqtt.py └── test_wallet_mqtt.py ├── old ├── payprocdummy.py ├── storedummy.py ├── storeecho.py ├── swagger │ └── wallet.yaml ├── walletdummy.conf └── walletdummy.py ├── pos_payment_request.json ├── unit ├── test_config.py ├── test_dispatcher.py ├── test_messages.py ├── test_payproc.py ├── test_runner.py ├── test_store.py └── test_wallet.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dmypy.json 3 | .idea 4 | .ipynb_checkpoints 5 | .mypy_cache 6 | .pyre 7 | .pytest_cache 8 | .python-version 9 | .vscode 10 | Untitled.ipynb 11 | __pycache__ 12 | manta.egg-info 13 | venv 14 | docs/_build 15 | build 16 | dist 17 | .coverage 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.7 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2019 Alessandro Viganò 2 | All rights reserved. 3 | 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | NIX := $(shell which nix) 4 | export TOPDIR := $(CURDIR) 5 | export VENVDIR := $(TOPDIR)/venv 6 | export SYS_PYTHON := $(shell which python3.7) 7 | 8 | ifeq ($(NIX),) 9 | export PYTHON := $(VENVDIR)/bin/python 10 | else 11 | export PYTHON := $(SYS_PYTHON) 12 | endif 13 | export SHELL := $(shell which bash) 14 | 15 | 16 | # This is the default target, when no target is specified on the command line 17 | .PHONY: all 18 | ifneq ($(NIX),) 19 | ifndef IN_NIX_SHELL 20 | 21 | all: activate-nix 22 | 23 | activate-nix: 24 | @printf "\nPLEASE ACTIVATE THE ENVIRONMENT USING THE COMMAND \"nix-shell\"\n" 25 | else 26 | 27 | VENV_CMD := virtualenv --no-setuptools 28 | all: virtualenv help 29 | 30 | endif 31 | else 32 | 33 | VENV_CMD := $(SYS_PYTHON) -m venv 34 | all: virtualenv help 35 | 36 | endif 37 | 38 | ACTIVATE_SCRIPT := $(VENVDIR)/bin/activate 39 | PIP := $(VENVDIR)/bin/pip 40 | REQUIREMENTS ?= requirements-dev.txt 41 | REQUIREMENTS_TIMESTAMP := $(VENVDIR)/$(REQUIREMENTS).timestamp 42 | 43 | 44 | help:: 45 | @printf "\nVirtualenv\n==========\n" 46 | 47 | help:: 48 | @printf "\nvirtualenv\n\tsetup the Python virtualenv and install required packages\n" 49 | 50 | .PHONY: virtualenv 51 | virtualenv: $(VENVDIR) requirements 52 | 53 | $(VENVDIR): 54 | @printf "Bootstrapping Python 3 virtualenv...\n" 55 | @$(VENV_CMD) --prompt $(notdir $(TOPDIR)) $(VENV_EXTRA) $@ 56 | @$(MAKE) -s upgrade-pip 57 | 58 | help:: 59 | @printf "\nupgrade-pip\n\tupgrade pip\n" 60 | 61 | .PHONY: upgrade-pip 62 | upgrade-pip: 63 | @printf "Upgrading pip...\n" 64 | @$(PIP) install --upgrade pip 65 | 66 | help:: 67 | @printf "\nrequirements\n\tinstall/update required Python packages\n" 68 | 69 | .PHONY: requirements 70 | requirements: $(REQUIREMENTS_TIMESTAMP) 71 | 72 | $(REQUIREMENTS_TIMESTAMP): $(REQUIREMENTS) 73 | @printf "Installing development requirements...\n" 74 | @PATH=$(TOPDIR)/bin:"$(PATH)" $(PIP) install -r $(REQUIREMENTS) 75 | touch $@ 76 | 77 | distclean:: 78 | rm -rf $(VENVDIR) 79 | 80 | help:: 81 | @printf "\nTesting\n=======\n" 82 | 83 | help:: 84 | @printf "\ntests\n\trun the configured test\n" 85 | 86 | .PHONY: tests 87 | tests: rst-tests type-tests unit-tests 88 | 89 | help:: 90 | @printf "\ntype-tests\n\trun the typechecks using mypy\n" 91 | 92 | .PHONY: type-tests 93 | type-tests: 94 | @mypy ./manta 95 | $(info Running type tests...) 96 | 97 | help:: 98 | @printf "\nunit-tests\n\trun the unittests using pytest\n" 99 | 100 | .PHONY: unit-tests 101 | unit-tests: 102 | $(info Running unit and integration tests...) 103 | @pytest ./tests 104 | 105 | help:: 106 | @printf "\nrst-tests\n\tcheck README.rst syntax\n" 107 | 108 | .PHONY: rst-tests 109 | $(info checking README.rst file syntax) 110 | @rst2html.py README.rst > /dev/null 111 | 112 | 113 | help:: 114 | @printf "\nDocumentation\n=============\n" 115 | 116 | help:: 117 | @printf "\ndocs\n\tcompile the documentation\n" 118 | 119 | .PHONY: docs 120 | docs: 121 | $(info compiling documentation...) 122 | @cd docs && $(MAKE) html 123 | $(info index is available at ./docs/_build/html/index.html) 124 | 125 | help:: 126 | @printf "\nDistribution\n============\n" 127 | 128 | help:: 129 | @printf "\ndist\n\tcreate distribution package\n" 130 | 131 | .PHONY: dist 132 | dist: 133 | $(info generating package...) 134 | @python setup.py sdist 135 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | simplejson = "*" 8 | cattrs = "*" 9 | cryptography = "*" 10 | certvalidator = "*" 11 | paho-mqtt = "*" 12 | aiohttp = "*" 13 | requests = "*" 14 | 15 | [dev-packages] 16 | pytest = "*" 17 | callee = "*" 18 | pytest-asyncio = "*" 19 | pytest-timeout = "*" 20 | sphinx = "*" 21 | sphinx-autodoc-typehints = {editable= true, git="https://github.com/agronholm/sphinx-autodoc-typehints.git"} 22 | sphinx-jsonschema = "*" 23 | sphinx_rtd_theme = "*" 24 | aiohttp-swagger = "*" 25 | nano-python = "*" 26 | coverage = "*" 27 | sphinx-rtd-theme = "*" 28 | inquirer = "*" 29 | 30 | [requires] 31 | python_version = "3.7" 32 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. -*- coding: utf-8 -*- 2 | 3 | ========================= 4 | Welcome to manta-python 5 | ========================= 6 | 7 | This repository contains the Python implementation of the **Manta 8 | Protocol**: it's a protocol to enable crypto coin transaction between 9 | POS (of any kind, hardware, vending machines, online), payment 10 | processors and wallets. 11 | 12 | Communication takes advantage of the MQTT_ protocol and needs a 13 | proper configured broker to work. 14 | 15 | More documentation can be found at 16 | https://appiapay.github.io/manta-python, what follows are some 17 | instructions for the developer. 18 | 19 | .. _MQTT: http://mqtt.org 20 | 21 | .. contents:: 22 | 23 | Installation 24 | ============ 25 | 26 | Primary repository 27 | ------------------ 28 | 29 | To start working on ``manta-python`` you must first checkout a copy of the 30 | main repository:: 31 | 32 | $ git git@github.com:appiapay/manta-python.git 33 | $ cd manta-python 34 | 35 | Requirements 36 | ------------ 37 | 38 | The code in this repository needs an MQTT_ broker to work correctly so 39 | if you plan to run the tests, you will be required of either install 40 | the mosquitto_ broker or use your own and modify the ``broker.start`` 41 | entry in the file ``tests/dummyconfig.yaml`` to ``false``. 42 | 43 | .. _mosquitto: http://mosquitto.org 44 | 45 | Task automation is achieved using `GNU Make`_. It should be easily 46 | installable on every UNIX-like platform. 47 | 48 | If you use Nix__ (it can be installed on GNU/Linux systems and 49 | macOS) all you need is to execute a single command:: 50 | 51 | $ nix-shell 52 | 53 | and that will install mosquitto_ and `GNU Make`_ for you. In such case 54 | you should skip the following section. 55 | 56 | .. _GNU Make: https://www.gnu.org/software/make/ 57 | __ https://nixos.org/nix/ 58 | 59 | Creation of the Python virtual environment 60 | ------------------------------------------ 61 | 62 | The next thing to do is the creation of the Python virtual environment 63 | with some required packages. Simply execute:: 64 | 65 | $ make 66 | 67 | a) that will initialize the environment with a set of required packages 68 | b) print an help on the available targets 69 | 70 | You **must** remember to activate the virtual environment **before** 71 | doing anything:: 72 | 73 | $ . venv/bin/activate 74 | 75 | Running the tests 76 | ----------------- 77 | 78 | To run the tests simply run the following command:: 79 | 80 | $ make tests 81 | 82 | That will run the unit tests and the integration tests. Remember to always use 83 | it before committing. 84 | -------------------------------------------------------------------------------- /certificates/root/certs/AppiaDeveloperCA.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDaDCCAlCgAwIBAgIINZv0BbLXm9AwDQYJKoZIhvcNAQELBQAwOjELMAkGA1UE 3 | BhMCVUsxDjAMBgNVBAoTBUFwcGlhMRswGQYDVQQDExJBcHBpYSBEZXZlbG9wZXIg 4 | Q0EwHhcNMTgxMDE3MDc0MjAwWhcNMjgxMDE3MDc0MjAwWjA6MQswCQYDVQQGEwJV 5 | SzEOMAwGA1UEChMFQXBwaWExGzAZBgNVBAMTEkFwcGlhIERldmVsb3BlciBDQTCC 6 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALLV0rgr0TYjTKMLOPQlHNp9 7 | V2NyfyTY6rZd3uLs6UWH4BZSAu2jlB40pWeIoFZb7+sBuJbGe4l1VKPgospynB/+ 8 | +qxDnMNjY2M41a4Gv2Mr261xfNKJ0Vwd1D7WK9XN+3p4BS2dEmv685dSk3AhbnhU 9 | RVcIFy6aUYCVjLZeg3M0CGPaGy6Zb0g8kt5mQdAQtFTE0wZ0cSUPea9QT+5kDs38 10 | Lc0jVo1QqB4DFpJ6ceg3sLSB2fGS6c4YEU8SKvu2rLk/VVJJstjFrAwvKQfx+oSx 11 | NPotU37C4zPG3wBfWb2o/DjFUPWyq6sjtXUb6kmzfcsdP50vN0K8LTpBF84CsqUC 12 | AwEAAaNyMHAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOJcBmAyRITBHOIqC 13 | s+sZ5eM0xlQwCwYDVR0PBAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzAeBglghkgB 14 | hvhCAQ0EERYPeGNhIGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBCwUAA4IBAQCpudBY 15 | QX4bm6La9cx7h4fruuBmC2NIF2GhobZzd1lEx5bEPtq5S6kx3Qr7pY0yoQtN+lpn 16 | XWJJQcks3a4WhF0YeqesBcLdlXqMCDsFU6A4yJ6x25FaoelMpv+Keoj+sYuNtcyb 17 | sfWjDvDaOU1jj76nLX+llMHAau0gALQrH39KCYkORwltOQgc98X/aX/UiBMBxSz8 18 | dCU0MPLl8dU8KnprtG2Ibik86J649o4EJr0lA01liQicr/viKrVOqzS6cq18hJaX 19 | zvYu2RVV+AtDHXXE462p3sT8Bk2iB979aDV3GsD0/WrRVwyPhi7YG6zM4otv59xF 20 | O+dbxo9YqCeAzbna 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /certificates/root/certs/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDWTCCAkGgAwIBAgIIbr1pbpGw/z8wDQYJKoZIhvcNAQELBQAwOjELMAkGA1UE 3 | BhMCVUsxDjAMBgNVBAoTBUFwcGlhMRswGQYDVQQDExJBcHBpYSBEZXZlbG9wZXIg 4 | Q0EwHhcNMTkwOTIzMDg1ODAwWhcNMjQwOTIzMDg1ODAwWjAcMQswCQYDVQQGEwJV 5 | SzENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 6 | AM8xOAXU3CfU5uewq4in2vPXGMO5CT0HGJTYDdr1xGgEBLbKVUfPfztrJxCq4MCP 7 | EdjFkezzSwHW1QVWB5dbkTGS2oWIxhfXuYmhS/i0nmcB4MFrBCrIH7pPcmu0vdQP 8 | +oOwRuEjc7x8va7cLZzs+GKJ3hWSnrgi5kd1YG8glZSxngD52qSBmEqyd+V9VB7G 9 | v8v913X8ekp67IyL+ZD0VPyijaszeAYdsOXWag1pV4e57+5CoGbuyueNJ8QCPsUO 10 | IscTmE9fqjLy4ZWwjkCdfocvOF08ykRQMqmLCg7+QKLcq9TlLNMDUXZG7qcP/cBx 11 | yaIuxSj6PuxORyrF5ZPrHFMCAwEAAaOBgDB+MAwGA1UdEwEB/wQCMAAwHQYDVR0O 12 | BBYEFGGoV9UB57w1ofNu2+vrMvhPvNiWMAsGA1UdDwQEAwIF4DAPBgNVHREECDAG 13 | ggR0ZXN0MBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNhIGNl 14 | cnRpZmljYXRlMA0GCSqGSIb3DQEBCwUAA4IBAQBeIsG5FEF0xoNsHlNHh90zWgEI 15 | PvQudUxutaJtxBah7Hwzkfh5NM4YIXioyTnpaeVGoc5oYq4Xn8snMaX1uw9gamVc 16 | DgfJ5Ll3LSvKZpo/Stf9XHu5udqCPKlXaOCFaAVBcdS/aSugToYHs762MjQsnrtG 17 | IobUvT/G4ANc9bYhWqkp7p3eqbkbssqvOTKVviBVITg+5tVjv9jXT95fkAhnknqd 18 | khYQNJg+DL8x1Xtvl5y2/OS7vAf5qAIYsVWdM3vsXLV7VDRyxNkE0gfaE9sp+WMW 19 | ap1CpqzN9NBQisr9gC+YFX3wFHcY5CXX5ph6lBU0ce1DzguRFVoq2euMfwfL 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /certificates/root/keys/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAzzE4BdTcJ9Tm57CriKfa89cYw7kJPQcYlNgN2vXEaAQEtspV 3 | R89/O2snEKrgwI8R2MWR7PNLAdbVBVYHl1uRMZLahYjGF9e5iaFL+LSeZwHgwWsE 4 | Ksgfuk9ya7S91A/6g7BG4SNzvHy9rtwtnOz4YoneFZKeuCLmR3VgbyCVlLGeAPna 5 | pIGYSrJ35X1UHsa/y/3Xdfx6SnrsjIv5kPRU/KKNqzN4Bh2w5dZqDWlXh7nv7kKg 6 | Zu7K540nxAI+xQ4ixxOYT1+qMvLhlbCOQJ1+hy84XTzKRFAyqYsKDv5Aotyr1OUs 7 | 0wNRdkbupw/9wHHJoi7FKPo+7E5HKsXlk+scUwIDAQABAoIBAHa1n4CP1deofueu 8 | bIluit98lP7GzIk/wJC/PSj/+RkTfFPyl2v+ccpseURuczlAf5wwjowxVP9nxEM7 9 | aDwLRaQenL83fy/39KDrXmjPq5YfGFQZaZjNcog/rrIRK1YOFZ1P6TxjIJOU8OT3 10 | 19Z6W1Gx5iF8ER41OLUmhisFg05ecYD1epS2/13+POBFQlj2xlEjWdnMuc39IbBm 11 | ses4uFtUTjUJZg1mj2zrcb6eSBEABls3aD2j89MGklme4FY6mbBIOEqdqvZbdVjb 12 | pOBYxyRpwXLWx0abVje5G84Ii1JKvDAIfTTC27Tvh0L3rZEyceRHNLEfgb0IoP3J 13 | jXj4XMECgYEA8qYEQJbd/oTVYLpVyz0VJolR184Xw9xm7YwzkJKYES9N5UD2Ze9P 14 | duEodz9a97sD1nyeo5bHIQ/3x/k4clBZvXj1KRVR5oyZsqSNUDu36BHG9ftejT/9 15 | rmU8MTltILr5q6PiwwRMMme5DAdEAQiufmTNBWxCn/CksiY12GyBbbMCgYEA2pfC 16 | oOdftXk4mW6YQxmv6atvkO129ooQwMdjHl+xdKMoiVgZZImgRrBwrfcwAGWXlKcd 17 | Jk4/gIGWogZ9Zi6VnvT4cucU+Td9Hy1YaLEIsGRe0VHBlt9PtX2aKn8TDheVV9zR 18 | suRjdtf9eFv3pPsfSsjtUoOy8eraqbhzzbtKhuECgYEAuaB3sFG6EnPcKNtjts9L 19 | 1udGTSnE1HlP5HpVGEyNONhPZwqDdQfWasNlCZTWKp5PGU7MrakzPP/SNpFZtVny 20 | zRgztPIybYUyL4/i63ZEAq1W3+clNjm2ACxCGAYujdN8HOqLF1W6VPU0gxcr1v6l 21 | PtwuW++uKF0YIZFJr/+SJDECgYBnMbvdMD2bs3WH4ZEaJFdKrfdGaQR/USClkbt2 22 | TGC/GoN5i1C5iMtUc9lOF2Le3RcZQ2dcfRY3eXX+Waf7hER4PVfJDtR07sAv5r1U 23 | 9zaN52Rn1vvYWOYNXRVZuJHrVjDXwNRyaqWWJlNv7aLUjQTxzjdTe/8Pe/rsorpw 24 | xllhoQKBgQDTbKjxtG6EWTAGxkhtBynoCQOU4sjW7pj0+/ed6ZVr6xOThCCWyVb7 25 | sF/siDSzN9rcQBUiCaU586+4oftwD3WaSXLV1E3W8pSfQomIVzHSo7A+tAh0UokF 26 | cEtdhpBw0thBR/ONeVpHuPFEYF+3uQfnGFn6d3pgk0XF1ARk011VuQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = manta 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. autosummary:: 5 | :toctree: api 6 | 7 | manta.payproc 8 | manta.store 9 | manta.wallet 10 | -------------------------------------------------------------------------------- /docs/api/manta.payproc.PayProc.rst: -------------------------------------------------------------------------------- 1 | manta.payproc.PayProc 2 | ===================== 3 | 4 | This class can be used to create a Manta Payment Processor. 5 | 6 | An example using the class is the ``manta.testing.payproc`` module. 7 | 8 | Here is an example of the initialization of the :class:`PayProc` class: 9 | 10 | .. code-block:: python 11 | :linenos: 12 | 13 | pp = PayProc(KEYFILE, host="localhost") 14 | pp.get_merchant = lambda x: MERCHANT 15 | pp.get_destinations = get_destinations 16 | pp.get_supported_cryptos = lambda device, payment_request: {'btc', 'xmr', 'nano'} 17 | 18 | The lines from 2 to 4 define the callbacks you need to implement. 19 | 20 | The class will callback to request information about which Merchant 21 | information to provide, which is the destination address, which 22 | supported crypto currencies. 23 | 24 | For example this is how get_destinations can be defined: 25 | 26 | .. code-block:: python 27 | 28 | DESTINATIONS = [ 29 | Destination( 30 | amount=Decimal("0.01"), 31 | destination_address="xrb_3d1ab61eswzsgx5arwqc3gw8xjcsxd7a5egtr69jixa5it9yu9fzct9nyjyx", 32 | crypto_currency="NANO" 33 | ), 34 | ] 35 | 36 | def get_destinations(application_id, merchant_order: MerchantOrderRequestMessage): 37 | if merchant_order.crypto_currency: 38 | destination = next(x for x in DESTINATIONS if x.crypto_currency == merchant_order.crypto_currency) 39 | return [destination] 40 | else: 41 | return DESTINATIONS 42 | 43 | 44 | Finally you need to start the :term:`MQTT` processing loop which is 45 | started in another thread. To activate it just execute: 46 | 47 | .. code-block:: python 48 | 49 | pp.run() 50 | 51 | 52 | Reference 53 | --------- 54 | .. currentmodule:: manta.payproc 55 | 56 | .. autoclass:: PayProc 57 | :members: 58 | 59 | -------------------------------------------------------------------------------- /docs/api/manta.payproc.TXStorage.rst: -------------------------------------------------------------------------------- 1 | manta.payproc.TXStorage 2 | ======================= 3 | 4 | .. currentmodule:: manta.payproc 5 | 6 | .. autoclass:: TXStorage 7 | :members: 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/api/manta.payproc.TXStorageMemory.rst: -------------------------------------------------------------------------------- 1 | manta.payproc.TXStorageMemory 2 | ============================= 3 | 4 | .. currentmodule:: manta.payproc 5 | 6 | .. autoclass:: TXStorageMemory 7 | :members: 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/api/manta.payproc.TransactionState.rst: -------------------------------------------------------------------------------- 1 | manta.payproc.TransactionState 2 | ============================== 3 | 4 | .. currentmodule:: manta.payproc 5 | 6 | .. autoclass:: TransactionState 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/api/manta.payproc.rst: -------------------------------------------------------------------------------- 1 | manta.payproc 2 | ============= 3 | 4 | PayProc library has been created to facilitate implementation of a 5 | Manta Payment Processor service. 6 | 7 | See :py:class:`manta.payproc.PayProc` for more details. 8 | 9 | .. currentmodule:: manta.payproc 10 | 11 | .. rubric:: Classes 12 | 13 | .. autosummary:: 14 | :toctree: 15 | 16 | PayProc 17 | TXStorage 18 | TXStorageMemory 19 | TransactionState 20 | 21 | 22 | .. rubric:: Exceptions 23 | 24 | .. autosummary:: 25 | 26 | SessionDoesNotExist 27 | -------------------------------------------------------------------------------- /docs/api/manta.store.rst: -------------------------------------------------------------------------------- 1 | manta.store 2 | =========== 3 | 4 | .. automodule:: manta.store 5 | 6 | This module implements a basic Manta :term:`Merchant`. An usage example of 7 | this class can be found in module ``manta.testing.store``. 8 | 9 | The API is implemented in the :class:`.Store` class and 10 | specifically by the :meth:`.Store.merchant_order_request` method 11 | that creates an :class:`~.messages.MerchantOrderRequestMessage` and 12 | sends it to the :term:`Payment Processor` that is managing the payment 13 | process. 14 | 15 | In order to work correctly, the class needs to be instantiated and 16 | connected to the :term:`MQTT` broker. This is implemented using an 17 | *asyncio* coroutine: 18 | 19 | .. code-block:: python3 20 | 21 | import asyncio 22 | 23 | from manta.store import Store 24 | 25 | store = Store('example_store', host='example.com') 26 | 27 | loop = asyncio.get_event_loop() 28 | loop.run_until_complete(store.connect()) 29 | 30 | 31 | Then the a new order needs to be created: 32 | 33 | .. code-block:: python3 34 | 35 | ack = loop.run_until_complete(store.merchant_order_request(amount=10, fiat='eur')) 36 | 37 | ack.url # contains the Manta URL 38 | 39 | When that is done, the :term:`Manta URL` needs to be transmitted to 40 | the :term:`Wallet` to pay, but this is out of the scope of this 41 | code. From the Merchant point of view the progress of the payment 42 | transaction can be monitored by looking into the 43 | :class:`~.messages.AckMessage` instances collected by the 44 | ``store.acks`` queue. The payment is considered complete when a 45 | received ack has status == ``PAID``: 46 | 47 | .. code-block:: python3 48 | 49 | from manta.messages import Status 50 | 51 | async def wait_for_complete(store): 52 | while True: 53 | ack = await store.acks.get() 54 | if ack.status is Status.PAID: 55 | break 56 | return ack 57 | 58 | final_ack = loop.run_until_complete(wait_for_complete(store)) 59 | 60 | Reference 61 | --------- 62 | 63 | .. autoclass:: manta.store.Store 64 | :members: 65 | -------------------------------------------------------------------------------- /docs/api/manta.wallet.rst: -------------------------------------------------------------------------------- 1 | manta.wallet 2 | ============ 3 | 4 | .. automodule:: manta.wallet 5 | 6 | This module implements a basic Manta :term:`Wallet`. An usage 7 | example of this class can be found in module 8 | ``manta.testing.wallet``. 9 | 10 | The API is implemented in the :class:`.Wallet` class and 11 | specifically by the :meth:`.Wallet.get_payment_request` and 12 | :meth:`.Wallet.send_payment` methods. The former requests the 13 | payment details to the :term:`Payment Processor` while the latter 14 | creates an :class:`~.messages.PaymentMessage` and sends it to the 15 | :term:`Payment Processor` that is managing the payment process in 16 | order to complete the payment process. 17 | 18 | To work as expected, an object of the :class:`.Wallet` is created 19 | using the :meth:`.Wallet.factory` classmethod from a :term:`Manta 20 | URL`: 21 | 22 | .. code-block:: python3 23 | 24 | import asyncio 25 | 26 | from manta.wallet import Wallet 27 | 28 | wallet = Wallet.factory("manta://developer.beappia.com/774747") 29 | 30 | All the other operations are implemented as *coroutines* and so 31 | they need an *asyncio loop* to work correctly. The first operation 32 | needed is to retrieve the payment details: 33 | 34 | .. code-block:: python3 35 | 36 | loop = asyncio.get_event_loop() 37 | 38 | envelope = loop.run_until_complete(wallet.get_payment_request()) 39 | payment_req = envelope.unpack() 40 | 41 | ...then a concrete wallet implementation is supposed to add the 42 | transaction on the block chain and then send these informations 43 | back to the :term:`Payment Processor`. We here simulate it with: 44 | 45 | .. code-block:: python3 46 | 47 | loop.run_until_complete(wallet.send_payment(transaction_hash=block, crypto_currency='NANO')) 48 | 49 | Like what happens with the :class:`~.store.Store` class, the 50 | progress of the payment session can be monitored by looking into 51 | the :class:`~.messages.AckMessage` instances collected by the 52 | ``wallet.acks`` queue. The payment is considered complete when a 53 | received ack has status == ``PAID``: 54 | 55 | .. code-block:: python3 56 | 57 | from manta.messages import Status 58 | 59 | async def wait_for_complete(wallet): 60 | while True: 61 | ack = await wallet.acks.get() 62 | if ack.status is Status.PAID: 63 | break 64 | return ack 65 | 66 | final_ack = loop.run_until_complete(wait_for_complete(wallet)) 67 | 68 | Reference 69 | --------- 70 | 71 | .. autoclass:: manta.wallet.Wallet 72 | :members: 73 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration file for the Sphinx documentation builder. 3 | # 4 | # This file does only contain a selection of the most common options. For a 5 | # full list see the documentation: 6 | # http://www.sphinx-doc.org/en/master/config 7 | 8 | # -- Path setup -------------------------------------------------------------- 9 | 10 | # If extensions (or modules to document with autodoc) are in another directory, 11 | # add these directories to sys.path here. If the directory is relative to the 12 | # documentation root, use os.path.abspath to make it absolute, like shown here. 13 | # 14 | import os 15 | import sys 16 | sys.path.insert(0, os.path.abspath('..')) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'manta' 22 | copyright = '2018, Alessandro Viganò' 23 | author = 'Alessandro Viganò' 24 | 25 | # The short X.Y version 26 | version = '' 27 | # The full version, including alpha/beta/rc tags 28 | release = '' 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.napoleon', 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.viewcode', 44 | 'sphinx.ext.autosummary', 45 | 'sphinx_autodoc_typehints', 46 | 'sphinx-jsonschema', 47 | 'sphinxcontrib.seqdiag', 48 | 'sphinxcontrib.asyncio', 49 | ] 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ['_templates'] 53 | 54 | # The suffix(es) of source filenames. 55 | # You can specify multiple suffix as a list of string: 56 | # 57 | # source_suffix = ['.rst', '.md'] 58 | source_suffix = '.rst' 59 | 60 | # The master toctree document. 61 | master_doc = 'index' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = None 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This pattern also affects html_static_path and html_extra_path . 73 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 74 | 75 | # The name of the Pygments (syntax highlighting) style to use. 76 | pygments_style = 'default' 77 | 78 | # Fontpath for seqdiag (truetype font) 79 | seqdiag_fontpath = os.path.join(os.path.dirname(__file__), 'fonts/Lato-Regular.ttf') 80 | seqdiag_antialias = True 81 | seqdiag_html_image_format = 'SVG' 82 | 83 | # -- Options for HTML output ------------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = 'sphinx_rtd_theme' 89 | 90 | # Theme options are theme-specific and customize the look and feel of a theme 91 | # further. For a list of options available for each theme, see the 92 | # documentation. 93 | # 94 | # html_theme_options = {} 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | # Custom sidebar templates, must be a dictionary that maps document names 102 | # to template names. 103 | # 104 | # The default sidebars (for documents that don't match any pattern) are 105 | # defined by theme itself. Builtin themes are using these templates by 106 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 107 | # 'searchbox.html']``. 108 | # 109 | # html_sidebars = {} 110 | 111 | 112 | # -- Options for HTMLHelp output --------------------------------------------- 113 | 114 | # Output file base name for HTML help builder. 115 | htmlhelp_basename = 'mantadoc' 116 | 117 | 118 | # -- Options for LaTeX output ------------------------------------------------ 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | 125 | # The font size ('10pt', '11pt' or '12pt'). 126 | # 127 | # 'pointsize': '10pt', 128 | 129 | # Additional stuff for the LaTeX preamble. 130 | # 131 | # 'preamble': '', 132 | 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, 140 | # author, documentclass [howto, manual, or own class]). 141 | latex_documents = [ 142 | (master_doc, 'manta.tex', 'manta Documentation', 143 | 'Alessandro Viganò', 'manual'), 144 | ] 145 | 146 | 147 | # -- Options for manual page output ------------------------------------------ 148 | 149 | # One entry per manual page. List of tuples 150 | # (source start file, name, description, authors, manual section). 151 | man_pages = [ 152 | (master_doc, 'manta', 'manta Documentation', 153 | [author], 1) 154 | ] 155 | 156 | 157 | # -- Options for Texinfo output ---------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | (master_doc, 'manta', 'manta Documentation', 164 | author, 'manta', 'One line description of project.', 165 | 'Miscellaneous'), 166 | ] 167 | 168 | napoleon_use_param = True 169 | 170 | autosummary_generate = True 171 | 172 | # -- Extensions to the Napoleon GoogleDocstring class --------------------- 173 | # Thanks to https://michaelgoerz.net/notes/extending-sphinx-napoleon-docstring-sections.html 174 | 175 | from sphinx.ext.napoleon.docstring import GoogleDocstring 176 | 177 | 178 | # first, we define new methods for any new sections and add them to the class 179 | def parse_keys_section(self, section): 180 | return self._format_fields('Keys', self._consume_fields()) 181 | 182 | 183 | GoogleDocstring._parse_keys_section = parse_keys_section 184 | 185 | 186 | def parse_attributes_section(self, section): 187 | return self._format_fields('Attributes', self._consume_fields()) 188 | 189 | 190 | GoogleDocstring._parse_attributes_section = parse_attributes_section 191 | 192 | 193 | def parse_class_attributes_section(self, section): 194 | return self._format_fields('Class Attributes', self._consume_fields()) 195 | 196 | 197 | GoogleDocstring._parse_class_attributes_section = parse_class_attributes_section 198 | 199 | 200 | # we now patch the parse method to guarantee that the the above methods are 201 | # assigned to the _section dict 202 | def patched_parse(self): 203 | self._sections['keys'] = self._parse_keys_section 204 | self._sections['class attributes'] = self._parse_class_attributes_section 205 | self._unpatched_parse() 206 | 207 | 208 | GoogleDocstring._unpatched_parse = GoogleDocstring._parse 209 | GoogleDocstring._parse = patched_parse 210 | -------------------------------------------------------------------------------- /docs/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appiapay/manta-python/889caacf3151c9c39ae96b97053cac064103a3f6/docs/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | Welcome to Manta Python Documentation! 3 | ======================================== 4 | 5 | This is an implementation of the Manta payment protocol for 6 | Python 3. If you want to dive in you may choose to learn first of the 7 | :doc:`protocol` or if you want to try some code, head over to the API 8 | entries for the :doc:`Store `, :doc:`Wallet 9 | ` and :doc:`Payment Processor 10 | `. 11 | 12 | 13 | Requirements 14 | ============ 15 | 16 | This library is compatible only with Python 3.7+. If you want to use 17 | the available testing infrastructure to test your own components you 18 | will need to have mosquitto_ broker (or another :term:`MQTT` broker) 19 | installed. 20 | 21 | .. _mosquitto: http://mosquitto.org 22 | 23 | 24 | Installation 25 | ============ 26 | 27 | To install this library, just type the following commands:: 28 | 29 | $ pip install manta-python 30 | 31 | or, if you want to use the testing infrastructure, execute instead:: 32 | 33 | $ pip install manta-python[runner] 34 | 35 | 36 | Testing infrastructure 37 | ====================== 38 | 39 | When installed with the testing infrastructure enable, you will find 40 | the command ``manta-runner`` in you execution path. If you have any 41 | problem finding it, you can run the equivalent ``python3 -m 42 | manta.testing``. 43 | 44 | The command is meant to take in a configuration file and run the 45 | specified services. A default configuration with all the services 46 | enabled can be obtained running the following command:: 47 | 48 | $ manta-runner --print-config 49 | 50 | By default this configuration will start the mosquitto_ :term:`MQTT` 51 | broker and all the example components that are used also in the tests, 52 | with their web port enabled: 53 | 54 | To setup the services with this configuration, simply execute the 55 | following commands:: 56 | 57 | $ manta-runner --print-config > /tmp/demo.yaml 58 | $ manta-runner -c /tmp/demo.yaml 59 | 60 | You will obtain a log like the following:: 61 | 62 | INFO:manta.testing.broker:Started Mosquitto broker on interface 'localhost' and port 41627. 63 | INFO:manta.payproc:Connected with result code 0 64 | INFO:manta.payproc:Subscribed to 'merchant_order_request/+' 65 | INFO:manta.payproc:Subscribed to 'merchant_order_cancel/+' 66 | INFO:manta.testing.runner:Started service payproc on address 'localhost' and port 8081 67 | INFO:manta.store:Connected 68 | INFO:manta.testing.runner:Started service store on address 'localhost' and port 8080 69 | INFO:manta.testing.runner:Started service wallet on address 'localhost' and port 8082 70 | INFO:dummy-runner:Configured services are now running. 71 | INFO:dummy-runner:==================== Hit CTRL-C to stop ==================== 72 | INFO:mosquitto:1551282335: New connection from ::1 on port 41627. 73 | INFO:mosquitto:1551282335: New client connected from ::1 as b9109520-b7af-41bf-99d0-bf2425008bc6 (c1, k60). 74 | INFO:mosquitto:1551282335: New connection from ::1 on port 41627. 75 | INFO:mosquitto:1551282335: New client connected from ::1 as 066284b2-e8c7-41aa-9595-3432b12665a2 (c1, k60). 76 | 77 | Like specified in the log, hit *CTRL-C* (or the equivalent keyboard 78 | combination that generates a ``KeyboardInterrupt`` exception on your 79 | OS) to teardown the services. 80 | 81 | The configured services are automatically started and connected to the 82 | port exposed by the broker. If enabled (as it is by default) each 83 | configured service exposes a web service that can be used to execute 84 | key APIs of each. To know what the entrypoints are you have (for now) 85 | to look into the files in the ``manta.testing`` subpackage or to look 86 | into the tests in the `github repository`_. 87 | 88 | .. _github repository: https://github.com/appiapay/manta-python 89 | 90 | Single component runners 91 | ~~~~~~~~~~~~~~~~~~~~~~~~ 92 | 93 | To ease the development of new components this library installs in 94 | your path executables to run the individual components, they are named 95 | ``manta-store``, ``manta-payproc`` and ``manta-wallet``. They are 96 | implemented by the same code of the collective runner but they offer a 97 | different user interface with more commandline arguments, e.g.:: 98 | 99 | $ manta-wallet --help 100 | usage: manta-wallet [-h] [-b BROKER] [--broker-port BROKER_PORT] [-p WEB_PORT] 101 | [--url URL] [-i] [-w WALLET] [-a ACCOUNT] [--cert CERT] 102 | [-c CONF] [--print-config] 103 | 104 | Run manta-python dummy wallet 105 | 106 | optional arguments: 107 | -h, --help show this help message and exit 108 | -b BROKER, --broker BROKER 109 | MQTT broker hostname (default: 'localhost') 110 | --broker-port BROKER_PORT 111 | MQTT broker port (default: 1883) 112 | -p WEB_PORT, --web-port WEB_PORT 113 | enable web interface on the specified port 114 | --url URL Manta-URL of the payment session to join and pay. It 115 | will automatically end the session when payment is 116 | completed 117 | -i, --interactive enable interactive payment interface (default: False) 118 | -w WALLET, --wallet WALLET 119 | hash of the nano wallet to use 120 | -a ACCOUNT, --account ACCOUNT 121 | hash of the nano account 122 | --cert CERT CA certificate used to validate the payment session 123 | -c CONF, --conf CONF path of a config file to load 124 | --print-config print a sample of the default configuration 125 | 126 | All three expect for the broker to be up and running 127 | already. ``manta-payproc`` and ``manta-wallet`` accept also a specific 128 | configuration file, please use the ``--print-config`` option to get a 129 | sample of that file. 130 | 131 | Tests 132 | ===== 133 | 134 | To run the tests you have to run the following commands:: 135 | 136 | $ git git@github.com:appiapay/manta-python.git 137 | $ cd manta-python 138 | $ pip install -r requirements-dev.txt 139 | 140 | Be aware that the same requirements specified for the `Testing 141 | Infrastructure`_ apply here too. If you want to use your own 142 | :term:`MQTT` broker you will have to modify the ``broker.start`` entry 143 | in the file ``tests/dummyconfig.yaml`` to ``false``. 144 | 145 | Then simply run:: 146 | 147 | $ make tests 148 | 149 | or, if ``make`` isn't available on your platform, just run:: 150 | 151 | $ pytest 152 | 153 | .. toctree:: 154 | :maxdepth: 3 155 | :caption: Contents: 156 | 157 | protocol 158 | api 159 | 160 | Indices and tables 161 | ================== 162 | 163 | * :ref:`genindex` 164 | * :ref:`modindex` 165 | * :ref:`search` 166 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=manta 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/protocol.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Manta Protocol 3 | ================ 4 | 5 | Manta is designed to be a safe, crypto agnostic, privacy 6 | friendly payment protocol. 7 | 8 | Glossary 9 | ======== 10 | 11 | Manta defines the transaction flow between three entities: 12 | 13 | .. glossary:: 14 | 15 | Merchant 16 | It's the initiator of the single payment transaction. It requires 17 | that some amount of money is paid by a **Wallet** and to do so it 18 | exchanges messages only with a **Payment Processor**. It's also called 19 | **Store** in the code. 20 | 21 | Payment Processor 22 | As the name suggests, it manages the payment 23 | process in its entirety. The other two parties never communicate 24 | directly but they exchange messages with it. 25 | 26 | Wallet 27 | End user who visits merchant’s web site, orders goods and/or 28 | service and pays for it. 29 | 30 | Technologies Used 31 | ================= 32 | 33 | .. glossary:: 34 | 35 | JSON 36 | `JavaScript Object Notation`_ is de facto standard for data 37 | serialization, and was standardized as :rfc:`8259`. 38 | 39 | MQTT 40 | `Message Queuing Telemetry Transport`_ is an ISO standard (ISO/IEC PRF 20922) 41 | publish-subscribe-based messaging protocol. 42 | 43 | Information exchange is organized in *topics* and clients 44 | communicates directly only with the *broker* service. A client can 45 | instruct the broker to *retain* a published message so that it can 46 | be delivered to future subscribers whose subscriptions match its 47 | topic name. 48 | 49 | 50 | .. _Message Queuing Telemetry Transport: 51 | .. _mqtt: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html 52 | 53 | .. _JavaScript Object Notation: https://json.org 54 | 55 | Communication protocol 56 | ====================== 57 | 58 | Standard `MQTT`:term: is used for communication between all the three 59 | parties. The exchanged messages are encoded in :term:`JSON` format. 60 | 61 | .. figure:: images/manta-protocol.svg 62 | 63 | High level overview of Manta Protocol 64 | 65 | .. toctree:: 66 | 67 | protocol/parameters 68 | protocol/flow 69 | protocol/messages 70 | protocol/topics 71 | -------------------------------------------------------------------------------- /docs/protocol/flow.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | Flow 3 | ====== 4 | 5 | Flow is initiated by Merchant. 6 | 7 | Merchant 8 | ======== 9 | 10 | The payment process from the :term:`Merchant`'s point of view: 11 | 12 | .. seqdiag:: 13 | :scale: 70 14 | 15 | activation = none; 16 | Merchant; "Payment Processor"; "MQTT Broker"; 17 | 18 | Merchant --> "MQTT Broker" [leftnote="(2) SUBSCRIBE to\n\"acks/{session_id}\""]; 19 | Merchant ->> "Payment Processor" [leftnote="PUBLISH on\n\"merchant_order_request/{application_id}\"", 20 | label="(3) MerchantOrderRequest"]; 21 | Merchant <<- "Payment Processor" [rightnote="PUBLISH on\n\"acks/{session_id}\"", 22 | label="(4) Ack[status = new]"]; 23 | Merchant <<- "Payment Processor" [rightnote="PUBLISH on\n\"acks/{session_id}\"", 24 | label="(5) Ack[status = paid]"]; 25 | Merchant --> "MQTT Broker" [leftnote="(6) UNSUBSCRIBE to\n\"acks/{session_id}\""]; 26 | 27 | 1. :term:`Merchant` generates a random :term:`session_id`. 28 | 29 | 2. :term:`Merchant` *subscribes* to the topic :ref:`acks/{session_id}`. 30 | 31 | 3. :term:`Merchant` create a 32 | `~manta.messages.MerchantOrderRequestMessage`:class: and *publishes* on 33 | topic `merchant_order_request/{application_id}`:ref:. 34 | 35 | `~manta.messages.MerchantOrderRequestMessage.crypto_currency`:obj: 36 | field should be empty if customer wants to pay with Manta. 37 | 38 | 4. :term:`Merchant` *receives* an :class:`~manta.messages.AckMessage` with 39 | status == "new" on topic :ref:`acks/{session_id}`. 40 | 41 | :term:`Merchant` can create QR code/NDEF Message with URL data 42 | 43 | 5. :term:`Merchant` *will receive* an 44 | :class:`~manta.messages.AckMessage` messages as payment changes 45 | state. With status == "paid" the transaction is completed. 46 | 47 | 6. :term:`Merchant` *unsubscribes* from the topic 48 | :ref:`acks/{session_id}`. 49 | 50 | Payment Processor 51 | ================= 52 | 53 | The payment process from the :term:`Payment Processor`'s side: 54 | 55 | 56 | .. seqdiag:: 57 | :scale: 70 58 | 59 | activation = none; 60 | Merchant; "Payment Processor"; Wallet; "MQTT Broker"; 61 | 62 | "Payment Processor" --> "MQTT Broker" [rightnote="(1a) SUBSCRIBE to\n\"merchant_order_request/+\""] 63 | "Payment Processor" --> "MQTT Broker" [rightnote="(1a) SUBSCRIBE to\n\"merchant_order_cancel/+\""] 64 | "Payment Processor" --> "MQTT Broker" [rightnote="PUBLISH on\n\"certificate\"", 65 | label="(1b) Manta CA certificate"] 66 | 67 | === initialization complete === 68 | 69 | Merchant ->> "Payment Processor" [leftnote="PUBLISH on\n\"merchant_order_request/{application_id}\"", 70 | label="(2) MerchantOrderRequest"]; 71 | Merchant <<- "Payment Processor" [rightnote="PUBLISH on\n\"acks/{session_id}\"", 72 | label="(2b) Ack[status = new]"]; 73 | ... crypto_currency == null ... 74 | 75 | "Payment Processor" --> "MQTT Broker" [rightnote="(2c) SUBSCRIBE to\n\"payments/{session_id}\""]; 76 | "Payment Processor" --> "MQTT Broker" [rightnote="(2c) SUBSCRIBE to\n\"payment_requests/{session_id}/+\""]; 77 | 78 | "Payment Processor" <<- Wallet [rightnote="(3) PUBLISH on\n\"payment_requests/{session_id}/{crypto_currency}\""]; 79 | "Payment Processor" ->> Wallet [leftnote="PUBLISH on\n\"payment_requests/{session_id}\"", 80 | label="(3a) PaymentRequestEnvelope"]; 81 | "Payment Processor" <<- Wallet [rightnote="PUBLISH on\n\"payments/{session_id}\"", 82 | label="(4a) PaymentMessage"]; 83 | Merchant <<- "Payment Processor" ->> Wallet [rightnote="PUBLISH on\n\"acks/{session_id}\"", 84 | label="(4b) Ack[status = paid]"]; 85 | 86 | 1. When it starts: 87 | 88 | a. it *subscribes* to :ref:`merchant_order_request/+` 89 | and :ref:`merchant_order_cancel/+` topics; 90 | 91 | b. it *publishes* the Manta CA certificate to :ref:`certificate` 92 | topic, with retention. 93 | 94 | 2. On message :class:`~manta.messages.MerchantOrderRequestMessage` on 95 | a specific :ref:`merchant_order_request/{application_id}` topic: 96 | 97 | a. generates an :class:`~manta.messages.AckMessage` with status == 98 | "new". The :attr:`~manta.messages.AckMessage.url` field is in 99 | manta format if field 100 | :attr:`~manta.messages.AckMessage.crypto_currency` is null 101 | (manta protocol), otherwise 102 | :attr:`~manta.messages.AckMessage.url` format will depend on the 103 | :attr:`~manta.messages.AckMessage.crypto_currency`; 104 | 105 | b. *publishes* this the :class:`~manta.messages.AckMessage` on the 106 | :ref:`acks/{session_id}` topic; 107 | 108 | If manta protocol is used: 109 | 110 | c. It *subscribes* to :ref:`payments/{session_id}` and 111 | :ref:`payment_requests/{session_id}/+` topics. 112 | 113 | 3. On an event on 114 | :ref:`payment_requests/{session_id}/{crypto_currency}` without any 115 | payload: 116 | 117 | a. creates a new 118 | :class:`~manta.messages.PaymentRequestMessage` and *publishes* it on 119 | :ref:`payment_requests/{session_id}` wrapped into a 120 | :class:`~manta.messages.PaymentRequestEnvelope` with retention. 121 | 122 | ``{crypto_currency}`` parameter can be "all" to request multiple 123 | cryptos. 124 | 125 | Destination should be specific to ``{crypto_currency}`` field. 126 | 127 | 4. On message (a) :class:`~manta.messages.PaymentMessage` on 128 | :ref:`payments/{session_id}` it starts monitoring blockchain and on 129 | progress *publishes* (b) :class:`~manta.messages.AckMessage` on 130 | :ref:`acks/{session_id}`. 131 | 132 | 133 | Manta enabled wallet 134 | ==================== 135 | 136 | The payment process from the :term:`Wallet` point of view: 137 | 138 | .. seqdiag:: 139 | :scale: 70 140 | 141 | activation = none; 142 | Wallet; "Payment Processor"; "MQTT Broker"; 143 | 144 | Wallet -> Wallet [rightnote="read manta URL"] 145 | Wallet --> "MQTT Broker" [leftnote="(1) SUBSCRIBE to\n\"payment_requests/{session_id}\""]; 146 | Wallet ->> "Payment Processor" [leftnote="(2) PUBLISH on\n\"payment_request_message/{session_id}/{crypto_currency}\""]; 147 | Wallet <<- "Payment Processor" [leftnote="PUBLISH on\n\"payment_requests/{session_id}\"", 148 | label="(3) PaymentRequest"]; 149 | Wallet ->> "Payment Processor" [leftnote="PUBLISH on\n\"payments/{session_id}\"", 150 | label="(5) Payment"]; 151 | Wallet --> "MQTT Broker" [leftnote="(6) SUBSCRIBE to\n\"acks/{session_id}\""]; 152 | Wallet <<- "Payment Processor" [rightnote="PUBLISH on\n\"acks/{session_id}\"", 153 | label="(7) Ack[status = paid]"]; 154 | 155 | 156 | 1. After receiving a :term:`Manta URL` via QR code or NFC it 157 | *subscribes* to :ref:`payment_requests/{session_id}`. 158 | 159 | 2. *Publishes* on :ref:`payment_requests/{session_id}/{crypto_currency}`. 160 | 161 | ``{crypto_currency}`` can be "all" to request multiple cryptos. 162 | 163 | 3. On :class:`~manta.messages.PaymentRequestMessage` on topic 164 | :ref:`payment_requests/{session_id}` if 165 | :attr:`~manta.messages.PaymentRequestMessage.destinations` field does 166 | not contain desired crypto, check *supported_cryptos* and eventually 167 | go back to 2). 168 | 169 | 4. *Verifies* :class:`~manta.messages.PaymentRequestMessage` signature. 170 | 171 | 5. After payment on blockchain *publishes* a 172 | :class:`~manta.messages.PaymentMessage` on 173 | :ref:`payments/{session_id}` topic. 174 | 175 | 6. *Subscribes* to the topic named :ref:`acks/{session_id}` 176 | 177 | 7. :term:`Wallet` *will receive* an 178 | :class:`~manta.messages.AckMessage` messages as payment changes 179 | state. With status == "paid" the transaction is completed. 180 | 181 | Complete Manta flow diagram 182 | =========================== 183 | 184 | .. figure:: ../images/manta-protocol-full.svg 185 | 186 | Detailed Manta Protocol flow 187 | 188 | You can |location_link|. 189 | 190 | .. |location_link| raw:: html 191 | 192 | Open diagram in new window 193 | -------------------------------------------------------------------------------- /docs/protocol/messages.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Messages 3 | ========== 4 | 5 | Messages are object that are use used to store and communicate the 6 | data of the :term:`Merchant` order and the correspondent 7 | :term:`Wallet` payment. They are serialize to/from term:`JSON` for 8 | transmission using :term:`MQTT` protocol. 9 | 10 | Ack 11 | === 12 | .. autoclass:: manta.messages.AckMessage 13 | .. autoclass:: manta.messages.Status 14 | :members: 15 | 16 | Merchant Order Request 17 | ====================== 18 | .. autoclass:: manta.messages.MerchantOrderRequestMessage 19 | 20 | Payment Request Message 21 | ======================= 22 | .. autoclass:: manta.messages.PaymentRequestMessage 23 | .. autoclass:: manta.messages.Merchant 24 | .. autoclass:: manta.messages.Destination 25 | .. autoclass:: manta.messages.PaymentRequestEnvelope 26 | 27 | Payment 28 | ======= 29 | .. autoclass:: manta.messages.PaymentMessage 30 | -------------------------------------------------------------------------------- /docs/protocol/parameters.rst: -------------------------------------------------------------------------------- 1 | Parameters 2 | ========== 3 | 4 | .. glossary:: 5 | 6 | application_id 7 | Unique :term:`POS` identifier. 8 | 9 | crypto_currency 10 | Name of the currency used to complete the monetary transaction. 11 | 12 | session_id 13 | Unique Session Identifier. 14 | 15 | Manta URL 16 | It's an Uniform Resource Locator of the Session initiated by the 17 | :term:`POS` and it's shared with the :term:`Wallet` via means like 18 | QR code or NFC. 19 | 20 | it's encoded as: 21 | 22 | manta://<*broker_host*>[:<*broker_port*>]/<*session_id*> 23 | 24 | where ``broker_host`` and ``broker_port`` are the IP address and 25 | TCP port where the broker service is listening. 26 | -------------------------------------------------------------------------------- /docs/protocol/topics.rst: -------------------------------------------------------------------------------- 1 | Topics 2 | ====== 3 | 4 | .. _acks/{session_id}: 5 | 6 | acks/{session_id} 7 | ----------------- 8 | :parameters: :term:`session_id` 9 | :publishers: :term:`Payment Processor` 10 | :subscribers: :term:`Merchant`, :term:`Wallet` 11 | 12 | Used by the :term:`Payment Processor` to publish 13 | :class:`~manta.messages.AckMessage`: it serves the purpose of 14 | informing the other parties of the :class:`~manta.messages.Status` and 15 | other primary data of the session. 16 | 17 | .. _certificate: 18 | 19 | certificate 20 | ----------- 21 | :parameters: none 22 | :publishers: :term:`Payment Processor` 23 | :subscribers: :term:`Wallet` 24 | 25 | Used by the :term:`Payment Processor` to publish the Manta CA 26 | certificate so that the wallet can use it to... TBD... 27 | 28 | .. _merchant_order_cancel/+: 29 | .. _merchant_order_cancel/{session_id}: 30 | 31 | merchant_order_cancel/{session_id} 32 | ---------------------------------- 33 | :parameters: :term:`session_id` 34 | :publishers: :term:`Merchant` 35 | :subscribers: :term:`Payment Processor` 36 | 37 | TBD 38 | 39 | .. _merchant_order_request/+: 40 | .. _merchant_order_request/{application_id}: 41 | 42 | merchant_order_request/{application_id} 43 | --------------------------------------- 44 | :parameters: :term:`application_id` 45 | :publishers: :term:`Merchant` 46 | :subscribers: :term:`Payment Processor` 47 | 48 | Used by the :term:`Merchant` to initiate a new session by publishing a 49 | :class:`~manta.messages.MerchantOrderRequestMessage`. 50 | 51 | .. _payment_requests/{session_id}: 52 | 53 | payment_requests/{session_id} 54 | ----------------------------- 55 | :parameters: :term:`session_id` 56 | :publishers: :term:`Payment Processor` 57 | :subscribers: :term:`Wallet` 58 | 59 | It's where the :term:`Payment Processor` publishes a 60 | :class:`~manta.messages.PaymentRequestEnvelope` in response to an 61 | event on the topic 62 | :ref:`payment_requests/{session_id}/{crypto_currency}` published by 63 | the :term:`Wallet`. 64 | 65 | .. _payment_requests/{session_id}/+: 66 | .. _payment_requests/{session_id}/{crypto_currency}: 67 | 68 | payment_requests/{session_id}/{crypto_currency} 69 | ----------------------------------------------- 70 | :parameters: :term:`session_id`, :term:`crypto_currency` 71 | :publishers: :term:`Wallet` 72 | :subscribers: :term:`Payment Processor` 73 | 74 | Used by the :term:`Wallet` to get informations about the 75 | payment. ``{crypto_currency}`` parameter can be "all" to request 76 | multiple cryptos. 77 | 78 | .. _payments/{session_id}: 79 | 80 | payments/{session_id} 81 | --------------------- 82 | :parameters: :term:`session_id` 83 | :publishers: :term:`Wallet` 84 | :subscribers: :term:`Payment Processor` 85 | 86 | It's where the :term:`Wallet` publishes informations about the 87 | successful monetary transaction encoded as a 88 | :class:`~manta.messages.PaymentMessage`. 89 | 90 | .. TODO: possibly document ``certificate`` topic and payload 91 | -------------------------------------------------------------------------------- /manta/__init__.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | MANTA_VERSION = '1.6' 6 | -------------------------------------------------------------------------------- /manta/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from abc import ABC, abstractmethod 7 | 8 | import paho.mqtt.client as mqtt 9 | 10 | 11 | class MantaComponent(ABC): 12 | """A base class where the common properties of PayProc, Store and Wallet are 13 | specified.""" 14 | 15 | "The Manta protocol broker host" 16 | host: str 17 | "The mqtt client instance used for communication" 18 | mqtt_client: mqtt.Client 19 | "The Manta protocol broker port" 20 | port: int 21 | 22 | @abstractmethod 23 | def on_connect(self, client: mqtt.Client, userdata, flags, rc): 24 | pass 25 | 26 | @abstractmethod 27 | def on_message(self, client: mqtt.Client, userdata, msg): 28 | pass 29 | -------------------------------------------------------------------------------- /manta/dispatcher.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | from __future__ import annotations 6 | 7 | import inspect 8 | import re 9 | from typing import List, Callable, Tuple 10 | 11 | 12 | class Dispatcher: 13 | callbacks: List[Tuple[str, Callable]] = [] 14 | 15 | def __init__(self, obj: object= None): 16 | self.callbacks = [] 17 | 18 | if obj is None: 19 | return 20 | 21 | for cls in inspect.getmro(obj.__class__): # Register all subclasses methods 22 | for key, value in cls.__dict__.items(): 23 | if inspect.isfunction(value): 24 | if hasattr(value, "dispatcher"): 25 | self.callbacks.append((value.dispatcher, value.__get__(obj))) 26 | 27 | def dispatch(self, topic: str, **kwargs): 28 | for callback in self.callbacks: 29 | result = re.match(callback[0], topic) 30 | if result: 31 | groups = result.groups() 32 | args = list(groups[:-1]) 33 | #To match # 34 | args = args + groups[-1].split("/") 35 | callback[1](*args, **kwargs) 36 | 37 | @staticmethod 38 | def mqtt_to_regex(topic: str): 39 | # escaped = re.escape(topic) 40 | 41 | return topic.replace("+", "([^/]+)").replace("#", "(.*)")+"$" 42 | 43 | @staticmethod 44 | def method_topic(topic): 45 | def decorator(f: Callable): 46 | f.dispatcher = Dispatcher.mqtt_to_regex(topic) # type: ignore 47 | return f 48 | 49 | return decorator 50 | 51 | def topic(self, topic): 52 | def decorator(f: Callable): 53 | self.callbacks.append((Dispatcher.mqtt_to_regex(topic), f)) 54 | return f 55 | 56 | return decorator 57 | -------------------------------------------------------------------------------- /manta/messages.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | # Cryptography library generates UserWarning with latest CFFI libraries 6 | import warnings 7 | 8 | warnings.filterwarnings("ignore", message="Global variable") 9 | 10 | import base64 11 | from decimal import Decimal 12 | from enum import Enum 13 | from typing import List, Set, TypeVar, Type, Optional, Union 14 | 15 | import attr 16 | import cattr 17 | from certvalidator import CertificateValidator, ValidationContext 18 | from cryptography import x509 19 | from cryptography.exceptions import InvalidSignature 20 | from cryptography.hazmat.backends import default_backend 21 | from cryptography.hazmat.primitives import hashes, serialization 22 | from cryptography.hazmat.primitives.asymmetric import padding 23 | from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey 24 | import simplejson as json 25 | 26 | from . import MANTA_VERSION 27 | 28 | 29 | class Status(Enum): 30 | """ 31 | Status for ack messages 32 | """ 33 | 34 | NEW = "new" #: Created after accepting Merchant Order 35 | INVALID = "invalid" #: Order invalid - Ex timeout. Additional info can be specified in Ack Memo field 36 | PENDING = "pending" #: Created after receiving payment from wallet 37 | CONFIRMING = ( 38 | "confirming" #: Paid received by Payment Processor but not yet confirmed 39 | ) 40 | PAID = "paid" #: Created after blockchain confirmation 41 | CANCELED = "canceled" #: Order has been canceled 42 | 43 | 44 | T = TypeVar("T", bound="Message") 45 | 46 | 47 | def drop_nonattrs(d: dict, type_: type) -> dict: 48 | """gets rid of all members of the dictionary that wouldn't fit in the given 'attrs' Type""" 49 | attrs_attrs = getattr(type_, "__attrs_attrs__", None) 50 | if attrs_attrs is None: 51 | raise ValueError(f"type {type_} is not an attrs class") 52 | 53 | attrs: Set[str] = {attr.name for attr in attrs_attrs} 54 | # attrs: Set[str] = {attr.name for attr in attrs_attrs if attr.init is True} 55 | 56 | return {key: val for key, val in d.items() if key in attrs} 57 | 58 | 59 | def structure_ignore_extras(d: dict, Type: type): 60 | return cattr.structure(drop_nonattrs(d, Type), Type) 61 | 62 | 63 | @attr.s 64 | class Message: 65 | def unstructure(self): 66 | cattr.register_unstructure_hook(Decimal, lambda d: str(d)) 67 | return cattr.unstructure(self) 68 | 69 | def to_json(self) -> str: 70 | return json.dumps(self.unstructure(), iterable_as_array=True) 71 | 72 | @classmethod 73 | # def from_json(cls, json_str: str): 74 | def from_json(cls: Type[T], json_str: str) -> T: 75 | d = json.loads(json_str) 76 | cattr.register_structure_hook(Decimal, lambda d, t: Decimal(d)) 77 | 78 | if "version" not in d: 79 | d["version"] = "" 80 | 81 | return structure_ignore_extras(d, cls) 82 | 83 | 84 | @attr.s(auto_attribs=True) 85 | class MerchantOrderRequestMessage(Message): 86 | """ 87 | Merchant Order Request 88 | 89 | Published by :term:`Merchant` on 90 | :ref:`merchant_order_request/{application_id}`. 91 | 92 | Args: 93 | amount: amount in fiat currency 94 | fiat_currency: fiat currency 95 | session_id: random uuid base64 safe 96 | crypto_currency: None for manta protocol. Specified for legacy 97 | version: Manta protocol version 98 | """ 99 | 100 | amount: Decimal 101 | session_id: str 102 | fiat_currency: str 103 | crypto_currency: Optional[str] = None 104 | version: Optional[str] = MANTA_VERSION 105 | 106 | 107 | @attr.s(auto_attribs=True) 108 | class AckMessage(Message): 109 | """ 110 | Ack Message 111 | 112 | Order progress message. 113 | 114 | Published by the :term:`Payment Processor` on :ref:`acks/{session_id}`. 115 | 116 | Args: 117 | txid: progressive transaction ID generated by Merchant 118 | status: ack type 119 | url: url to be used for QR Code or NFC. Used in NEW 120 | amount: amount in crypto currency. Used in NEW 121 | transaction_hash: hash of transaction. After PENDING 122 | memo: extra text field 123 | version: Manta protocol version 124 | """ 125 | 126 | txid: str 127 | status: Status 128 | url: Optional[str] = None 129 | amount: Optional[Decimal] = None 130 | transaction_hash: Optional[str] = None 131 | transaction_currency: Optional[str] = None 132 | memo: Optional[str] = None 133 | version: Optional[str] = MANTA_VERSION 134 | 135 | 136 | @attr.s(auto_attribs=True) 137 | class Destination(Message): 138 | """ 139 | Destination 140 | 141 | Args: 142 | amount: amount in crypto currency 143 | destination_address: destination address for payment 144 | crypto_currency: crypto_currency (ex. NANO, BTC...)mo 145 | """ 146 | 147 | amount: Decimal 148 | destination_address: str 149 | crypto_currency: str 150 | 151 | 152 | @attr.s(auto_attribs=True) 153 | class Merchant(Message): 154 | """ 155 | Merchant 156 | 157 | Args: 158 | name: merchant name 159 | address: merchant address 160 | """ 161 | 162 | name: str 163 | address: Optional[str] = None 164 | 165 | 166 | @attr.s(auto_attribs=True) 167 | class PaymentRequestMessage(Message): 168 | """ 169 | Payment Request 170 | 171 | Generated after request on 172 | :ref:`payment_requests/{session_id}/{crypto_currency}`. 173 | 174 | Published in Envelope to Payment Processor on 175 | :ref:`payment_requests/{session_id}`. 176 | 177 | Args: 178 | merchant: merchant data 179 | amount: amount in fiat currency 180 | fiat_currency: fiat currency 181 | destinations: list of destination addresses 182 | supported_cryptos: list of supported crypto currencies 183 | 184 | """ 185 | 186 | merchant: Merchant 187 | amount: Decimal 188 | fiat_currency: str 189 | destinations: List[Destination] 190 | supported_cryptos: Set[str] 191 | 192 | def get_envelope(self, key: RSAPrivateKey): 193 | json_message = self.to_json() 194 | signature = base64.b64encode( 195 | key.sign(json_message.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256()) 196 | ) 197 | 198 | return PaymentRequestEnvelope( 199 | message=json_message, signature=signature.decode("utf-8") 200 | ) 201 | 202 | def get_destination(self, crypto: str) -> Optional[Destination]: 203 | try: 204 | return next(d for d in self.destinations if d.crypto_currency == crypto) 205 | except StopIteration: 206 | return None 207 | 208 | 209 | @attr.s(auto_attribs=True) 210 | class PaymentRequestEnvelope(Message): 211 | """ 212 | Payment Request Envelope 213 | 214 | Envelope with :class:`.PaymentRequestMessage` and signature 215 | 216 | Published by :term:`Payment Processor` on 217 | :ref:`payment_requests/{session_id}`. 218 | 219 | Args: 220 | message: message as json string 221 | signature: PKCS#1 v1.5 signature of the message field 222 | version: Manta protocol version 223 | """ 224 | 225 | message: str 226 | signature: str 227 | version: Optional[str] = MANTA_VERSION 228 | 229 | def unpack(self) -> PaymentRequestMessage: 230 | pr = PaymentRequestMessage.from_json(self.message) 231 | return pr 232 | 233 | def verify(self, certificate: Union[str, x509.Certificate]) -> bool: 234 | if isinstance(certificate, x509.Certificate): 235 | cert = certificate 236 | else: 237 | if certificate.startswith("-----BEGIN CERTIFICATE-----"): 238 | pem = certificate.encode() 239 | else: 240 | with open(certificate, "rb") as my_file: 241 | pem = my_file.read() 242 | 243 | cert = x509.load_pem_x509_certificate(pem, default_backend()) 244 | 245 | try: 246 | cert.public_key().verify( 247 | base64.b64decode(self.signature), 248 | self.message.encode("utf-8"), 249 | padding.PKCS1v15(), 250 | hashes.SHA256(), 251 | ) 252 | return True 253 | except InvalidSignature: 254 | return False 255 | 256 | 257 | @attr.s(auto_attribs=True) 258 | class PaymentMessage(Message): 259 | """ 260 | Payment Message 261 | 262 | Published by :term:`Wallet` on :ref:`payments/{session_id}` 263 | 264 | Args: 265 | crypto_currency: crypto currency used for payment 266 | transaction_hash: hash of transaction 267 | version: Manta protocol version 268 | 269 | """ 270 | 271 | crypto_currency: str 272 | transaction_hash: str 273 | version: Optional[str] = MANTA_VERSION 274 | 275 | 276 | def verify_chain(certificate: Union[str, x509.Certificate], ca: str): 277 | if isinstance(certificate, x509.Certificate): 278 | pem = certificate.public_bytes(serialization.Encoding.PEM) 279 | else: 280 | if certificate.startswith("-----BEGIN CERTIFICATE-----"): 281 | pem = certificate.encode() 282 | else: 283 | with open(certificate, "rb") as my_file: 284 | pem = my_file.read() 285 | # cert = x509.load_pem_x509_certificate(pem, default_backend()) 286 | 287 | with open(ca, "rb") as my_file: 288 | pem_ca = my_file.read() 289 | # ca = x509.load_pem_x509_certificate(pem_ca, default_backend()) 290 | 291 | trust_roots = [pem_ca] 292 | context = ValidationContext(trust_roots=trust_roots) 293 | 294 | validator = CertificateValidator(pem, validation_context=context) 295 | return validator.validate_usage({"digital_signature"}) 296 | -------------------------------------------------------------------------------- /manta/store.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | """ 6 | Library with a basic implementation of a Manta :term:`POS`. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import asyncio 12 | import base64 13 | from decimal import Decimal 14 | import logging 15 | import uuid 16 | from typing import List, Dict, Optional 17 | 18 | import paho.mqtt.client as mqtt 19 | 20 | from .base import MantaComponent 21 | from .messages import MerchantOrderRequestMessage, AckMessage, Status 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def generate_session_id() -> str: 27 | return base64.b64encode(uuid.uuid4().bytes, b"-_").decode("utf-8") 28 | # The following is more secure 29 | # return base64.b64encode(M2Crypto.m2.rand_bytes(num_bytes)) 30 | 31 | 32 | def wrap_callback(f): 33 | def wrapper(self: Store, *args): 34 | self.loop.call_soon_threadsafe(f, self, *args) 35 | 36 | return wrapper 37 | 38 | 39 | class Store(MantaComponent): 40 | """ 41 | Implements a Manta :term:`POS`. This class needs an *asyncio* loop 42 | to run correctly as some of its features are implemented as 43 | *coroutines*. 44 | 45 | Args: 46 | device_id: Device unique identifier (also called :term:`application_id`) 47 | associated with the :term:`POS` 48 | host: Hostname of the Manta broker 49 | client_options: A Dict of options to be passed to MQTT Client (like 50 | username, password) 51 | port: port of the Manta broker 52 | 53 | Attributes: 54 | acks: queue of :class:`~.messages.AckMessage` instances 55 | device_id: Device unique identifier (also called 56 | :term:`application_id`) associated with the :term:`POS` 57 | loop: the *asyncio* loop that manages the asynchronous parts of this 58 | object 59 | session_id: :term:`session_id` of the ongoing session, if any 60 | """ 61 | loop: asyncio.AbstractEventLoop 62 | connected: asyncio.Event 63 | device_id: str 64 | session_id: Optional[str] = None 65 | acks: asyncio.Queue 66 | first_connect = False 67 | subscriptions: List[str] = [] 68 | 69 | def __init__(self, device_id: str, host: str = "localhost", 70 | client_options: Dict = None, port: int = 1883): 71 | client_options = {} if client_options is None else client_options 72 | 73 | self.device_id = device_id 74 | self.host = host 75 | self.mqtt_client = mqtt.Client(**client_options) 76 | self.mqtt_client.on_connect = self.on_connect 77 | self.mqtt_client.on_message = self.on_message 78 | self.mqtt_client.on_disconnect = self.on_disconnect 79 | try: 80 | self.loop = asyncio.get_event_loop() 81 | except RuntimeError: 82 | self.loop = asyncio.new_event_loop() 83 | asyncio.set_event_loop(self.loop) 84 | 85 | self.acks = asyncio.Queue(loop=self.loop) 86 | self.connected = asyncio.Event(loop=self.loop) 87 | self.port = port 88 | 89 | def close(self): 90 | """Disconnect and stop :term:`MQTT` client's processing loop.""" 91 | self.mqtt_client.disconnect() 92 | self.mqtt_client.loop_stop() 93 | 94 | # noinspection PyUnusedLocal 95 | @wrap_callback 96 | def on_disconnect(self, client, userdata, rc): 97 | self.connected.clear() 98 | 99 | # noinspection PyUnusedLocal 100 | @wrap_callback 101 | def on_connect(self, client, userdata, flags, rc): 102 | logger.info("Connected") 103 | self.connected.set() 104 | 105 | # noinspection PyUnusedLocal 106 | @wrap_callback 107 | def on_message(self, client: mqtt.Client, userdata, msg): 108 | logger.info("Got {} on {}".format(msg.payload, msg.topic)) 109 | tokens = msg.topic.split('/') 110 | 111 | if tokens[0] == 'acks': 112 | session_id = tokens[1] 113 | logger.info("Got ack message") 114 | ack = AckMessage.from_json(msg.payload) 115 | self.acks.put_nowait(ack) 116 | 117 | def subscribe(self, topic: str): 118 | """ 119 | Subscribe to the given :term:`MQTT` topic. 120 | 121 | Args: 122 | topic: string containing the topic name. 123 | """ 124 | self.mqtt_client.subscribe(topic) 125 | self.subscriptions.append(topic) 126 | 127 | def clean(self): 128 | """ 129 | Clean the :class:`~.messages.AckMessage` queue and unsubscribe 130 | from any active :term:`MQTT` subscriptions. 131 | 132 | """ 133 | self.acks = asyncio.Queue() 134 | 135 | if len(self.subscriptions) > 0: 136 | self.mqtt_client.unsubscribe(self.subscriptions) 137 | 138 | self.subscriptions.clear() 139 | 140 | async def connect(self): 141 | """ 142 | Connect to the :term:`MQTT` broker and wait for the connection 143 | confirmation. 144 | 145 | This is a coroutine. 146 | """ 147 | if not self.first_connect: 148 | self.mqtt_client.connect(self.host, port=self.port) 149 | self.mqtt_client.loop_start() 150 | self.first_connect = True 151 | 152 | await self.connected.wait() 153 | 154 | async def merchant_order_request(self, amount: Decimal, fiat: str, 155 | crypto: str = None) -> AckMessage: 156 | """ 157 | Create a new Merchant Order and publish it to the 158 | :ref:`merchant_order_request/{application_id}` topic. Raises an 159 | exception if an :class:`~.messages.AckMessage` isn't received 160 | in less than 3 seconds. 161 | 162 | Args: 163 | amount: Fiat Amount requested 164 | fiat: Fiat Currency requested (ex. 'EUR') 165 | crypto: Crypto Currency requested (ex. 'NANO') 166 | Returns: 167 | ack message with status 'NEW' if confirmed by Payment Processor or 168 | Timeout Exception 169 | 170 | This is a coroutine. 171 | """ 172 | await self.connect() 173 | self.clean() 174 | self.session_id = generate_session_id() 175 | request = MerchantOrderRequestMessage( 176 | amount=amount, 177 | session_id=self.session_id, 178 | fiat_currency=fiat, 179 | crypto_currency=crypto 180 | ) 181 | 182 | self.subscribe("acks/{}".format(self.session_id)) 183 | self.mqtt_client.publish("merchant_order_request/{}".format(self.device_id), 184 | request.to_json()) 185 | 186 | logger.info("Publishing merchant_order_request for session {}".format(self.session_id)) 187 | 188 | result: AckMessage = await asyncio.wait_for(self.acks.get(), 3) 189 | 190 | if result.status != Status.NEW: 191 | raise Exception("Invalid ack") 192 | 193 | return result 194 | -------------------------------------------------------------------------------- /manta/testing/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from __future__ import annotations 7 | 8 | from contextlib import closing, contextmanager 9 | from decimal import Decimal 10 | import os 11 | import re 12 | import pathlib 13 | import socket 14 | import subprocess 15 | import sys 16 | import time 17 | from typing import (Any, Awaitable, Callable, Iterator, Optional, Sequence, 18 | Tuple, Union) 19 | 20 | import aiohttp.web 21 | import attr 22 | import file_config 23 | 24 | 25 | from ..base import MantaComponent 26 | 27 | 28 | def get_next_free_tcp_port(bind_address: str = 'localhost') -> int: 29 | """Return the next free TCP port on the ``bind_address`` interface. 30 | 31 | Args: 32 | bind_address: optional *name* or *IP address* of the interface where 33 | the free port need to be looked up. If not specified, ``localhost`` 34 | will be used 35 | Returns: 36 | The found port number 37 | """ 38 | with closing(socket.socket()) as sock: 39 | sock.bind((bind_address, 0)) 40 | return sock.getsockname()[1] 41 | 42 | 43 | def port_can_be_bound(port: int, bind_address: str = 'localhost') -> bool: 44 | """Returns true if a given port can be bound for listening. 45 | 46 | Args: 47 | port: the port to check 48 | bind_address: optional IP address of the interface where to bind the 49 | port to. If not specified, ``localhost`` will be used 50 | Returns 51 | A boolean that will be ``True`` if the port can be bound 52 | """ 53 | with closing(socket.socket()) as sock: 54 | try: 55 | sock.bind((bind_address, port)) 56 | result = True 57 | except OSError: 58 | result = False 59 | return result 60 | 61 | 62 | def start_program(args: Sequence[str], sentinel: bytes, 63 | log_stream: str = 'stderr', 64 | timeout: int = 5) -> Tuple[subprocess.Popen, bytearray]: 65 | """Start a program and check if it launched as expected. 66 | 67 | Args: 68 | args: a sequence of the program and its arguments 69 | sentinel: bytes string to lookup in the ``log_stream``. It can be 70 | a *regexp* 71 | log_stream: optional name of the stream where to look for the sentinel. 72 | Valid values are ``stdout`` and ``stderr``. if not specified 73 | ``stderr`` will be used 74 | timeout: optional amount of time to wait for the ``sentinel`` to 75 | be found in the ``log_stream`` 76 | Returns: 77 | a tuple with the following elements: ``(, )`` 78 | """ 79 | assert log_stream in ('stdout', 'stderr') 80 | stream_cfg = {log_stream: subprocess.PIPE} 81 | 82 | process = subprocess.Popen(args, **stream_cfg) # type: ignore 83 | 84 | stream = getattr(process, log_stream) 85 | if sys.platform in ('linux', 'darwin'): 86 | # set the O_NONBLOCK flag of p.stdout file descriptor 87 | # don't know what to do for the Windows platform 88 | from fcntl import fcntl, F_GETFL, F_SETFL 89 | flags = fcntl(stream, F_GETFL) 90 | fcntl(stream, F_SETFL, flags | os.O_NONBLOCK) 91 | 92 | out = bytearray() 93 | attempts = 0 94 | seconds = 0.2 95 | max_attempts = int(timeout) / seconds 96 | while attempts < max_attempts: 97 | attempts += 1 98 | time.sleep(seconds) 99 | 100 | try: 101 | o = os.read(stream.fileno(), 256) 102 | if o: 103 | out += o 104 | except BlockingIOError: 105 | pass 106 | 107 | if re.search(sentinel, out): 108 | break 109 | else: 110 | process.kill() 111 | raise RuntimeError( 112 | f'{args[0]} failed to start or startup detection' 113 | f'failed, after {timeout} seconds:\n' 114 | f'log_stream:\n{out.decode()}\n') 115 | return (process, out) 116 | 117 | 118 | @contextmanager 119 | def launch_program(args: Sequence[str], sentinel: bytes, 120 | log_stream: str = 'stderr', 121 | timeout: int = 5) -> Iterator: 122 | """Launch a program and check if it launched as expected. To be used in 123 | **with** statement expressions. Kills the subprocess at the end. 124 | 125 | Arguments are the same of :function:`start_program` 126 | 127 | Yields: 128 | a tuple with the following elements: ``(, )`` 129 | """ 130 | process, log = start_program(args, sentinel, log_stream, timeout) 131 | yield (process, log) 132 | process.kill() 133 | 134 | 135 | def msg2config(cls: type, suffix: str = 'Config') -> Any: 136 | """Convert a Message class coming from :module:`manta.messages` module to a 137 | config class.""" 138 | assert isinstance(cls, type) and hasattr(cls, '__attrs_attrs__'), \ 139 | 'Wrong class type' 140 | aattrs = getattr(cls, '__attrs_attrs__') 141 | 142 | def gen_var(att: attr.Attribute) -> attr.Attribute: 143 | t = att.type 144 | added_opts = {} 145 | # take the first not-None type if t is an Union type 146 | if getattr(t, '__origin__', None) is Union: 147 | for type_choice in t.__args__: # type: ignore 148 | # isinstance() cannot be used with typing.* 149 | if type_choice is not type(None): 150 | t = type_choice 151 | break 152 | else: 153 | raise ValueError(f'Cannot extrapolate valid type from {t!r}') 154 | # Setup a special encoder/decoder pair for Decimal type that 155 | # isn't supported directly by (py)YAML 156 | if t is Decimal: 157 | added_opts['encoder'] = lambda v: f'{v!s}' 158 | added_opts['decoder'] = lambda v: Decimal(v) # type: ignore 159 | if 'type' not in added_opts: 160 | added_opts['type'] = t # type: ignore 161 | return file_config.var(default=att.default, **added_opts) 162 | 163 | ns = {a.name: gen_var(a) for a in aattrs} 164 | return file_config.config(type(cls.__name__ + suffix, (), ns)) 165 | 166 | 167 | def config2msg(config_inst: Any, msg_cls: Any): 168 | """Convert a configuration instance to a specific message type.""" 169 | return msg_cls(**{a.name: getattr(config_inst, a.name) 170 | for a in msg_cls.__attrs_attrs__}) 171 | 172 | 173 | @attr.s(auto_attribs=True) 174 | class AppRunnerConfig: 175 | """Configuration exchanged between the configurator and an AppRunner.""" 176 | 177 | "callable used to ``start`` the Manta component" 178 | starter: Callable[[], Union[None, Awaitable, bool]] 179 | "callable used to ``stop`` the Manta component" 180 | stopper: Callable[[], Union[None, Awaitable]] 181 | "the configured Manta component" 182 | manta: Optional[MantaComponent] = None 183 | "``True`` if binding port reallocation is allowed" 184 | allow_port_reallocation: Optional[bool] = True 185 | "interface address where the listening port should be bound to" 186 | web_bind_address: Optional[str] = 'localhost' 187 | "which port number to bind" 188 | web_bind_port: Optional[int] = None 189 | """routes that the web server should allow""" 190 | web_routes: Optional[aiohttp.web_routedef.RouteTableDef] = None 191 | 192 | 193 | def get_tests_dir(): 194 | here = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) 195 | return here / '..' / '..' / 'tests' 196 | -------------------------------------------------------------------------------- /manta/testing/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import annotations 4 | 5 | from argparse import ArgumentParser, Namespace 6 | import asyncio 7 | from functools import partial 8 | import logging 9 | from typing import Callable, List, Optional 10 | 11 | from .broker import launch_mosquitto_from_config 12 | from . import config 13 | from .runner import AppRunner 14 | from .payproc import dummy_payproc 15 | from .store import dummy_store 16 | from .wallet import dummy_wallet 17 | 18 | logger = logging.getLogger('dummy-runner') 19 | 20 | 21 | def parse_cmdline(args: List = None) -> Namespace: 22 | parser = ArgumentParser(description="Run manta-python dummy services") 23 | parser.add_argument('-c', '--conf', help="path to the configuration file") 24 | parser.add_argument('--print-config', action='store_true', 25 | help="print a sample of the default configuration") 26 | return parser.parse_args(args) 27 | 28 | 29 | def config_parser_single(parser: ArgumentParser = None, 30 | comp_name: str = 'component') -> ArgumentParser: 31 | if parser is None: 32 | parser = ArgumentParser(description=f"Run manta-python dummy {comp_name}") 33 | parser.add_argument('-b', '--broker', default="localhost", 34 | help="MQTT broker hostname (default: 'localhost')") 35 | parser.add_argument('--broker-port', type=int, default=1883, 36 | help="MQTT broker port (default: 1883)") 37 | parser.add_argument('-p', '--web-port', type=int, 38 | help="enable web interface on the specified port") 39 | return parser 40 | 41 | 42 | def parse_store_cmdline(args: List = None) -> Namespace: 43 | parser = config_parser_single(comp_name='store') 44 | return parser.parse_args(args) 45 | 46 | 47 | def parse_payproc_cmdline(args: List = None) -> Namespace: 48 | parser = config_parser_single(comp_name='payment processor') 49 | parser.add_argument('-c', '--conf', 50 | help="path of a config file to load") 51 | parser.add_argument('--print-config', action='store_true', 52 | help="print a sample of the default configuration") 53 | return parser.parse_args(args) 54 | 55 | 56 | def parse_wallet_cmdline(args: List = None) -> Namespace: 57 | parser = config_parser_single(comp_name='wallet') 58 | parser.add_argument('--url', help="Manta-URL of the payment session to join" 59 | " and pay. It will automatically end the session when" 60 | " payment is completed") 61 | parser.add_argument('-i', '--interactive', action='store_true', 62 | help="enable interactive payment interface (default: False)") 63 | parser.add_argument('-w', '--wallet', 64 | help="hash of the nano wallet to use") 65 | parser.add_argument('-a', '--account', help="hash of the nano account") 66 | parser.add_argument('--cert', help="CA certificate used to validate the " 67 | "payment session") 68 | parser.add_argument('-c', '--conf', 69 | help="path of a config file to load") 70 | parser.add_argument('--print-config', action='store_true', 71 | help="print a sample of the default configuration") 72 | return parser.parse_args(args) 73 | 74 | 75 | def check_args(parsed_args: Namespace) -> Optional[config.IntegrationConfig]: 76 | if parsed_args.print_config: 77 | print(config.get_full_config(enable_web=True).dumps_yaml()) 78 | else: 79 | if not parsed_args.conf: 80 | print('A path to a configuration file is mandatory.') 81 | exit(1) 82 | cfg = config.read_config_file(parsed_args.conf) 83 | if cfg.broker is None: 84 | print('A "broker" section is mandatory.') 85 | exit(1) 86 | return cfg 87 | return None 88 | 89 | 90 | def check_args_store(parsed_args: Namespace) -> Optional[config.IntegrationConfig]: 91 | web_enable = parsed_args.web_port is not None 92 | return config.IntegrationConfig( # type: ignore 93 | broker=config.BrokerConfig( # type: ignore 94 | allow_port_reallocation=False, 95 | start=False, 96 | host=parsed_args.broker, 97 | port=parsed_args.broker_port), 98 | store=config.DummyStoreConfig( # type: ignore 99 | web=config.DummyStoreConfig.StoreWebConfig( # type: ignore 100 | enable=web_enable, 101 | bind_port=parsed_args.web_port, 102 | allow_port_reallocation=False 103 | ) 104 | ) 105 | ) 106 | 107 | 108 | def check_args_payproc(parsed_args: Namespace) -> Optional[config.IntegrationConfig]: 109 | web_enable = parsed_args.web_port is not None 110 | if parsed_args.print_config: 111 | print(config.get_default_dummypayproc_config().dumps_yaml()) 112 | return None 113 | if parsed_args.conf is None: 114 | pp_conf = config.get_default_dummypayproc_config() 115 | else: 116 | pp_conf = config.read_payproc_config_file(parsed_args.conf) 117 | pp_conf.web = config.DummyPayProcConfig.PayProcWebConfig( # type: ignore 118 | enable=web_enable, 119 | bind_port=parsed_args.web_port, 120 | allow_port_reallocation=False 121 | ) 122 | return config.IntegrationConfig( # type: ignore 123 | broker=config.BrokerConfig( # type: ignore 124 | allow_port_reallocation=False, 125 | start=False, 126 | host=parsed_args.broker, 127 | port=parsed_args.broker_port), 128 | payproc=pp_conf 129 | ) 130 | 131 | 132 | def check_args_wallet(parsed_args: Namespace) -> Optional[config.IntegrationConfig]: 133 | web_enable = parsed_args.web_port is not None 134 | if parsed_args.print_config: 135 | print(config.DummyWalletConfig( # type: ignore 136 | account="changeme", certificate="changeme", 137 | wallet="changeme").dumps_yaml()) 138 | return None 139 | if parsed_args.conf is None: 140 | wall_conf = config.DummyWalletConfig() 141 | else: 142 | wall_conf = config.read_wallet_config_file(parsed_args.conf) 143 | wall_conf.web = config.DummyWalletConfig.WalletWebConfig( # type: ignore 144 | enable=web_enable, 145 | bind_port=parsed_args.web_port, 146 | allow_port_reallocation=False 147 | ) 148 | if parsed_args.account: 149 | wall_conf.account = parsed_args.account 150 | if parsed_args.cert: 151 | wall_conf.certificate = parsed_args.cert 152 | if parsed_args.wallet: 153 | wall_conf.wallet = parsed_args.wallet 154 | if parsed_args.interactive: 155 | wall_conf.interactive = parsed_args.interactive 156 | if parsed_args.url: 157 | wall_conf.url = parsed_args.url 158 | return config.IntegrationConfig( # type: ignore 159 | broker=config.BrokerConfig( # type: ignore 160 | allow_port_reallocation=False, 161 | start=False, 162 | host=parsed_args.broker, 163 | port=parsed_args.broker_port), 164 | wallet=wall_conf 165 | ) 166 | 167 | 168 | def init_logging(level=logging.INFO): 169 | logging.basicConfig(level=level) 170 | 171 | 172 | def service(name: str, configurator: Callable, config: config.IntegrationConfig): 173 | svc_config = getattr(config, name) 174 | if svc_config is None: 175 | return partial(null_starter, name), partial(null_stopper, name) 176 | else: 177 | return AppRunner.start_stop(configurator, config, name) 178 | 179 | 180 | def run_services(config: config.IntegrationConfig, loop=None): 181 | if loop is None: 182 | loop = asyncio.get_event_loop() 183 | with launch_mosquitto_from_config(config.broker, read_log=True) as broker: 184 | subproc, host, port, log = broker 185 | # merge possible port reallocation 186 | config.broker.port = port 187 | stoppers = [] 188 | started: List[bool] = [] 189 | for sname, configurator in (('payproc', dummy_payproc), 190 | ('store', dummy_store), 191 | ('wallet', dummy_wallet)): 192 | start, stop = service(sname, configurator, config) 193 | res = loop.run_until_complete(start()) 194 | # real (non null_*) starters always return a boolean 195 | if isinstance(res, bool): 196 | started += [res] 197 | stoppers.append(stop()) 198 | logger.info("Configured services are now running.") 199 | logger.info("==================== Hit CTRL-C to stop " 200 | "====================") 201 | try: 202 | # if res is True the service requested exit after single 203 | # operation 204 | if not (len(started) == 1 and started[0] is True): 205 | loop.run_forever() 206 | except KeyboardInterrupt: 207 | pass 208 | finally: 209 | logger.info("==================== Shutting down " 210 | "====================") 211 | loop.run_until_complete(asyncio.wait(stoppers, loop=loop)) 212 | loop.close() 213 | 214 | 215 | def main(args=None, log_level=logging.INFO, check_function: Callable = None, 216 | parse_function: Callable = None): 217 | if check_function is None: 218 | check_function = check_args 219 | if parse_function is None: 220 | parse_function = parse_cmdline 221 | config = check_function(parse_function(args)) 222 | if config is not None: 223 | init_logging(log_level) 224 | run_services(config) 225 | 226 | 227 | store_main = partial(main, check_function=check_args_store, 228 | parse_function=parse_store_cmdline) 229 | payproc_main = partial(main, check_function=check_args_payproc, 230 | parse_function=parse_payproc_cmdline) 231 | wallet_main = partial(main, check_function=check_args_wallet, 232 | parse_function=parse_wallet_cmdline) 233 | 234 | 235 | async def null_starter(name: str): 236 | logger.info("Not starting service %r", name) 237 | 238 | 239 | async def null_stopper(name: str): 240 | pass 241 | 242 | if __name__ == '__main__': 243 | main() 244 | -------------------------------------------------------------------------------- /manta/testing/broker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from contextlib import contextmanager 7 | import logging 8 | import tempfile 9 | import threading 10 | from typing import Optional 11 | 12 | from . import get_next_free_tcp_port, launch_program, port_can_be_bound 13 | from .config import BrokerConfig 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | DEFAULT_MQTT_PORT = 1883 18 | 19 | MOSQUITTO_CONF = """\ 20 | bind_address {} 21 | port {} 22 | persistence false 23 | """ 24 | 25 | 26 | @contextmanager 27 | def launch_mosquitto(bind_address: str = 'localhost', 28 | bind_port: Optional[int] = None, 29 | exec_path: Optional[str] = None, 30 | allow_port_reallocation: bool = True): 31 | """Launch mosquitto executable, providing it a minimal configuration useful 32 | for testing purposes. 33 | 34 | It's intended to be used in a ``with`` statement 35 | 36 | Args: 37 | bind_address: optional interface address 38 | bind_port: optional listening port. If none is specified, one will be 39 | allocated 40 | exec_path: optional path to the mosquitto executable 41 | Yields 42 | A tuple containing four elements:: 43 | 44 | (, , , ) 45 | """ 46 | if bind_port is None: 47 | bind_port = DEFAULT_MQTT_PORT 48 | if not port_can_be_bound(bind_port, bind_address) and allow_port_reallocation: 49 | bind_port = get_next_free_tcp_port(bind_address) 50 | elif (not port_can_be_bound(bind_port, bind_address) 51 | and not allow_port_reallocation): 52 | raise RuntimeError(f"Port '{bind_port}' cannot be bound on {bind_address}") 53 | if exec_path is None: 54 | exec_path = 'mosquitto' 55 | 56 | with tempfile.NamedTemporaryFile(suffix='.conf') as config: 57 | config.write(MOSQUITTO_CONF.format(bind_address, bind_port).encode('utf-8')) 58 | config.flush() 59 | 60 | with launch_program([exec_path, '-c', config.name], 61 | b'mosquitto version.*starting') as daemon: 62 | process, out = daemon 63 | yield (process, bind_address, bind_port, out) 64 | 65 | 66 | @contextmanager 67 | def launch_mosquitto_from_config(cfg: BrokerConfig, read_log=False): 68 | """Given a configuration instance, start a broker process. 69 | 70 | Args: 71 | cfg: a configuration instance 72 | Yields 73 | A tuple containing four elements if :any:`cfg.start` is True:: 74 | 75 | (, , , ) 76 | """ 77 | if cfg.start: 78 | with launch_mosquitto( 79 | bind_address=cfg.host, bind_port=cfg.port, exec_path=cfg.path, 80 | allow_port_reallocation=cfg.allow_port_reallocation) as mos: 81 | logger.info("Started Mosquitto broker on interface %r and port" 82 | " %r.", mos[1], mos[2]) 83 | if read_log: 84 | mos_log = logging.getLogger('mosquitto') 85 | read_event = threading.Event() 86 | 87 | def run(event): 88 | """Read moquitto stderr in a non-blocking fashon.""" 89 | proc = mos[0] 90 | while not event.wait(0.3): 91 | log = b'' 92 | while True: 93 | read_log = proc.stderr.read(256) 94 | if read_log is None or len(read_log) == 0: 95 | break 96 | else: 97 | log += read_log 98 | if len(log) > 0: 99 | lines = log.splitlines() 100 | for l in lines: 101 | mos_log.info(l.decode('utf-8')) 102 | 103 | log_thread = threading.Thread(target=run, args=(read_event,)) 104 | log_thread.start() 105 | try: 106 | yield mos 107 | finally: 108 | if read_log: 109 | read_event.set() 110 | log_thread.join() 111 | logger.info("Stopped Mosquitto broker") 112 | else: 113 | logger.info("Not starting Mosquitto broker. Services will connect to " 114 | "services on host %r and port %r", cfg.host, cfg.port) 115 | yield (None, cfg.host, cfg.port, None) 116 | -------------------------------------------------------------------------------- /manta/testing/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from decimal import Decimal 7 | import io 8 | from typing import List 9 | 10 | from file_config import config, var 11 | 12 | from ..messages import Destination, Merchant 13 | from . import msg2config, get_tests_dir 14 | 15 | 16 | @config 17 | class BrokerConfig: 18 | """Basic broker configuration.""" 19 | 20 | "allow listening port reallocation" 21 | allow_port_reallocation = var(bool, default=True, required=False) 22 | "start a new broker if true" 23 | start = var(bool, default=True) 24 | "broker listening host/interface" 25 | host = var(str, default='localhost', required=False) 26 | path = var(str, default=None, required=False) 27 | "broker listening port" 28 | port = var(int, default=1883, required=False) 29 | 30 | 31 | @config 32 | class WebConfig: 33 | 34 | enable = var(bool, default=False) 35 | allow_port_reallocation = var(bool, default=True, 36 | required=False) 37 | bind_address = var(str, default='localhost', required=False) 38 | bind_port = var(int, default=None, required=False) 39 | 40 | 41 | _nano_dest = ("xrb_3d1ab61eswzsgx5arwqc3gw8xjcsxd7a5egtr69jixa5i" 42 | "t9yu9fzct9nyjyx") 43 | 44 | 45 | @config 46 | class DummyPayProcConfig: 47 | """Configuration Type for the dummy PayProc component.""" 48 | 49 | DestinationConfig = msg2config(Destination) 50 | MerchantConfig = msg2config(Merchant) 51 | 52 | @config 53 | class PayProcWebConfig(WebConfig): 54 | 55 | bind_port = var(int, default=8081, required=False) 56 | 57 | supported_cryptos = var(List[str], unique=True, min=1) 58 | destinations = var(List[DestinationConfig], min=1) # type: ignore 59 | keyfile = var(str) 60 | certfile = var(str) 61 | merchant = var(MerchantConfig) 62 | web = var(PayProcWebConfig, default=PayProcWebConfig(), required=False) 63 | 64 | 65 | @config 66 | class DummyStoreConfig: 67 | """Configuration type for the dummy Store component.""" 68 | 69 | @config 70 | class StoreWebConfig(WebConfig): 71 | 72 | bind_port = var(int, default=8080, required=False) 73 | 74 | web = var(StoreWebConfig, default=StoreWebConfig(), required=False) 75 | 76 | 77 | @config 78 | class DummyWalletConfig: 79 | 80 | @config 81 | class WalletWebConfig(WebConfig): 82 | 83 | bind_port = var(int, default=8082, required=False) 84 | 85 | account = var(str, required=False) 86 | wallet = var(str, required=False) 87 | certificate = var(str, required=False) 88 | interactive = var(bool, default=False, required=False) 89 | url = var(str, required=False) 90 | web = var(WalletWebConfig, default=WalletWebConfig(), required=False) 91 | 92 | 93 | @config 94 | class IntegrationConfig: 95 | """Config type for the integration utilities.""" 96 | 97 | broker = var(BrokerConfig) 98 | payproc = var(DummyPayProcConfig, required=False) 99 | store = var(DummyStoreConfig, required=False) 100 | wallet = var(DummyWalletConfig, required=False) 101 | 102 | 103 | def get_default_dummypayproc_config(): 104 | return DummyPayProcConfig( 105 | supported_cryptos=['btc', 'xmr', 'nano'], 106 | destinations=[DummyPayProcConfig.DestinationConfig( 107 | destination_address=_nano_dest, 108 | crypto_currency='NANO', 109 | amount=Decimal("0.01"))], 110 | keyfile=str(get_tests_dir() / 111 | 'certificates/root/keys/' 112 | 'test.key'), 113 | certfile=str(get_tests_dir() / 114 | 'certificates/root/certs/' 115 | 'test.crt'), 116 | merchant=DummyPayProcConfig.MerchantConfig( 117 | name='Merchant 1', 118 | address='5th Avenue')) 119 | 120 | 121 | def get_full_config(enable_web=False): 122 | config = IntegrationConfig( 123 | broker=BrokerConfig(), 124 | payproc=get_default_dummypayproc_config(), 125 | store=DummyStoreConfig(), 126 | wallet=DummyWalletConfig()) 127 | if enable_web: 128 | config.payproc.web.enable = True 129 | config.store.web.enable = True 130 | config.wallet.web.enable = True 131 | return config 132 | 133 | 134 | def read_config_file(path: str) -> IntegrationConfig: 135 | """Read a file containing a configuration and return a config object.""" 136 | return IntegrationConfig.load_yaml(io.open(path, encoding='utf-8')) # type: ignore 137 | 138 | 139 | def read_payproc_config_file(path: str) -> DummyPayProcConfig: 140 | """Read a file containing a configuration and return a config object.""" 141 | return DummyPayProcConfig.load_yaml(io.open(path, encoding='utf-8')) # type: ignore 142 | 143 | 144 | def read_wallet_config_file(path: str) -> DummyWalletConfig: 145 | """Read a file containing a configuration and return a config object.""" 146 | return DummyWalletConfig.load_yaml(io.open(path, encoding='utf-8')) # type: ignore 147 | -------------------------------------------------------------------------------- /manta/testing/payproc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from functools import partial 7 | import logging 8 | from typing import List 9 | 10 | import aiohttp 11 | 12 | from ..messages import MerchantOrderRequestMessage, Destination, Merchant 13 | from ..payproc import PayProc 14 | from . import AppRunnerConfig, config2msg 15 | from .runner import AppRunner 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | def dummy_payproc(runner: AppRunner) -> AppRunnerConfig: 21 | assert runner.app_config is not None 22 | assert runner.app_config.payproc is not None 23 | 24 | from .config import DummyPayProcConfig 25 | 26 | cfg: DummyPayProcConfig = runner.app_config.payproc 27 | 28 | pp = PayProc(cfg.keyfile, cert_file=cfg.certfile, host=runner.app_config.broker.host, 29 | port=runner.app_config.broker.port) 30 | merchant = config2msg(cfg.merchant, Merchant) 31 | destinations = [config2msg(d, Destination) for d in cfg.destinations] 32 | cryptos = set(cfg.supported_cryptos) 33 | pp.get_merchant = lambda x: merchant 34 | pp.get_destinations = partial(_get_destinations, destinations) 35 | pp.get_supported_cryptos = lambda device, payment_request: cryptos 36 | 37 | if cfg.web is not None and cfg.web.enable: 38 | routes = aiohttp.web.RouteTableDef() 39 | 40 | @routes.post("/confirm") 41 | async def merchant_order(request: aiohttp.web.Request): 42 | try: 43 | logger.info("Got confirm request") 44 | json = await request.json() 45 | pp.confirm(json['session_id']) 46 | 47 | return aiohttp.web.json_response("ok") 48 | 49 | except Exception: 50 | raise aiohttp.web.HTTPInternalServerError() 51 | 52 | more_params = dict(web_routes=routes, 53 | allow_port_reallocation=cfg.web.allow_port_reallocation, 54 | web_bind_address=cfg.web.bind_address, 55 | web_bind_port=cfg.web.bind_port) 56 | else: 57 | more_params = {} 58 | 59 | def starter() -> bool: 60 | pp.run() 61 | return False 62 | 63 | def stopper() -> None: 64 | pp.mqtt_client.loop_stop() 65 | 66 | return AppRunnerConfig(manta=pp, starter=starter, stopper=stopper, 67 | **more_params) 68 | 69 | 70 | def _get_destinations(destinations: List[Destination], application_id, 71 | merchant_order: MerchantOrderRequestMessage): 72 | if merchant_order.crypto_currency: 73 | destination = next(x for x in destinations 74 | if x.crypto_currency == merchant_order.crypto_currency) 75 | return [destination] 76 | else: 77 | return destinations 78 | -------------------------------------------------------------------------------- /manta/testing/runner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from __future__ import annotations 7 | 8 | import asyncio 9 | import inspect 10 | import logging 11 | import threading 12 | from typing import Awaitable, Callable, Dict, Optional, Tuple, Union 13 | 14 | import aiohttp.web 15 | 16 | from ..base import MantaComponent 17 | from . import AppRunnerConfig, get_next_free_tcp_port, port_can_be_bound 18 | from .config import IntegrationConfig 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class AppRunner: 24 | """A tool for running the testing/demo components.""" 25 | 26 | "configuration for all the componentss" 27 | app_config: IntegrationConfig 28 | loop: asyncio.AbstractEventLoop 29 | "manta component returned by the configurator" 30 | manta: Optional[MantaComponent] = None 31 | thread: Optional[threading.Thread] = None 32 | "AIOHTTP instance" 33 | web: Optional[aiohttp.web.Application] = None 34 | "IP address of the interface of the listening HTTP port" 35 | web_bind_address: Optional[str] = None 36 | "port of HTTP listening socket" 37 | web_bind_port: Optional[int] = None 38 | url: Optional[str] = None 39 | 40 | @classmethod 41 | def start_stop(cls, configurator: Callable[[AppRunner], 42 | AppRunnerConfig], 43 | app_config: IntegrationConfig, 44 | name: str = 'unknown'): 45 | runner = cls(configurator, app_config) 46 | 47 | async def start(): 48 | res = await runner.start() 49 | if runner.web is None: 50 | logger.info("Started service %s", name) 51 | else: 52 | logger.info("Started service %s on address %r and port %r", 53 | name, runner.web_bind_address, 54 | runner.web_bind_port) 55 | return res 56 | 57 | async def stop(): 58 | res = await runner.stop() 59 | logger.info("Stopped service %s", name) 60 | return res 61 | 62 | return start, stop 63 | 64 | def __init__(self, configurator: Callable[[AppRunner], 65 | AppRunnerConfig], 66 | app_config: IntegrationConfig): 67 | 68 | "callable used to configure the application" 69 | self.configurator: Callable[[AppRunner], AppRunnerConfig] = configurator 70 | self.app_config = app_config 71 | 72 | "the callable used to start the manta app" 73 | self.starter: Optional[Callable[[], Union[None, Awaitable, bool]]] = None 74 | "the callable used to stop the manta app" 75 | self.stopper: Optional[Callable[[], Union[None, Awaitable]]] = None 76 | 77 | def _init_app(self) -> None: 78 | run_config = self.configurator(self) 79 | self.manta = run_config.manta 80 | self.starter = run_config.starter 81 | self.stopper = run_config.stopper 82 | if run_config.web_routes is not None: 83 | assert isinstance(run_config.web_bind_address, str) 84 | if run_config.web_bind_port is not None \ 85 | and not port_can_be_bound(run_config.web_bind_port, 86 | run_config.web_bind_address) \ 87 | and run_config.allow_port_reallocation: 88 | run_config.web_bind_port = None 89 | if run_config.web_bind_port is None: 90 | if run_config.allow_port_reallocation: 91 | run_config.web_bind_port = get_next_free_tcp_port( 92 | run_config.web_bind_address) 93 | else: 94 | RuntimeError(f'Cannot bind port {run_config.web_bind_port}') 95 | web = aiohttp.web.Application() 96 | web.add_routes(run_config.web_routes) 97 | self.web = web 98 | self.web_bind_address = run_config.web_bind_address 99 | self.web_bind_port = run_config.web_bind_port 100 | self.url = f'http://{self.web_bind_address}:{self.web_bind_port}' 101 | else: 102 | self.web = None 103 | self.web_bind_address = None 104 | self.web_bind_port = None 105 | self.url = None 106 | 107 | def _run(self, *args, **kwargs) -> None: 108 | started = False 109 | try: 110 | loop = self.loop = _get_event_loop() 111 | self.loop.run_until_complete(self._start(args=args, kwargs=kwargs)) 112 | started = True 113 | self.loop.run_forever() 114 | self.loop.stop() 115 | finally: 116 | if started: 117 | self.loop.run_until_complete(self._stop()) 118 | loop.close() 119 | 120 | async def _start(self, *, new_thread: bool = False, args: Tuple = None, 121 | kwargs: Dict = None): 122 | if args is None: 123 | args = () 124 | if kwargs is None: 125 | kwargs = {} 126 | self._init_app() 127 | # setup manta/mqtt 128 | assert callable(self.starter) 129 | # TODO: fix the type for proxy invocation 130 | start_res = self.starter(*args, **kwargs) # type: ignore 131 | if inspect.isawaitable(start_res): 132 | assert start_res is not None # help mypy 133 | assert not isinstance(start_res, bool) # help mypy 134 | start_res = await start_res 135 | # setup aiohttp 136 | if self.web is not None: 137 | assert isinstance(self.web, aiohttp.web.Application) 138 | self._runner = aiohttp.web.AppRunner(self.web, 139 | handle_signals=not new_thread) 140 | await self._runner.setup() 141 | self._site = aiohttp.web.TCPSite(self._runner, 142 | self.web_bind_address, 143 | self.web_bind_port) 144 | await self._site.start() 145 | return start_res 146 | 147 | async def _stop(self): 148 | # tear down aiohttp 149 | if self.web is not None: 150 | await self._site.stop() 151 | await self._runner.cleanup() 152 | # tear down manta/mqtt 153 | assert callable(self.stopper) 154 | stop_res = self.stopper() 155 | if inspect.isawaitable(stop_res): 156 | await stop_res 157 | 158 | def _stop_thread(self) -> None: 159 | self.loop.stop() 160 | 161 | def start(self, *args, new_thread: bool = False, 162 | **kwargs) -> Union[None, Awaitable]: 163 | "Start the manta application." 164 | if self.thread is None: 165 | if new_thread: 166 | self.thread = threading.Thread(target=self._run, args=args, 167 | kwargs=kwargs) 168 | self.thread.start() 169 | else: 170 | return self._start(args=args, kwargs=kwargs) 171 | return None 172 | 173 | def stop(self) -> None: 174 | if self.thread is None: 175 | return self._stop() 176 | else: 177 | self.loop.call_soon_threadsafe(self._stop_thread) 178 | self.thread.join() 179 | self.thread = None 180 | 181 | 182 | def _get_event_loop(): 183 | try: 184 | loop = asyncio.get_event_loop() 185 | except RuntimeError: 186 | loop = asyncio.new_event_loop() 187 | asyncio.set_event_loop(loop) 188 | return loop 189 | 190 | 191 | def run_event_loop(): 192 | loop = _get_event_loop() 193 | 194 | async def noop(): 195 | print("======== Running on ========\n" 196 | "(Press CTRL+C to quit)") 197 | while True: 198 | await asyncio.sleep(3600) 199 | 200 | loop.run_until_complete(noop) 201 | -------------------------------------------------------------------------------- /manta/testing/store.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from __future__ import annotations 7 | 8 | import logging 9 | from threading import Timer 10 | from typing import Awaitable 11 | 12 | import aiohttp 13 | import paho.mqtt.client as mqtt 14 | 15 | from ..store import Store 16 | from . import AppRunnerConfig 17 | from .runner import AppRunner 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | def echo() -> mqtt.Client: 23 | """Returns an MQTT client configured as store echo.""" 24 | 25 | import time 26 | 27 | from ..messages import AckMessage, Status 28 | 29 | WAITING_INTERVAL = 4 30 | 31 | def on_connect(client: mqtt.Client, userdata, flags, rc): 32 | logger.info("Connected") 33 | client.subscribe("acks/#") 34 | 35 | def on_message(client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): 36 | ack: AckMessage = AckMessage.from_json(msg.payload) 37 | tokens = msg.topic.split('/') 38 | session_id = tokens[1] 39 | 40 | if ack.status == Status.NEW: 41 | new_ack = AckMessage(txid=ack.txid, 42 | status=Status.PENDING, 43 | transaction_currency="NANO", 44 | transaction_hash="B50DB45966850AD4B11ECFDD8A" 45 | "E0A7AD97DF74864631D8C1495E46DDDFC1802A") 46 | logging.info("Waiting...") 47 | 48 | time.sleep(WAITING_INTERVAL) 49 | 50 | logging.info("Sending first ack...") 51 | 52 | client.publish("acks/{}".format(session_id), new_ack.to_json()) 53 | 54 | new_ack.status = Status.PAID 55 | 56 | logging.info("Waiting...") 57 | 58 | def pub_second_ack(): 59 | logging.info("Sending second ack...") 60 | client.publish("acks/{}".format(session_id), new_ack.to_json()) 61 | 62 | t = Timer(WAITING_INTERVAL, pub_second_ack) 63 | t.start() 64 | 65 | client = mqtt.Client(protocol=mqtt.MQTTv31) 66 | 67 | client.on_connect = on_connect 68 | client.on_message = on_message 69 | return client 70 | 71 | 72 | def dummy_store(runner: AppRunner) -> AppRunnerConfig: 73 | from decimal import Decimal 74 | 75 | assert runner.app_config is not None 76 | assert runner.app_config.store is not None 77 | 78 | from .config import DummyStoreConfig 79 | 80 | cfg: DummyStoreConfig = runner.app_config.store 81 | store = Store('dummy_store', host=runner.app_config.broker.host, 82 | port=runner.app_config.broker.port) 83 | 84 | if cfg.web is not None and cfg.web.enable: 85 | routes = aiohttp.web.RouteTableDef() 86 | 87 | @routes.post("/merchant_order") 88 | async def merchant_order(request: aiohttp.web.Request): 89 | try: 90 | json = await request.json() 91 | logger.info("New http requets: %s" % json) 92 | json['amount'] = Decimal(json['amount']) 93 | 94 | reply = await store.merchant_order_request(**json) 95 | 96 | return aiohttp.web.Response(body=reply.to_json(), 97 | content_type="application/json") 98 | 99 | except Exception: 100 | logger.exception("Error during '/merchant_order' web endpoint") 101 | raise aiohttp.web.HTTPInternalServerError() 102 | 103 | more_params = dict(web_routes=routes, 104 | allow_port_reallocation=cfg.web.allow_port_reallocation, 105 | web_bind_address=cfg.web.bind_address, 106 | web_bind_port=cfg.web.bind_port) 107 | else: 108 | more_params = {} 109 | 110 | async def starter() -> bool: 111 | await store.connect() 112 | return False 113 | 114 | def stopper() -> None: 115 | store.mqtt_client.loop_stop() 116 | 117 | return AppRunnerConfig(manta=store, starter=starter, stopper=stopper, 118 | **more_params) 119 | -------------------------------------------------------------------------------- /manta/testing/wallet.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from __future__ import annotations 7 | 8 | from asyncio import TimeoutError 9 | import logging 10 | import sys 11 | 12 | import aiohttp 13 | from cryptography import x509 14 | from cryptography.x509 import NameOID 15 | import inquirer 16 | import nano 17 | 18 | from ..messages import (verify_chain, Destination, 19 | PaymentRequestEnvelope, PaymentRequestMessage) 20 | from ..wallet import Wallet 21 | from . import AppRunnerConfig 22 | from .runner import AppRunner 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def dummy_wallet(runner: AppRunner) -> AppRunnerConfig: 28 | assert runner.app_config is not None 29 | assert runner.app_config.wallet is not None 30 | 31 | from .config import DummyWalletConfig 32 | 33 | cfg: DummyWalletConfig = runner.app_config.wallet 34 | if cfg.web is not None and cfg.web.enable: 35 | routes = aiohttp.web.RouteTableDef() 36 | 37 | @routes.post("/scan") 38 | async def scan(request: aiohttp.web.Request): 39 | try: 40 | json = await request.json() 41 | logger.info("Got scan request for {}".format(json['url'])) 42 | await pay(json['url']) 43 | 44 | return aiohttp.web.json_response("ok") 45 | 46 | except Exception: 47 | logger.exception("Error while executing '/scan' web endpoint") 48 | raise aiohttp.web.HTTPInternalServerError() 49 | 50 | more_params = dict(web_routes=routes, 51 | allow_port_reallocation=cfg.web.allow_port_reallocation, 52 | web_bind_address=cfg.web.bind_address, 53 | web_bind_port=cfg.web.bind_port) 54 | else: 55 | more_params = {} 56 | 57 | async def starter(): 58 | nonlocal runner, cfg 59 | runner.pay = pay 60 | if cfg.url is not None: 61 | await pay(cfg.url, once=True) 62 | return True # inform the runner that we want it to stop 63 | 64 | def pay(url: str, *args, **kwargs): 65 | nonlocal runner, cfg 66 | wallet = Wallet.factory(url) 67 | runner.manta = wallet 68 | kwargs['wallet'] = wallet 69 | kwargs.setdefault('interactive', cfg.interactive) 70 | kwargs.setdefault('account', cfg.account) 71 | kwargs.setdefault('ca_certificate', cfg.certificate) 72 | kwargs.setdefault('nano_wallet', cfg.wallet) 73 | return _get_payment(*args, **kwargs) 74 | 75 | def stopper(): 76 | if isinstance(runner.manta, Wallet): 77 | runner.manta.mqtt_client.loop_stop() 78 | 79 | return AppRunnerConfig(starter=starter, # type: ignore 80 | stopper=stopper, **more_params) 81 | 82 | 83 | async def _get_payment(url: str = None, 84 | interactive: bool = False, 85 | nano_wallet: str = None, 86 | account: str = None, 87 | ca_certificate: str = None, 88 | wallet: Wallet = None, 89 | once: bool = False): 90 | 91 | if wallet is None: 92 | assert url is not None 93 | wallet = Wallet.factory(url) 94 | 95 | assert wallet is not None 96 | try: 97 | envelope = await _get_payment_request(wallet, once=once) 98 | except TimeoutError as e: 99 | if once: 100 | return 101 | else: 102 | raise e 103 | 104 | certificate: x509.Certificate = None 105 | 106 | verified = False 107 | 108 | if ca_certificate: 109 | certificate = await wallet.get_certificate() 110 | verified = _verify_envelope(envelope, certificate, ca_certificate) 111 | 112 | payment_req = envelope.unpack() 113 | logger.info("Payment request: {}".format(payment_req)) 114 | 115 | if interactive: 116 | await _interactive_payment(wallet, payment_req, nano_wallet=nano_wallet, 117 | account=account, certificate=certificate, 118 | ca_certificate=ca_certificate, once=once, 119 | verified=verified) 120 | else: 121 | await wallet.send_payment("myhash", 122 | payment_req.destinations[0].crypto_currency) 123 | 124 | ack = await wallet.acks.get() 125 | print(ack) 126 | 127 | 128 | async def _get_payment_request(wallet: Wallet, crypto_currency: str = 'all', 129 | once: bool = False) -> PaymentRequestEnvelope: 130 | try: 131 | envelope = await wallet.get_payment_request(crypto_currency) 132 | except TimeoutError as e: 133 | logger.error("Timeout exception in waiting for payment") 134 | raise e 135 | return envelope 136 | 137 | 138 | async def _interactive_payment(wallet: Wallet, payment_req: PaymentRequestMessage, 139 | nano_wallet: str = None, account: str = None, 140 | certificate: x509.Certificate = None, 141 | ca_certificate: str = None, 142 | once: bool = False, 143 | verified: bool = False): 144 | """Pay a ``PaymentRequestMessage`` interactively asking questions on 145 | ``sys.stdout`` and reading answers on ``sys.stdin``.""" 146 | 147 | options = [x for x in payment_req.supported_cryptos] 148 | questions = [inquirer.List('crypto', 149 | message=' What crypto you want to pay with?', 150 | choices=options)] 151 | answers = inquirer.prompt(questions) 152 | chosen_crypto = answers['crypto'] 153 | 154 | # Check if we have already the destination 155 | destination = payment_req.get_destination(chosen_crypto) 156 | 157 | # Otherwise ask payment provider 158 | if not destination: 159 | logger.info('Requesting payment request for {}'.format(chosen_crypto)) 160 | try: 161 | envelope = await _get_payment_request(wallet, chosen_crypto, once=once) 162 | except TimeoutError as e: 163 | if once: 164 | return 165 | else: 166 | raise e 167 | 168 | verified = False 169 | 170 | if ca_certificate: 171 | verified = _verify_envelope(envelope, certificate, 172 | ca_certificate) 173 | 174 | # Double unpack, why? 175 | payment_req = envelope.unpack() 176 | logger.info("Payment request: {}".format(payment_req)) 177 | destination = payment_req.get_destination(chosen_crypto) 178 | 179 | if answers['crypto'] == 'NANO': 180 | rpc = nano.rpc.Client(host="http://localhost:7076") 181 | balance = rpc.account_balance(account=account) 182 | print() 183 | print("Actual balance: {}".format( 184 | str(nano.convert(from_unit="raw", to_unit="XRB", 185 | value=balance['balance'])) 186 | )) 187 | 188 | if not verified: 189 | print("WARNING!!!! THIS IS NOT VERIFIED REQUEST") 190 | 191 | destination = payment_req.get_destination('NANO') 192 | 193 | assert isinstance(destination, Destination) 194 | if _query_yes_no("Pay {} {} ({} {}) to {}".format( 195 | destination.amount, destination.crypto_currency, 196 | payment_req.amount, payment_req.fiat_currency, 197 | payment_req.merchant)): 198 | amount = int(nano.convert(from_unit='XRB', to_unit="raw", 199 | value=destination.amount)) 200 | 201 | print(amount) 202 | 203 | block = rpc.send(wallet=nano_wallet, 204 | source=account, 205 | destination=destination.destination_address, 206 | amount=amount) 207 | 208 | await wallet.send_payment(transaction_hash=block, 209 | crypto_currency='NANO') 210 | elif answers['crypto'] == 'TESTCOIN': 211 | destination = payment_req.get_destination('TESTCOIN') 212 | await wallet.send_payment(transaction_hash='test_hash', 213 | crypto_currency='TESTCOIN') 214 | else: 215 | print("Not supported!") 216 | sys.exit() 217 | 218 | 219 | def _query_yes_no(question, default="yes"): 220 | """Ask a yes/no question via raw_input() and return their answer. 221 | 222 | "question" is a string that is presented to the user. 223 | "default" is the presumed answer if the user just hits . 224 | It must be "yes" (the default), "no" or None (meaning 225 | an answer is required of the user). 226 | 227 | The "answer" return value is True for "yes" or False for "no". 228 | """ 229 | valid = {"yes": True, "y": True, "ye": True, 230 | "no": False, "n": False} 231 | if default is None: 232 | prompt = " [y/n] " 233 | elif default == "yes": 234 | prompt = " [Y/n] " 235 | elif default == "no": 236 | prompt = " [y/N] " 237 | else: 238 | raise ValueError("invalid default answer: '%s'" % default) 239 | 240 | while True: 241 | sys.stdout.write(question + prompt) 242 | choice = input().lower() 243 | if default is not None and choice == '': 244 | return valid[default] 245 | elif choice in valid: 246 | return valid[choice] 247 | else: 248 | sys.stdout.write("Please respond with 'yes' or 'no' " 249 | "(or 'y' or 'n').\n") 250 | 251 | 252 | def _verify_envelope(envelope: PaymentRequestEnvelope, certificate, 253 | ca_certificate) -> bool: 254 | verified = False 255 | 256 | if ca_certificate: 257 | path = verify_chain(certificate, ca_certificate) 258 | 259 | if path: 260 | if envelope.verify(certificate): 261 | verified = True 262 | logger.info("Verified Request") 263 | logger.info("Certificate issued to {}".format( 264 | certificate.subject.get_attributes_for_oid( 265 | NameOID.COMMON_NAME)[0].value)) 266 | else: 267 | logger.error("Invalid Signature") 268 | else: 269 | logger.error("Invalid Certification Path") 270 | 271 | return verified 272 | -------------------------------------------------------------------------------- /manta/wallet.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | """ 6 | Library with a basic implementation of a Manta :term:`Wallet`. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import asyncio 12 | import logging 13 | import re 14 | from typing import Match, Optional, Union 15 | 16 | from cryptography import x509 17 | from cryptography.hazmat.backends import default_backend 18 | import paho.mqtt.client as mqtt 19 | 20 | from .base import MantaComponent 21 | from .messages import PaymentRequestEnvelope, PaymentMessage, AckMessage 22 | 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def wrap_callback(f): 28 | def wrapper(self: Wallet, *args): 29 | self.loop.call_soon_threadsafe(f, self, *args) 30 | 31 | return wrapper 32 | 33 | 34 | class Wallet(MantaComponent): 35 | """ 36 | Implements a Manta :term:`Wallet`. This class needs an *asyncio* loop 37 | to run correctly as some of its features are implemented as 38 | *coroutines*. 39 | 40 | This is usually instantiated from a :term:`Manta URL` using the 41 | :meth:`.factory` classmethod. 42 | 43 | Args: 44 | url: a string containing a :term:`Manta URL` 45 | session_id: a session_id 46 | host: :term:`MQTT` broker IP addresses 47 | port: optional port number of the broker service 48 | 49 | Attributes: 50 | acks: queue of :class:`~.messages.AckMessage` instances 51 | loop: the *asyncio* loop that manages the asynchronous parts of this 52 | object 53 | session_id: :term:`session_id` of the ongoing session, if any 54 | """ 55 | loop: asyncio.AbstractEventLoop 56 | connected: asyncio.Event 57 | port: int 58 | session_id: str 59 | payment_request_future: Optional[asyncio.Future] = None 60 | certificate_future: Optional[asyncio.Future] = None 61 | acks: asyncio.Queue 62 | first_connect = False 63 | 64 | @classmethod 65 | def factory(cls, url: str) -> Union[Wallet, None]: 66 | """ 67 | This creates an instance from a :term:`Manta URL`. Can be ``None`` 68 | if the URL is invalid. 69 | 70 | Args: 71 | url: manta url (ex. manta://developer.beappia.com/2848839943) 72 | 73 | Returns: 74 | a new configured but unconnected instance 75 | """ 76 | match = cls.parse_url(url) 77 | if match: 78 | port = 1883 if match[2] is None else int(match[2]) 79 | return cls(url, match[3], host=match[1], port=port) 80 | else: 81 | return None 82 | 83 | def __init__(self, url: str, session_id: str, host: str = "localhost", 84 | port: int = 1883): 85 | self.host = host 86 | self.port = port 87 | self.session_id = session_id 88 | 89 | self.mqtt_client = mqtt.Client() 90 | self.mqtt_client.on_connect = self.on_connect 91 | self.mqtt_client.on_message = self.on_message 92 | self.mqtt_client.on_disconnect = self.on_disconnect 93 | 94 | try: 95 | self.loop = asyncio.get_event_loop() 96 | except RuntimeError: 97 | self.loop = asyncio.new_event_loop() 98 | asyncio.set_event_loop(self.loop) 99 | 100 | self.acks = asyncio.Queue(loop=self.loop) 101 | self.connected = asyncio.Event(loop=self.loop) 102 | 103 | def close(self): 104 | """Disconnect and stop :term:`MQTT` client's processing loop.""" 105 | self.mqtt_client.disconnect() 106 | self.mqtt_client.loop_stop() 107 | 108 | @wrap_callback 109 | def on_disconnect(self, client, userdata, rc): 110 | self.connected.clear() 111 | 112 | @wrap_callback 113 | def on_connect(self, client: mqtt.Client, userdata, flags, rc): 114 | logger.info("Connected") 115 | self.certificate_future = self.loop.create_future() 116 | client.subscribe("certificate") 117 | self.connected.set() 118 | 119 | @wrap_callback 120 | def on_message(self, client: mqtt.Client, userdata, msg): 121 | logger.info("New message {} on {}".format(msg.payload, msg.topic)) 122 | tokens = msg.topic.split('/') 123 | 124 | if tokens[0] == "payment_requests": 125 | envelope = PaymentRequestEnvelope.from_json(msg.payload) 126 | assert self.payment_request_future is not None 127 | self.loop.call_soon_threadsafe(self.payment_request_future.set_result, envelope) 128 | elif tokens[0] == "acks": 129 | ack = AckMessage.from_json(msg.payload) 130 | self.acks.put_nowait(ack) 131 | elif tokens[0] == "certificate": 132 | assert self.certificate_future is not None 133 | self.loop.call_soon_threadsafe(self.certificate_future.set_result, msg.payload) 134 | 135 | @staticmethod 136 | def parse_url(url: str) -> Optional[Match]: 137 | """ 138 | Convenience method to check if Manta url is valid 139 | Args: 140 | url: manta url (ex. manta://developer.beappia.com/2848839943) 141 | 142 | Returns: 143 | A match object 144 | """ 145 | # TODO: What is session format? 146 | pattern = r"^manta://((?:\w|\.)+)(?::(\d+))?/(.+)$" 147 | return re.match(pattern, url) 148 | 149 | async def connect(self): 150 | """ 151 | Connect to the :term:`MQTT` broker and wait for the connection 152 | confirmation. 153 | 154 | This is a coroutine. 155 | """ 156 | if not self.first_connect: 157 | self.mqtt_client.connect(self.host, port=self.port) 158 | self.mqtt_client.loop_start() 159 | self.first_connect = True 160 | 161 | await self.connected.wait() 162 | 163 | async def get_certificate(self) -> x509.Certificate: 164 | """ 165 | Get :term:`Payment Processor`'s certificate retained by the 166 | :term:`MQTT` broker service. 167 | 168 | This is a coroutine 169 | """ 170 | await self.connect() 171 | assert self.certificate_future is not None 172 | certificate = await self.certificate_future 173 | return x509.load_pem_x509_certificate(certificate, default_backend()) 174 | 175 | async def get_payment_request(self, crypto_currency: str = "all") -> PaymentRequestEnvelope: 176 | """ 177 | Get the :class:`~.messages.PaymentRequestMessage` for specific crypto 178 | currency, or ``all`` to obtain informations about all the supported 179 | crypto currencies. 180 | 181 | Args: 182 | crypto_currency: crypto to request payment for (ex. 'NANO') 183 | 184 | Returns: 185 | The Payment Request Envelope. It's ``all`` by default 186 | 187 | This is a coroutine 188 | """ 189 | await self.connect() 190 | 191 | self.payment_request_future = self.loop.create_future() 192 | self.mqtt_client.subscribe("payment_requests/{}".format(self.session_id)) 193 | self.mqtt_client.publish("payment_requests/{}/{}".format(self.session_id, crypto_currency)) 194 | 195 | logger.info("Published payment_requests/{}".format(self.session_id)) 196 | 197 | result = await asyncio.wait_for(self.payment_request_future, 3) 198 | return result 199 | 200 | async def send_payment(self, transaction_hash: str, crypto_currency: str): 201 | """ 202 | Send payment info 203 | 204 | Args: 205 | transaction_hash: the hash of transaction sent to blockchain 206 | crypto_currency: the crypto currency used for transaction 207 | 208 | This is a coroutine 209 | """ 210 | await self.connect() 211 | message = PaymentMessage( 212 | transaction_hash=transaction_hash, 213 | crypto_currency=crypto_currency 214 | ) 215 | self.mqtt_client.subscribe("acks/{}".format(self.session_id)) 216 | self.mqtt_client.publish("payments/{}".format(self.session_id), 217 | message.to_json(), qos=1) 218 | -------------------------------------------------------------------------------- /mosquitto.conf: -------------------------------------------------------------------------------- 1 | persistence false 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | [mypy-paho.*,cryptography.*,cattr.*,certvalidator.*,file_config.*,inquirer.*,nano.*] 3 | ignore_missing_imports = True 4 | -------------------------------------------------------------------------------- /notebooks/.ipynb_checkpoints/Bip32-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [], 3 | "metadata": {}, 4 | "nbformat": 4, 5 | "nbformat_minor": 2 6 | } 7 | -------------------------------------------------------------------------------- /notebooks/Bip32.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 5, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from pycoin.key.key_from_text import key_from_text\n", 10 | "from pycoin.key import Key\n", 11 | "from pycoin.key.BIP32Node import BIP32Node" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 4, 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "data": { 21 | "text/plain": [ 22 | "" 23 | ] 24 | }, 25 | "execution_count": 4, 26 | "metadata": {}, 27 | "output_type": "execute_result" 28 | } 29 | ], 30 | "source": [ 31 | "mKey.from_text('xpub68aEVREZHhr6nvSJGwBVUyzjpvoUTvsPTgXuTyBzZd5ZhtVo4BJrd1dHoCKAVfANDz9WMQrbKTj3pyjp7uCq8otBcsoZZPmFY4eNEYUgaec')" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 17, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "master=BIP32Node.from_text('xpub6DLJbt93Rui7cdCWApb6C6kbs9kSKfZWgqrc61EVwTttxytgaMEbBbPpfH85AtjmWDyizqxMk5xFPsjnxTwX7LsEFtMcbYwEsNacoKS5xZe')" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 24, 46 | "metadata": {}, 47 | "outputs": [ 48 | { 49 | "data": { 50 | "text/plain": [ 51 | "" 52 | ] 53 | }, 54 | "execution_count": 24, 55 | "metadata": {}, 56 | "output_type": "execute_result" 57 | } 58 | ], 59 | "source": [ 60 | "k1=master.subkey_for_path('0/0/0')\n", 61 | "k1" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 26, 67 | "metadata": {}, 68 | "outputs": [ 69 | { 70 | "data": { 71 | "text/plain": [ 72 | "" 73 | ] 74 | }, 75 | "execution_count": 26, 76 | "metadata": {}, 77 | "output_type": "execute_result" 78 | } 79 | ], 80 | "source": [ 81 | "Key.from_text('xpub6JhHnr8dboiqkMuk3tB42RLgbqrLKkK1M7Z8FQXQhT26MTne6ZJs8bnzD8UgkkySps7sCfEggD3y5kHBpJ6wQT2J1mgFbLRPvj9Mq5zFdXZ').subkey(0)" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 27, 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "data": { 91 | "text/plain": [ 92 | "" 93 | ] 94 | }, 95 | "execution_count": 27, 96 | "metadata": {}, 97 | "output_type": "execute_result" 98 | } 99 | ], 100 | "source": [ 101 | "master.subkey_for_path('0/0/0/0')" 102 | ] 103 | } 104 | ], 105 | "metadata": { 106 | "kernelspec": { 107 | "display_name": "Python 3", 108 | "language": "python", 109 | "name": "python3" 110 | }, 111 | "language_info": { 112 | "codemirror_mode": { 113 | "name": "ipython", 114 | "version": 3 115 | }, 116 | "file_extension": ".py", 117 | "mimetype": "text/x-python", 118 | "name": "python", 119 | "nbconvert_exporter": "python", 120 | "pygments_lexer": "ipython3", 121 | "version": "3.6.3" 122 | }, 123 | "toc": { 124 | "nav_menu": {}, 125 | "number_sections": true, 126 | "sideBar": true, 127 | "skip_h1_title": false, 128 | "toc_cell": false, 129 | "toc_position": {}, 130 | "toc_section_display": "block", 131 | "toc_window_display": false 132 | } 133 | }, 134 | "nbformat": 4, 135 | "nbformat_minor": 2 136 | } 137 | -------------------------------------------------------------------------------- /notebooks/Messages.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 2, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "sys.path.insert(0, \"..\")" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 3, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from manta.messages import Destination, PaymentRequestMessage, verify_chain, PaymentMessage, AckMessage, Status, \\\n", 28 | " Merchant, MerchantOrderRequestMessage\n", 29 | "from cryptography.hazmat.primitives.serialization import load_pem_private_key\n", 30 | "from cryptography.hazmat.backends import default_backend\n", 31 | "from decimal import Decimal" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 4, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "MERCHANT = Merchant(\n", 41 | " name=\"Merchant 1\",\n", 42 | " address=\"5th Avenue\"\n", 43 | ")" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 5, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "DESTINATIONS = [\n", 53 | " Destination(\n", 54 | " amount=\"5\",\n", 55 | " destination_address=\"btc_daddress\",\n", 56 | " crypto_currency=\"btc\"\n", 57 | " ),\n", 58 | " Destination(\n", 59 | " amount=\"10\",\n", 60 | " destination_address=\"nano_daddress\",\n", 61 | " crypto_currency=\"nano\"\n", 62 | " ),\n", 63 | "\n", 64 | "]" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 6, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "PRIVATE_KEY = \"../certificates/root/keys/test.key\"" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 7, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "with open(PRIVATE_KEY, 'rb') as myfile:\n", 83 | " key_data = myfile.read()\n", 84 | "\n", 85 | " key = load_pem_private_key(key_data, password=None, backend=default_backend())\n", 86 | "\n", 87 | " message = PaymentRequestMessage(\n", 88 | " merchant=MERCHANT,\n", 89 | " amount=\"10\",\n", 90 | " fiat_currency=\"euro\",\n", 91 | " destinations=DESTINATIONS,\n", 92 | " supported_cryptos={'btc', 'xmr', 'nano'}\n", 93 | "\n", 94 | " )" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 8, 100 | "metadata": {}, 101 | "outputs": [ 102 | { 103 | "data": { 104 | "text/plain": [ 105 | "'{\"message\": \"{\\\\\"merchant\\\\\": {\\\\\"name\\\\\": \\\\\"Merchant 1\\\\\", \\\\\"address\\\\\": \\\\\"5th Avenue\\\\\"}, \\\\\"amount\\\\\": \\\\\"10\\\\\", \\\\\"fiat_currency\\\\\": \\\\\"euro\\\\\", \\\\\"destinations\\\\\": [{\\\\\"amount\\\\\": \\\\\"5\\\\\", \\\\\"destination_address\\\\\": \\\\\"btc_daddress\\\\\", \\\\\"crypto_currency\\\\\": \\\\\"btc\\\\\"}, {\\\\\"amount\\\\\": \\\\\"10\\\\\", \\\\\"destination_address\\\\\": \\\\\"nano_daddress\\\\\", \\\\\"crypto_currency\\\\\": \\\\\"nano\\\\\"}], \\\\\"supported_cryptos\\\\\": [\\\\\"xmr\\\\\", \\\\\"btc\\\\\", \\\\\"nano\\\\\"]}\", \"signature\": \"HgdmAEw2ztHWHYs0FMPUWusZiIN+TRwgYaAGycJ1rCcqrvP9Qo4VVwg1E1ZCqeEHHlmndrmVDY4//QH7g9cgsFohgK+qw5Zo3sv7pdxndOovhaAHrLmyU/WkNl+62QxVNMt6FkHxwDmzyyKtK84CygVpnuNLFKutTDTadUVsxJuHlPicKwhRBMilrURBfjguLo1BbSqmoiH8TnUGVMExjVhI5lSic3eYtd7SCxvRguw+CLF6IP0VCz3eOx7xBZvY75jAkYc82xA8z8PVy8yxn/hPpvqQguXep4ZWx2pvRtEQifmSGZZzQkzumsKg7NEGGC3E6zhhfuWwSFvlk3LeQg==\"}'" 106 | ] 107 | }, 108 | "execution_count": 8, 109 | "metadata": {}, 110 | "output_type": "execute_result" 111 | } 112 | ], 113 | "source": [ 114 | "message.get_envelope(key).to_json()" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 9, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "data": { 124 | "text/plain": [ 125 | "'{\"crypto_currency\": \"nano\", \"transaction_hash\": \"hash1\"}'" 126 | ] 127 | }, 128 | "execution_count": 9, 129 | "metadata": {}, 130 | "output_type": "execute_result" 131 | } 132 | ], 133 | "source": [ 134 | "PaymentMessage(crypto_currency=\"nano\", transaction_hash=\"hash1\").to_json()" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": 10, 140 | "metadata": {}, 141 | "outputs": [ 142 | { 143 | "data": { 144 | "text/plain": [ 145 | "'{\"txid\": \"0\", \"status\": \"pending\", \"url\": null, \"amount\": null, \"transaction_hash\": \"myhash\", \"transaction_currency\": null, \"memo\": null}'" 146 | ] 147 | }, 148 | "execution_count": 10, 149 | "metadata": {}, 150 | "output_type": "execute_result" 151 | } 152 | ], 153 | "source": [ 154 | " AckMessage(\n", 155 | " txid=\"0\",\n", 156 | " transaction_hash=\"myhash\",\n", 157 | " status=Status.PENDING\n", 158 | " ).to_json()\n" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 11, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "order =MerchantOrderRequestMessage(amount=Decimal(10), session_id=\"123\", fiat_currency=\"EUR\")" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 12, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "import cattr" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 18, 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "from decimal import BasicContext, Context" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 28, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "TWOPLACES = Decimal(10) ** -4 " 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 31, 200 | "metadata": {}, 201 | "outputs": [ 202 | { 203 | "data": { 204 | "text/plain": [ 205 | "Decimal('0.0493')" 206 | ] 207 | }, 208 | "execution_count": 31, 209 | "metadata": {}, 210 | "output_type": "execute_result" 211 | } 212 | ], 213 | "source": [ 214 | "Decimal(\"0.04926108374384236453201970443\").quantize(Decimal(\"0.0001\"))" 215 | ] 216 | } 217 | ], 218 | "metadata": { 219 | "kernelspec": { 220 | "display_name": "Python 3", 221 | "language": "python", 222 | "name": "python3" 223 | }, 224 | "language_info": { 225 | "codemirror_mode": { 226 | "name": "ipython", 227 | "version": 3 228 | }, 229 | "file_extension": ".py", 230 | "mimetype": "text/x-python", 231 | "name": "python", 232 | "nbconvert_exporter": "python", 233 | "pygments_lexer": "ipython3", 234 | "version": "3.7.0" 235 | }, 236 | "toc": { 237 | "nav_menu": {}, 238 | "number_sections": true, 239 | "sideBar": true, 240 | "skip_h1_title": false, 241 | "toc_cell": false, 242 | "toc_position": {}, 243 | "toc_section_display": "block", 244 | "toc_window_display": false 245 | }, 246 | "pycharm": { 247 | "stem_cell": { 248 | "cell_type": "raw", 249 | "source": [], 250 | "metadata": { 251 | "collapsed": false 252 | } 253 | } 254 | } 255 | }, 256 | "nbformat": 4, 257 | "nbformat_minor": 2 258 | } -------------------------------------------------------------------------------- /notebooks/RSAFUN.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 14, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from cryptography import x509" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 15, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from cryptography.hazmat.backends import default_backend" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 16, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from cryptography.hazmat.primitives.asymmetric import dsa, rsa" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 17, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "from cryptography.hazmat.primitives.serialization import load_pem_private_key" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 18, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "from cryptography.hazmat.primitives import hashes\n", 46 | "from cryptography.hazmat.primitives.asymmetric import padding\n", 47 | "from cryptography.hazmat.primitives import serialization" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 49, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "import base64\n", 57 | "import binascii" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 20, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "with open('/Users/avigano/Developer/appiapay/certificates/root/certs/test.crt', 'rb') as myfile:\n", 67 | " pem = myfile.read()" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 21, 73 | "metadata": {}, 74 | "outputs": [ 75 | { 76 | "data": { 77 | "text/plain": [ 78 | "b'-----BEGIN CERTIFICATE-----\\nMIIDQjCCAiqgAwIBAgIRAPr1QV+OmLIjsRH4mOxjy1EwDQYJKoZIhvcNAQELBQAw\\nKDEmMCQGA1UEAxMdTmFub3JheSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTgw\\nNjEyMDUyMDI5WhcNMTkwNjEyMDUyMDI5WjAeMRwwGgYDVQQDExN3d3cuYnJhaW5i\\nbG9ja3MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1O+cC40X\\ntt1F87C7SHSckdEI6PeJKGaNGBfKLI8q+G3tbjFYe+A83skUkkcQd9yICZQmVZ4Q\\nqd3LaaPhOy+sDxkvytcTMwHj/N3IBq9IpZK1nVAm1s2pNcvsAGTdKPfB0P8hHtNN\\nrwi3c2G0NOP0UVDe3yDzeAacJMtbIHrMDG7Sb8ObITd1NZd2NNv9Ou/RfmAb/pni\\ngyHITZdzDGPCkP4x3Y3iLLQbdbC8U4nIHKw39HTAN/+FYkWDFRIz8fdcI3EbksEv\\ncgs3f63OR9uHvUNkqvueAwBTa2bFopzs7Li+oAuVhA/UgXwgpsFF1pSzZz8WWYkc\\nyDeJG9O3hRPc9QIDAQABo3EwbzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI\\nKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTTRDdHAcsOFNpRxBt4RlQbwagO\\nqjAfBgNVHSMEGDAWgBRwkhWOFhgXtaH1sKfkCQL78Mfy5TANBgkqhkiG9w0BAQsF\\nAAOCAQEAmJIyiA+m9duBYR+p1IwR/DyEssZ0YtCVaq375c567a6owPZSUEGi+cUj\\nxsEsxPkl6DrgZzEathvoUVNVlA1YyHwIFXp0n5Qd0OlQ66WnQD16i4CygdGTpAex\\n8oWK/6mUdXxIIEUHaiv5UYHQhfwCb+c0yNFeN+uQ2SfDwID20NjZNLGKQzYZ+JVI\\nQED2ofs5p/xm7qe/Ht58u6TqAYjxDO4OqSspzH2e6a2EIjVe81DvrfHnatDUar5m\\n+XkSTmuqyX0wmxZ2E2hhlJkhyLCadkP3Hor2s3nUpkqKH3bSUJ5U/TuvvxrEEt0I\\nz4TYl3Vuacma4wEMQGqhJSWv0gjRQg==\\n-----END CERTIFICATE-----\\n'" 79 | ] 80 | }, 81 | "execution_count": 21, 82 | "metadata": {}, 83 | "output_type": "execute_result" 84 | } 85 | ], 86 | "source": [ 87 | "pem" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 22, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "cert = x509.load_pem_x509_certificate(pem, default_backend())" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 23, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "with open('/Users/avigano/Developer/appiapay/certificates/root/keys/test.key', 'rb') as myfile:\n", 106 | " key_data = myfile.read()" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 24, 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "data": { 116 | "text/plain": [ 117 | "b'-----BEGIN RSA PRIVATE KEY-----\\nMIIEpQIBAAKCAQEA1O+cC40Xtt1F87C7SHSckdEI6PeJKGaNGBfKLI8q+G3tbjFY\\ne+A83skUkkcQd9yICZQmVZ4Qqd3LaaPhOy+sDxkvytcTMwHj/N3IBq9IpZK1nVAm\\n1s2pNcvsAGTdKPfB0P8hHtNNrwi3c2G0NOP0UVDe3yDzeAacJMtbIHrMDG7Sb8Ob\\nITd1NZd2NNv9Ou/RfmAb/pnigyHITZdzDGPCkP4x3Y3iLLQbdbC8U4nIHKw39HTA\\nN/+FYkWDFRIz8fdcI3EbksEvcgs3f63OR9uHvUNkqvueAwBTa2bFopzs7Li+oAuV\\nhA/UgXwgpsFF1pSzZz8WWYkcyDeJG9O3hRPc9QIDAQABAoIBAQCmxC4DQfJDrlK9\\nwzk6StHgxcTjqBJMiNyR9PfLJCl0PavJNG5nPjyOAw/QbEWyig4k6lmHjm7giqtn\\nxgh88R4hCQnMI9uOhDmJbizdR2RvAFKqrP5uFs4iKt5fhJ9NGZU62MWYvcbGgd4j\\nSG75SVqsYNjcCZOE+jBKBNYOvv2V8bppPhNIXs8iS3sUbZYDN5M4jQHo4O0j1MQi\\n2wDQpwAHg04QSnTERfz8K7yDucY7BBNSFQNHTG2fDq6uJk5Llj5NmLdMol+TSnHM\\nqnS5viuyHYHPl+WCT+QzUS+0kx5F71N2tLeX8fdMmY2GRmElkzFCOQ5kc5zU95lH\\nXCdwgoqVAoGBANaC597cyoX73SbBy0pOm7VW+16CUP76F0H1nIeEOTq5HkNf5LKB\\ngrigij0r6wJxN85l4hmxoMX8f4PpG3PY64gR2HpR+GYPtdNuieZnhb3Q+r/DpnkE\\n0OIjqngQCPlqT1UAkrG1GDYLOEtirkJKAoBPhRx9mi6JLFju3ki7UkLfAoGBAP4e\\ns8gqYTIxoTnHV8U6jnuleYGhKLk3bI1CJ1JRk9S+tJsvPOErd0lQq545suwhYXUp\\nj85FDbgSw0eiAZJBz/jwJioSan1QgfcBxahXyMqTLwsDza4U8mV37dhTGKXXgLFe\\nrAvmlLVHDYWsmHIevKFSeqo77Nlx6Q5+jyR6pw6rAoGAP8A10vkBQ2J/7iXIGfRU\\nuEb6e7L1CWIgCV1KQMgeDgK4KMPV/usYg3BKxTVJKbemIzQKRyKQKmcJKpXbr8k2\\n7oCHOosj7Ikcu5Jtb0ky6R+zdcxarDqvLZX18qqpUB61Jwj9j8zHPkCFYXoZWeAO\\n8D0xzS7S5KOlx2RuMWViZDcCgYEAid9UgWRk6Zu9sqByAWL8zR8BZpBujNcCQT3E\\nIch64XE6gfvGFxDDHnbzNdxuM+kEfFG5YRtcDyO26ZV/LsAgOxroSelF94mHieFf\\nQS+nlCj43AwLOsjInr7Lv5OOCuR6QUFxLN/EjPno3z6+UyRUCV67iMMMhQllfeSy\\newNEwhMCgYEAijETQlQTaEBRj4UD9rH4vUZfNnZO1NOetDBeXaLRLU7NJxTeZ/Tt\\nchFd+tlGvwi4ynJ4lPsoyMYvD8DFj0nXUUpDD/b07fsPPDrRgnKHiiPJF2rUN0IB\\nRWBnMUHLYluKopDqkoVAulUZ/QLWhmwvV4CV7G5PtIDpzmT3ycF2hqs=\\n-----END RSA PRIVATE KEY-----\\n'" 118 | ] 119 | }, 120 | "execution_count": 24, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | } 124 | ], 125 | "source": [ 126 | "key_data" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 27, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "key = load_pem_private_key(key_data, password= None, backend= default_backend())" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": 29, 141 | "metadata": {}, 142 | "outputs": [ 143 | { 144 | "data": { 145 | "text/plain": [ 146 | "" 147 | ] 148 | }, 149 | "execution_count": 29, 150 | "metadata": {}, 151 | "output_type": "execute_result" 152 | } 153 | ], 154 | "source": [ 155 | "key" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 54, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "message = b'hello'" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 59, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "signature = key.sign(message, \n", 174 | " padding.PSS(\n", 175 | " mgf=padding.MGF1(hashes.SHA256()),\n", 176 | " salt_length=padding.PSS.MAX_LENGTH\n", 177 | " ),\n", 178 | " hashes.SHA256())" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 61, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "signature = key.sign(message, \n", 188 | " padding.PKCS1v15(),\n", 189 | " hashes.SHA256())" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 62, 195 | "metadata": {}, 196 | "outputs": [ 197 | { 198 | "data": { 199 | "text/plain": [ 200 | "'0hyJSUUJwwMMgG82cT3q/TTijd5+4U97EHjO7kWIEvpLTW9/3gnIaFtXxCqcwge+yQ9Mn2f4W3smL7xzxoqe18XC0yb3wSlLcQZV6dNk0l1gnc1Gaar8aR43lYsG+zDrJHPMJ8IpmIxzY4Wg9hZ7sJkSSML3d0+5Ds9T/5nM1zIa/7op6o0QiY3awztA+iO2BAsKvKcUI/9V1GGww9KHSVUrZOsaJPvxql6hrMiVDaf0TW17osWgWnY8kvdRiKagaKQvm562dpINrdXxnSOB7SL8C37bnaPRRWd66tjzqp2tUGdJB/ORVDXuu+BCZ3RnWYsw47nICRxoO585E/uDkQ=='" 201 | ] 202 | }, 203 | "execution_count": 62, 204 | "metadata": {}, 205 | "output_type": "execute_result" 206 | } 207 | ], 208 | "source": [ 209 | "base64.b64encode(signature).decode('utf-8')" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 58, 215 | "metadata": {}, 216 | "outputs": [ 217 | { 218 | "data": { 219 | "text/plain": [ 220 | "b'd21c89494509c3030c806f36713deafd34e28dde7ee14f7b1078ceee458812fa4b4d6f7fde09c8685b57c42a9cc207bec90f4c9f67f85b7b262fbc73c68a9ed7c5c2d326f7c1294b710655e9d364d25d609dcd4669aafc691e37958b06fb30eb2473cc27c229988c736385a0f6167bb0991248c2f7774fb90ecf53ff99ccd7321affba29ea8d10898ddac33b40fa23b6040b0abca71423ff55d461b0c3d28749552b64eb1a24fbf1aa5ea1acc8950da7f44d6d7ba2c5a05a763c92f75188a6a068a42f9b9eb676920dadd5f19d2381ed22fc0b7edb9da3d145677aead8f3aa9dad50674907f3915435eebbe042677467598b30e3b9c8091c683b9f3913fb8391'" 221 | ] 222 | }, 223 | "execution_count": 58, 224 | "metadata": {}, 225 | "output_type": "execute_result" 226 | } 227 | ], 228 | "source": [ 229 | "binascii.hexlify(signature)" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": 36, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "cert.public_key().verify(\n", 239 | " signature,\n", 240 | " message,\n", 241 | " padding.PSS(\n", 242 | " mgf=padding.MGF1(hashes.SHA256()),\n", 243 | " salt_length=padding.PSS.MAX_LENGTH\n", 244 | " ),\n", 245 | " hashes.SHA256()\n", 246 | ")" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": null, 252 | "metadata": {}, 253 | "outputs": [], 254 | "source": [ 255 | "base64.b64encode(signature).decode('utf-8')" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": null, 261 | "metadata": {}, 262 | "outputs": [], 263 | "source": [ 264 | "key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption())" 265 | ] 266 | } 267 | ], 268 | "metadata": { 269 | "kernelspec": { 270 | "display_name": "Python 3", 271 | "language": "python", 272 | "name": "python3" 273 | }, 274 | "language_info": { 275 | "codemirror_mode": { 276 | "name": "ipython", 277 | "version": 3 278 | }, 279 | "file_extension": ".py", 280 | "mimetype": "text/x-python", 281 | "name": "python", 282 | "nbconvert_exporter": "python", 283 | "pygments_lexer": "ipython3", 284 | "version": "3.7.0" 285 | }, 286 | "toc": { 287 | "nav_menu": {}, 288 | "number_sections": true, 289 | "sideBar": true, 290 | "skip_h1_title": false, 291 | "toc_cell": false, 292 | "toc_position": {}, 293 | "toc_section_display": "block", 294 | "toc_window_display": false 295 | }, 296 | "pycharm": { 297 | "stem_cell": { 298 | "cell_type": "raw", 299 | "source": [], 300 | "metadata": { 301 | "collapsed": false 302 | } 303 | } 304 | } 305 | }, 306 | "nbformat": 4, 307 | "nbformat_minor": 2 308 | } -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -r requirements-tests.txt 3 | -r requirements-docs.txt 4 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-autodoc-typehints 3 | sphinx-jsonschema 4 | sphinx-rtd-theme 5 | sphinxcontrib-seqdiag 6 | sphinxcontrib-asyncio 7 | -------------------------------------------------------------------------------- /requirements-tests.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | callee 3 | docutils 4 | file_config[pyyaml] >= 0.3.6 5 | inquirer 6 | mypy 7 | nano-python 8 | pytest 9 | pytest-asyncio 10 | pytest-cov 11 | pyyaml 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs>=18.2.0 2 | cattrs>=0.9.0 3 | certvalidator>=0.11.1 4 | cryptography>=2.4.2 5 | paho-mqtt>=1.5.0 6 | simplejson>=3.16.0 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --cov=manta 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Manta Python 3 | # Manta Protocol Implementation for Python 4 | # Copyright (C) 2018-2019 Alessandro Viganò 5 | 6 | from io import open 7 | import os 8 | 9 | from setuptools import setup 10 | 11 | from manta import MANTA_VERSION 12 | 13 | here_dir = os.path.dirname(__file__) 14 | 15 | with open(os.path.join(here_dir, "requirements.txt")) as req_file: 16 | requirements = req_file.read().splitlines() 17 | 18 | with open(os.path.join(here_dir, "requirements-tests.txt")) as req_file: 19 | requirements_tests = req_file.read().splitlines() 20 | 21 | with open(os.path.join(here_dir, "README.rst"), encoding="utf-8") as f: 22 | README = f.read() 23 | 24 | 25 | setup( 26 | name="manta", 27 | version="1.6.3", 28 | description="Manta protocol components", 29 | long_description=README, 30 | packages=["manta", "manta.testing"], 31 | url="https://appiapay.github.io/manta-python", 32 | license="BSD", 33 | author="Alessandro Viganò", 34 | author_email="alessandro@appia.co", 35 | install_requires=requirements, 36 | extras_require={"runner": requirements_tests,}, 37 | python_requires=">=3.7", 38 | classifiers=[ 39 | "Development Status :: 4 - Beta", 40 | "Programming Language :: Python :: 3", 41 | "Programming Language :: Python :: 3.7", 42 | "Intended Audience :: Developers", 43 | "License :: OSI Approved :: BSD License", 44 | "Topic :: System :: Networking", 45 | "Topic :: Office/Business :: Financial :: Point-Of-Sale", 46 | ], 47 | entry_points={ 48 | "console_scripts": [ 49 | "manta-runner=manta.testing.__main__:main", 50 | "manta-store=manta.testing.__main__:store_main", 51 | "manta-payproc=manta.testing.__main__:payproc_main", 52 | "manta-wallet=manta.testing.__main__:wallet_main", 53 | ], 54 | }, 55 | ) 56 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | with import {}; 3 | with pkgs.python37Packages; 4 | 5 | stdenv.mkDerivation { 6 | name = "manta-backend-impurePythonEnv"; 7 | buildInputs = [ 8 | python37Full 9 | python37Packages.virtualenv 10 | python37Packages.pip 11 | python37Packages.pillow 12 | # The following are build dependencies 13 | gnumake 14 | libffi 15 | mosquitto 16 | openssl 17 | ]; 18 | src = null; 19 | shellHook = '' 20 | # set SOURCE_DATE_EPOCH so that we can use python wheels 21 | SOURCE_DATE_EPOCH=$(date +%s) 22 | export PATH=$PWD/venv/bin:$PATH 23 | make 24 | ''; 25 | } 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appiapay/manta-python/889caacf3151c9c39ae96b97053cac064103a3f6/tests/__init__.py -------------------------------------------------------------------------------- /tests/certificates: -------------------------------------------------------------------------------- 1 | ../certificates -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | import os 6 | import pathlib 7 | import pytest 8 | 9 | pytest.register_assert_rewrite("tests.utils") 10 | 11 | # it's a fixture used in the tests 12 | from .utils import mock_mqtt # noqa E402 13 | 14 | 15 | @pytest.fixture(scope='session') 16 | def tests_dir(): 17 | return pathlib.Path(os.path.dirname(os.path.realpath(__file__))) 18 | 19 | 20 | @pytest.fixture(scope='session') 21 | def config_str(tests_dir): 22 | config_file = open(tests_dir / 'dummyconfig.yaml') 23 | return config_file.read() 24 | 25 | 26 | @pytest.fixture(scope='session', 27 | params=[pytest.param(False, id='direct'), 28 | pytest.param(True, id='web')]) 29 | def config(request, config_str): 30 | from manta.testing.config import IntegrationConfig 31 | 32 | config = IntegrationConfig.loads_yaml(config_str) 33 | enable_web = request.param 34 | if enable_web: 35 | config.payproc.web.enable = True 36 | config.store.web.enable = True 37 | config.wallet.web.enable = True 38 | return config 39 | 40 | 41 | @pytest.fixture(scope='session') 42 | def broker(config): 43 | from manta.testing.broker import launch_mosquitto_from_config 44 | 45 | with launch_mosquitto_from_config(config.broker) as connection_data: 46 | # listening config may have changed due to automatic 47 | # reallocation in case some other process is listening already 48 | _, host, port, _ = connection_data 49 | config.broker.host = host 50 | config.broker.port = port 51 | yield connection_data 52 | 53 | 54 | @pytest.fixture(scope='function') 55 | async def dummy_store(config, broker): 56 | from manta.testing.runner import AppRunner 57 | from manta.testing.store import dummy_store 58 | 59 | runner = AppRunner(dummy_store, config) 60 | await runner.start() 61 | yield runner 62 | await runner.stop() 63 | 64 | 65 | @pytest.fixture(scope='function') 66 | async def dummy_wallet(config, broker): 67 | from manta.testing.runner import AppRunner 68 | from manta.testing.wallet import dummy_wallet 69 | 70 | runner = AppRunner(dummy_wallet, config) 71 | await runner.start() 72 | # wallet start() adds pay() method to the runner 73 | yield runner 74 | await runner.stop() 75 | 76 | 77 | @pytest.fixture(scope='function') 78 | async def dummy_payproc(config, broker): 79 | from manta.testing.runner import AppRunner 80 | from manta.testing.payproc import dummy_payproc 81 | 82 | runner = AppRunner(dummy_payproc, config) 83 | await runner.start() 84 | yield runner 85 | await runner.stop() 86 | 87 | 88 | @pytest.fixture() 89 | def web_get(event_loop): 90 | import functools 91 | import requests 92 | 93 | def get(*args, **kwargs): 94 | return event_loop.run_in_executor( 95 | None, functools.partial(requests.get, **kwargs), *args) 96 | return get 97 | 98 | @pytest.fixture() 99 | def web_post(event_loop): 100 | import functools 101 | import requests 102 | 103 | def post(*args, **kwargs): 104 | return event_loop.run_in_executor( 105 | None, functools.partial(requests.post, **kwargs), *args) 106 | return post 107 | -------------------------------------------------------------------------------- /tests/dummyconfig.yaml: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | --- 3 | broker: 4 | allow_port_reallocation: true 5 | start: true 6 | host: localhost 7 | port: 1883 8 | payproc: 9 | supported_cryptos: [btc, xmr, nano] 10 | destinations: 11 | - amount: '0.01' 12 | destination_address: xrb_3d1ab61eswzsgx5arwqc3gw8xjcsxd7a5egtr69jixa5it9yu9fzct9nyjyx 13 | crypto_currency: NANO 14 | keyfile: tests/certificates/root/keys/test.key 15 | merchant: 16 | name: Merchant 1 17 | address: 5th Avenue 18 | web: 19 | enable: false 20 | allow_port_reallocation: true 21 | bind_address: localhost 22 | bind_port: 8081 23 | store: 24 | web: 25 | enable: false 26 | allow_port_reallocation: true 27 | bind_address: localhost 28 | bind_port: 8080 29 | wallet: 30 | interactive: false 31 | web: 32 | enable: false 33 | allow_port_reallocation: true 34 | bind_address: localhost 35 | bind_port: 8082 36 | -------------------------------------------------------------------------------- /tests/integration/test_store_mqtt.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | import pytest 6 | 7 | from manta.messages import Status 8 | from manta.store import Store 9 | 10 | # logging.basicConfig(level=logging.INFO) 11 | 12 | 13 | @pytest.fixture 14 | async def store(broker) -> Store: 15 | _, host, port, _ = broker 16 | return Store('device1', host=host, port=port) 17 | 18 | 19 | @pytest.mark.timeout(2) 20 | @pytest.mark.asyncio 21 | async def test_connect(store): 22 | # noinspection PyUnresolvedReferences 23 | await store.connect() 24 | store.close() 25 | 26 | 27 | @pytest.mark.timeout(2) 28 | @pytest.mark.asyncio 29 | async def test_generate_payment_request(store, dummy_payproc): 30 | # noinspection PyUnresolvedReferences 31 | ack = await store.merchant_order_request(amount=10, fiat='eur') 32 | assert ack.url.startswith("manta://") 33 | 34 | 35 | # noinspection PyUnresolvedReferences 36 | @pytest.mark.timeout(5) 37 | @pytest.mark.asyncio 38 | async def test_ack(store, dummy_wallet, dummy_payproc, web_post): 39 | 40 | ack = await store.merchant_order_request(amount=10, fiat='eur') 41 | if dummy_wallet.url: 42 | web_post(dummy_wallet.url + "/scan", json={"url": ack.url}) 43 | else: 44 | await dummy_wallet.pay(url=ack.url) 45 | ack_message = await store.acks.get() 46 | 47 | assert Status.PENDING == ack_message.status 48 | 49 | 50 | @pytest.mark.timeout(5) 51 | @pytest.mark.asyncio 52 | # noinspection PyUnresolvedReferences 53 | async def test_ack_paid(store, dummy_wallet, dummy_payproc, web_post): 54 | await test_ack(store, dummy_wallet, dummy_payproc, web_post) 55 | 56 | if dummy_payproc.url: 57 | web_post(dummy_payproc.url + "/confirm", 58 | json={'session_id': store.session_id}) 59 | else: 60 | dummy_payproc.manta.confirm(store.session_id) 61 | 62 | ack_message = await store.acks.get() 63 | 64 | assert Status.PAID == ack_message.status 65 | 66 | 67 | @pytest.mark.timeout(5) 68 | @pytest.mark.asyncio 69 | # noinspection PyUnresolvedReferences 70 | async def test_store_complete_session(store, dummy_wallet, dummy_payproc, 71 | web_post): 72 | ack = await store.merchant_order_request(amount=10, fiat='eur') 73 | await dummy_wallet.pay(url=ack.url) 74 | dummy_payproc.manta.confirm(dummy_wallet.manta.session_id) 75 | while True: 76 | ack = await store.acks.get() 77 | if ack.status is Status.PAID: 78 | break 79 | assert ack.status is Status.PAID 80 | -------------------------------------------------------------------------------- /tests/integration/test_wallet_mqtt.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | import asyncio 6 | from dataclasses import dataclass 7 | from decimal import Decimal 8 | import logging 9 | 10 | import pytest 11 | 12 | from manta.messages import PaymentRequestEnvelope, Status 13 | from manta.wallet import Wallet 14 | 15 | 16 | @pytest.mark.timeout(2) 17 | @pytest.mark.asyncio 18 | async def test_connect(broker): 19 | _, host, port, _ = broker 20 | wallet = Wallet.factory('manta://localhost:{}/123'.format(port)) 21 | await wallet.connect() 22 | wallet.close() 23 | 24 | 25 | # See https://github.com/pytest-dev/pytest-asyncio/issues/68 26 | @pytest.yield_fixture(scope="class") 27 | def event_loop(): 28 | loop = asyncio.get_event_loop_policy().new_event_loop() 29 | yield loop 30 | loop.close() 31 | 32 | 33 | @pytest.fixture(scope="class") 34 | async def session_data(): 35 | @dataclass 36 | class Session: 37 | wallet: Wallet = None 38 | envelope: PaymentRequestEnvelope = None 39 | 40 | session = Session 41 | return session 42 | 43 | 44 | @pytest.mark.incremental 45 | class TestWallet: 46 | 47 | @pytest.mark.asyncio 48 | async def test_get_payment_request(self, dummy_payproc, dummy_store, 49 | web_post): 50 | if dummy_store.url: 51 | r = await web_post(dummy_store.url + "/merchant_order", 52 | json={"amount": "10", "fiat": "EUR"}) 53 | logging.info(r) 54 | ack_message = r.json() 55 | url = ack_message['url'] 56 | else: 57 | ack_message = await dummy_store.manta.merchant_order_request( 58 | amount=Decimal("10"), fiat='EUR') 59 | url = ack_message.url 60 | logging.info(url) 61 | wallet = Wallet.factory(url) 62 | 63 | envelope = await wallet.get_payment_request('NANO') 64 | self.pr = envelope.unpack() 65 | 66 | assert 10 == self.pr.amount 67 | assert "EUR" == self.pr.fiat_currency 68 | return wallet 69 | 70 | @pytest.mark.asyncio 71 | async def test_send_payment(self, dummy_payproc, dummy_store, web_post): 72 | # noinspection PyUnresolvedReferences 73 | 74 | wallet = await self.test_get_payment_request(dummy_payproc, 75 | dummy_store, web_post) 76 | 77 | await wallet.send_payment(crypto_currency="NANO", transaction_hash="myhash") 78 | 79 | ack = await wallet.acks.get() 80 | 81 | assert Status.PENDING == ack.status 82 | return wallet 83 | 84 | @pytest.mark.asyncio 85 | async def test_ack_on_confirmation(self, session_data, dummy_payproc, 86 | dummy_store, web_post): 87 | # noinspection PyUnresolvedReferences 88 | wallet = await self.test_send_payment(dummy_payproc, dummy_store, 89 | web_post) 90 | 91 | dummy_payproc.manta.confirm(wallet.session_id) 92 | 93 | if dummy_payproc.url: 94 | web_post(dummy_payproc.url + "/confirm", 95 | json={'session_id': wallet.session_id}) 96 | 97 | ack = await wallet.acks.get() 98 | 99 | assert Status.PAID == ack.status 100 | -------------------------------------------------------------------------------- /tests/old/payprocdummy.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | from decimal import Decimal 6 | import logging 7 | 8 | from aiohttp import web 9 | 10 | from manta.messages import MerchantOrderRequestMessage, Destination, Merchant 11 | from manta.payproc import PayProc 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | KEYFILE = "certificates/root/keys/test.key" 17 | DESTINATIONS = [ 18 | Destination( 19 | amount=Decimal("0.01"), 20 | destination_address="xrb_3d1ab61eswzsgx5arwqc3gw8xjcsxd7a5egtr69jixa5it9yu9fzct9nyjyx", 21 | crypto_currency="NANO" 22 | ), 23 | 24 | ] 25 | 26 | MERCHANT = Merchant( 27 | name="Merchant 1", 28 | address="5th Avenue" 29 | ) 30 | 31 | 32 | def get_destinations(application_id, merchant_order: MerchantOrderRequestMessage): 33 | if merchant_order.crypto_currency: 34 | destination = next(x for x in DESTINATIONS 35 | if x.crypto_currency == merchant_order.crypto_currency) 36 | return [destination] 37 | else: 38 | return DESTINATIONS 39 | 40 | 41 | pp = PayProc(KEYFILE) 42 | pp.get_merchant = lambda x: MERCHANT 43 | pp.get_destinations = get_destinations 44 | pp.get_supported_cryptos = lambda device, payment_request: {'btc', 'xmr', 'nano'} 45 | 46 | routes = web.RouteTableDef() 47 | 48 | 49 | @routes.post("/confirm") 50 | async def merchant_order(request: web.Request): 51 | try: 52 | logger.info("Got confirm request") 53 | json = await request.json() 54 | pp.confirm(json['session_id']) 55 | 56 | return web.json_response("ok") 57 | 58 | except Exception: 59 | raise web.HTTPInternalServerError() 60 | 61 | 62 | logging.basicConfig(level=logging.INFO) 63 | app = web.Application() 64 | app.add_routes(routes) 65 | pp.run() 66 | web.run_app(app, port=8081) 67 | -------------------------------------------------------------------------------- /tests/old/storedummy.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | from decimal import Decimal 6 | import logging 7 | import traceback 8 | 9 | from aiohttp import web 10 | 11 | from manta.store import Store 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | routes = web.RouteTableDef() 16 | store = Store('dummy_store') 17 | 18 | 19 | @routes.post("/merchant_order") 20 | async def merchant_order(request: web.Request): 21 | try: 22 | json = await request.json() 23 | logger.info("New http requets: %s" % json) 24 | json['amount'] = Decimal(json['amount']) 25 | 26 | reply = await store.merchant_order_request(**json) 27 | 28 | return web.Response(body=reply.to_json(), content_type="application/json") 29 | 30 | except Exception: 31 | traceback.print_exc() 32 | raise web.HTTPInternalServerError() 33 | 34 | 35 | logging.basicConfig(level=logging.INFO) 36 | app = web.Application() 37 | app.add_routes(routes) 38 | web.run_app(app, port=8080) 39 | -------------------------------------------------------------------------------- /tests/old/storeecho.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | import logging 6 | import time 7 | from threading import Timer 8 | 9 | import paho.mqtt.client as mqtt 10 | 11 | from manta.messages import AckMessage, Status 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | WAITING_INTERVAL = 4 16 | 17 | 18 | def on_connect(client: mqtt.Client, userdata, flags, rc): 19 | logger.info("Connected") 20 | client.subscribe("acks/#") 21 | 22 | 23 | def on_message(client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): 24 | ack: AckMessage = AckMessage.from_json(msg.payload) 25 | tokens = msg.topic.split('/') 26 | session_id = tokens[1] 27 | 28 | if ack.status == Status.NEW: 29 | new_ack = AckMessage(txid=ack.txid, 30 | status=Status.PENDING, 31 | transaction_currency="NANO", 32 | transaction_hash="B50DB45966850AD4B11ECFDD8AE0A7AD97DF74864631D8C1495E46DDDFC1802A") 33 | logging.info("Waiting...") 34 | 35 | time.sleep(WAITING_INTERVAL) 36 | 37 | logging.info("Sending first ack...") 38 | 39 | client.publish("acks/{}".format(session_id), new_ack.to_json()) 40 | 41 | new_ack.status = Status.PAID 42 | 43 | logging.info("Waiting...") 44 | 45 | def pub_second_ack(): 46 | logging.info("Sending second ack...") 47 | client.publish("acks/{}".format(session_id), new_ack.to_json()) 48 | 49 | t = Timer(WAITING_INTERVAL, pub_second_ack) 50 | t.start() 51 | 52 | 53 | if __name__ == "__main__": 54 | logging.basicConfig(level=logging.INFO) 55 | client = mqtt.Client(protocol=mqtt.MQTTv31) 56 | 57 | client.on_connect = on_connect 58 | client.on_message = on_message 59 | 60 | client.connect("localhost") 61 | client.loop_forever() 62 | -------------------------------------------------------------------------------- /tests/old/swagger/wallet.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | description: "Manta Dummy Wallet for Integration Testing" 4 | version: "1.0.0" 5 | title: "Manta Dummy Wallet" 6 | 7 | paths: 8 | /scan: 9 | post: 10 | description: Simulate scan QR 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/xml 15 | parameters: 16 | - in: body 17 | name: url 18 | schema: 19 | type: object 20 | required: 21 | - url 22 | properties: 23 | url: 24 | type: string 25 | 26 | 27 | responses: 28 | 405: 29 | description: "Invalid input" -------------------------------------------------------------------------------- /tests/old/walletdummy.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | WALLET = BAC42739F46912DD58227DC67CAE40057FF4A3504188236B58312ADCA840A9AD 3 | ACCOUNT = xrb_1rtp8rgjq3tkekhjfaptza8hatc8djsmcpb6owanomaiysugxuhmiik4qok3 4 | CERTIFICATE = /Users/avigano/Developer/manta-python/certificates/root/certs/AppiaDeveloperCA.crt 5 | INTERACTIVE = True 6 | -------------------------------------------------------------------------------- /tests/old/walletdummy.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | import argparse 6 | import asyncio 7 | from concurrent.futures._base import TimeoutError 8 | import configparser 9 | import logging 10 | import os 11 | import sys 12 | import traceback 13 | 14 | from aiohttp import web 15 | from cryptography.x509 import NameOID 16 | import inquirer 17 | import nano 18 | 19 | from manta.messages import verify_chain, PaymentRequestEnvelope 20 | from manta.wallet import Wallet 21 | 22 | ONCE = False 23 | 24 | logger = logging.getLogger(__name__) 25 | routes = web.RouteTableDef() 26 | app = web.Application() 27 | 28 | 29 | def query_yes_no(question, default="yes"): 30 | """Ask a yes/no question via raw_input() and return their answer. 31 | 32 | "question" is a string that is presented to the user. 33 | "default" is the presumed answer if the user just hits . 34 | It must be "yes" (the default), "no" or None (meaning 35 | an answer is required of the user). 36 | 37 | The "answer" return value is True for "yes" or False for "no". 38 | """ 39 | valid = {"yes": True, "y": True, "ye": True, 40 | "no": False, "n": False} 41 | if default is None: 42 | prompt = " [y/n] " 43 | elif default == "yes": 44 | prompt = " [Y/n] " 45 | elif default == "no": 46 | prompt = " [y/N] " 47 | else: 48 | raise ValueError("invalid default answer: '%s'" % default) 49 | 50 | while True: 51 | sys.stdout.write(question + prompt) 52 | choice = input().lower() 53 | if default is not None and choice == '': 54 | return valid[default] 55 | elif choice in valid: 56 | return valid[choice] 57 | else: 58 | sys.stdout.write("Please respond with 'yes' or 'no' " 59 | "(or 'y' or 'n').\n") 60 | 61 | 62 | async def get_payment_request(wallet: Wallet, crypto_currency: str = 'all') -> PaymentRequestEnvelope: 63 | try: 64 | envelope = await wallet.get_payment_request(crypto_currency) 65 | except TimeoutError as e: 66 | if ONCE: 67 | print("Timeout exception in waiting for payment") 68 | sys.exit(1) 69 | else: 70 | raise e 71 | 72 | return envelope 73 | 74 | 75 | def verify_envelope(envelope: PaymentRequestEnvelope, certificate, ca_certificate) -> bool: 76 | verified = False 77 | 78 | if ca_certificate: 79 | path = verify_chain(certificate, ca_certificate) 80 | 81 | if path: 82 | if envelope.verify(certificate): 83 | verified = True 84 | logger.info("Verified Request") 85 | logger.info("Certificate issued to {}".format( 86 | certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value)) 87 | else: 88 | logger.error("Invalid Signature") 89 | else: 90 | logger.error("Invalid Certification Path") 91 | 92 | return verified 93 | 94 | 95 | async def get_payment(url: str, 96 | interactive: bool = False, 97 | nano_wallet: str = None, 98 | account: str = None, 99 | ca_certificate: str = None): 100 | wallet = Wallet.factory(url) 101 | 102 | envelope = await get_payment_request(wallet) 103 | 104 | verified = False 105 | certificate = None 106 | 107 | if ca_certificate: 108 | certificate = await wallet.get_certificate() 109 | 110 | verified = verify_envelope(envelope, certificate, ca_certificate) 111 | 112 | pr = envelope.unpack() 113 | 114 | logger.info("Payment request: {}".format(pr)) 115 | 116 | options = [x for x in pr.supported_cryptos] 117 | 118 | questions = [inquirer.List('crypto', 119 | message=' What crypto you want to pay with?', 120 | choices=options)] 121 | 122 | if interactive: 123 | answers = inquirer.prompt(questions) 124 | 125 | chosen_crypto = answers['crypto'] 126 | 127 | # Check if we have already the destination 128 | destination = pr.get_destination(chosen_crypto) 129 | 130 | # Otherwise ask payment provider 131 | if not destination: 132 | logger.info('Requesting payment request for {}'.format(chosen_crypto)) 133 | envelope = await get_payment_request(wallet, chosen_crypto) 134 | verified = False 135 | 136 | if ca_certificate: 137 | verified = verify_envelope(envelope, certificate, ca_certificate) 138 | 139 | pr = envelope.unpack() 140 | logger.info("Payment request: {}".format(pr)) 141 | 142 | destination = pr.get_destination(chosen_crypto) 143 | 144 | if answers['crypto'] == 'NANO': 145 | rpc = nano.rpc.Client(host="http://localhost:7076") 146 | balance = rpc.account_balance(account=account) 147 | print() 148 | print("Actual balance: {}".format( 149 | str(nano.convert(from_unit="raw", to_unit="XRB", value=balance['balance'])) 150 | )) 151 | 152 | if not verified: 153 | print("WARNING!!!! THIS IS NOT VERIFIED REQUEST") 154 | 155 | destination = pr.get_destination('NANO') 156 | 157 | if query_yes_no("Pay {} {} ({} {}) to {}".format(destination.amount, 158 | destination.crypto_currency, 159 | pr.amount, 160 | pr.fiat_currency, 161 | pr.merchant)): 162 | amount = int(nano.convert(from_unit='XRB', to_unit="raw", value=destination.amount)) 163 | 164 | print(amount) 165 | 166 | block = rpc.send(wallet=nano_wallet, 167 | source=account, 168 | destination=destination.destination_address, 169 | amount=amount) 170 | 171 | await wallet.send_payment(transaction_hash=block, crypto_currency='NANO') 172 | elif answers['crypto'] == 'TESTCOIN': 173 | destination = pr.get_destination('TESTCOIN') 174 | await wallet.send_payment(transaction_hash='test_hash', crypto_currency='TESTCOIN') 175 | else: 176 | print("Not supported!") 177 | sys.exit() 178 | 179 | else: 180 | await wallet.send_payment("myhash", pr.destinations[0].crypto_currency) 181 | 182 | ack = await wallet.acks.get() 183 | print(ack) 184 | 185 | 186 | @routes.post("/scan") 187 | async def scan(request: web.Request): 188 | try: 189 | json = await request.json() 190 | logger.info("Got scan request for {}".format(json['url'])) 191 | await get_payment(json['url']) 192 | 193 | return web.json_response("ok") 194 | 195 | except Exception: 196 | traceback.print_exc() 197 | raise web.HTTPInternalServerError() 198 | 199 | 200 | logging.basicConfig(level=logging.INFO) 201 | 202 | config = configparser.ConfigParser() 203 | 204 | folder = os.path.dirname(os.path.realpath(__file__)) 205 | 206 | file = os.path.join(folder, 'walletdummy.conf') 207 | 208 | config.read(file) 209 | default = config['DEFAULT'] 210 | 211 | parser = argparse.ArgumentParser(description="Wallet Dummy for Testing") 212 | parser.add_argument('url', metavar="url", type=str, nargs="?") 213 | parser.add_argument('-i', '--interactive', action='store_true', default=default.get('interactive', False)) 214 | parser.add_argument('--wallet', type=str, default=default.get('wallet', None)) 215 | parser.add_argument('--account', type=str, default=default.get('account', None)) 216 | parser.add_argument('--certificate', type=str, default=default.get('certificate', None)) 217 | 218 | ns = parser.parse_args() 219 | 220 | print(ns) 221 | 222 | if len([x for x in (ns.wallet, ns.account) if x is not None]) == 1: 223 | parser.error("--wallet and --account must be given together") 224 | 225 | if ns.url: 226 | ONCE = True 227 | loop = asyncio.get_event_loop() 228 | 229 | loop.run_until_complete(get_payment(url=ns.url, 230 | interactive=ns.interactive, 231 | nano_wallet=ns.wallet, 232 | account=ns.account, 233 | ca_certificate=ns.certificate)) 234 | 235 | else: 236 | app.add_routes(routes) 237 | # swagger_file = os.path.join(os.path.dirname(__file__), 'swagger/wallet.yaml') 238 | # setup_swagger(app, swagger_from_file=swagger_file) 239 | web.run_app(app, port=8082) 240 | -------------------------------------------------------------------------------- /tests/pos_payment_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "amount": 5, 3 | "session_id": "1423", 4 | "crypto_currency": "nano", 5 | "fiat_currency": "eur" 6 | } 7 | -------------------------------------------------------------------------------- /tests/unit/test_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | 6 | def test_msg2config(): 7 | from manta.messages import Destination 8 | import file_config 9 | 10 | from manta.testing import msg2config 11 | 12 | DestinationConfig = msg2config(Destination) 13 | 14 | assert file_config.utils.is_config_type(DestinationConfig) 15 | assert len(Destination.__attrs_attrs__) == \ 16 | len(DestinationConfig.__attrs_attrs__) 17 | 18 | 19 | @pytest.mark.filterwarnings("ignore:unhandled translation") 20 | @pytest.mark.filterwarnings("ignore:field modifier") 21 | @pytest.mark.xfail 22 | def test_config_validation(config_str): 23 | import file_config 24 | 25 | from manta.testing.config import IntegrationConfig 26 | 27 | config = IntegrationConfig.loads_yaml(config_str) 28 | 29 | assert file_config.validate(config) is None 30 | 31 | 32 | @pytest.mark.filterwarnings("ignore:unhandled translation") 33 | @pytest.mark.filterwarnings("ignore:field modifier") 34 | def test_config_data_validation(config_str): 35 | from decimal import Decimal 36 | 37 | from manta.testing.config import IntegrationConfig 38 | 39 | config = IntegrationConfig.loads_yaml(config_str) 40 | 41 | assert isinstance(config.payproc.destinations[0].amount, Decimal) 42 | 43 | 44 | @pytest.mark.filterwarnings("ignore:unhandled translation") 45 | def test_config2msg(): 46 | from decimal import Decimal 47 | 48 | import file_config 49 | 50 | from manta.messages import Destination 51 | from manta.testing import config2msg 52 | from manta.testing.config import DummyPayProcConfig 53 | 54 | dest = Destination( 55 | amount=Decimal("0.01"), 56 | destination_address="xrb_3d1ab61eswzsgx5arwqc3gw8xjcsxd7a5egtr69jixa5it9yu9fzct9nyjyx", 57 | crypto_currency="NANO" 58 | ) 59 | cfg_dest = DummyPayProcConfig.DestinationConfig( 60 | amount=Decimal("0.01"), 61 | destination_address="xrb_3d1ab61eswzsgx5arwqc3gw8xjcsxd7a5egtr69jixa5it9yu9fzct9nyjyx", 62 | crypto_currency="NANO" 63 | ) 64 | 65 | assert file_config.validate(cfg_dest) is None 66 | 67 | converted = config2msg(cfg_dest, Destination) 68 | assert converted == dest 69 | 70 | 71 | def test_config_defaults(): 72 | from textwrap import dedent 73 | 74 | from file_config import config, var 75 | 76 | @config 77 | class TestConfig: 78 | foo = var(str, default="Default", required=False) 79 | bar = var(str, default="Default", required=False) 80 | 81 | yaml = dedent("""\ 82 | foo: goofy 83 | """) 84 | 85 | json = dedent("""\ 86 | {"foo": "goofy"} 87 | """) 88 | 89 | internal_cfg = TestConfig(foo="goofy") 90 | yaml_cfg = TestConfig.loads_yaml(yaml) 91 | json_cfg = TestConfig.loads_json(json) 92 | 93 | assert internal_cfg.foo == "goofy" and internal_cfg.bar == "Default" 94 | assert json_cfg.foo == "goofy" and json_cfg.bar == "Default" 95 | assert yaml_cfg.foo == "goofy" and yaml_cfg.bar == "Default" 96 | 97 | 98 | def test_unique_list(): 99 | from textwrap import dedent 100 | from typing import List 101 | 102 | import file_config 103 | from file_config import config, var 104 | import jsonschema.exceptions 105 | 106 | @config 107 | class ListCfg: 108 | 109 | foo = var(List[str], unique=True) 110 | 111 | cfg = ListCfg(foo=['a', 'b', 'c']) 112 | 113 | assert file_config.validate(cfg) is None 114 | 115 | with pytest.raises(jsonschema.exceptions.ValidationError): 116 | cfg = ListCfg(foo=['a', 'a', 'c']) 117 | assert file_config.validate(cfg) is None 118 | 119 | yaml = dedent("""\ 120 | foo: 121 | - a 122 | - a 123 | - c 124 | """) 125 | 126 | cfg = ListCfg.loads_yaml(yaml) 127 | with pytest.raises(jsonschema.exceptions.ValidationError): 128 | assert file_config.validate(cfg) is None 129 | -------------------------------------------------------------------------------- /tests/unit/test_dispatcher.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | from unittest.mock import MagicMock 6 | 7 | from manta.dispatcher import Dispatcher 8 | 9 | 10 | # def test_dispatcher(): 11 | # d = Dispatcher() 12 | # m = MagicMock() 13 | # d.callbacks.append(("^payment_requests/(.*)", m)) 14 | # d.dispatch("payment_requests/leonardo") 15 | # 16 | # m.assert_called_with("leonardo") 17 | 18 | 19 | def test_mqtt_to_regex(): 20 | r = Dispatcher.mqtt_to_regex("payment_requests/+") 21 | assert "payment_requests/([^/]+)$" == r 22 | 23 | 24 | def test_register_topic(): 25 | d = Dispatcher() 26 | m = MagicMock() 27 | 28 | @d.topic("payment_requests/+") 29 | def my_callback(arg1): 30 | m(arg1) 31 | 32 | d.dispatch("payment_requests/leonardo") 33 | m.assert_called_with("leonardo") 34 | 35 | 36 | def test_register_topic_b(): 37 | d = Dispatcher() 38 | m = MagicMock() 39 | 40 | @d.topic("merchant_order_request/+") 41 | def my_callback(arg1): 42 | m(arg1) 43 | 44 | d.dispatch("merchant_order_request/leonardo/123") 45 | m.assert_not_called() 46 | 47 | 48 | def test_register_topic_multiple_args(): 49 | d = Dispatcher() 50 | m = MagicMock() 51 | 52 | assert d.callbacks == [] 53 | 54 | @d.topic("payment_requests/+/subtopic/+") 55 | def my_callback2(*args): 56 | m(*args) 57 | 58 | d.dispatch("payment_requests/arg1/subtopic/arg2") 59 | m.assert_called_with("arg1", "arg2") 60 | 61 | 62 | def test_register_topic_multiple_args_kwargs(): 63 | d = Dispatcher() 64 | m = MagicMock() 65 | 66 | assert d.callbacks == [] 67 | 68 | @d.topic("payment_requests/+/subtopic/+") 69 | def my_callback2(arg1, arg2, payload): 70 | m(arg1, arg2, payload) 71 | 72 | d.dispatch("payment_requests/arg1/subtopic/arg2", payload= "my_payload") 73 | m.assert_called_with("arg1", "arg2", "my_payload") 74 | 75 | 76 | def test_register_topic_multiple_args_pound(): 77 | d = Dispatcher() 78 | m = MagicMock() 79 | 80 | assert d.callbacks == [] 81 | 82 | @d.topic("payment_requests/+/subtopic/+/subtopic2/#") 83 | def my_callback3(*args): 84 | m(*args) 85 | 86 | d.dispatch("payment_requests/arg1/subtopic/arg2/subtopic2/arg3/arg4") 87 | m.assert_called_with("arg1", "arg2", "arg3", "arg4") 88 | 89 | 90 | def test_register_in_class(): 91 | m = MagicMock() 92 | 93 | class MyClass(): 94 | def __init__(self): 95 | self.d = Dispatcher(self) 96 | 97 | @Dispatcher.method_topic("payment_requests/+/subtopic/+") 98 | def my_method(self, *args): 99 | m(*args) 100 | 101 | c = MyClass() 102 | c.d.dispatch("payment_requests/arg1/subtopic/arg2") 103 | m.assert_called_with("arg1", "arg2") 104 | 105 | 106 | def test_register_in_class_with_kwargs(): 107 | m = MagicMock() 108 | 109 | class MyClass(): 110 | def __init__(self): 111 | self.d = Dispatcher(self) 112 | 113 | @Dispatcher.method_topic("payment_requests/+/subtopic/+") 114 | def my_method(self, arg1, arg2, payload): 115 | m(arg1, arg2, payload) 116 | 117 | c = MyClass() 118 | c.d.dispatch("payment_requests/arg1/subtopic/arg2", payload="mypayload") 119 | m.assert_called_with("arg1", "arg2", "mypayload") 120 | -------------------------------------------------------------------------------- /tests/unit/test_messages.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | from manta.messages import MerchantOrderRequestMessage 6 | 7 | 8 | def test_serialize(): 9 | payload = '{"amount": 10, "session_id": "pXbNKx8YRJ2dsIjJIfEuQA==", "fiat_currency": "eur", "crypto_currency": null}' 10 | order = MerchantOrderRequestMessage.from_json(payload) 11 | -------------------------------------------------------------------------------- /tests/unit/test_payproc.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | import pytest 6 | import simplejson as json 7 | import attr 8 | from callee import Matcher 9 | from cryptography.hazmat.primitives import serialization 10 | 11 | from manta.messages import ( 12 | Destination, 13 | MerchantOrderRequestMessage, 14 | PaymentRequestMessage, 15 | PaymentMessage, 16 | AckMessage, 17 | Status, 18 | Merchant, 19 | ) 20 | from manta.payproc import PayProc, TXStorageMemory 21 | 22 | # pytest.register_assert_rewrite("tests.utils") 23 | from tests.utils import JsonContains 24 | from decimal import Decimal 25 | 26 | PRIV_KEY_DATA = b"""\ 27 | -----BEGIN RSA PRIVATE KEY----- 28 | MIIEpQIBAAKCAQEA1O+cC40Xtt1F87C7SHSckdEI6PeJKGaNGBfKLI8q+G3tbjFY 29 | e+A83skUkkcQd9yICZQmVZ4Qqd3LaaPhOy+sDxkvytcTMwHj/N3IBq9IpZK1nVAm 30 | 1s2pNcvsAGTdKPfB0P8hHtNNrwi3c2G0NOP0UVDe3yDzeAacJMtbIHrMDG7Sb8Ob 31 | ITd1NZd2NNv9Ou/RfmAb/pnigyHITZdzDGPCkP4x3Y3iLLQbdbC8U4nIHKw39HTA 32 | N/+FYkWDFRIz8fdcI3EbksEvcgs3f63OR9uHvUNkqvueAwBTa2bFopzs7Li+oAuV 33 | hA/UgXwgpsFF1pSzZz8WWYkcyDeJG9O3hRPc9QIDAQABAoIBAQCmxC4DQfJDrlK9 34 | wzk6StHgxcTjqBJMiNyR9PfLJCl0PavJNG5nPjyOAw/QbEWyig4k6lmHjm7giqtn 35 | xgh88R4hCQnMI9uOhDmJbizdR2RvAFKqrP5uFs4iKt5fhJ9NGZU62MWYvcbGgd4j 36 | SG75SVqsYNjcCZOE+jBKBNYOvv2V8bppPhNIXs8iS3sUbZYDN5M4jQHo4O0j1MQi 37 | 2wDQpwAHg04QSnTERfz8K7yDucY7BBNSFQNHTG2fDq6uJk5Llj5NmLdMol+TSnHM 38 | qnS5viuyHYHPl+WCT+QzUS+0kx5F71N2tLeX8fdMmY2GRmElkzFCOQ5kc5zU95lH 39 | XCdwgoqVAoGBANaC597cyoX73SbBy0pOm7VW+16CUP76F0H1nIeEOTq5HkNf5LKB 40 | grigij0r6wJxN85l4hmxoMX8f4PpG3PY64gR2HpR+GYPtdNuieZnhb3Q+r/DpnkE 41 | 0OIjqngQCPlqT1UAkrG1GDYLOEtirkJKAoBPhRx9mi6JLFju3ki7UkLfAoGBAP4e 42 | s8gqYTIxoTnHV8U6jnuleYGhKLk3bI1CJ1JRk9S+tJsvPOErd0lQq545suwhYXUp 43 | j85FDbgSw0eiAZJBz/jwJioSan1QgfcBxahXyMqTLwsDza4U8mV37dhTGKXXgLFe 44 | rAvmlLVHDYWsmHIevKFSeqo77Nlx6Q5+jyR6pw6rAoGAP8A10vkBQ2J/7iXIGfRU 45 | uEb6e7L1CWIgCV1KQMgeDgK4KMPV/usYg3BKxTVJKbemIzQKRyKQKmcJKpXbr8k2 46 | 7oCHOosj7Ikcu5Jtb0ky6R+zdcxarDqvLZX18qqpUB61Jwj9j8zHPkCFYXoZWeAO 47 | 8D0xzS7S5KOlx2RuMWViZDcCgYEAid9UgWRk6Zu9sqByAWL8zR8BZpBujNcCQT3E 48 | Ich64XE6gfvGFxDDHnbzNdxuM+kEfFG5YRtcDyO26ZV/LsAgOxroSelF94mHieFf 49 | QS+nlCj43AwLOsjInr7Lv5OOCuR6QUFxLN/EjPno3z6+UyRUCV67iMMMhQllfeSy 50 | ewNEwhMCgYEAijETQlQTaEBRj4UD9rH4vUZfNnZO1NOetDBeXaLRLU7NJxTeZ/Tt 51 | chFd+tlGvwi4ynJ4lPsoyMYvD8DFj0nXUUpDD/b07fsPPDrRgnKHiiPJF2rUN0IB 52 | RWBnMUHLYluKopDqkoVAulUZ/QLWhmwvV4CV7G5PtIDpzmT3ycF2hqs= 53 | -----END RSA PRIVATE KEY----- 54 | """ 55 | 56 | CERT_DATA = b"""\ 57 | -----BEGIN CERTIFICATE----- 58 | MIIDQjCCAiqgAwIBAgIRAPr1QV+OmLIjsRH4mOxjy1EwDQYJKoZIhvcNAQELBQAw 59 | KDEmMCQGA1UEAxMdTmFub3JheSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTgw 60 | NjEyMDUyMDI5WhcNMTkwNjEyMDUyMDI5WjAeMRwwGgYDVQQDExN3d3cuYnJhaW5i 61 | bG9ja3MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1O+cC40X 62 | tt1F87C7SHSckdEI6PeJKGaNGBfKLI8q+G3tbjFYe+A83skUkkcQd9yICZQmVZ4Q 63 | qd3LaaPhOy+sDxkvytcTMwHj/N3IBq9IpZK1nVAm1s2pNcvsAGTdKPfB0P8hHtNN 64 | rwi3c2G0NOP0UVDe3yDzeAacJMtbIHrMDG7Sb8ObITd1NZd2NNv9Ou/RfmAb/pni 65 | gyHITZdzDGPCkP4x3Y3iLLQbdbC8U4nIHKw39HTAN/+FYkWDFRIz8fdcI3EbksEv 66 | cgs3f63OR9uHvUNkqvueAwBTa2bFopzs7Li+oAuVhA/UgXwgpsFF1pSzZz8WWYkc 67 | yDeJG9O3hRPc9QIDAQABo3EwbzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI 68 | KwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTTRDdHAcsOFNpRxBt4RlQbwagO 69 | qjAfBgNVHSMEGDAWgBRwkhWOFhgXtaH1sKfkCQL78Mfy5TANBgkqhkiG9w0BAQsF 70 | AAOCAQEAmJIyiA+m9duBYR+p1IwR/DyEssZ0YtCVaq375c567a6owPZSUEGi+cUj 71 | xsEsxPkl6DrgZzEathvoUVNVlA1YyHwIFXp0n5Qd0OlQ66WnQD16i4CygdGTpAex 72 | 8oWK/6mUdXxIIEUHaiv5UYHQhfwCb+c0yNFeN+uQ2SfDwID20NjZNLGKQzYZ+JVI 73 | QED2ofs5p/xm7qe/Ht58u6TqAYjxDO4OqSspzH2e6a2EIjVe81DvrfHnatDUar5m 74 | +XkSTmuqyX0wmxZ2E2hhlJkhyLCadkP3Hor2s3nUpkqKH3bSUJ5U/TuvvxrEEt0I 75 | z4TYl3Vuacma4wEMQGqhJSWv0gjRQg== 76 | -----END CERTIFICATE----- 77 | """ 78 | 79 | HELLO_SIGNED = ( 80 | b"LJH1BHPP/KmEnqyz24eb3ph8nyhS9TjVT1jnw7oSU3vbwoj9MMePwBifBbnpvFHl6KSUnTcX0I3OK6MSdF" 81 | b"m6/1I+i7RkyNeAIkN/boF46xRucuaaevfk5PWuHKJSPsQt6QLs3TyQUet+WLTu8sxIs29+wLTn71dzFfAe45YesIOoKhboyiPO23" 82 | b"Di8sLuFQCiW4uau4SttMK8+MCHMmQzShdu922JMHFv1l2sbqfnM0LNFzWIbVs35Q4pNow0P6gzECSOpREwdy5S793YJdA7goZNCM" 83 | b"QB6LpOEnuXBeA1wJ5t3fnSANUvXewyaMiNIXz93vh9UrDel7NITHo46dVKXw==" 84 | ) 85 | 86 | KEY_FILENAME = "certificates/root/keys/test.key" 87 | CERTIFICATE_FILENAME = "certificates/root/certs/AppiaDeveloperCA.crt" 88 | 89 | DESTINATIONS = [ 90 | Destination( 91 | amount=Decimal("5"), destination_address="btc_daddress", crypto_currency="btc" 92 | ), 93 | Destination( 94 | amount=Decimal("10"), 95 | destination_address="nano_daddress", 96 | crypto_currency="nano", 97 | ), 98 | ] 99 | 100 | MERCHANT = Merchant(name="Merchant 1", address="5th Avenue") 101 | 102 | 103 | @pytest.fixture 104 | def payproc(): 105 | # noinspection PyUnusedLocal 106 | def get_destinations(device, merchant_order: MerchantOrderRequestMessage): 107 | if merchant_order.crypto_currency: 108 | destination = next( 109 | x 110 | for x in DESTINATIONS 111 | if x.crypto_currency == merchant_order.crypto_currency 112 | ) 113 | return [destination] 114 | else: 115 | return DESTINATIONS 116 | 117 | pp = PayProc(KEY_FILENAME, cert_file=CERTIFICATE_FILENAME) 118 | pp.get_merchant = lambda x: MERCHANT 119 | 120 | pp.get_destinations = get_destinations 121 | pp.get_supported_cryptos = lambda device, payment_request: {"btc", "xmr", "nano"} 122 | return pp 123 | 124 | 125 | def test_key_from_keydata(): 126 | key = PayProc.key_from_keydata(PRIV_KEY_DATA) 127 | assert PRIV_KEY_DATA == key.private_bytes( 128 | encoding=serialization.Encoding.PEM, 129 | format=serialization.PrivateFormat.TraditionalOpenSSL, 130 | encryption_algorithm=serialization.NoEncryption(), 131 | ) 132 | 133 | 134 | def test_sign(): 135 | pp = PayProc(KEY_FILENAME) 136 | print(pp.sign(b"Hello")) 137 | assert HELLO_SIGNED == pp.sign(b"Hello") 138 | 139 | 140 | def test_generate_payment_request(): 141 | pp = PayProc(KEY_FILENAME, cert_file=CERTIFICATE_FILENAME) 142 | pp.get_merchant = lambda x: MERCHANT 143 | pp.get_destinations = lambda device, payment_request: [ 144 | Destination( 145 | amount=Decimal(5), destination_address="xrb123", crypto_currency="NANO" 146 | ) 147 | ] 148 | pp.get_supported_cryptos = lambda device, payment_request: ["BTC", "XMR", "NANO"] 149 | 150 | payment_request = MerchantOrderRequestMessage( 151 | amount=Decimal(10), 152 | fiat_currency="EURO", 153 | session_id="123", 154 | crypto_currency="NANO", 155 | ) 156 | 157 | envelope = pp.generate_payment_request("device1", payment_request) 158 | 159 | expected_message = PaymentRequestMessage( 160 | merchant=MERCHANT, 161 | amount=Decimal(10), 162 | fiat_currency="EURO", 163 | destinations=[ 164 | Destination( 165 | amount=Decimal(5), destination_address="xrb123", crypto_currency="NANO" 166 | ) 167 | ], 168 | supported_cryptos={"BTC", "XMR", "NANO"}, 169 | ) 170 | 171 | assert expected_message == envelope.unpack() 172 | 173 | 174 | def test_on_connect(mock_mqtt, payproc): 175 | payproc.run() 176 | 177 | with open(CERTIFICATE_FILENAME, "r") as myfile: 178 | certificate = myfile.read() 179 | 180 | mock_mqtt.subscribe.assert_any_call("merchant_order_request/+") 181 | mock_mqtt.subscribe.assert_any_call("merchant_order_cancel/+") 182 | mock_mqtt.publish.assert_called_with("certificate", certificate, retain=True) 183 | 184 | 185 | def test_receive_merchant_order_request(mock_mqtt, payproc): 186 | request = MerchantOrderRequestMessage( 187 | amount=Decimal("1000"), session_id="1423", fiat_currency="eur", 188 | ) 189 | 190 | expected = AckMessage(txid="0", url="manta://localhost/1423", status=Status.NEW) 191 | 192 | mock_mqtt.push("merchant_order_request/device1", request.to_json()) 193 | 194 | mock_mqtt.publish.assert_any_call("acks/1423", JsonContains(expected)) 195 | mock_mqtt.subscribe.assert_any_call("payments/1423") 196 | mock_mqtt.subscribe.assert_any_call("payment_requests/1423/+") 197 | 198 | 199 | def test_receive_merchant_order_request_unkwnown_field(mock_mqtt, payproc): 200 | request = MerchantOrderRequestMessage( 201 | amount=Decimal("1000"), session_id="1423", fiat_currency="eur" 202 | ) 203 | 204 | expected = AckMessage(txid="0", url="manta://localhost/1423", status=Status.NEW) 205 | 206 | request_json = request.unstructure() 207 | request_json["extra_field"] = "extra" 208 | 209 | mock_mqtt.push("merchant_order_request/device1", json.dumps(request_json)) 210 | 211 | mock_mqtt.publish.assert_any_call("acks/1423", JsonContains(expected)) 212 | mock_mqtt.subscribe.assert_any_call("payments/1423") 213 | mock_mqtt.subscribe.assert_any_call("payment_requests/1423/+") 214 | 215 | 216 | def test_receive_merchant_order_request_empty_string(mock_mqtt, payproc): 217 | request = MerchantOrderRequestMessage( 218 | amount=Decimal("1000"), 219 | session_id="1423", 220 | fiat_currency="eur", 221 | crypto_currency="", 222 | ) 223 | 224 | expected = AckMessage(txid="0", url="manta://localhost/1423", status=Status.NEW) 225 | 226 | mock_mqtt.push("merchant_order_request/device1", request.to_json()) 227 | 228 | mock_mqtt.publish.assert_any_call("acks/1423", JsonContains(expected)) 229 | mock_mqtt.subscribe.assert_any_call("payments/1423") 230 | 231 | 232 | def test_receive_merchant_cancel_order(mock_mqtt, payproc): 233 | test_receive_merchant_order_request(mock_mqtt, payproc) 234 | 235 | mock_mqtt.push("merchant_order_cancel/1423", "") 236 | 237 | expected = AckMessage( 238 | txid="0", 239 | url="manta://localhost/1423", 240 | status=Status.INVALID, 241 | memo="Canceled by Merchant", 242 | ) 243 | 244 | mock_mqtt.publish.assert_called_with("acks/1423", JsonContains(expected)) 245 | 246 | 247 | def test_receive_merchant_order_request_legacy(mock_mqtt, payproc): 248 | request = MerchantOrderRequestMessage( 249 | amount=Decimal("1000"), 250 | session_id="1423", 251 | fiat_currency="eur", 252 | crypto_currency="btc", 253 | ) 254 | 255 | expected = AckMessage( 256 | txid="0", url="bitcoin:btc_daddress?amount=5", status=Status.NEW 257 | ) 258 | 259 | mock_mqtt.push("merchant_order_request/device1", request.to_json()) 260 | mock_mqtt.publish.assert_any_call("acks/1423", JsonContains(expected)) 261 | 262 | 263 | def test_get_payment_request(mock_mqtt, payproc): 264 | test_receive_merchant_order_request(mock_mqtt, payproc) 265 | mock_mqtt.push("payment_requests/1423/btc", "") 266 | 267 | destination = Destination( 268 | amount=Decimal("5"), destination_address="btc_daddress", crypto_currency="btc" 269 | ) 270 | 271 | expected = PaymentRequestMessage( 272 | merchant=MERCHANT, 273 | fiat_currency="eur", 274 | amount=Decimal("1000"), 275 | destinations=[destination], 276 | supported_cryptos={"nano", "btc", "xmr"}, 277 | ) 278 | 279 | class PMEqual(Matcher): 280 | payment_request: PaymentRequestMessage 281 | 282 | def __init__(self, payment_request: PaymentRequestMessage): 283 | self.payment_request = payment_request 284 | 285 | def match(self, value): 286 | decoded = json.loads(value) 287 | message = PaymentRequestMessage.from_json(decoded["message"]) 288 | assert self.payment_request == message 289 | return True 290 | 291 | mock_mqtt.publish.assert_called_with("payment_requests/1423", PMEqual(expected)) 292 | 293 | 294 | def test_get_payment_request_all(mock_mqtt, payproc): 295 | test_receive_merchant_order_request(mock_mqtt, payproc) 296 | mock_mqtt.push("payment_requests/1423/all", "") 297 | 298 | expected = PaymentRequestMessage( 299 | merchant=MERCHANT, 300 | fiat_currency="eur", 301 | amount=Decimal("1000"), 302 | destinations=DESTINATIONS, 303 | supported_cryptos={"nano", "btc", "xmr"}, 304 | ) 305 | 306 | class PMEqual(Matcher): 307 | payment_request: PaymentRequestMessage 308 | 309 | def __init__(self, payment_request: PaymentRequestMessage): 310 | self.payment_request = payment_request 311 | 312 | def match(self, value): 313 | decoded = json.loads(value) 314 | message = PaymentRequestMessage.from_json(decoded["message"]) 315 | assert self.payment_request == message 316 | return True 317 | 318 | mock_mqtt.publish.assert_called_with("payment_requests/1423", PMEqual(expected)) 319 | 320 | 321 | def test_payment_message(mock_mqtt, payproc): 322 | test_get_payment_request_all(mock_mqtt, payproc) 323 | message = PaymentMessage(crypto_currency="NANO", transaction_hash="myhash") 324 | 325 | ack = AckMessage( 326 | txid="0", 327 | status=Status.PENDING, 328 | transaction_hash="myhash", 329 | transaction_currency="NANO", 330 | ) 331 | 332 | mock_mqtt.push("payments/1423", message.to_json()) 333 | mock_mqtt.publish.assert_called_with("acks/1423", JsonContains(ack)) 334 | 335 | 336 | def test_payment_message_unsupported(mock_mqtt, payproc): 337 | test_get_payment_request_all(mock_mqtt, payproc) 338 | message = PaymentMessage(crypto_currency="ETH", transaction_hash="myhash") 339 | 340 | ack = AckMessage( 341 | txid="0", 342 | status=Status.PENDING, 343 | transaction_hash="myhash", 344 | transaction_currency="NANO", 345 | ) 346 | 347 | mock_mqtt.reset_mock() 348 | mock_mqtt.push("payments/1423", message.to_json()) 349 | mock_mqtt.publish.assert_not_called() 350 | 351 | 352 | def test_confirming(mock_mqtt, payproc): 353 | test_payment_message(mock_mqtt, payproc) 354 | payproc.confirming("1423") 355 | 356 | ack = AckMessage( 357 | txid="0", 358 | status=Status.CONFIRMING, 359 | transaction_hash="myhash", 360 | transaction_currency="NANO", 361 | ) 362 | 363 | mock_mqtt.publish.assert_called_with("acks/1423", JsonContains(ack)) 364 | 365 | 366 | def test_confirm(mock_mqtt, payproc): 367 | test_payment_message(mock_mqtt, payproc) 368 | payproc.confirm("1423") 369 | 370 | ack = AckMessage( 371 | txid="0", 372 | status=Status.PAID, 373 | transaction_hash="myhash", 374 | transaction_currency="NANO", 375 | ) 376 | 377 | mock_mqtt.publish.assert_called_with("acks/1423", JsonContains(ack)) 378 | 379 | 380 | # Lightning network wallets will not send the payment message 381 | # We need to specify transaction_hash and transaction_currency 382 | def test_confirm_without_payment_message(mock_mqtt, payproc): 383 | test_get_payment_request_all(mock_mqtt, payproc) 384 | payproc.confirm("1423", transaction_hash="myhash", transaction_currency="NANO") 385 | 386 | ack = AckMessage( 387 | txid="0", 388 | status=Status.PAID, 389 | transaction_hash="myhash", 390 | transaction_currency="NANO", 391 | ) 392 | 393 | mock_mqtt.publish.assert_called_with("acks/1423", JsonContains(ack)) 394 | 395 | 396 | def test_invalidate(mock_mqtt, payproc): 397 | test_payment_message(mock_mqtt, payproc) 398 | payproc.invalidate("1423", "Timeout") 399 | 400 | ack = AckMessage( 401 | txid="0", 402 | status=Status.INVALID, 403 | transaction_hash="myhash", 404 | transaction_currency="NANO", 405 | memo="Timeout", 406 | ) 407 | 408 | mock_mqtt.publish.assert_called_with("acks/1423", JsonContains(ack)) 409 | 410 | 411 | @pytest.fixture() 412 | def tx_storage() -> TXStorageMemory: 413 | return TXStorageMemory() 414 | 415 | 416 | class TestTXStorageMemory: 417 | def test_create(self, tx_storage: TXStorageMemory): 418 | ack = AckMessage(amount=Decimal("10"), status=Status.NEW, txid="0") 419 | order = MerchantOrderRequestMessage( 420 | amount=Decimal("10"), session_id="123", fiat_currency="EUR" 421 | ) 422 | 423 | tx_storage.create(0, "123", "app0@user0", order, ack) 424 | 425 | assert 1 == len(tx_storage) 426 | 427 | def test_get_state(self, tx_storage: TXStorageMemory): 428 | self.test_create(tx_storage) 429 | state = tx_storage.get_state_for_session("123") 430 | assert Status.NEW == state.ack.status 431 | assert "app0@user0" == state.application 432 | 433 | def test_new_ack(self, tx_storage: TXStorageMemory): 434 | self.test_create(tx_storage) 435 | state = tx_storage.get_state_for_session("123") 436 | new_ack = attr.evolve(state.ack, status=Status.PENDING) 437 | 438 | state.ack = new_ack 439 | 440 | assert 1 == len(tx_storage) 441 | 442 | state = tx_storage.get_state_for_session("123") 443 | 444 | assert state.ack.status == Status.PENDING 445 | 446 | def test_session_does_not_exist(self, tx_storage): 447 | assert not tx_storage.session_exists("123") 448 | 449 | def test_session_exist(self, tx_storage): 450 | self.test_create(tx_storage) 451 | 452 | assert tx_storage.session_exists("123") 453 | 454 | def test_ack_paid(self, tx_storage): 455 | self.test_create(tx_storage) 456 | ack = AckMessage(amount=Decimal("10"), status=Status.NEW, txid="1") 457 | order = MerchantOrderRequestMessage( 458 | amount=Decimal("10"), session_id="321", fiat_currency="EUR" 459 | ) 460 | 461 | tx_storage.create(1, "321", "app0@user0", order, ack) 462 | 463 | state = tx_storage.get_state_for_session("123") 464 | new_ack = attr.evolve(state.ack, status=Status.PAID) 465 | 466 | state.ack = new_ack 467 | 468 | assert 1 == len(tx_storage) 469 | assert Status.NEW == tx_storage.get_state_for_session("321").ack.status 470 | -------------------------------------------------------------------------------- /tests/unit/test_runner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from manta.testing import AppRunnerConfig 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_basic_configuration(config): 10 | from manta.testing.runner import AppRunner 11 | 12 | d = dict(start=False, stop=False) 13 | 14 | def configurator(runner): 15 | 16 | def starter(): 17 | nonlocal d 18 | d['start'] = True 19 | 20 | def stopper(): 21 | nonlocal d 22 | d['stop'] = True 23 | 24 | return AppRunnerConfig(starter=starter, stopper=stopper) 25 | 26 | runn = AppRunner(configurator, config) 27 | assert not d['start'] 28 | assert not d['stop'] 29 | 30 | await runn.start() 31 | assert d['start'] 32 | 33 | await runn.stop() 34 | assert d['stop'] 35 | 36 | 37 | def test_basic_configuration_other_thread(config, event_loop): 38 | import time 39 | import _thread 40 | from manta.testing.runner import AppRunner 41 | 42 | d = dict(start=False, stop=False, ident=_thread.get_ident(), 43 | start_ident=None, stop_ident=None) 44 | 45 | def configurator(runner): 46 | 47 | def starter(): 48 | nonlocal d 49 | d['start'] = True 50 | d['start_ident'] = _thread.get_ident() 51 | 52 | def stopper(): 53 | nonlocal d 54 | d['stop'] = True 55 | d['stop_ident'] = _thread.get_ident() 56 | 57 | return AppRunnerConfig(starter=starter, stopper=stopper) 58 | 59 | runn = AppRunner(configurator, config) 60 | assert not d['start'] 61 | assert not d['stop'] 62 | 63 | async def run(): 64 | await runn.start() 65 | await runn.stop() 66 | 67 | event_loop.run_until_complete(run()) 68 | 69 | assert d['start'] 70 | assert d['stop'] 71 | 72 | assert d['ident'] == d['start_ident'] and d['start_ident'] == d['stop_ident'] 73 | 74 | d = dict(start=False, stop=False, ident=_thread.get_ident(), 75 | start_ident=None, stop_ident=None) 76 | assert not d['start'] 77 | assert not d['stop'] 78 | 79 | runn.start(new_thread=True) 80 | time.sleep(0.2) 81 | runn.stop() 82 | assert d['start'] 83 | assert d['stop'] 84 | 85 | assert d['ident'] != d['start_ident'] and d['start_ident'] == d['stop_ident'] 86 | 87 | 88 | def test_basic_configuration_coro(config, event_loop): 89 | from manta.testing.runner import AppRunner 90 | 91 | d = dict(start=False, stop=False) 92 | 93 | def configurator(runner): 94 | 95 | def starter(): 96 | nonlocal d 97 | 98 | async def start_coro(): 99 | nonlocal d 100 | d['start'] = True 101 | 102 | return start_coro() 103 | 104 | def stopper(): 105 | nonlocal d 106 | d['stop'] = True 107 | 108 | async def stop_coro(): 109 | nonlocal d 110 | d['stop'] = True 111 | 112 | return stop_coro() 113 | 114 | return AppRunnerConfig(starter=starter, stopper=stopper) 115 | 116 | runn = AppRunner(configurator, config) 117 | assert not d['start'] 118 | assert not d['stop'] 119 | 120 | event_loop.run_until_complete(runn.start()) 121 | assert d['start'] 122 | 123 | event_loop.run_until_complete(runn.stop()) 124 | assert d['stop'] 125 | 126 | 127 | @pytest.mark.asyncio 128 | async def test_web_responsiveness(config, web_get): 129 | import aiohttp 130 | import requests 131 | 132 | from manta.testing.runner import AppRunner 133 | 134 | def configurator(runner): 135 | 136 | routes = aiohttp.web.RouteTableDef() 137 | d = dict(web_get=False) 138 | 139 | @routes.get('/test') 140 | async def web_test(request): 141 | nonlocal d 142 | d['web_get'] = True 143 | return aiohttp.web.json_response("ok") 144 | 145 | def starter(): 146 | pass 147 | 148 | def stopper(): 149 | pass 150 | 151 | return AppRunnerConfig(starter=starter, stopper=stopper, 152 | web_routes=routes, 153 | allow_port_reallocation=True, 154 | web_bind_address='localhost', 155 | web_bind_port=9000) 156 | 157 | runn = AppRunner(configurator, config) 158 | 159 | await runn.start() 160 | 161 | resp = await web_get(runn.url + '/test') 162 | assert 'ok' in resp.text 163 | 164 | await runn.stop() 165 | -------------------------------------------------------------------------------- /tests/unit/test_store.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | import re 6 | 7 | import pytest 8 | 9 | from manta.store import Store 10 | from manta.messages import AckMessage, Status, MerchantOrderRequestMessage 11 | 12 | BASE64PATTERN = "(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?" 13 | BASE64PATTERNSAFE = "(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=)?" 14 | 15 | 16 | def reply(topic, payload): 17 | order = MerchantOrderRequestMessage.from_json(payload) 18 | tokens = topic.split("/") 19 | device = tokens[1] 20 | 21 | r = AckMessage( 22 | txid="0", 23 | status=Status.NEW, 24 | url="manta://testpp.com/{}".format(order.session_id) 25 | ) 26 | 27 | return "acks/{}".format(device), r.to_json() 28 | 29 | 30 | @pytest.mark.timeout(2) 31 | @pytest.mark.asyncio 32 | async def test_connect(mock_mqtt): 33 | store = Store("device1") 34 | await store.connect() 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_generate_payment_request(mock_mqtt): 39 | store = Store('device1') 40 | 41 | def se(topic, payload): 42 | nonlocal mock_mqtt 43 | 44 | if topic == "merchant_order_request/device1": 45 | order = MerchantOrderRequestMessage.from_json(payload) 46 | reply = AckMessage( 47 | status=Status.NEW, 48 | url="manta://testpp.com/{}".format(order.session_id), 49 | txid="0" 50 | ) 51 | 52 | topic = "acks/{}".format(order.session_id) 53 | 54 | mock_mqtt.push(topic, reply.to_json()) 55 | 56 | mock_mqtt.publish.side_effect = se 57 | 58 | ack = await store.merchant_order_request(amount=10, fiat='eur') 59 | mock_mqtt.subscribe.assert_any_call("acks/{}".format(store.session_id)) 60 | assert re.match("^manta:\/\/testpp\.com\/" + BASE64PATTERNSAFE + "$", ack.url) 61 | 62 | 63 | @pytest.mark.asyncio 64 | async def test_ack(mock_mqtt): 65 | store = Store('device1') 66 | ack_message: AckMessage = None 67 | 68 | expected_ack = AckMessage(txid='1234', transaction_hash="hash_1234", status=Status.PENDING) 69 | 70 | mock_mqtt.push('acks/123', expected_ack.to_json()) 71 | 72 | ack_message = await store.acks.get() 73 | assert expected_ack == ack_message 74 | -------------------------------------------------------------------------------- /tests/unit/test_wallet.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | from decimal import Decimal 6 | import logging 7 | 8 | from cryptography.hazmat.backends import default_backend 9 | from cryptography.hazmat.primitives.serialization import load_pem_private_key 10 | from cryptography.x509 import NameOID 11 | import pytest 12 | 13 | from manta.messages import (Destination, PaymentRequestMessage, verify_chain, 14 | PaymentMessage, AckMessage, Status, Merchant) 15 | from manta.wallet import Wallet 16 | 17 | # noinspection PyUnresolvedReferences 18 | from tests.utils import JsonContains 19 | 20 | DESTINATIONS = [ 21 | Destination( 22 | amount=Decimal(5), 23 | destination_address="btc_daddress", 24 | crypto_currency="btc" 25 | ), 26 | Destination( 27 | amount=Decimal(10), 28 | destination_address="nano_daddress", 29 | crypto_currency="nano" 30 | ), 31 | 32 | ] 33 | 34 | MERCHANT = Merchant( 35 | name="Merchant 1", 36 | address="5th Avenue" 37 | ) 38 | 39 | PRIVATE_KEY = "certificates/root/keys/test.key" 40 | CERTIFICATE = "certificates/root/certs/test.crt" 41 | CA_CERTIFICATE = "certificates/root/certs/AppiaDeveloperCA.crt" 42 | 43 | 44 | @pytest.fixture 45 | def payment_request(): 46 | with open(PRIVATE_KEY, 'rb') as myfile: 47 | key_data = myfile.read() 48 | 49 | key = load_pem_private_key(key_data, password=None, backend=default_backend()) 50 | 51 | message = PaymentRequestMessage( 52 | merchant=MERCHANT, 53 | amount=Decimal(10), 54 | fiat_currency="euro", 55 | destinations=DESTINATIONS, 56 | supported_cryptos={'btc', 'xmr', 'nano'} 57 | 58 | ) 59 | 60 | return message.get_envelope(key) 61 | 62 | 63 | def test_parse_url(): 64 | match = Wallet.parse_url("manta://localhost/JqhCQ64gTYi02xu4GhBzZg==") 65 | assert "localhost" == match[1] 66 | assert "JqhCQ64gTYi02xu4GhBzZg==" == match[3] 67 | 68 | 69 | def test_parse_url_with_port(): 70 | match = Wallet.parse_url("manta://127.0.0.1:8000/123") 71 | assert "127.0.0.1" == match[1] 72 | assert "8000" == match[2] 73 | assert "123" == match[3] 74 | 75 | 76 | def test_factory(mock_mqtt): 77 | wallet = Wallet.factory("manta://127.0.0.1/123") 78 | assert wallet.host == "127.0.0.1" 79 | assert wallet.port == 1883 80 | assert wallet.session_id == "123" 81 | 82 | 83 | @pytest.mark.asyncio 84 | async def test_get_certificate(mock_mqtt): 85 | with open(CERTIFICATE, 'rb') as myfile: 86 | pem = myfile.read() 87 | 88 | def se(topic): 89 | nonlocal mock_mqtt, pem 90 | 91 | if topic == "certificate": 92 | mock_mqtt.push("certificate", pem) 93 | else: 94 | assert True, "Unknown Topic" 95 | 96 | wallet = Wallet.factory("manta://localhost:8000/123") 97 | 98 | mock_mqtt.subscribe.side_effect = se 99 | certificate = await wallet.get_certificate() 100 | 101 | mock_mqtt.subscribe.assert_called_with("certificate") 102 | assert "test" == certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value 103 | 104 | 105 | @pytest.mark.timeout(2) 106 | @pytest.mark.asyncio 107 | async def test_get_payment_request(mock_mqtt, payment_request, caplog): 108 | caplog.set_level(logging.INFO) 109 | wallet = Wallet.factory("manta://localhost:8000/123") 110 | 111 | # noinspection PyUnusedLocal 112 | def se(topic, payload=None): 113 | nonlocal mock_mqtt, payment_request 114 | 115 | if topic == "payment_requests/123/btc": 116 | mock_mqtt.push("payment_requests/123", payment_request.to_json()) 117 | else: 118 | assert True, "Unknown topic" 119 | 120 | mock_mqtt.publish.side_effect = se 121 | 122 | envelope = await wallet.get_payment_request("btc") 123 | assert envelope.unpack() == payment_request.unpack() 124 | assert envelope.verify(CERTIFICATE) 125 | 126 | @pytest.mark.asyncio 127 | async def test_send_payment(mock_mqtt): 128 | wallet = Wallet.factory("manta://localhost:8000/123") 129 | 130 | await wallet.send_payment(transaction_hash="myhash", crypto_currency="nano") 131 | 132 | expected = PaymentMessage( 133 | transaction_hash="myhash", 134 | crypto_currency="nano" 135 | ) 136 | 137 | mock_mqtt.subscribe.assert_called_with("acks/123") 138 | mock_mqtt.publish.assert_called_with("payments/123", JsonContains(expected), qos=1) 139 | 140 | 141 | @pytest.mark.asyncio 142 | async def test_on_ack(mock_mqtt): 143 | wallet = Wallet.factory("manta://localhost:8000/123") 144 | 145 | expected = AckMessage( 146 | txid="0", 147 | transaction_hash="myhash", 148 | status=Status.PENDING 149 | ) 150 | 151 | mock_mqtt.push("acks/123", expected.to_json()) 152 | 153 | ack_message = await wallet.acks.get() 154 | 155 | assert ack_message == expected 156 | 157 | 158 | def test_verify_chain(): 159 | path = verify_chain(CERTIFICATE, CA_CERTIFICATE) 160 | assert path 161 | 162 | 163 | def test_verify_chain_str(): 164 | with open(CERTIFICATE, 'r') as myfile: 165 | pem = myfile.read() 166 | 167 | path = verify_chain(pem, CA_CERTIFICATE) 168 | assert path 169 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # Manta Python 2 | # Manta Protocol Implementation for Python 3 | # Copyright (C) 2018-2019 Alessandro Viganò 4 | 5 | import difflib 6 | import pprint 7 | from typing import NamedTuple, Dict 8 | from unittest.mock import MagicMock 9 | 10 | from callee import Matcher 11 | import cattr 12 | import pytest 13 | import paho.mqtt.client as mqtt 14 | import simplejson as json 15 | 16 | from manta.messages import Message 17 | 18 | 19 | def is_namedtuple_instance(x): 20 | t = type(x) 21 | b = t.__bases__ 22 | if len(b) != 1 or b[0] != tuple: return False 23 | f = getattr(t, '_fields', None) 24 | if not isinstance(f, tuple): return False 25 | return all(type(n) == str for n in f) 26 | 27 | 28 | class MQTTMock(MagicMock): 29 | def push(self, topic, payload): 30 | self.on_message(self, None, MQTTMessage(topic, payload)) 31 | 32 | 33 | class MQTTMessage(NamedTuple): 34 | topic: any 35 | payload: any 36 | 37 | 38 | def compare_dicts(d1, d2): 39 | return ('\n' + '\n'.join(difflib.ndiff( 40 | pprint.pformat(d1).splitlines(), 41 | pprint.pformat(d2).splitlines()))) 42 | 43 | 44 | class JsonContains(Matcher): 45 | obj: Dict 46 | 47 | def __init__(self, d): 48 | if isinstance(d, Message): 49 | self.obj = cattr.unstructure(d) 50 | else: 51 | self.obj = d 52 | 53 | def match(self, value): 54 | actual = json.loads(value) 55 | # check if subset 56 | assert self.obj.items() <= actual.items(), compare_dicts(self.obj, actual) 57 | # assert self.obj == actual 58 | return True 59 | 60 | 61 | @pytest.fixture 62 | def mock_mqtt(monkeypatch): 63 | mock = MQTTMock() 64 | mock.return_value = mock 65 | 66 | def connect(host, port=1883): 67 | nonlocal mock 68 | mock.on_connect(mock, None, None, None) 69 | 70 | mock.connect.side_effect = connect 71 | 72 | monkeypatch.setattr(mqtt, 'Client', mock) 73 | return mock 74 | --------------------------------------------------------------------------------