├── jobcoin ├── __init__.py ├── cli.py ├── util.py ├── api.py └── jobcoin.py ├── tests ├── __init__.py ├── test_cli.py ├── test_jobcoin.py ├── test_util.py └── test_api.py ├── requirements_test.txt ├── requirements.txt ├── imgs ├── Jobcoin_Mixer.png ├── Mixer_example.png ├── Mixer_example_error.png ├── Mixer_example_error_2.png └── Mixer_example_error_3.png ├── .env_sample ├── Pipfile ├── Makefile ├── tox.ini ├── setup.py ├── .gitignore └── README.md /jobcoin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest==3.5.0 2 | nose==1.3.7 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.7 2 | python-dotenv==0.16.0 3 | requests==2.20.0 4 | tox==3.23.0 -------------------------------------------------------------------------------- /imgs/Jobcoin_Mixer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypherpunk-symposium/coin-mixer-py/HEAD/imgs/Jobcoin_Mixer.png -------------------------------------------------------------------------------- /imgs/Mixer_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypherpunk-symposium/coin-mixer-py/HEAD/imgs/Mixer_example.png -------------------------------------------------------------------------------- /imgs/Mixer_example_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypherpunk-symposium/coin-mixer-py/HEAD/imgs/Mixer_example_error.png -------------------------------------------------------------------------------- /imgs/Mixer_example_error_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypherpunk-symposium/coin-mixer-py/HEAD/imgs/Mixer_example_error_2.png -------------------------------------------------------------------------------- /imgs/Mixer_example_error_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypherpunk-symposium/coin-mixer-py/HEAD/imgs/Mixer_example_error_3.png -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pytest 3 | import nose.tools as nt 4 | from jobcoin import jobcoin 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/test_jobcoin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pytest 3 | import nose.tools as nt 4 | from jobcoin import jobcoin 5 | 6 | 7 | -------------------------------------------------------------------------------- /.env_sample: -------------------------------------------------------------------------------- 1 | API_ADDRESS_URL = '' 2 | API_TRANSACTIONS_URL = '' 3 | SIGNIFICANT_DIGITS = 17 4 | HOUSE_ADDRESS = 'Jobcoin-House' 5 | FEE_PERCENTAGE = 10 6 | MAX_WITHDRAW_VALUE = 1 7 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | click = "==6.7" 8 | requests = "==2.20.0" 9 | python-dotenv = "==0.16.0" 10 | pytest = "==3.5.0" 11 | nose = "==1.3.7" 12 | 13 | 14 | [dev-packages] 15 | 16 | [requires] 17 | python_version = "3.9" 18 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pytest 3 | import nose.tools as nt 4 | from pathlib import Path 5 | from jobcoin import util 6 | 7 | 8 | def test_combine_url_success(): 9 | url = 'http://test.com/' 10 | parameter = 'test.html' 11 | result = util.combine_url(url, parameter) 12 | nt.assert_true(result=='http://test.com/test.html') 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean test install lint 2 | 3 | clean: 4 | @find . -iname '*.py[co]' -delete 5 | @find . -iname '__pycache__' -delete 6 | @rm -rf '.pytest_cache' 7 | @rm -rf dist/ 8 | @rm -rf build/ 9 | @rm -rf *.egg-info 10 | @rm -rf Pipfile.lock 11 | @rm -rf .tox 12 | 13 | test: 14 | tox 15 | 16 | install: 17 | python3 setup.py install 18 | 19 | lint: 20 | tox -e lint 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.4 3 | envlist = formatting, py36, py37, py38, py39, pypy, benchmark 4 | skip_missing_interpreters = true 5 | 6 | [testenv] 7 | setenv = 8 | COVERAGE_FILE = {toxinidir}/.coverage.{envname} 9 | deps = 10 | -r {toxinidir}/requirements_test.txt 11 | commands = 12 | pytest 13 | 14 | [testenv:lint] 15 | skip_install = true 16 | deps = flake8 17 | commands = flake8 jobcoin/ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='jobcoin', 5 | version='0.0.1', 6 | packages=find_packages(include=['jobcoin', 'jobcoin.*']), 7 | author='Mia Stein', 8 | install_requires=[ 9 | 'click', 10 | 'requests', 11 | 'python-dotenv' 12 | ], 13 | entry_points={ 14 | 'console_scripts': ['jobcoin=jobcoin.cli:main'] 15 | }, 16 | ) 17 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pytest 3 | import requests 4 | import nose.tools as nt 5 | from jobcoin import api 6 | 7 | 8 | @pytest.fixture 9 | def response_get_balance(address): 10 | return requests.get('http://jobcoin.gemini.com/aide-sports/api/addresses/{}'.format(address)) 11 | 12 | 13 | def test_get_balance_200(): 14 | response = response_get_balance('Alice') 15 | nt.assert_true(response.ok) 16 | 17 | 18 | def test_get_balance_response_not_none(): 19 | response = response_get_balance('Alice') 20 | nt.assert_is_not_none(response) 21 | 22 | 23 | def test_get_balance_response_empty_address(): 24 | response = response_get_balance('') 25 | nt.assert_true(response.status_code==404) 26 | -------------------------------------------------------------------------------- /jobcoin/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | cli.py 4 | 5 | CLI entry point for Jobcoin app. 6 | """ 7 | 8 | import sys 9 | import click 10 | 11 | import jobcoin.util as util 12 | from jobcoin.jobcoin import Jobcoin 13 | 14 | 15 | @click.command() 16 | def main(args=None): 17 | 18 | client = Jobcoin() 19 | 20 | print('✨ Welcome to the Jobcoin mixer! ✨\n') 21 | 22 | while True: 23 | addresses = click.prompt( 24 | 'Please enter a comma-separated list of new, unused Jobcoin ' 25 | 'addresses where your mixed Jobcoins will be sent.', 26 | prompt_suffix='\n[blank to quit] > ', 27 | default='', 28 | show_default=False) 29 | 30 | # Validate list of addresses 31 | personal_addresses = util.get_addresses_list(addresses) 32 | client.validate_addresses(personal_addresses) 33 | 34 | # Generate a deposit address 35 | deposit_address = util.generate_deposit_address() 36 | click.echo( 37 | '\n✅ You may now send Jobcoins to address {deposit_address}. ' 38 | 'They will be mixed and sent to your destination addresses.' 39 | .format(deposit_address=deposit_address)) 40 | 41 | # Get source address 42 | source_address = click.prompt( 43 | '\nPlease enter the (source) address. ', 44 | prompt_suffix='\n[source address] > ', 45 | default='', 46 | show_default=False) 47 | 48 | # Get number of coins to be deposited 49 | deposit = click.prompt( 50 | '\nPlease enter the value to be transferred.', 51 | prompt_suffix='\n[deposit value] > ', 52 | default='', 53 | show_default=False) 54 | 55 | # Run mix algorithm 56 | print('\n✅ Starting Jobcoin Mixer algorithm') 57 | client.mix_algorithm(deposit, 58 | source_address, 59 | deposit_address, 60 | personal_addresses) 61 | 62 | 63 | if __name__ == '__main__': 64 | sys.exit(main()) 65 | -------------------------------------------------------------------------------- /jobcoin/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | util.py 4 | 5 | Implements util methods for other modules. 6 | """ 7 | 8 | import os 9 | import sys 10 | import uuid 11 | 12 | from pathlib import Path 13 | from dotenv import load_dotenv 14 | 15 | 16 | def get_config(key, env_path=None): 17 | """Given a key, get the value from the env file. 18 | 19 | Arguments: 20 | key (str) 21 | """ 22 | env_path = env_path or Path('.') / '.env' 23 | load_dotenv(dotenv_path=env_path) 24 | value = os.getenv(key) 25 | 26 | if not value: 27 | print('📛 Please set {} in .env'.format(key)) 28 | sys.exit(0) 29 | return value 30 | 31 | 32 | def combine_url(url, parameter): 33 | """Ensures that a URL is well-composed. 34 | 35 | Arguments: 36 | url (str) 37 | parameter (str) 38 | """ 39 | url = url.strip('/') 40 | return '{}/{}'.format(url, parameter) 41 | 42 | 43 | def round_float(num): 44 | """Ensures that a string preserves desired precision when converted to float. 45 | Arguments: 46 | num (str) 47 | Returns: 48 | num (float) 49 | """ 50 | try: 51 | return round(float(num), int(get_config('SIGNIFICANT_DIGITS'))) 52 | except ValueError: 53 | print('📛 Value needs to be a number.') 54 | sys.exit(0) 55 | 56 | 57 | def generate_deposit_address(): 58 | """Creates a disposible hexadecimal address. 59 | 60 | Returns: 61 | hex address (str) 62 | """ 63 | return uuid.uuid4().hex 64 | 65 | 66 | def get_addresses_list(addresses): 67 | """Format a string of addresses. 68 | 69 | Arguments: 70 | addresses (str) 71 | Returns: 72 | addresses (list) 73 | """ 74 | if not addresses: 75 | sys.exit(0) 76 | addresses = addresses.split() 77 | return [_.strip(',') for _ in addresses] 78 | 79 | 80 | def is_close(a, b=0.0, rel_tol=1e-09, abs_tol=0.0): 81 | """Deals with float comparison. 82 | 83 | Arguments: 84 | a, b (floats) 85 | Returns: 86 | closeness between a and b (float) 87 | """ 88 | return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /jobcoin/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | api.py 4 | 5 | This module implements a client for Jobcoin API RESTful endpoints. 6 | All requests calls are handled in this module. 7 | """ 8 | 9 | import sys 10 | import requests 11 | 12 | import jobcoin.util as util 13 | 14 | 15 | def _get_request(url): 16 | """Sends a GET request to the given URL. 17 | 18 | Parameters: 19 | url (str) 20 | Returns: 21 | response (str) 22 | """ 23 | try: 24 | return requests.get(url) 25 | except requests.exceptions.RequestException as e: 26 | print('📛 GET request error: {}:'.format(e)) 27 | sys.exit(0) 28 | 29 | 30 | def _post_request(url, payload): 31 | """Sends a POST request to the given URL and payload. 32 | 33 | Parameters: 34 | url (str) 35 | payload (dict) 36 | Returns: 37 | response (str) 38 | """ 39 | try: 40 | return requests.post(url, data=payload) 41 | except requests.exceptions.RequestException as e: 42 | print('📛 POST request error: {}:'.format(e)) 43 | sys.exit(0) 44 | 45 | 46 | def get_balance(address): 47 | """Gets the balance from a given address. 48 | 49 | Parameters: 50 | address (str) 51 | Returns: 52 | balance (float): unused address returns a balance of 0 53 | """ 54 | 55 | url = util.combine_url(util.get_config('API_ADDRESS_URL'), address) 56 | response = _get_request(url) 57 | 58 | if response.status_code == 200: 59 | data = response.json() 60 | 61 | try: 62 | return util.round_float(data['balance']) 63 | except KeyError as e: 64 | print('📛 Could not get balance from {}: '.format(address), 65 | 'Response is not well-formatted: {}'.format(e)) 66 | 67 | else: 68 | print('📛 Could not get balance from {}:'.format(address), 69 | 'status code {}'.format(response.status_code)) 70 | sys.exit(0) 71 | 72 | 73 | def get_transactions(): 74 | """Gets a list of all the transations in Jobcoin. 75 | 76 | Returns: 77 | transactions (list of dicts) 78 | """ 79 | # NOTE: The list of transactions might become very large with time. 80 | # if no check is done in the server, an enhancement would add this check, 81 | # sort by timestamp, or truncate after a certain size limit. 82 | 83 | url = util.get_config('API_TRANSACTIONS_URL') 84 | response = _get_request(url) 85 | 86 | if response.status_code == 200: 87 | return response.json() 88 | 89 | else: 90 | print('📛 Could not get transaction list: ', 91 | 'status code {status}'.format(status=response.status_code)) 92 | sys.exit(0) 93 | 94 | 95 | def post_transaction(from_address, to_address, amount): 96 | """Issue a transaction between two given address and a given amount. 97 | 98 | Input Arguments: 99 | from_address (str) 100 | to_address (str) 101 | amount (str) 102 | Returns: 103 | status (str) 104 | """ 105 | 106 | url = util.get_config('API_TRANSACTIONS_URL') 107 | payload = { 108 | 'fromAddress': from_address, 109 | 'toAddress': to_address, 110 | 'amount': amount 111 | } 112 | response = _post_request(url, payload) 113 | 114 | if response.status_code == 422: 115 | print('📛 Could not post transaction: Insufficient funds!') 116 | sys.exit(0) 117 | elif response.status_code == 400: 118 | print('📛 Could not post transaction: Amount must be over 0!') 119 | sys.exit(0) 120 | elif response.status_code != 200: 121 | print('📛 Could not post transaction: ', 122 | 'status code {}'.format(response.status_code)) 123 | sys.exit(0) 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## jobcoin: a cryptocurrency coin mixer 2 | 3 |
4 |

5 | 6 |

7 |
8 | 9 | #### 👉🏼 cryptocurrency coins operate on a pseudonymous system, not an anonymous protocol, so our coin mixer proof-of-concept offers an approach to enhance privacy on the network—for a small fee 😉 10 | 11 |
12 | 13 | --- 14 | 15 | ### setting up 16 | 17 |
18 | 19 | * set up your `.env` file 20 | 21 | ``` 22 | cp .env_sample .env 23 | make install 24 | ``` 25 | 26 | * with the following variables: 27 | 28 | ``` 29 | API_ADDRESS_URL = '' 30 | API_TRANSACTIONS_URL = '' 31 | SIGNIFICANT_DIGITS = 17 32 | HOUSE_ADDRESS = 'Jobcoin-House' 33 | FEE_PERCENTAGE = 0.1 34 | WITHDRAW_MAX_VALUE = 1 35 | ``` 36 | 37 |
38 | 39 | ##### `HOUSE_ADDRESS` 40 | 41 | * the ephemeral `hex` *deposit address* moves the coins to this address, where it's then mixed with other coins 42 | 43 | 44 | ##### `SIGNIFICANT_DIGITS` 45 | 46 | * as Jobcoin Mixer deals with float transactions, this variable sets the desired precision when converting strings to float 47 | 48 | ##### `FEE_PERCENTAGE` 49 | 50 | * an integer number representing the percentage fee to be collected for the mixing service (set to 0 for no fee) 51 | 52 | 53 | ##### `MAX_WITHDRAW_VALUE` 54 | 55 | * set the value for small withdrawal values for which Jobcoin Mixer will move from the House address to each personal addresses 56 | * jobcoin Mixer *cares about your privacy*, so setting this to smaller values makes the transactions more discrete 57 | * f you would like to have Jobcoin Mixer withdrawing all coins in one unique transaction, simply leave this constant empty (`None`) 58 | 59 |
60 | 61 | ---- 62 | 63 | ### running 64 | 65 |
66 | 67 | ``` 68 | jobcoin 69 | ``` 70 | 71 |
72 | 73 | --- 74 | 75 | ### usage 76 | 77 |
78 | 79 | * successful flow 80 | 81 |

82 | 83 |

84 | 85 |
86 | 87 | * personal address is not unused 88 | 89 |

90 | 91 |

92 | 93 |
94 | 95 | * insufficient funds 96 | 97 |

98 | 99 |

100 | 101 |
102 | 103 | * given coin amount is zero 104 | 105 |

106 | 107 |

108 | 109 |
110 | 111 | ---- 112 | 113 | ### developer corner 114 | 115 |
116 | 117 | * running a linter 118 | 119 | ``` 120 | make lint 121 | ``` 122 |
123 | 124 | * running unit tests 125 | 126 | ``` 127 | make test 128 | ``` 129 | 130 |
131 | 132 | * cleaning dist, dev, test, files 133 | 134 | ``` 135 | make clean 136 | ``` 137 | 138 |
139 | 140 | --- 141 | 142 | ### open tasks 143 | 144 |
145 | 146 | * improve unit tests. Add missing tests for `test_jobcoin.py`, `test_cli.py`, and `test_util.py`; add tests for failures and success, with better mocking and fixtures 147 | * improve private method `_is_empty()` as it loops over all the transactions address; as the list increases, this will take too long 148 | * deal with the increased size of the list of transactions being pulled from the server every time 149 | * convert the code to pure Python 3 (e.g., `-> return` in the module name, etc.); make sure the dependencies install Python3 libraries 150 | * adding `logging` everywhere, with different types of logging levels 151 | * Iimprove rules for linting 152 | 153 | -------------------------------------------------------------------------------- /jobcoin/jobcoin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | jobcoin.py 4 | 5 | Implements a class for Jobcoin API client. 6 | """ 7 | 8 | from __future__ import division 9 | 10 | import sys 11 | 12 | import jobcoin.api as api 13 | import jobcoin.util as util 14 | 15 | 16 | class Jobcoin(object): 17 | def __init__(self): 18 | # NOTE: We are using a "public" house address to keep track 19 | # of the polling on the server-side. Another option is to keep 20 | # it in the client-side by creating a (not ephemeral) data 21 | # structure that would keep track of all the balances in disk 22 | # or in memory, without the need for API calls. 23 | self.house_address = util.get_config('HOUSE_ADDRESS') 24 | 25 | def _is_empty(self, address): 26 | """Boolean test whether an address was previously seen in Jobcoin. 27 | 28 | Arguments: 29 | address (str) 30 | Returns: 31 | True if the address is empty (bool) 32 | """ 33 | # NOTE: As the list of address gets larger, this method becomes 34 | # non-optimal. A quicker solution could be simply whether the given 35 | # address has balance zero (as the server API returns balance zero 36 | # for non-existent addresses. However, this does not guarantee that 37 | # the address was not used before, and it would be considered a "less 38 | # private" option. 39 | 40 | transactions = api.get_transactions() 41 | used_addresses = set() 42 | for transaction in transactions: 43 | used_addresses.add(transaction['toAddress']) 44 | 45 | if address in used_addresses: 46 | return False 47 | return True 48 | 49 | def validate_addresses(self, addresses): 50 | """Checks whether all addresses in a list are unused. 51 | 52 | Arguments: 53 | addresses_list (list of strings) 54 | """ 55 | used_list = [] 56 | for address in addresses: 57 | if not self._is_empty(address): 58 | used_list.append(address) 59 | 60 | if used_list: 61 | print('📛 Addresses need to be unused...') 62 | for address in used_list: 63 | print('📛 "{}" is not an empty address'.format(address)) 64 | sys.exit(0) 65 | 66 | def _transfer_to_deposit_address(self, deposit, source_address, deposit_address): 67 | """Transfer coins from source address to the disposable deposit address. 68 | 69 | Arguments: 70 | deposit (str) 71 | source_address (str) 72 | deposit_address(str) 73 | """ 74 | api.post_transaction(source_address, deposit_address, deposit) 75 | 76 | def _calculate_withdraw_value(self, deposit, personal_addresses): 77 | """Calculate how much each personal address receives (after fees). 78 | 79 | Arguments: 80 | deposit (str) 81 | personal_addresses (list of str) 82 | """ 83 | withdraw_before_fee = deposit / len(personal_addresses) 84 | fee = int(util.get_config('FEE_PERCENTAGE')) 85 | 86 | if fee > 0: 87 | return withdraw_before_fee - withdraw_before_fee * (fee/100) 88 | else: 89 | return withdraw_before_fee 90 | 91 | def _run_mix_algorithm(self, deposit, deposit_address, personal_addresses): 92 | """Implements a simple mixer that runs small withdraws from the house 93 | to a list of personal addresses. 94 | 95 | Arguments: 96 | deposit (str) 97 | deposit_address (str) 98 | personal_addresses (list of str) 99 | """ 100 | api.post_transaction(deposit_address, self.house_address, deposit) 101 | withdraw = self._calculate_withdraw_value(deposit, personal_addresses) 102 | 103 | max_withdraw_value = int(util.get_config('MAX_WITHDRAW_VALUE')) 104 | if max_withdraw_value: 105 | num_of_withdraw = withdraw // max_withdraw_value 106 | last_withdraw = withdraw % max_withdraw_value 107 | 108 | for address in personal_addresses: 109 | counter = 0 110 | while num_of_withdraw > counter: 111 | api.post_transaction(self.house_address, address, max_withdraw_value) 112 | counter += 1 113 | if last_withdraw: 114 | api.post_transaction(self.house_address, address, last_withdraw) 115 | 116 | def _print_results(self, deposit, source_address, personal_addresses): 117 | """Prints results at the end of the mixing. 118 | 119 | Arguments: 120 | deposit (str) 121 | source_address (str) 122 | personal_addresses (list of str) 123 | """ 124 | print('✅ House withdraws {} coin(s) at time'.format(util.get_config('MAX_WITHDRAW_VALUE'))) 125 | pretty_addresses = ', '.join(personal_addresses) 126 | balance_personal_addressess = api.get_balance(personal_addresses[0]) 127 | balance_house_address = api.get_balance(util.get_config('HOUSE_ADDRESS')) 128 | 129 | print('✅ Successfully mixed {} coins from'.format(deposit), 130 | '{0} to {1}'.format(source_address, pretty_addresses)) 131 | print('✅ Our fee is {}%, so each address'.format(util.get_config('FEE_PERCENTAGE')), 132 | 'has now {} coin(s)\n'.format(balance_personal_addressess)) 133 | print('🤫 House has {} coin(s)\n'.format(balance_house_address)) 134 | 135 | def mix_algorithm(self, deposit, source_address, deposit_address, personal_addresses): 136 | """Entry point of this classes, selecting the mixing algorithm to be used. 137 | 138 | Arguments: 139 | deposit (str) 140 | source_address (str) 141 | personal_addresses (list of str) 142 | """ 143 | deposit = util.round_float(deposit) 144 | 145 | self._transfer_to_deposit_address(deposit, source_address, deposit_address) 146 | 147 | self._run_mix_algorithm(deposit, deposit_address, personal_addresses) 148 | 149 | self._print_results(deposit, source_address, personal_addresses) 150 | --------------------------------------------------------------------------------