From 0e9489f97125ea2ece492c378b25b9ff2ca6dc5a Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 25 Apr 2019 19:08:07 +0200 Subject: [PATCH 01/60] Adds uitest to Makefile --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 2ebde71..fe41929 100644 --- a/Makefile +++ b/Makefile @@ -58,3 +58,7 @@ clean: test: $(TOX) + +uitest: + . $(ACTIVATE_PATH) && \ + $(PYTHON) -m kivyunittest --folder src/tests/ui/ --pythonpath src/ From 38282859d896dca2386982c5671c2405d5270743 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Apr 2019 19:41:01 +0200 Subject: [PATCH 02/60] WIP migration to Python3 draft --- Makefile | 2 +- requirements/requirements.txt | 10 +-- src/pywalib.py | 132 ++++++++++++++++++++++++++++++++-- tox.ini | 2 +- 4 files changed, 135 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index fe41929..889af06 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ NPROC=`grep -c '^processor' /proc/cpuinfo` all: system_dependencies opencv virtualenv virtualenv: - test -d venv || virtualenv -p python2 venv + test -d venv || virtualenv -p python3 venv . venv/bin/activate $(PIP) install Cython==0.26.1 $(PIP) install -r requirements/requirements.txt diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f1f4c49..1929c15 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,7 +2,7 @@ https://github.com/ethereum/pyethereum/archive/2e53bf3.zip#egg=pyethereum devp2p==0.9.3 # 2017/06/10 develop -https://github.com/ethereum/pyethapp/archive/409331e88a397ce5276c430aff4a8866d413e45d.zip#egg=pyethapp +# https://github.com/ethereum/pyethapp/archive/409331e88a397ce5276c430aff4a8866d413e45d.zip#egg=pyethapp # https://github.com/ethereum/pyethapp/issues/274 https://github.com/mfranciszkiewicz/pyelliptic/archive/1.5.10.tar.gz#egg=pyelliptic eth-hash==0.1.1 @@ -15,9 +15,9 @@ rlp==0.6.0 # on Ubuntu 18.04 Bionic, the compile error related to `MIX_INIT_MOD` is fixed by installing: https://github.com/kivy/kivy/archive/d8ef8c2834293098bc404c0432049b2761f9b721.zip#egg=kivy # Kivy==1.10.0 -Kivy-Garden==0.1.4 -https://gitlab.com/kivymd/KivyMD/repository/archive.zip?ref=e81c02afbca915a4d71c85d3486f6710b53df2c1#egg=kivymd +# Kivy-Garden==0.1.4 +# https://gitlab.com/kivymd/KivyMD/repository/archive.zip?ref=e81c02afbca915a4d71c85d3486f6710b53df2c1#egg=kivymd Pillow==4.1.1 raven==6.1.0 -https://github.com/AndreMiras/garden.zbarcam/archive/20171220.zip -qrcode==5.3 +# https://github.com/AndreMiras/garden.zbarcam/archive/20171220.zip +# qrcode==5.3 diff --git a/src/pywalib.py b/src/pywalib.py index c1527d8..6c5c742 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -5,14 +5,20 @@ import os import shutil from os.path import expanduser +from enum import Enum import requests import rlp from devp2p.app import BaseApp +from eth_utils import to_checksum_address from ethereum.tools.keys import PBKDF2_CONSTANTS from ethereum.transactions import Transaction from ethereum.utils import denoms, normalize_address -from pyethapp.accounts import Account, AccountsService +# from pyethapp.accounts import Account, AccountsService +from web3 import HTTPProvider, Web3 +from pyethapp_accounts import Account +from eth_account import Account as EthAccount +from ethereum_utils import AccountUtils ETHERSCAN_API_KEY = None ROUND_DIGITS = 3 @@ -33,14 +39,25 @@ class NoTransactionFoundException(UnknownEtherscanException): pass +class ChainID(Enum): + MAINNET = 1 + MORDEN = 2 + ROPSTEN = 3 + + class PyWalib(object): def __init__(self, keystore_dir=None): if keystore_dir is None: keystore_dir = PyWalib.get_default_keystore_path() + self.account_utils = AccountUtils(keystore_dir=keystore_dir) + self.chain_id = ChainID.MAINNET + self.provider = HTTPProvider('https://mainnet.infura.io') + self.web3 = Web3(self.provider) + # TODO: not needed anymore self.app = BaseApp( config=dict(accounts=dict(keystore_dir=keystore_dir))) - AccountsService.register_with_app(self.app) + # AccountsService.register_with_app(self.app) @staticmethod def handle_etherscan_error(response_json): @@ -140,6 +157,7 @@ def get_out_transaction_history(address): out_transactions.append(transaction) return out_transactions + # TODO: can be removed since the migration to web3 @staticmethod def get_nonce(address): """ @@ -153,6 +171,7 @@ def get_nonce(address): nonce = len(out_transactions) return nonce + # TODO: is this still used after web3 migration? @staticmethod def handle_etherscan_tx_error(response_json): """ @@ -166,6 +185,20 @@ def handle_etherscan_tx_error(response_json): else: raise UnknownEtherscanException(response_json) + # TODO: is this still used after web3 migration? + @staticmethod + def handle_web3_exception(exception): + """ + TODO + """ + error = exception.args[0] + if error is not None: + code = error.get("code") + if code in [-32000, -32010]: + raise InsufficientFundsException() + else: + raise UnknownEtherscanException(response_json) + @staticmethod def add_transaction(tx): """ @@ -192,7 +225,58 @@ def add_transaction(tx): # the response differs from the other responses return tx_hash - def transact(self, to, value=0, data='', sender=None, startgas=25000, + def transact(self, to, value=0, data='', sender=None, gas=25000, + gasprice=60 * denoms.shannon): + """ + Signs and broadcasts a transaction. + Returns transaction hash. + """ + """ + # TODO: + # sender -> wallet_path + wallet_path = wallet_info[sender]['path'] + # wallet_info[sender]['password'] + wallet_path = 'TODO' + wallet_encrypted = load_keyfile(wallet_path) + address = wallet_encrypted['address'] + """ + sender = sender or web3.eth.coinbase + address = sender + from_address_normalized = to_checksum_address(address) + nonce = self.web3.eth.getTransactionCount(from_address_normalized) + transaction = { + 'chainId': self.chain_id.value, + 'gas': gas, + 'gasPrice': gasprice, + 'nonce': nonce, + 'value': value, + } + # TODO + # private_key = EthAccount.decrypt(wallet_encrypted, wallet_password) + account = self.account_utils.get_by_address(address) + private_key = account.privkey + signed_tx = self.web3.eth.account.signTransaction( + transaction, private_key) + try: + tx_hash = self.web3.eth.sendRawTransaction(signed_tx.rawTransaction) + except ValueError as e: + self.handle_web3_exception(e) + """ + sender = sender or web3.eth.coinbase + transaction = { + 'to': to, + 'value': value, + 'data': data, + 'from': sender, + 'gas': gas, + 'gasPrice': gasprice, + } + tx_hash = web3.eth.sendTransaction(transaction) + """ + return tx_hash + + + def transact_old(self, to, value=0, data='', sender=None, gas=25000, gasprice=60 * denoms.shannon): """ Inspired from pyethapp/console_service.py except that we use @@ -205,15 +289,37 @@ def transact(self, to, value=0, data='', sender=None, startgas=25000, to = normalize_address(to, allow_blank=True) nonce = PyWalib.get_nonce(sender) # creates the transaction - tx = Transaction(nonce, gasprice, startgas, to, value, data) + tx = Transaction(nonce, gasprice, gas, to, value, data) + + + signed_tx = self.web3.eth.account.signTransaction( + transaction, private_key) + tx_hash = self.web3.eth.sendRawTransaction(signed_tx.rawTransaction) + return tx_hash + + # TODO: migration to web3 # then signs it self.app.services.accounts.sign_tx(sender, tx) + # TODO: not needed anymore after web3 assert tx.sender == sender PyWalib.add_transaction(tx) return tx + # TODO: AccountUtils.new_account() + # TODO: update security_ratio param @staticmethod def new_account_helper(password, security_ratio=None): + """ + Helper method for creating an account in memory. + Returns the created account. + security_ratio is a ratio of the default PBKDF2 iterations. + Ranging from 1 to 100 means 100% of the iterations. + """ + account = self.account_utils.new_account(password=password) + return account + + @staticmethod + def new_account_helper_old(password, security_ratio=None): """ Helper method for creating an account in memory. Returns the created account. @@ -252,7 +358,18 @@ def deleted_account_dir(keystore_dir): deleted_keystore_dir_name) return deleted_keystore_dir + # TODO: update docstring + # TODO: update security_ratio def new_account(self, password, security_ratio=None): + """ + Creates an account on the disk and returns it. + security_ratio is a ratio of the default PBKDF2 iterations. + Ranging from 1 to 100 means 100% of the iterations. + """ + account = self.account_utils.new_account(password=password) + return account + + def new_account_old(self, password, security_ratio=None): """ Creates an account on the disk and returns it. security_ratio is a ratio of the default PBKDF2 iterations. @@ -292,6 +409,13 @@ def update_account_password( """ The current_password is optional if the account is already unlocked. """ + raise NotImplementedError('refs #19') + + def update_account_password_old( + self, account, new_password, current_password=None): + """ + The current_password is optional if the account is already unlocked. + """ if current_password is not None: account.unlock(current_password) # make sure the PBKDF2 param stays the same diff --git a/tox.ini b/tox.ini index ec177ff..b1f7bd0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pep8,isort-check,py27 +envlist = pep8,isort-check,py36 # no setup.py to be ran skipsdist = True # trick to enable pre-installation of Cython From 0fd0fb9dc79a9f45a2dcaf171daacf9339b3aa2a Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Apr 2019 20:04:10 +0200 Subject: [PATCH 03/60] WIP helper modules, refs #19 --- src/ethereum_utils.py | 119 ++++++++++++++++++++++ src/pyethapp_accounts.py | 207 +++++++++++++++++++++++++++++++++++++++ src/pywalib.py | 3 +- 3 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/ethereum_utils.py create mode 100644 src/pyethapp_accounts.py diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py new file mode 100644 index 0000000..cce2cc0 --- /dev/null +++ b/src/ethereum_utils.py @@ -0,0 +1,119 @@ +import os + +from pyethapp_accounts import Account + + +class AccountUtils: + + def __init__(self, keystore_dir): + self.keystore_dir = keystore_dir + self._accounts = None + + def get_account_list(self): + """ + Returns the Account list. + """ + if self._accounts is not None: + return self._accounts + keyfiles = [] + for item in os.listdir(self.keystore_dir): + item_path = os.path.join(self.keystore_dir, item) + if os.path.isfile(item_path): + keyfiles.append(item_path) + # starts caching after `listdir()` call so if it fails + # (e.g. `PermissionError`) account list won't be empty next call + self._accounts = [] + for keyfile in keyfiles: + account = Account.load(path=keyfile) + self._accounts.append(account) + return self._accounts + + def new_account(self, password, key: bytes = None, iterations=None): + """ + Creates an account on the disk and returns it. + :param key: the private key, or `None` to generate a random one + """ + account = Account.new(password, key=key, uuid=None, iterations=iterations) + account.path = os.path.join(self.keystore_dir, account.address.hex()) + self.add_account(account) + return account + + def add_account(self, account): + with open(account.path, 'w') as f: + f.write(account.dump()) + if self._accounts is None: + self._accounts = [] + self._accounts.append(account) + return account + + @staticmethod + def deleted_account_dir(keystore_dir): + """ + Given a `keystore_dir`, returns the corresponding + `deleted_keystore_dir`. + >>> keystore_dir = '/tmp/keystore' + >>> AccountUtils.deleted_account_dir(keystore_dir) + u'/tmp/keystore-deleted' + >>> keystore_dir = '/tmp/keystore/' + >>> AccountUtils.deleted_account_dir(keystore_dir) + u'/tmp/keystore-deleted' + """ + keystore_dir = keystore_dir.rstrip('/') + keystore_dir_name = os.path.basename(keystore_dir) + deleted_keystore_dir_name = "%s-deleted" % (keystore_dir_name) + deleted_keystore_dir = os.path.join( + os.path.dirname(keystore_dir), + deleted_keystore_dir_name) + return deleted_keystore_dir + + def delete_account(self, account): + """ + Deletes the given `account` from the `keystore_dir` directory. + Then deletes it from the `AccountsService` account manager instance. + In fact, moves it to another location; another directory at the same + level. + """ + # lazy loading + import shutil + keystore_dir = self.keystore_dir + deleted_keystore_dir = self.deleted_account_dir(keystore_dir) + # create the deleted account dir if required + if not os.path.exists(deleted_keystore_dir): + os.makedirs(deleted_keystore_dir) + # "removes" it from the file system + account_filename = os.path.basename(account.path) + deleted_account_path = os.path.join( + deleted_keystore_dir, account_filename) + shutil.move(account.path, deleted_account_path) + self._accounts.remove(account) + + def get_by_address(self, address): + """Get an account by its address. + + Note that even if an account with the given address exists, it might not be found if it is + locked. Also, multiple accounts with the same address may exist, in which case the first + one is returned (and a warning is logged). + + :raises: `KeyError` if no matching account can be found + """ + assert len(address) == 20 + accounts = [account for account in self._accounts if account.address == address] + if len(accounts) == 0: + raise KeyError('account not found by address', address=address.encode('hex')) + elif len(accounts) > 1: + log.warning('multiple accounts with same address found', address=address.encode('hex')) + return accounts[0] + + def sign_tx(self, address, tx): + self.get_by_address(address).sign_tx(tx) + + def update_account_password( + self, account, new_password, current_password=None): + """ + The current_password is optional if the account is already unlocked. + """ + if current_password is not None: + account.unlock(current_password) + key = account.privkey + self.delete_account(account) + self.new_account(new_password, key=key, iterations=None) diff --git a/src/pyethapp_accounts.py b/src/pyethapp_accounts.py new file mode 100644 index 0000000..5cb99a5 --- /dev/null +++ b/src/pyethapp_accounts.py @@ -0,0 +1,207 @@ +import json +import os + +import eth_account +from eth_keyfile import create_keyfile_json, decode_keyfile_json +from eth_keys import keys +from eth_utils import decode_hex, encode_hex, remove_0x_prefix + + +def to_string(value): + if isinstance(value, bytes): + return value + if isinstance(value, str): + return bytes(value, 'utf-8') + if isinstance(value, int): + return bytes(str(value), 'utf-8') + + +class Account: + """ + Represents an account. + :ivar keystore: the key store as a dictionary (as decoded from json) + :ivar locked: `True` if the account is locked and neither private nor + public keys can be accessed, otherwise `False` + :ivar path: absolute path to the associated keystore file (`None` for + in-memory accounts) + """ + + def __init__(self, keystore: dict, password: bytes = None, path=None): + self.keystore = keystore + try: + self._address = decode_hex(self.keystore['address']) + except KeyError: + self._address = None + self.locked = True + if password is not None: + password = to_string(password) + self.unlock(password) + if path is not None: + self.path = os.path.abspath(path) + else: + self.path = None + + @classmethod + def new(cls, password: bytes, key: bytes = None, uuid=None, path=None, + iterations=None): + """ + Create a new account. + Note that this creates the account in memory and does not store it on + disk. + :param password: the password used to encrypt the private key + :param key: the private key, or `None` to generate a random one + :param uuid: an optional id + """ + if key is None: + account = eth_account.Account.create() + key = account.privateKey + + # [NOTE]: key and password should be bytes + password = str.encode(password) + + # encrypted = eth_account.Account.encrypt(account.privateKey, password) + keystore = create_keyfile_json(key, password, iterations=iterations) + keystore['id'] = uuid + return Account(keystore, password, path) + + @classmethod + def load(cls, path, password: bytes = None): + """ + Load an account from a keystore file. + :param path: full path to the keyfile + :param password: the password to decrypt the key file or `None` to + leave it encrypted + """ + with open(path) as f: + keystore = json.load(f) + # if not keys.check_keystore_json(keystore): + # raise ValueError('Invalid keystore file') + return Account(keystore, password, path=path) + + def dump(self, include_address=True, include_id=True): + """ + Dump the keystore for later disk storage. + The result inherits the entries `'crypto'` and `'version`' from + `account.keystore`, and adds `'address'` and `'id'` in accordance with + the parameters `'include_address'` and `'include_id`'. + If address or id are not known, they are not added, even if requested. + :param include_address: flag denoting if the address should be included + or not + :param include_id: flag denoting if the id should be included or not + """ + d = {} + d['crypto'] = self.keystore['crypto'] + d['version'] = self.keystore['version'] + if include_address and self.address is not None: + d['address'] = encode_hex(self.address) + if include_id and self.uuid is not None: + d['id'] = str(self.uuid) + return json.dumps(d) + + def unlock(self, password: bytes): + """ + Unlock the account with a password. + If the account is already unlocked, nothing happens, even if the + password is wrong. + :raises: :exc:`ValueError` (originating in ethereum.keys) if the + password is wrong (and the account is locked) + """ + if self.locked: + password = to_string(password) + self._privkey = decode_keyfile_json(self.keystore, password) + self.locked = False + # get address such that it stays accessible after a subsequent lock + self.address + + def lock(self): + """ + Relock an unlocked account. + This method sets `account.privkey` to `None` (unlike `account.address` + which is preserved). + After calling this method, both `account.privkey` and `account.pubkey` + are `None. + `account.address` stays unchanged, even if it has been derived from the + private key. + """ + self._privkey = None + self.locked = True + + @property + def privkey(self): + """ + The account's private key or `None` if the account is locked + """ + if not self.locked: + return self._privkey + else: + return None + + @property + def pubkey(self): + """ + The account's public key or `None` if the account is locked + """ + if not self.locked: + pk = keys.PrivateKey(self.privkey) + return remove_0x_prefix(pk.public_key.to_address()) + else: + return None + + @property + def address(self): + """ + The account's address or `None` if the address is not stored in the key + file and cannot be reconstructed (because the account is locked) + """ + if self._address: + pass + elif 'address' in self.keystore: + self._address = decode_hex(self.keystore['address']) + elif not self.locked: + pk = keys.PrivateKey(self.privkey) + self._address = decode_hex(pk.public_key.to_address()) + else: + return None + return self._address + + @property + def uuid(self): + """ + An optional unique identifier, formatted according to UUID version 4, + or `None` if the account does not have an id + """ + try: + return self.keystore['id'] + except KeyError: + return None + + @uuid.setter + def uuid(self, value): + """ + Set the UUID. Set it to `None` in order to remove it. + """ + if value is not None: + self.keystore['id'] = value + elif 'id' in self.keystore: + self.keystore.pop('id') + + # TODO: not yet migrated + # def sign_tx(self, tx): + # """ + # Sign a Transaction with the private key of this account. + # If the account is unlocked, this is equivalent to + # `tx.sign(account.privkey)`. + # :param tx: the :class:`ethereum.transactions.Transaction` to sign + # :raises: :exc:`ValueError` if the account is locked + # """ + # if self.privkey: + # tx.sign(self.privkey) + # else: + # raise ValueError('Locked account cannot sign tx') + + def __repr__(self): + if self.address is not None: + address = encode_hex(self.address) + else: + address = '?' + return f'' diff --git a/src/pywalib.py b/src/pywalib.py index 6c5c742..bf5070c 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -409,7 +409,8 @@ def update_account_password( """ The current_password is optional if the account is already unlocked. """ - raise NotImplementedError('refs #19') + self.account_utils.update_account_password( + account, new_password, current_password) def update_account_password_old( self, account, new_password, current_password=None): From c65e8176878b22fa4e2ce5561d8d128056603354 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Apr 2019 20:06:32 +0200 Subject: [PATCH 04/60] get_account_list() migration --- src/pywalib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywalib.py b/src/pywalib.py index bf5070c..7797293 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -440,7 +440,7 @@ def get_account_list(self): """ Returns the Account list. """ - accounts = self.app.services.accounts + accounts = self.account_utils.get_account_list() return accounts def get_main_account(self): From 3f37506176847970583153aaadd1536c9372bdcf Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Apr 2019 20:07:59 +0200 Subject: [PATCH 05/60] test_handle_etherscan_tx_error() --- src/tests/test_pywalib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index ef9486c..a55196d 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -320,7 +320,7 @@ def test_handle_etherscan_tx_error(self): } with self.assertRaises(UnknownEtherscanException) as e: PyWalib.handle_etherscan_tx_error(response_json) - self.assertEqual(e.exception.message, response_json) + self.assertEqual(e.exception.args[0], response_json) # no error response_json = {'jsonrpc': '2.0', 'id': 1} self.assertEqual( From e37fb0da6b347f08d99ce0105ee5692082251d3a Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Apr 2019 20:10:52 +0200 Subject: [PATCH 06/60] address_hex() --- src/pywalib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywalib.py b/src/pywalib.py index 7797293..85c9a04 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -79,7 +79,7 @@ def address_hex(address): Normalizes address. """ prefix = "0x" - address_hex = prefix + normalize_address(address).encode("hex") + address_hex = prefix + normalize_address(address).hex() return address_hex @staticmethod From 0e266884a7aada7663da9098eb0ffd3f2e77fbea Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Apr 2019 20:13:04 +0200 Subject: [PATCH 07/60] test_handle_etherscan_error() --- src/tests/test_pywalib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index a55196d..02707f1 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -168,7 +168,7 @@ def test_handle_etherscan_error(self): } with self.assertRaises(UnknownEtherscanException) as e: PyWalib.handle_etherscan_error(response_json) - self.assertEqual(e.exception.message, response_json) + self.assertEqual(e.exception.args[0], response_json) # no error response_json = { 'message': 'OK', 'result': [], 'status': '1' From c39dc8a5f9a3138a6b9303dcdc0830fb97461f5c Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Apr 2019 20:15:47 +0200 Subject: [PATCH 08/60] delete_account() --- src/pywalib.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index 85c9a04..14310ae 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -385,24 +385,10 @@ def new_account_old(self, password, security_ratio=None): def delete_account(self, account): """ Deletes the given `account` from the `keystore_dir` directory. - Then deletes it from the `AccountsService` account manager instance. In fact, moves it to another location; another directory at the same level. """ - app = self.app - keystore_dir = app.services.accounts.keystore_dir - deleted_keystore_dir = PyWalib.deleted_account_dir(keystore_dir) - # create the deleted account dir if required - if not os.path.exists(deleted_keystore_dir): - os.makedirs(deleted_keystore_dir) - # "removes" it from the file system - account_filename = os.path.basename(account.path) - deleted_account_path = os.path.join( - deleted_keystore_dir, account_filename) - shutil.move(account.path, deleted_account_path) - # deletes it from the `AccountsService` account manager instance - account_service = self.get_account_list() - account_service.accounts.remove(account) + self.account_utils.delete_account(account) def update_account_password( self, account, new_password, current_password=None): From 39bf1d8fb79f40e020eceab01afefaae89ac50fb Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Apr 2019 20:17:00 +0200 Subject: [PATCH 09/60] test_address_hex() --- src/tests/test_pywalib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index 02707f1..c388df7 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -196,7 +196,7 @@ def test_address_hex(self): with self.assertRaises(Exception) as context: PyWalib.address_hex(address) self.assertEqual( - context.exception.message, + context.exception.args[0], "Invalid address format: '%s'" % (address)) def test_get_balance(self): From 3c0611d38b6db149e30d2c7bb56076f7c93fb65e Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 29 Apr 2019 18:19:55 +0200 Subject: [PATCH 10/60] update_account_password() --- src/ethereum_utils.py | 14 +++++++++----- src/pyethapp_accounts.py | 4 ++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py index cce2cc0..f50f09c 100644 --- a/src/ethereum_utils.py +++ b/src/ethereum_utils.py @@ -39,8 +39,7 @@ def new_account(self, password, key: bytes = None, iterations=None): return account def add_account(self, account): - with open(account.path, 'w') as f: - f.write(account.dump()) + account.dump_to_disk() if self._accounts is None: self._accounts = [] self._accounts.append(account) @@ -110,10 +109,15 @@ def sign_tx(self, address, tx): def update_account_password( self, account, new_password, current_password=None): """ + Updates the current account instance. The current_password is optional if the account is already unlocked. """ if current_password is not None: account.unlock(current_password) - key = account.privkey - self.delete_account(account) - self.new_account(new_password, key=key, iterations=None) + new_account = Account.new( + password=new_password, + key=account.privkey, + uuid=account.uuid, + path=account.path) + account.keystore = new_account.keystore + account.dump_to_disk() diff --git a/src/pyethapp_accounts.py b/src/pyethapp_accounts.py index 5cb99a5..5b134f1 100644 --- a/src/pyethapp_accounts.py +++ b/src/pyethapp_accounts.py @@ -98,6 +98,10 @@ def dump(self, include_address=True, include_id=True): d['id'] = str(self.uuid) return json.dumps(d) + def dump_to_disk(self, include_address=True, include_id=True): + with open(self.path, 'w') as f: + f.write(self.dump(include_address, include_id)) + def unlock(self, password: bytes): """ Unlock the account with a password. From 2cdd242319531fb79a980fc38698d9157bdd2ad4 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 29 Apr 2019 18:33:20 +0200 Subject: [PATCH 11/60] code cleaning --- src/ethereum_utils.py | 24 +++++++----- src/pywalib.py | 85 +------------------------------------------ tox.ini | 2 +- 3 files changed, 17 insertions(+), 94 deletions(-) diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py index f50f09c..c49c874 100644 --- a/src/ethereum_utils.py +++ b/src/ethereum_utils.py @@ -33,7 +33,8 @@ def new_account(self, password, key: bytes = None, iterations=None): Creates an account on the disk and returns it. :param key: the private key, or `None` to generate a random one """ - account = Account.new(password, key=key, uuid=None, iterations=iterations) + account = Account.new( + password, key=key, uuid=None, iterations=iterations) account.path = os.path.join(self.keystore_dir, account.address.hex()) self.add_account(account) return account @@ -87,20 +88,23 @@ def delete_account(self, account): self._accounts.remove(account) def get_by_address(self, address): - """Get an account by its address. - - Note that even if an account with the given address exists, it might not be found if it is - locked. Also, multiple accounts with the same address may exist, in which case the first - one is returned (and a warning is logged). - + """ + Get an account by its address. + Note that even if an account with the given address exists, it might + not be found if it is locked. + Also, multiple accounts with the same address may exist, in which case + the first one is returned (and a warning is logged). :raises: `KeyError` if no matching account can be found """ assert len(address) == 20 - accounts = [account for account in self._accounts if account.address == address] + accounts = [acc for acc in self._accounts if acc.address == address] if len(accounts) == 0: - raise KeyError('account not found by address', address=address.encode('hex')) + raise KeyError( + 'account not found by address', address=address.encode('hex')) elif len(accounts) > 1: - log.warning('multiple accounts with same address found', address=address.encode('hex')) + log.warning( + 'multiple accounts with same address found', + address=address.encode('hex')) return accounts[0] def sign_tx(self, address, tx): diff --git a/src/pywalib.py b/src/pywalib.py index 14310ae..6334b97 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -3,7 +3,6 @@ from __future__ import print_function, unicode_literals import os -import shutil from os.path import expanduser from enum import Enum @@ -12,12 +11,10 @@ from devp2p.app import BaseApp from eth_utils import to_checksum_address from ethereum.tools.keys import PBKDF2_CONSTANTS -from ethereum.transactions import Transaction from ethereum.utils import denoms, normalize_address # from pyethapp.accounts import Account, AccountsService from web3 import HTTPProvider, Web3 from pyethapp_accounts import Account -from eth_account import Account as EthAccount from ethereum_utils import AccountUtils ETHERSCAN_API_KEY = None @@ -231,15 +228,6 @@ def transact(self, to, value=0, data='', sender=None, gas=25000, Signs and broadcasts a transaction. Returns transaction hash. """ - """ - # TODO: - # sender -> wallet_path - wallet_path = wallet_info[sender]['path'] - # wallet_info[sender]['password'] - wallet_path = 'TODO' - wallet_encrypted = load_keyfile(wallet_path) - address = wallet_encrypted['address'] - """ sender = sender or web3.eth.coinbase address = sender from_address_normalized = to_checksum_address(address) @@ -251,73 +239,17 @@ def transact(self, to, value=0, data='', sender=None, gas=25000, 'nonce': nonce, 'value': value, } - # TODO - # private_key = EthAccount.decrypt(wallet_encrypted, wallet_password) account = self.account_utils.get_by_address(address) private_key = account.privkey signed_tx = self.web3.eth.account.signTransaction( transaction, private_key) try: - tx_hash = self.web3.eth.sendRawTransaction(signed_tx.rawTransaction) + tx_hash = self.web3.eth.sendRawTransaction( + signed_tx.rawTransaction) except ValueError as e: self.handle_web3_exception(e) - """ - sender = sender or web3.eth.coinbase - transaction = { - 'to': to, - 'value': value, - 'data': data, - 'from': sender, - 'gas': gas, - 'gasPrice': gasprice, - } - tx_hash = web3.eth.sendTransaction(transaction) - """ - return tx_hash - - - def transact_old(self, to, value=0, data='', sender=None, gas=25000, - gasprice=60 * denoms.shannon): - """ - Inspired from pyethapp/console_service.py except that we use - Etherscan for retrieving the nonce as we as for broadcasting the - transaction. - Arg value is in Wei. - """ - # account.unlock(password) - sender = normalize_address(sender or self.get_main_account().address) - to = normalize_address(to, allow_blank=True) - nonce = PyWalib.get_nonce(sender) - # creates the transaction - tx = Transaction(nonce, gasprice, gas, to, value, data) - - - signed_tx = self.web3.eth.account.signTransaction( - transaction, private_key) - tx_hash = self.web3.eth.sendRawTransaction(signed_tx.rawTransaction) return tx_hash - # TODO: migration to web3 - # then signs it - self.app.services.accounts.sign_tx(sender, tx) - # TODO: not needed anymore after web3 - assert tx.sender == sender - PyWalib.add_transaction(tx) - return tx - - # TODO: AccountUtils.new_account() - # TODO: update security_ratio param - @staticmethod - def new_account_helper(password, security_ratio=None): - """ - Helper method for creating an account in memory. - Returns the created account. - security_ratio is a ratio of the default PBKDF2 iterations. - Ranging from 1 to 100 means 100% of the iterations. - """ - account = self.account_utils.new_account(password=password) - return account - @staticmethod def new_account_helper_old(password, security_ratio=None): """ @@ -369,19 +301,6 @@ def new_account(self, password, security_ratio=None): account = self.account_utils.new_account(password=password) return account - def new_account_old(self, password, security_ratio=None): - """ - Creates an account on the disk and returns it. - security_ratio is a ratio of the default PBKDF2 iterations. - Ranging from 1 to 100 means 100% of the iterations. - """ - account = PyWalib.new_account_helper(password, security_ratio) - app = self.app - account.path = os.path.join( - app.services.accounts.keystore_dir, account.address.encode('hex')) - self.app.services.accounts.add_account(account) - return account - def delete_account(self, account): """ Deletes the given `account` from the `keystore_dir` directory. diff --git a/tox.ini b/tox.ini index b1f7bd0..5e39fd9 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,6 @@ commands = flake8 src/ exclude = src/python-for-android/ [testenv:isort-check] -basepython = python2 +basepython = python3 commands = isort --check-only --recursive src/ From 8652ced60fe209863c8bf6c90f96125ecf84b775 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 2 May 2019 23:19:32 +0200 Subject: [PATCH 12/60] Code and dependency cleaning Removed old Ethereum libs and code deps. --- requirements/requirements.txt | 18 +++----------- src/pywalib.py | 46 ++--------------------------------- src/tests/test_import.py | 25 ------------------- 3 files changed, 5 insertions(+), 84 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 1929c15..ad8c106 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,20 +1,8 @@ -# 2017/06/08 develop -https://github.com/ethereum/pyethereum/archive/2e53bf3.zip#egg=pyethereum -devp2p==0.9.3 -# 2017/06/10 develop -# https://github.com/ethereum/pyethapp/archive/409331e88a397ce5276c430aff4a8866d413e45d.zip#egg=pyethapp -# https://github.com/ethereum/pyethapp/issues/274 -https://github.com/mfranciszkiewicz/pyelliptic/archive/1.5.10.tar.gz#egg=pyelliptic eth-hash==0.1.1 -pyethash==0.1.27 +eth-utils==1.4.1 +web3==4.8.1 rlp==0.6.0 -# on Ubuntu 16.04 Xenial, the error: -# x11 - ImportError: No module named window_x11 -# is fixed by installing kivy master -# https://github.com/kivy/kivy/archive/27e3b90eae2a0155b22a435f1b6f65c913519db6.zip#egg=kivy -# on Ubuntu 18.04 Bionic, the compile error related to `MIX_INIT_MOD` is fixed by installing: -https://github.com/kivy/kivy/archive/d8ef8c2834293098bc404c0432049b2761f9b721.zip#egg=kivy -# Kivy==1.10.0 +# Kivy==1.10.1 # Kivy-Garden==0.1.4 # https://gitlab.com/kivymd/KivyMD/repository/archive.zip?ref=e81c02afbca915a4d71c85d3486f6710b53df2c1#egg=kivymd Pillow==4.1.1 diff --git a/src/pywalib.py b/src/pywalib.py index 6334b97..9c55eca 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -8,11 +8,8 @@ import requests import rlp -from devp2p.app import BaseApp from eth_utils import to_checksum_address -from ethereum.tools.keys import PBKDF2_CONSTANTS -from ethereum.utils import denoms, normalize_address -# from pyethapp.accounts import Account, AccountsService +from ethereum.utils import normalize_address from web3 import HTTPProvider, Web3 from pyethapp_accounts import Account from ethereum_utils import AccountUtils @@ -51,10 +48,6 @@ def __init__(self, keystore_dir=None): self.chain_id = ChainID.MAINNET self.provider = HTTPProvider('https://mainnet.infura.io') self.web3 = Web3(self.provider) - # TODO: not needed anymore - self.app = BaseApp( - config=dict(accounts=dict(keystore_dir=keystore_dir))) - # AccountsService.register_with_app(self.app) @staticmethod def handle_etherscan_error(response_json): @@ -223,7 +216,7 @@ def add_transaction(tx): return tx_hash def transact(self, to, value=0, data='', sender=None, gas=25000, - gasprice=60 * denoms.shannon): + gasprice=60 * (10 ** 9)): """ Signs and broadcasts a transaction. Returns transaction hash. @@ -250,26 +243,6 @@ def transact(self, to, value=0, data='', sender=None, gas=25000, self.handle_web3_exception(e) return tx_hash - @staticmethod - def new_account_helper_old(password, security_ratio=None): - """ - Helper method for creating an account in memory. - Returns the created account. - security_ratio is a ratio of the default PBKDF2 iterations. - Ranging from 1 to 100 means 100% of the iterations. - """ - # TODO: perform validation on security_ratio (within allowed range) - if security_ratio: - default_iterations = PBKDF2_CONSTANTS["c"] - new_iterations = int((default_iterations * security_ratio) / 100) - PBKDF2_CONSTANTS["c"] = new_iterations - uuid = None - account = Account.new(password, uuid=uuid) - # reverts to previous iterations - if security_ratio: - PBKDF2_CONSTANTS["c"] = default_iterations - return account - @staticmethod def deleted_account_dir(keystore_dir): """ @@ -317,21 +290,6 @@ def update_account_password( self.account_utils.update_account_password( account, new_password, current_password) - def update_account_password_old( - self, account, new_password, current_password=None): - """ - The current_password is optional if the account is already unlocked. - """ - if current_password is not None: - account.unlock(current_password) - # make sure the PBKDF2 param stays the same - default_iterations = PBKDF2_CONSTANTS["c"] - account_iterations = account.keystore["crypto"]["kdfparams"]["c"] - PBKDF2_CONSTANTS["c"] = account_iterations - self.app.services.accounts.update_account(account, new_password) - # reverts to previous iterations - PBKDF2_CONSTANTS["c"] = default_iterations - @staticmethod def get_default_keystore_path(): """ diff --git a/src/tests/test_import.py b/src/tests/test_import.py index 26c99ae..87d20be 100644 --- a/src/tests/test_import.py +++ b/src/tests/test_import.py @@ -6,10 +6,6 @@ class ModulesImportTestCase(unittest.TestCase): Simple test cases, verifying core modules were installed properly. """ - def test_pyethash(self): - import pyethash - self.assertIsNotNone(pyethash.get_seedhash(0)) - def test_hashlib_sha3(self): import hashlib import sha3 @@ -25,27 +21,6 @@ def test_scrypt(self): decrypted = scrypt.decrypt(data, 'password', maxtime=0.5) self.assertEqual(decrypted, 'a secret message') - def test_pyethereum(self): - from ethereum import compress, utils - self.assertIsNotNone(compress) - self.assertIsNotNone(utils) - - def test_pyethapp(self): - from pyethapp.accounts import Account - from ethereum.tools.keys import PBKDF2_CONSTANTS - # backup iterations - iterations_backup = PBKDF2_CONSTANTS['c'] - # speeds up the test - PBKDF2_CONSTANTS['c'] = 100 - password = "foobar" - uuid = None - account = Account.new(password, uuid=uuid) - # restore iterations - PBKDF2_CONSTANTS['c'] = iterations_backup - address = account.address.encode('hex') - self.assertIsNotNone(account) - self.assertIsNotNone(address) - def test_zbarcam(self): from zbarcam import zbarcam # zbarcam imports PIL and monkey patches it so it has From 4df022fb813bf42f051e24ace08c1919e045f023 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 2 May 2019 23:20:12 +0200 Subject: [PATCH 13/60] Reduced default gas price --- src/pywalib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywalib.py b/src/pywalib.py index 9c55eca..494e5df 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -216,7 +216,7 @@ def add_transaction(tx): return tx_hash def transact(self, to, value=0, data='', sender=None, gas=25000, - gasprice=60 * (10 ** 9)): + gasprice=5 * (10 ** 9)): """ Signs and broadcasts a transaction. Returns transaction hash. From 4c82ead729b0cc43e200a4d45eee7f6233c50c07 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 2 May 2019 23:20:43 +0200 Subject: [PATCH 14/60] isort --- src/pywalib.py | 5 +++-- src/pywallet/navigation.py | 1 - src/pywallet/scrollablelabel.py | 1 - src/tests/ui/test_ui_base.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index 494e5df..ec70301 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -3,16 +3,17 @@ from __future__ import print_function, unicode_literals import os -from os.path import expanduser from enum import Enum +from os.path import expanduser import requests import rlp from eth_utils import to_checksum_address from ethereum.utils import normalize_address from web3 import HTTPProvider, Web3 -from pyethapp_accounts import Account + from ethereum_utils import AccountUtils +from pyethapp_accounts import Account ETHERSCAN_API_KEY = None ROUND_DIGITS = 3 diff --git a/src/pywallet/navigation.py b/src/pywallet/navigation.py index 2b722e0..fe1ba32 100644 --- a/src/pywallet/navigation.py +++ b/src/pywallet/navigation.py @@ -7,7 +7,6 @@ from pywallet.utils import load_kv_from_py - load_kv_from_py(__file__) diff --git a/src/pywallet/scrollablelabel.py b/src/pywallet/scrollablelabel.py index da023bb..bf67c33 100644 --- a/src/pywallet/scrollablelabel.py +++ b/src/pywallet/scrollablelabel.py @@ -3,7 +3,6 @@ from pywallet.utils import load_kv_from_py - load_kv_from_py(__file__) diff --git a/src/tests/ui/test_ui_base.py b/src/tests/ui/test_ui_base.py index 4f5269d..eb2a0b3 100644 --- a/src/tests/ui/test_ui_base.py +++ b/src/tests/ui/test_ui_base.py @@ -10,11 +10,11 @@ from tempfile import mkdtemp import kivymd -import mock import requests from kivy.clock import Clock import main +import mock import pywalib from pywallet.switchaccount import SwitchAccount from pywallet.utils import Dialog From d2e0d50a81c2165a692c77725dfd45b23972cd56 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 2 May 2019 23:34:51 +0200 Subject: [PATCH 15/60] Basic transact() test --- src/tests/test_pywalib.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index c388df7..b6aff3b 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -2,6 +2,7 @@ import shutil import unittest from tempfile import mkdtemp +from unittest import mock from pywalib import (InsufficientFundsException, NoTransactionFoundException, PyWalib, UnknownEtherscanException) @@ -327,6 +328,20 @@ def test_handle_etherscan_tx_error(self): PyWalib.handle_etherscan_tx_error(response_json), None) + def test_transact(self): + """ + Basic transact() test, makes sure web3 sendRawTransaction gets called. + """ + pywalib = self.pywalib + account = self.helper_new_account() + to = ADDRESS + sender = account.address + value_wei = 100 + with mock.patch('web3.eth.Eth.sendRawTransaction') \ + as m_sendRawTransaction: + pywalib.transact(to=to, value=value_wei, sender=sender) + self.assertTrue(m_sendRawTransaction.called) + def test_transact_no_funds(self): """ Tries to send a transaction from an address with no funds. From 08cf304da5f9afb231214fac2a281aedad7d4d1e Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 2 May 2019 23:44:42 +0200 Subject: [PATCH 16/60] test_transact_no_sender() and bugfix no sender case --- src/pywalib.py | 3 +-- src/tests/test_pywalib.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index ec70301..8bc41cc 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -222,8 +222,7 @@ def transact(self, to, value=0, data='', sender=None, gas=25000, Signs and broadcasts a transaction. Returns transaction hash. """ - sender = sender or web3.eth.coinbase - address = sender + address = sender or self.get_main_account().address from_address_normalized = to_checksum_address(address) nonce = self.web3.eth.getTransactionCount(from_address_normalized) transaction = { diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index b6aff3b..db8a2fc 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -342,6 +342,32 @@ def test_transact(self): pywalib.transact(to=to, value=value_wei, sender=sender) self.assertTrue(m_sendRawTransaction.called) + def test_transact_no_sender(self): + """ + The sender parameter should default to the main account. + Makes sure the transaction is being signed by the available account. + """ + pywalib = self.pywalib + account = self.helper_new_account() + to = ADDRESS + value_wei = 100 + with mock.patch('web3.eth.Eth.sendRawTransaction') \ + as m_sendRawTransaction, \ + mock.patch('web3.eth.Eth.account.signTransaction') \ + as m_signTransaction: + pywalib.transact(to=to, value=value_wei) + self.assertTrue(m_sendRawTransaction.called) + m_signTransaction.call_args_list + transaction = { + 'chainId': 1, + 'gas': 25000, + 'gasPrice': 5000000000, + 'nonce': 0, + 'value': value_wei, + } + expected_call = mock.call(transaction, account.privkey) + self.assertEqual(m_signTransaction.call_args_list, [expected_call]) + def test_transact_no_funds(self): """ Tries to send a transaction from an address with no funds. From b9669d6ddaf3450f25afffb4ed31583b00193aa0 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 3 May 2019 00:01:39 +0200 Subject: [PATCH 17/60] tests and fixes handle_web3_exception() --- src/pywalib.py | 17 +++++++---------- src/tests/test_pywalib.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index 8bc41cc..ee49df2 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -13,7 +13,6 @@ from web3 import HTTPProvider, Web3 from ethereum_utils import AccountUtils -from pyethapp_accounts import Account ETHERSCAN_API_KEY = None ROUND_DIGITS = 3 @@ -176,19 +175,17 @@ def handle_etherscan_tx_error(response_json): else: raise UnknownEtherscanException(response_json) - # TODO: is this still used after web3 migration? @staticmethod - def handle_web3_exception(exception): + def handle_web3_exception(exception: ValueError): """ - TODO + Raises the appropriated typed exception on web3 ValueError exception. """ error = exception.args[0] - if error is not None: - code = error.get("code") - if code in [-32000, -32010]: - raise InsufficientFundsException() - else: - raise UnknownEtherscanException(response_json) + code = error.get("code") + if code in [-32000, -32010]: + raise InsufficientFundsException(error) + else: + raise UnknownEtherscanException(error) @staticmethod def add_transaction(tx): diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index db8a2fc..975ee25 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -298,7 +298,7 @@ def test_handle_etherscan_tx_error(self): """ Checks handle_etherscan_tx_error() error handling. """ - # no transaction found + # insufficient funds response_json = { 'jsonrpc': '2.0', 'id': 1, 'error': { 'message': @@ -328,6 +328,34 @@ def test_handle_etherscan_tx_error(self): PyWalib.handle_etherscan_tx_error(response_json), None) + def test_handle_web3_exception(self): + """ + Checks handle_web3_exception() error handling. + """ + # insufficient funds + exception = ValueError({ + 'code': -32000, + 'message': 'insufficient funds for gas * price + value' + }) + with self.assertRaises(InsufficientFundsException) as e: + PyWalib.handle_web3_exception(exception) + self.assertEqual(e.exception.args[0], exception.args[0]) + # unknown error code + exception = ValueError({ + 'code': 0, + 'message': 'Unknown error' + }) + with self.assertRaises(UnknownEtherscanException) as e: + PyWalib.handle_web3_exception(exception) + self.assertEqual(e.exception.args[0], exception.args[0]) + # no code + exception = ValueError({ + 'message': 'Unknown error' + }) + with self.assertRaises(UnknownEtherscanException) as e: + PyWalib.handle_web3_exception(exception) + self.assertEqual(e.exception.args[0], exception.args[0]) + def test_transact(self): """ Basic transact() test, makes sure web3 sendRawTransaction gets called. From 40e9f229ab9e6aec02f318d0064253b436edb7df Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 3 May 2019 00:02:26 +0200 Subject: [PATCH 18/60] removed handle_etherscan_tx_error() and add_transaction() Unused after migration to web3 --- src/pywalib.py | 40 --------------------------------------- src/tests/test_pywalib.py | 34 --------------------------------- 2 files changed, 74 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index ee49df2..e3eedf2 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -161,20 +161,6 @@ def get_nonce(address): nonce = len(out_transactions) return nonce - # TODO: is this still used after web3 migration? - @staticmethod - def handle_etherscan_tx_error(response_json): - """ - Raises an exception on unexpected response. - """ - error = response_json.get("error") - if error is not None: - code = error.get("code") - if code in [-32000, -32010]: - raise InsufficientFundsException() - else: - raise UnknownEtherscanException(response_json) - @staticmethod def handle_web3_exception(exception: ValueError): """ @@ -187,32 +173,6 @@ def handle_web3_exception(exception: ValueError): else: raise UnknownEtherscanException(error) - @staticmethod - def add_transaction(tx): - """ - POST transaction to etherscan.io. - """ - tx_hex = rlp.encode(tx).encode("hex") - # use https://etherscan.io/pushTx to debug - print("tx_hex:", tx_hex) - url = 'https://api.etherscan.io/api' - url += '?module=proxy&action=eth_sendRawTransaction' - if ETHERSCAN_API_KEY: - '&apikey=%' % ETHERSCAN_API_KEY - # TODO: handle 504 timeout, 403 and other errors from etherscan - response = requests.post(url, data={'hex': tx_hex}) - # response is like: - # {'jsonrpc': '2.0', 'result': '0x24a8...14ea', 'id': 1} - # or on error like this: - # {'jsonrpc': '2.0', 'id': 1, 'error': { - # 'message': 'Insufficient funds...', 'code': -32010, 'data': None}} - response_json = response.json() - print("response_json:", response_json) - PyWalib.handle_etherscan_tx_error(response_json) - tx_hash = response_json['result'] - # the response differs from the other responses - return tx_hash - def transact(self, to, value=0, data='', sender=None, gas=25000, gasprice=5 * (10 ** 9)): """ diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index 975ee25..3dd34dc 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -294,40 +294,6 @@ def test_get_nonce_no_transaction(self): nonce = PyWalib.get_nonce(address) self.assertEqual(nonce, 0) - def test_handle_etherscan_tx_error(self): - """ - Checks handle_etherscan_tx_error() error handling. - """ - # insufficient funds - response_json = { - 'jsonrpc': '2.0', 'id': 1, 'error': { - 'message': - 'Insufficient funds. ' - 'The account you tried to send transaction from does not ' - 'have enough funds. Required 10001500000000000000 and' - 'got: 53856999715015294.', - 'code': -32010, 'data': None - } - } - with self.assertRaises(InsufficientFundsException): - PyWalib.handle_etherscan_tx_error(response_json) - # unknown error - response_json = { - 'jsonrpc': '2.0', 'id': 1, 'error': { - 'message': - 'Unknown error', - 'code': 0, 'data': None - } - } - with self.assertRaises(UnknownEtherscanException) as e: - PyWalib.handle_etherscan_tx_error(response_json) - self.assertEqual(e.exception.args[0], response_json) - # no error - response_json = {'jsonrpc': '2.0', 'id': 1} - self.assertEqual( - PyWalib.handle_etherscan_tx_error(response_json), - None) - def test_handle_web3_exception(self): """ Checks handle_web3_exception() error handling. From 94114e0002454d7ff8abd4c453bb0feaca543fb6 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 3 May 2019 00:04:12 +0200 Subject: [PATCH 19/60] rlp no longer strong/explicit requirement --- requirements/requirements.txt | 1 - src/pywalib.py | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ad8c106..c127f70 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,7 +1,6 @@ eth-hash==0.1.1 eth-utils==1.4.1 web3==4.8.1 -rlp==0.6.0 # Kivy==1.10.1 # Kivy-Garden==0.1.4 # https://gitlab.com/kivymd/KivyMD/repository/archive.zip?ref=e81c02afbca915a4d71c85d3486f6710b53df2c1#egg=kivymd diff --git a/src/pywalib.py b/src/pywalib.py index e3eedf2..b958e1f 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -7,7 +7,6 @@ from os.path import expanduser import requests -import rlp from eth_utils import to_checksum_address from ethereum.utils import normalize_address from web3 import HTTPProvider, Web3 From 19e257474311cadc0e5356238780b4dc6958b3dd Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 3 May 2019 00:06:53 +0200 Subject: [PATCH 20/60] Adds test_ethereum_utils.py Imported from EtherollApp v2019.0426 --- src/tests/test_ethereum_utils.py | 164 +++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/tests/test_ethereum_utils.py diff --git a/src/tests/test_ethereum_utils.py b/src/tests/test_ethereum_utils.py new file mode 100644 index 0000000..146d33c --- /dev/null +++ b/src/tests/test_ethereum_utils.py @@ -0,0 +1,164 @@ +import os +import shutil +import unittest +from tempfile import TemporaryDirectory, mkdtemp +from unittest import mock + +from ethereum_utils import AccountUtils +from pyethapp_accounts import Account + +PASSWORD = "password" + + +class TestAccountUtils(unittest.TestCase): + + def setUp(self): + self.keystore_dir = mkdtemp() + self.account_utils = AccountUtils(self.keystore_dir) + + def tearDown(self): + shutil.rmtree(self.keystore_dir, ignore_errors=True) + + def test_new_account(self): + """ + Simple account creation test case. + 1) verifies the current account list is empty + 2) creates a new account and verify we can retrieve it + 3) tries to unlock the account + """ + # 1) verifies the current account list is empty + self.assertEqual(self.account_utils.get_account_list(), []) + # 2) creates a new account and verify we can retrieve it + password = PASSWORD + account = self.account_utils.new_account(password, iterations=1) + self.assertEqual(len(self.account_utils.get_account_list()), 1) + self.assertEqual(account, self.account_utils.get_account_list()[0]) + # 3) tries to unlock the account + # it's unlocked by default after creation + self.assertFalse(account.locked) + # let's lock it and unlock it back + account.lock() + self.assertTrue(account.locked) + account.unlock(password) + self.assertFalse(account.locked) + + def test_get_account_list(self): + """ + Makes sure get_account_list() loads properly accounts from file system. + """ + password = PASSWORD + self.assertEqual(self.account_utils.get_account_list(), []) + account = self.account_utils.new_account(password, iterations=1) + self.assertEqual(len(self.account_utils.get_account_list()), 1) + account = self.account_utils.get_account_list()[0] + self.assertIsNotNone(account.path) + # removes the cache copy and checks again if it gets loaded + self.account_utils._accounts = None + self.assertEqual(len(self.account_utils.get_account_list()), 1) + account = self.account_utils.get_account_list()[0] + self.assertIsNotNone(account.path) + + def test_get_account_list_error(self): + """ + get_account_list() should not cache empty account on PermissionError. + """ + # creates a temporary account that we'll try to list + password = PASSWORD + account = Account.new(password, uuid=None, iterations=1) + account.path = os.path.join(self.keystore_dir, account.address.hex()) + with open(account.path, 'w') as f: + f.write(account.dump()) + # `listdir()` can raise a `PermissionError` + with mock.patch('os.listdir') as mock_listdir: + mock_listdir.side_effect = PermissionError + with self.assertRaises(PermissionError): + self.account_utils.get_account_list() + # the empty account list should not be catched and loading it again + # should show the existing account on file system + self.assertEqual(len(self.account_utils.get_account_list()), 1) + self.assertEqual( + self.account_utils.get_account_list()[0].address, account.address) + + def test_get_account_list_no_dir(self): + """ + The keystore directory should be created if it doesn't exist, refs: + https://github.com/AndreMiras/PyWallet/issues/133 + """ + # nominal case when the directory already exists + with TemporaryDirectory() as keystore_dir: + self.assertTrue(os.path.isdir(keystore_dir)) + account_utils = AccountUtils(keystore_dir) + self.assertEqual(account_utils.get_account_list(), []) + # when the directory doesn't exist it should also be created + self.assertFalse(os.path.isdir(keystore_dir)) + account_utils = AccountUtils(keystore_dir) + self.assertTrue(os.path.isdir(keystore_dir)) + self.assertEqual(account_utils.get_account_list(), []) + shutil.rmtree(keystore_dir, ignore_errors=True) + + def test_deleted_account_dir(self): + """ + The deleted_account_dir() helper method should be working + with and without trailing slash. + """ + expected_deleted_keystore_dir = '/tmp/keystore-deleted' + keystore_dirs = [ + # without trailing slash + '/tmp/keystore', + # with one trailing slash + '/tmp/keystore/', + # with two trailing slashes + '/tmp/keystore//', + ] + for keystore_dir in keystore_dirs: + self.assertEqual( + AccountUtils.deleted_account_dir(keystore_dir), + expected_deleted_keystore_dir) + + def test_delete_account(self): + """ + Creates a new account and delete it. + Then verify we can load the account from the backup/trash location. + """ + password = PASSWORD + account = self.account_utils.new_account(password, iterations=1) + address = account.address + self.assertEqual(len(self.account_utils.get_account_list()), 1) + # deletes the account and verifies it's not loaded anymore + self.account_utils.delete_account(account) + self.assertEqual(len(self.account_utils.get_account_list()), 0) + # even recreating the AccountUtils object + self.account_utils = AccountUtils(self.keystore_dir) + self.assertEqual(len(self.account_utils.get_account_list()), 0) + # tries to reload it from the backup/trash location + deleted_keystore_dir = AccountUtils.deleted_account_dir( + self.keystore_dir) + self.account_utils = AccountUtils(deleted_keystore_dir) + self.assertEqual(len(self.account_utils.get_account_list()), 1) + self.assertEqual( + self.account_utils.get_account_list()[0].address, address) + + def test_delete_account_already_exists(self): + """ + If the destination (backup/trash) directory where the account is moved + already exists, it should be handled gracefully. + This could happens if the account gets deleted, then reimported and + deleted again, refs: + https://github.com/AndreMiras/PyWallet/issues/88 + """ + password = PASSWORD + account = self.account_utils.new_account(password, iterations=1) + # creates a file in the backup/trash folder that would conflict + # with the deleted account + deleted_keystore_dir = AccountUtils.deleted_account_dir( + self.keystore_dir) + os.makedirs(deleted_keystore_dir) + account_filename = os.path.basename(account.path) + deleted_account_path = os.path.join( + deleted_keystore_dir, account_filename) + # create that file + open(deleted_account_path, 'a').close() + # then deletes the account and verifies it worked + self.assertEqual(len(self.account_utils.get_account_list()), 1) + self.account_utils.delete_account(account) + self.assertEqual(len(self.account_utils.get_account_list()), 0) From ffebc0615dc1c67fd41710439cffd51202e9acc8 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 3 May 2019 00:09:24 +0200 Subject: [PATCH 21/60] Creates keystore dir if needed --- src/ethereum_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py index c49c874..591de88 100644 --- a/src/ethereum_utils.py +++ b/src/ethereum_utils.py @@ -8,6 +8,7 @@ class AccountUtils: def __init__(self, keystore_dir): self.keystore_dir = keystore_dir self._accounts = None + os.makedirs(keystore_dir, exist_ok=True) def get_account_list(self): """ From fc0cfd56dcf7f77136710655667c50d3d8f5b0f6 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 3 May 2019 00:14:53 +0200 Subject: [PATCH 22/60] Adds basic logging --- src/ethereum_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py index 591de88..566d6be 100644 --- a/src/ethereum_utils.py +++ b/src/ethereum_utils.py @@ -1,8 +1,19 @@ +import logging import os from pyethapp_accounts import Account +def get_logger(name): + logger = logging.getLogger(name) + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + return logger + + +log = get_logger(__name__) + + class AccountUtils: def __init__(self, keystore_dir): From e68e176c2a597bbf7fab4ca489e8632bcef975c3 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 3 May 2019 22:19:57 +0200 Subject: [PATCH 23/60] Updates to last garden.zbarcam version --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index c127f70..8a9c47b 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,5 +6,5 @@ web3==4.8.1 # https://gitlab.com/kivymd/KivyMD/repository/archive.zip?ref=e81c02afbca915a4d71c85d3486f6710b53df2c1#egg=kivymd Pillow==4.1.1 raven==6.1.0 -# https://github.com/AndreMiras/garden.zbarcam/archive/20171220.zip +https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip # qrcode==5.3 From aa96ea65e536a721e30c95cfffaaf26f628a9538 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 4 May 2019 22:55:21 +0200 Subject: [PATCH 24/60] get_balance() --- src/pywalib.py | 30 +++++++++++++++--------------- src/tests/test_pywalib.py | 30 +++++++++--------------------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index b958e1f..b3d7bf8 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -8,7 +8,6 @@ import requests from eth_utils import to_checksum_address -from ethereum.utils import normalize_address from web3 import HTTPProvider, Web3 from ethereum_utils import AccountUtils @@ -38,7 +37,7 @@ class ChainID(Enum): ROPSTEN = 3 -class PyWalib(object): +class PyWalib: def __init__(self, keystore_dir=None): if keystore_dir is None: @@ -62,22 +61,13 @@ def handle_etherscan_error(response_json): raise UnknownEtherscanException(response_json) assert message == "OK" - @staticmethod - def address_hex(address): - """ - Normalizes address. - """ - prefix = "0x" - address_hex = prefix + normalize_address(address).hex() - return address_hex - @staticmethod def get_balance(address): """ Retrieves the balance from etherscan.io. The balance is returned in ETH rounded to the second decimal. """ - address = PyWalib.address_hex(address) + address = to_checksum_address(address) url = 'https://api.etherscan.io/api' url += '?module=account&action=balance' url += '&address=%s' % address @@ -93,12 +83,22 @@ def get_balance(address): balance_eth = round(balance_eth, ROUND_DIGITS) return balance_eth + def get_balance_web3(self, address): + """ + The balance is returned in ETH rounded to the second decimal. + """ + address = to_checksum_address(address) + balance_wei = self.web3.eth.getBalance(address) + balance_eth = balance_wei / float(pow(10, 18)) + balance_eth = round(balance_eth, ROUND_DIGITS) + return balance_eth + @staticmethod def get_transaction_history(address): """ Retrieves the transaction history from etherscan.io. """ - address = PyWalib.address_hex(address) + address = to_checksum_address(address) url = 'https://api.etherscan.io/api' url += '?module=account&action=txlist' url += '&sort=asc' @@ -114,12 +114,12 @@ def get_transaction_history(address): value_wei = int(transaction['value']) value_eth = value_wei / float(pow(10, 18)) value_eth = round(value_eth, ROUND_DIGITS) - from_address = PyWalib.address_hex(transaction['from']) + from_address = to_checksum_address(transaction['from']) to_address = transaction['to'] # on contract creation, "to" is replaced by the "contractAddress" if not to_address: to_address = transaction['contractAddress'] - to_address = PyWalib.address_hex(to_address) + to_address = to_checksum_address(to_address) sent = from_address == address received = not sent extra_dict = { diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index 3dd34dc..62a4bc4 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -178,34 +178,22 @@ def test_handle_etherscan_error(self): PyWalib.handle_etherscan_error(response_json), None) - def test_address_hex(self): + def test_get_balance(self): """ - Checks handle_etherscan_error() error handling. + Checks get_balance() returns a float. """ - expected_addresss = ADDRESS - # no 0x prefix - address_no_prefix = ADDRESS.lower().strip("0x") - address = address_no_prefix - normalized = PyWalib.address_hex(address) - self.assertEqual(normalized, expected_addresss) - # uppercase - address = "0x" + address_no_prefix.upper() - normalized = PyWalib.address_hex(address) - self.assertEqual(normalized, expected_addresss) - # prefix cannot be uppercase - address = "0X" + address_no_prefix.upper() - with self.assertRaises(Exception) as context: - PyWalib.address_hex(address) - self.assertEqual( - context.exception.args[0], - "Invalid address format: '%s'" % (address)) + pywalib = self.pywalib + address = ADDRESS + balance_eth = pywalib.get_balance(address) + self.assertTrue(type(balance_eth), float) - def test_get_balance(self): + def test_get_balance_web3(self): """ Checks get_balance() returns a float. """ + pywalib = self.pywalib address = ADDRESS - balance_eth = PyWalib.get_balance(address) + balance_eth = pywalib.get_balance_web3(address) self.assertTrue(type(balance_eth), float) def helper_get_history(self, transactions): From 82b3a720789ebebd91ec5fab17b1b0eb93be0b58 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 4 May 2019 23:15:16 +0200 Subject: [PATCH 25/60] deps and install deps fixes --- Makefile | 2 +- requirements/requirements.txt | 6 +++--- tox.ini | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 889af06..913878e 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ VENV_NAME=venv ACTIVATE_PATH=$(VENV_NAME)/bin/activate PIP=`. $(ACTIVATE_PATH); which pip` TOX=`which tox` -GARDEN=`. $(ACTIVATE_PATH); which garden` +GARDEN=$(VENV_NAME)/bin/garden PYTHON=$(VENV_NAME)/bin/python SYSTEM_DEPENDENCIES=virtualenv build-essential libpython2.7-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8a9c47b..32df6cd 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,8 +3,8 @@ eth-utils==1.4.1 web3==4.8.1 # Kivy==1.10.1 # Kivy-Garden==0.1.4 -# https://gitlab.com/kivymd/KivyMD/repository/archive.zip?ref=e81c02afbca915a4d71c85d3486f6710b53df2c1#egg=kivymd +https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz#egg=kivymd Pillow==4.1.1 raven==6.1.0 -https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip -# qrcode==5.3 +https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip#egg=zbarcam +qrcode==5.3 diff --git a/tox.ini b/tox.ini index 5e39fd9..9778b39 100644 --- a/tox.ini +++ b/tox.ini @@ -28,4 +28,4 @@ exclude = src/python-for-android/ [testenv:isort-check] basepython = python3 commands = - isort --check-only --recursive src/ + isort --check-only --recursive --diff src/ From 21ccb85a9ac6449d7b64cf3112cd5a6053bc78b2 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 4 May 2019 23:23:37 +0200 Subject: [PATCH 26/60] UI migration from ethereum.utils to eth_utils --- src/main.py | 2 +- src/pywallet/send.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.py b/src/main.py index 13af02b..092da0e 100755 --- a/src/main.py +++ b/src/main.py @@ -46,7 +46,7 @@ class PyWalletApp(App): def build(self): self.icon = "docs/images/icon.png" - return Controller(info='PyWallet') + return Controller() @property def controller(self): diff --git a/src/pywallet/send.py b/src/pywallet/send.py index c3fa60c..1653a73 100644 --- a/src/pywallet/send.py +++ b/src/pywallet/send.py @@ -1,4 +1,4 @@ -from ethereum.utils import normalize_address +from eth_utils import to_checksum_address from kivy.app import App from kivy.logger import Logger from kivy.properties import NumericProperty, StringProperty @@ -25,8 +25,8 @@ def verify_to_address_field(self): title = "Input error" body = "Invalid address field" try: - normalize_address(self.send_to_address) - except Exception: + to_checksum_address(self.send_to_address) + except ValueError: dialog = Dialog.create_dialog(title, body) dialog.open() return False From 449a79f6d93996a9b4147036ec9796a3f2372618 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 4 May 2019 23:54:58 +0200 Subject: [PATCH 27/60] UI test fixes - encode("hex") -> hex() - mock -> unittest.mock - _Thread__target.func_name -> _target.__name__ --- requirements/test_requirements.txt | 1 - src/ethereum_utils.py | 4 ++-- src/pywallet/aliasform.py | 2 +- src/pywallet/controller.py | 12 ++++++------ src/pywallet/history.py | 4 ++-- src/pywallet/managekeystore.py | 2 +- src/pywallet/navigation.py | 2 +- src/pywallet/overview.py | 2 +- src/pywallet/receive.py | 2 +- src/pywallet/send.py | 2 +- src/pywallet/switchaccount.py | 2 +- src/tests/ui/test_ui_base.py | 17 +++++++++-------- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt index 8f97763..e49b2f4 100644 --- a/requirements/test_requirements.txt +++ b/requirements/test_requirements.txt @@ -3,5 +3,4 @@ isort==4.2.5 flake8==3.3.0 coverage==4.4.1 -mock==2.0.0 kivyunittest==0.1.8 diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py index 566d6be..30480aa 100644 --- a/src/ethereum_utils.py +++ b/src/ethereum_utils.py @@ -112,11 +112,11 @@ def get_by_address(self, address): accounts = [acc for acc in self._accounts if acc.address == address] if len(accounts) == 0: raise KeyError( - 'account not found by address', address=address.encode('hex')) + 'account not found by address', address=address.hex()) elif len(accounts) > 1: log.warning( 'multiple accounts with same address found', - address=address.encode('hex')) + address=address.hex()) return accounts[0] def sign_tx(self, address, tx): diff --git a/src/pywallet/aliasform.py b/src/pywallet/aliasform.py index 766d8f7..61213c6 100644 --- a/src/pywallet/aliasform.py +++ b/src/pywallet/aliasform.py @@ -20,7 +20,7 @@ def __init__(self, account, **kwargs): # circular ref from pywallet.controller import Controller super(AliasForm, self).__init__(**kwargs) - self.address = "0x" + account.address.encode("hex") + self.address = "0x" + account.address.hex() try: self.alias = Controller.get_address_alias(self.address) except KeyError: diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index 7ab8e77..3ddf90c 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -231,7 +231,7 @@ def delete_account_alias(cls, account): """ Deletes the alias for the given account. """ - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() store = cls.get_store() alias_dict = store['alias'] alias_dict.pop(address) @@ -251,7 +251,7 @@ def set_account_alias(cls, account, alias): except KeyError: pass return - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() store = cls.get_store() try: alias_dict = store['alias'] @@ -275,7 +275,7 @@ def get_account_alias(cls, account): """ Returns the alias of the given Account object. """ - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() return cls.get_address_alias(address) @staticmethod @@ -287,7 +287,7 @@ def src_dir(): def update_toolbar_title_balance(self, instance=None, value=None): if self.current_account is None: return - address = '0x' + self.current_account.address.encode("hex") + address = '0x' + self.current_account.address.hex() try: balance = self.accounts_balance[address] except KeyError: @@ -318,7 +318,7 @@ def fetch_balance(self): """ if self.current_account is None: return - address = '0x' + self.current_account.address.encode("hex") + address = '0x' + self.current_account.address.hex() try: balance = PyWalib.get_balance(address) except ConnectionError: @@ -355,7 +355,7 @@ def copy_address_clipboard(self): Copies the current account address to the clipboard. """ account = self.current_account - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() Clipboard.copy(address) def prompt_alias_dialog(self): diff --git a/src/pywallet/history.py b/src/pywallet/history.py index 495d2dc..0de018a 100644 --- a/src/pywallet/history.py +++ b/src/pywallet/history.py @@ -74,7 +74,7 @@ def update_history_list(self, instance=None, value=None): """ if self.current_account is None: return - address = '0x' + self.current_account.address.encode("hex") + address = '0x' + self.current_account.address.hex() try: transactions = self.controller.accounts_history[address] except KeyError: @@ -91,7 +91,7 @@ def update_history_list(self, instance=None, value=None): def fetch_history(self): if self.current_account is None: return - address = '0x' + self.current_account.address.encode("hex") + address = '0x' + self.current_account.address.hex() try: transactions = PyWalib.get_transaction_history(address) except ConnectionError: diff --git a/src/pywallet/managekeystore.py b/src/pywallet/managekeystore.py index d9a4444..b24363d 100644 --- a/src/pywallet/managekeystore.py +++ b/src/pywallet/managekeystore.py @@ -141,7 +141,7 @@ def on_current_account(self, instance, account): # Controller.current_account to None if account is None: return - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() self.address_property = address diff --git a/src/pywallet/navigation.py b/src/pywallet/navigation.py index fe1ba32..c3cf1ac 100644 --- a/src/pywallet/navigation.py +++ b/src/pywallet/navigation.py @@ -32,7 +32,7 @@ def on_current_account(self, account): # Controller.current_account to None if account is None: return - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() self.address_property = address def _update_specific_text_color(self, instance, value): diff --git a/src/pywallet/overview.py b/src/pywallet/overview.py index d6a4467..b832575 100644 --- a/src/pywallet/overview.py +++ b/src/pywallet/overview.py @@ -43,7 +43,7 @@ def update_current_account_string(self): if self.current_account is None: return account = self.current_account - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() self.current_account_string = address def on_current_account(self, instance, account): diff --git a/src/pywallet/receive.py b/src/pywallet/receive.py index 3d59312..27acf31 100644 --- a/src/pywallet/receive.py +++ b/src/pywallet/receive.py @@ -41,7 +41,7 @@ def update_address_property(self): Updates address_property from current_account. """ account = self.current_account - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() self.address_property = address def on_current_account(self, instance, account): diff --git a/src/pywallet/send.py b/src/pywallet/send.py index 1653a73..eca2696 100644 --- a/src/pywallet/send.py +++ b/src/pywallet/send.py @@ -84,7 +84,7 @@ def unlock_send_transaction(self): """ controller = App.get_running_app().controller pywalib = controller.pywalib - address = normalize_address(self.send_to_address) + address = to_checksum_address(self.send_to_address) amount_eth = round(self.send_amount, ROUND_DIGITS) amount_wei = int(amount_eth * pow(10, 18)) # TODO: not the main account, but the current account diff --git a/src/pywallet/switchaccount.py b/src/pywallet/switchaccount.py index 3189b6f..80bb387 100644 --- a/src/pywallet/switchaccount.py +++ b/src/pywallet/switchaccount.py @@ -30,7 +30,7 @@ def create_item(self, account): """ # circular ref from pywallet.controller import Controller - address = "0x" + account.address.encode("hex") + address = "0x" + account.address.hex() # gets the alias if exists try: text = Controller.get_address_alias(address) diff --git a/src/tests/ui/test_ui_base.py b/src/tests/ui/test_ui_base.py index eb2a0b3..8f85817 100644 --- a/src/tests/ui/test_ui_base.py +++ b/src/tests/ui/test_ui_base.py @@ -8,13 +8,13 @@ import unittest from functools import partial from tempfile import mkdtemp +from unittest import mock import kivymd import requests from kivy.clock import Clock import main -import mock import pywalib from pywallet.switchaccount import SwitchAccount from pywallet.utils import Dialog @@ -127,7 +127,7 @@ def helper_test_create_first_account(self, app): create_account_thread = threading.enumerate()[1] self.assertEqual(type(create_account_thread), threading.Thread) self.assertEqual( - create_account_thread._Thread__target.func_name, + create_account_thread._target.__name__, "create_account") # waits for the end of the thread create_account_thread.join() @@ -214,7 +214,7 @@ def helper_test_create_account_form(self, app): self.assertEqual( type(create_account_thread), threading.Thread) self.assertEqual( - create_account_thread._Thread__target.func_name, + create_account_thread._target.__name__, "create_account") # waits for the end of the thread create_account_thread.join() @@ -284,8 +284,9 @@ def helper_test_send(self, app): # unlock_send_transaction() thread should be running self.assertEqual(len(threading.enumerate()), 2) thread = threading.enumerate()[1] + self.assertEqual(type(thread), threading.Thread) self.assertEqual( - thread._Thread__target.func_name, 'unlock_send_transaction') + thread._target.__name__, 'unlock_send_transaction') thread.join() # checks snackbar messages self.assertEqual( @@ -322,8 +323,8 @@ def helper_test_address_alias(self, app): account1 = pywalib.get_account_list()[0] # creates a second account account2 = pywalib.new_account(password="password", security_ratio=1) - address1 = '0x' + account1.address.encode("hex") - address2 = '0x' + account2.address.encode("hex") + address1 = '0x' + account1.address.hex() + address2 = '0x' + account2.address.hex() Controller = main.Controller @staticmethod @@ -399,7 +400,7 @@ def helper_test_delete_account(self, app): manage_existing = controller.manage_existing account_address_id = manage_existing.ids.account_address_id account = pywalib.get_account_list()[0] - account_address = '0x' + account.address.encode("hex") + account_address = '0x' + account.address.hex() self.assertEqual(account_address_id.text, account_address) # clicks delete delete_button_id = manage_existing.ids.delete_button_id @@ -550,7 +551,7 @@ def helper_test_controller_fetch_balance(self, app): mock_get_balance.return_value = balance thread = controller.fetch_balance() thread.join() - address = '0x' + account.address.encode("hex") + address = '0x' + account.address.hex() mock_get_balance.assert_called_with(address) # and the balance updated self.assertEqual( From d422c4a6ac0e47118b7568d31fb750706fc11188 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 5 May 2019 00:00:35 +0200 Subject: [PATCH 28/60] sha3 and scrypt are not in the deps anymore --- src/tests/test_import.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/tests/test_import.py b/src/tests/test_import.py index 87d20be..08bcb04 100644 --- a/src/tests/test_import.py +++ b/src/tests/test_import.py @@ -8,18 +8,7 @@ class ModulesImportTestCase(unittest.TestCase): def test_hashlib_sha3(self): import hashlib - import sha3 self.assertIsNotNone(hashlib.sha3_512()) - self.assertIsNotNone(sha3.keccak_512()) - - def test_scrypt(self): - import scrypt - # This will take at least 0.1 seconds - data = scrypt.encrypt('a secret message', 'password', maxtime=0.1) - self.assertIsNotNone(data) - # 'scrypt\x00\r\x00\x00\x00\x08\x00\x00\x00\x01RX9H' - decrypted = scrypt.decrypt(data, 'password', maxtime=0.5) - self.assertEqual(decrypted, 'a secret message') def test_zbarcam(self): from zbarcam import zbarcam From c88ba5b5ac7ffe8608f6b9d679fa892d90635741 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 5 May 2019 00:07:50 +0200 Subject: [PATCH 29/60] StringIOCBWrite fixes The error was: ``` AttributeError: 'str' object has no attribute 'decode' ``` --- src/pywallet/utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pywallet/utils.py b/src/pywallet/utils.py index 51466bb..2b77e8f 100644 --- a/src/pywallet/utils.py +++ b/src/pywallet/utils.py @@ -68,11 +68,9 @@ def write(self, s): Calls the StringIO.write() method then the callback_write with given string parameter. """ - # io.StringIO expects unicode - s_unicode = s.decode('utf-8') - super(StringIOCBWrite, self).write(s_unicode) + super(StringIOCBWrite, self).write(s) if self.callback_write is not None: - self.callback_write(s_unicode) + self.callback_write(s) class Dialog(object): From 9cdf8113b41c506b832ec9a0338a88183fc36fa2 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 5 May 2019 22:24:38 +0200 Subject: [PATCH 30/60] Update CHANGELOG --- src/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 3b7a076..c27bbfa 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [Unreleased] + + - Migration to Python3, refs #19 + ## [v20180729] - Linux & Android Travis build on Docker, refs #37 From 8d21b150b396898327d211df12ab96c68e763dc6 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 5 May 2019 22:27:05 +0200 Subject: [PATCH 31/60] Minor requirements.txt fix --- requirements/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 32df6cd..ab7a110 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,8 +1,7 @@ eth-hash==0.1.1 eth-utils==1.4.1 web3==4.8.1 -# Kivy==1.10.1 -# Kivy-Garden==0.1.4 +Kivy==1.10.1 https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz#egg=kivymd Pillow==4.1.1 raven==6.1.0 From 19eeef3b8c2c818f10853895614a1d5f74831a50 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 5 May 2019 23:03:25 +0200 Subject: [PATCH 32/60] WIP Major buildozer.spec update, refs #146 Align with last buildozer and p4a upstream changes and align with new requirements.txt file. --- buildozer.spec | 76 +++++++++++++++-------------------- requirements/requirements.txt | 8 ++-- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index 38ba886..1bccd56 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -23,7 +23,7 @@ source.include_exts = py,png,jpg,kv,atlas,md # (list) List of directory to exclude (let empty to not exclude anything) #source.exclude_dirs = tests, bin -source.exclude_dirs = bin, venv, src/python-for-android +source.exclude_dirs = python-for-android # (list) List of exclusions using pattern matching #source.exclude_patterns = license,images/*/*.jpg @@ -38,42 +38,24 @@ version.filename = %(source.dir)s/version.py # (list) Application requirements # comma seperated e.g. requirements = sqlite3,kivy requirements = - hostpython2, - kivy, android, - setuptools, - openssl, - pycryptodome, - pysha3, - ethash, - scrypt, - bitcoin, - rlp==0.6.0, - repoze.lru, - PyYAML, - https://github.com/ethereum/pyethereum/archive/2e53bf3.zip, - devp2p==0.9.3, - coincurve==7.1.0, - gevent, - pbkdf2, - https://github.com/ethereum/pyethapp/archive/409331e88a397ce5276c430aff4a8866d413e45d.zip, - https://gitlab.com/kivymd/KivyMD/repository/archive.zip?ref=e81c02afbca915a4d71c85d3486f6710b53df2c1, - requests, - eth-hash==0.1.1, - pyelliptic==1.5.7, cffi==1.11.5, - libsecp256k1==355a38f, - asn1crypto==0.24.0, - coincurve==7.1.0, - qrcode, - contextlib2, - raven, - libiconv, - libzbar, - zbar, - pil, - https://github.com/AndreMiras/garden.zbarcam/archive/20171220.zip - + eth-hash==0.1.1, + eth-utils==1.4.1, + gevent, + https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip, + https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz, + Kivy==90c86f8, + openssl, + Pillow==4.1.1, + pycryptodome==3.4.6, + python3, + qrcode==5.3, + raven==6.1.0, + requests==2.20.0, + rlp==1.0.3, + setuptools==40.9.0, + web3==4.8.1 # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes @@ -125,20 +107,22 @@ fullscreen = 0 #android.presplash_color = #FFFFFF # (list) Permissions -#android.permissions = INTERNET android.permissions = INTERNET, CAMERA -# (int) Android API to use -#android.api = 19 +# (int) Target Android API, should be as high as possible. +android.api = 27 -# (int) Minimum API required -#android.minapi = 9 +# (int) Minimum API your APK will support. +android.minapi = 21 # (int) Android SDK version to use -#android.sdk = 20 +android.sdk = 20 # (str) Android NDK version to use -#android.ndk = 9c +android.ndk = 17c + +# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. +android.ndk_api = 21 # (bool) Use --private data storage (True) or --dir public storage (False) #android.private_storage = True @@ -157,6 +141,12 @@ android.permissions = INTERNET, CAMERA # when an update is due and you just want to test/build your package # android.skip_update = False +# (bool) If True, then automatically accept SDK license +# agreements. This is intended for automation only. If set to False, +# the default, you will be shown the license when first running +# buildozer. +android.accept_sdk_license = True + # (str) Android entry point, default is ok for Kivy-based app #android.entrypoint = org.renpy.android.PythonActivity @@ -265,7 +255,7 @@ p4a.local_recipes = %(source.dir)s/python-for-android/recipes/ [buildozer] # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) -log_level = 1 +log_level = 2 # (int) Display warning if buildozer is run as root (0 = False, 1 = True) warn_on_root = 1 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ab7a110..fd63059 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,9 +1,9 @@ eth-hash==0.1.1 eth-utils==1.4.1 -web3==4.8.1 -Kivy==1.10.1 +https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip#egg=zbarcam https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz#egg=kivymd +Kivy==1.10.1 Pillow==4.1.1 -raven==6.1.0 -https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip#egg=zbarcam qrcode==5.3 +raven==6.1.0 +web3==4.8.1 From bf8ddb67de19d660dc08ddc0e9e7d461f6e74cad Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 5 May 2019 23:21:19 +0200 Subject: [PATCH 33/60] Removes outdated local recipes Also update buildozer.spec --- buildozer.spec | 3 +- requirements/requirements.txt | 2 +- .../recipes/cffi/__init__.py | 52 -- .../recipes/cffi/disable-pkg-config.patch | 29 - .../recipes/coincurve/__init__.py | 42 -- .../recipes/coincurve/cross_compile.patch | 12 - .../coincurve/drop_setup_requires.patch | 12 - .../recipes/coincurve/find_lib.patch | 13 - .../recipes/coincurve/no-download.patch | 13 - .../recipes/coincurve/setup.py.patch | 22 - .../recipes/hostpython2/Setup | 495 ------------------ .../recipes/hostpython2/__init__.py | 69 --- .../recipes/libsecp256k1/__init__.py | 33 -- .../recipes/scrypt/__init__.py | 38 -- .../recipes/scrypt/remove_librt.patch | 20 - 15 files changed, 2 insertions(+), 853 deletions(-) delete mode 100644 src/python-for-android/recipes/cffi/__init__.py delete mode 100644 src/python-for-android/recipes/cffi/disable-pkg-config.patch delete mode 100644 src/python-for-android/recipes/coincurve/__init__.py delete mode 100644 src/python-for-android/recipes/coincurve/cross_compile.patch delete mode 100644 src/python-for-android/recipes/coincurve/drop_setup_requires.patch delete mode 100644 src/python-for-android/recipes/coincurve/find_lib.patch delete mode 100644 src/python-for-android/recipes/coincurve/no-download.patch delete mode 100644 src/python-for-android/recipes/coincurve/setup.py.patch delete mode 100644 src/python-for-android/recipes/hostpython2/Setup delete mode 100644 src/python-for-android/recipes/hostpython2/__init__.py delete mode 100644 src/python-for-android/recipes/libsecp256k1/__init__.py delete mode 100644 src/python-for-android/recipes/scrypt/__init__.py delete mode 100644 src/python-for-android/recipes/scrypt/remove_librt.patch diff --git a/buildozer.spec b/buildozer.spec index 1bccd56..1c587ea 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -47,8 +47,7 @@ requirements = https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz, Kivy==90c86f8, openssl, - Pillow==4.1.1, - pycryptodome==3.4.6, + Pillow==5.2.0, python3, qrcode==5.3, raven==6.1.0, diff --git a/requirements/requirements.txt b/requirements/requirements.txt index fd63059..0919628 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,7 +3,7 @@ eth-utils==1.4.1 https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip#egg=zbarcam https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz#egg=kivymd Kivy==1.10.1 -Pillow==4.1.1 +Pillow==5.2.0 qrcode==5.3 raven==6.1.0 web3==4.8.1 diff --git a/src/python-for-android/recipes/cffi/__init__.py b/src/python-for-android/recipes/cffi/__init__.py deleted file mode 100644 index 1492b5e..0000000 --- a/src/python-for-android/recipes/cffi/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -from pythonforandroid.recipe import CompiledComponentsPythonRecipe - - -class CffiRecipe(CompiledComponentsPythonRecipe): - name = 'cffi' - version = '1.11.5' - url = 'https://pypi.python.org/packages/source/c/cffi/cffi-{version}.tar.gz' - - depends = [('python2', 'python3crystax'), 'setuptools', 'pycparser', 'libffi'] - - patches = ['disable-pkg-config.patch'] - - call_hostpython_via_targetpython = False - install_in_hostpython = True - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(CffiRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # sets linker to use the correct gcc (cross compiler) - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' - libffi = self.get_recipe('libffi', self.ctx) - includes = libffi.get_include_dirs(arch) - env['CFLAGS'] = ' -I'.join([env.get('CFLAGS', '')] + includes) - env['LDFLAGS'] = (env.get('CFLAGS', '') + ' -L' + - self.ctx.get_libs_dir(arch.arch)) - env['LDFLAGS'] += ' -L{}'.format(os.path.join(self.ctx.bootstrap.build_dir, 'libs', arch.arch)) - # required for libc and libdl - ndk_dir = self.ctx.ndk_platform - ndk_lib_dir = os.path.join(ndk_dir, 'usr', 'lib') - env['LDFLAGS'] += ' -L{}'.format(ndk_lib_dir) - env['LDFLAGS'] += " --sysroot={}".format(self.ctx.ndk_platform) - env['PYTHONPATH'] = ':'.join([ - self.ctx.get_site_packages_dir(), - env['BUILDLIB_PATH'], - ]) - python_version = self.ctx.python_recipe.version[0:3] - if self.ctx.ndk == 'crystax': - # only keeps major.minor (discards patch) - ndk_dir_python = os.path.join(self.ctx.ndk_dir, 'sources/python/', python_version) - env['LDFLAGS'] += ' -L{}'.format(os.path.join(ndk_dir_python, 'libs', arch.arch)) - env['LDFLAGS'] += ' -lpython{}m'.format(python_version) - # until `pythonforandroid/archs.py` gets merged upstream: - # https://github.com/kivy/python-for-android/pull/1250/files#diff-569e13021e33ced8b54385f55b49cbe6 - env['CFLAGS'] += ' -I{}/include/python/'.format(ndk_dir_python) - else: - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() - env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python{}'.format(python_version) - env['LDFLAGS'] += " -lpython{}".format(python_version) - return env - - -recipe = CffiRecipe() diff --git a/src/python-for-android/recipes/cffi/disable-pkg-config.patch b/src/python-for-android/recipes/cffi/disable-pkg-config.patch deleted file mode 100644 index 56346bb..0000000 --- a/src/python-for-android/recipes/cffi/disable-pkg-config.patch +++ /dev/null @@ -1,29 +0,0 @@ -diff -Naur cffi-1.4.2/setup.py b/setup.py ---- cffi-1.4.2/setup.py 2015-12-21 12:09:47.000000000 -0600 -+++ b/setup.py 2015-12-23 10:20:40.590622524 -0600 -@@ -5,8 +5,7 @@ - - sources = ['c/_cffi_backend.c'] - libraries = ['ffi'] --include_dirs = ['/usr/include/ffi', -- '/usr/include/libffi'] # may be changed by pkg-config -+include_dirs = [] - define_macros = [] - library_dirs = [] - extra_compile_args = [] -@@ -67,14 +66,7 @@ - sys.stderr.write("The above error message can be safely ignored\n") - - def use_pkg_config(): -- if sys.platform == 'darwin' and os.path.exists('/usr/local/bin/brew'): -- use_homebrew_for_libffi() -- -- _ask_pkg_config(include_dirs, '--cflags-only-I', '-I', sysroot=True) -- _ask_pkg_config(extra_compile_args, '--cflags-only-other') -- _ask_pkg_config(library_dirs, '--libs-only-L', '-L', sysroot=True) -- _ask_pkg_config(extra_link_args, '--libs-only-other') -- _ask_pkg_config(libraries, '--libs-only-l', '-l') -+ pass - - def use_homebrew_for_libffi(): - # We can build by setting: diff --git a/src/python-for-android/recipes/coincurve/__init__.py b/src/python-for-android/recipes/coincurve/__init__.py deleted file mode 100644 index 921655e..0000000 --- a/src/python-for-android/recipes/coincurve/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -from pythonforandroid.recipe import PythonRecipe, CompiledComponentsPythonRecipe - - -class CoincurveRecipe(CompiledComponentsPythonRecipe): - version = '7.1.0' - url = 'https://github.com/ofek/coincurve/archive/{version}.tar.gz' - call_hostpython_via_targetpython = False - depends = [('python2', 'python3crystax'), 'setuptools', - 'libffi', 'cffi', 'libsecp256k1'] - patches = [ - "cross_compile.patch", "drop_setup_requires.patch", - "find_lib.patch", "no-download.patch", "setup.py.patch"] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(CoincurveRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # sets linker to use the correct gcc (cross compiler) - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' - libsecp256k1 = self.get_recipe('libsecp256k1', self.ctx) - libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch) - env['LDFLAGS'] += ' -L{}'.format(os.path.join(libsecp256k1_dir, '.libs')) - env['CFLAGS'] += ' -I' + os.path.join(libsecp256k1_dir, 'include') - # only keeps major.minor (discards patch) - python_version = self.ctx.python_recipe.version[0:3] - # required additional library and path for Crystax - if self.ctx.ndk == 'crystax': - ndk_dir_python = os.path.join(self.ctx.ndk_dir, 'sources/python/', python_version) - env['LDFLAGS'] += ' -L{}'.format(os.path.join(ndk_dir_python, 'libs', arch.arch)) - env['LDFLAGS'] += ' -lpython{}m'.format(python_version) - # until `pythonforandroid/archs.py` gets merged upstream: - # https://github.com/kivy/python-for-android/pull/1250/files#diff-569e13021e33ced8b54385f55b49cbe6 - env['CFLAGS'] += ' -I{}/include/python/'.format(ndk_dir_python) - else: - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() - env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python{}'.format(python_version) - env['LDFLAGS'] += " -lpython{}".format(python_version) - env['LDFLAGS'] += " -lsecp256k1" - return env - - -recipe = CoincurveRecipe() - diff --git a/src/python-for-android/recipes/coincurve/cross_compile.patch b/src/python-for-android/recipes/coincurve/cross_compile.patch deleted file mode 100644 index fbbdd49..0000000 --- a/src/python-for-android/recipes/coincurve/cross_compile.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/setup.py b/setup.py -index c224fb2..bf925bd 100644 ---- a/setup.py -+++ b/setup.py -@@ -182,6 +182,7 @@ class build_clib(_build_clib): - '--disable-dependency-tracking', - '--with-pic', - '--enable-module-recovery', -+ "--host=%s" % os.environ['TOOLCHAIN_PREFIX'], - '--disable-jni', - '--prefix', - os.path.abspath(self.build_clib), diff --git a/src/python-for-android/recipes/coincurve/drop_setup_requires.patch b/src/python-for-android/recipes/coincurve/drop_setup_requires.patch deleted file mode 100644 index 9994b3f..0000000 --- a/src/python-for-android/recipes/coincurve/drop_setup_requires.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/setup.py b/setup.py -index c224fb2..466e789 100644 ---- a/setup.py -+++ b/setup.py -@@ -250,7 +250,6 @@ else: - def has_c_libraries(self): - return not has_system_lib() - setup_kwargs = dict( -- setup_requires=['cffi>=1.3.0', 'pytest-runner>=2.6.2'], - ext_package='coincurve', - cffi_modules=['_cffi_build/build.py:ffi'], - cmdclass={ diff --git a/src/python-for-android/recipes/coincurve/find_lib.patch b/src/python-for-android/recipes/coincurve/find_lib.patch deleted file mode 100644 index 3d3c41d..0000000 --- a/src/python-for-android/recipes/coincurve/find_lib.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/setup_support.py b/setup_support.py -index e7a4f2e..72f0c4d 100644 ---- a/setup_support.py -+++ b/setup_support.py -@@ -68,6 +69,8 @@ def build_flags(library, type_, path): - - - def _find_lib(): -+ # we're picking up the recipe one -+ return True - from cffi import FFI - ffi = FFI() - try: diff --git a/src/python-for-android/recipes/coincurve/no-download.patch b/src/python-for-android/recipes/coincurve/no-download.patch deleted file mode 100644 index fcf4d20..0000000 --- a/src/python-for-android/recipes/coincurve/no-download.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/setup.py b/setup.py -index c224fb2..d5f6d1a 100644 ---- a/setup.py -+++ b/setup.py -@@ -51,6 +51,8 @@ if [int(i) for i in setuptools_version.split('.', 2)[:2]] < [3, 3]: - - - def download_library(command): -+ # we will use the custom libsecp256k1 recipe -+ return - if command.dry_run: - return - libdir = absolute('libsecp256k1') diff --git a/src/python-for-android/recipes/coincurve/setup.py.patch b/src/python-for-android/recipes/coincurve/setup.py.patch deleted file mode 100644 index bdc7ab4..0000000 --- a/src/python-for-android/recipes/coincurve/setup.py.patch +++ /dev/null @@ -1,22 +0,0 @@ -From bf3a0684e9b0af29d9777f61d6e7e1d3cc0f2803 Mon Sep 17 00:00:00 2001 -From: Kieran Prasch -Date: Thu, 19 Jul 2018 14:11:48 -0700 -Subject: [PATCH] Exclude tests in setup.py - ---- - setup.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/setup.py b/setup.py -index 0b579f1..0a793ed 100644 ---- a/setup.py -+++ b/setup.py -@@ -277,7 +277,7 @@ def has_c_libraries(self): - install_requires=['asn1crypto', 'cffi>=1.3.0'], - tests_require=['pytest>=2.8.7'], - -- packages=find_packages(exclude=('_cffi_build', '_cffi_build.*', 'libsecp256k1')), -+ packages=find_packages(exclude=('_cffi_build', '_cffi_build.*', 'libsecp256k1', 'tests')), - - distclass=Distribution, - zip_safe=False, diff --git a/src/python-for-android/recipes/hostpython2/Setup b/src/python-for-android/recipes/hostpython2/Setup deleted file mode 100644 index d21c893..0000000 --- a/src/python-for-android/recipes/hostpython2/Setup +++ /dev/null @@ -1,495 +0,0 @@ -# -*- makefile -*- -# The file Setup is used by the makesetup script to construct the files -# Makefile and config.c, from Makefile.pre and config.c.in, -# respectively. The file Setup itself is initially copied from -# Setup.dist; once it exists it will not be overwritten, so you can edit -# Setup to your heart's content. Note that Makefile.pre is created -# from Makefile.pre.in by the toplevel configure script. - -# (VPATH notes: Setup and Makefile.pre are in the build directory, as -# are Makefile and config.c; the *.in and *.dist files are in the source -# directory.) - -# Each line in this file describes one or more optional modules. -# Modules enabled here will not be compiled by the setup.py script, -# so the file can be used to override setup.py's behavior. - -# Lines have the following structure: -# -# ... [ ...] [ ...] [ ...] -# -# is anything ending in .c (.C, .cc, .c++ are C++ files) -# is anything starting with -I, -D, -U or -C -# is anything ending in .a or beginning with -l or -L -# is anything else but should be a valid Python -# identifier (letters, digits, underscores, beginning with non-digit) -# -# (As the makesetup script changes, it may recognize some other -# arguments as well, e.g. *.so and *.sl as libraries. See the big -# case statement in the makesetup script.) -# -# Lines can also have the form -# -# = -# -# which defines a Make variable definition inserted into Makefile.in -# -# Finally, if a line contains just the word "*shared*" (without the -# quotes but with the stars), then the following modules will not be -# built statically. The build process works like this: -# -# 1. Build all modules that are declared as static in Modules/Setup, -# combine them into libpythonxy.a, combine that into python. -# 2. Build all modules that are listed as shared in Modules/Setup. -# 3. Invoke setup.py. That builds all modules that -# a) are not builtin, and -# b) are not listed in Modules/Setup, and -# c) can be build on the target -# -# Therefore, modules declared to be shared will not be -# included in the config.c file, nor in the list of objects to be -# added to the library archive, and their linker options won't be -# added to the linker options. Rules to create their .o files and -# their shared libraries will still be added to the Makefile, and -# their names will be collected in the Make variable SHAREDMODS. This -# is used to build modules as shared libraries. (They can be -# installed using "make sharedinstall", which is implied by the -# toplevel "make install" target.) (For compatibility, -# *noconfig* has the same effect as *shared*.) -# -# In addition, *static* explicitly declares the following modules to -# be static. Lines containing "*static*" and "*shared*" may thus -# alternate throughout this file. - -# NOTE: As a standard policy, as many modules as can be supported by a -# platform should be present. The distribution comes with all modules -# enabled that are supported by most platforms and don't require you -# to ftp sources from elsewhere. - - -# Some special rules to define PYTHONPATH. -# Edit the definitions below to indicate which options you are using. -# Don't add any whitespace or comments! - -# Directories where library files get installed. -# DESTLIB is for Python modules; MACHDESTLIB for shared libraries. -DESTLIB=$(LIBDEST) -MACHDESTLIB=$(BINLIBDEST) - -# NOTE: all the paths are now relative to the prefix that is computed -# at run time! - -# Standard path -- don't edit. -# No leading colon since this is the first entry. -# Empty since this is now just the runtime prefix. -DESTPATH= - -# Site specific path components -- should begin with : if non-empty -SITEPATH= - -# Standard path components for test modules -TESTPATH= - -# Path components for machine- or system-dependent modules and shared libraries -MACHDEPPATH=:plat-$(MACHDEP) -EXTRAMACHDEPPATH= - -# Path component for the Tkinter-related modules -# The TKPATH variable is always enabled, to save you the effort. -TKPATH=:lib-tk - -# Path component for old modules. -OLDPATH=:lib-old - -COREPYTHONPATH=$(DESTPATH)$(SITEPATH)$(TESTPATH)$(MACHDEPPATH)$(EXTRAMACHDEPPATH)$(TKPATH)$(OLDPATH) -PYTHONPATH=$(COREPYTHONPATH) - - -# The modules listed here can't be built as shared libraries for -# various reasons; therefore they are listed here instead of in the -# normal order. - -# This only contains the minimal set of modules required to run the -# setup.py script in the root of the Python source tree. - -posix posixmodule.c # posix (UNIX) system calls -errno errnomodule.c # posix (UNIX) errno values -pwd pwdmodule.c # this is needed to find out the user's home dir - # if $HOME is not set -_sre _sre.c # Fredrik Lundh's new regular expressions -_codecs _codecsmodule.c # access to the builtin codecs and codec registry - -# The zipimport module is always imported at startup. Having it as a -# builtin module avoids some bootstrapping problems and reduces overhead. -zipimport zipimport.c - -# The rest of the modules listed in this file are all commented out by -# default. Usually they can be detected and built as dynamically -# loaded modules by the new setup.py script added in Python 2.1. If -# you're on a platform that doesn't support dynamic loading, want to -# compile modules statically into the Python binary, or need to -# specify some odd set of compiler switches, you can uncomment the -# appropriate lines below. - -# ====================================================================== - -# The Python symtable module depends on .h files that setup.py doesn't track -_symtable symtablemodule.c - -# The SGI specific GL module: - -GLHACK=-Dclear=__GLclear -#gl glmodule.c cgensupport.c -I$(srcdir) $(GLHACK) -lgl -lX11 - -# Pure module. Cannot be linked dynamically. -# -DWITH_QUANTIFY, -DWITH_PURIFY, or -DWITH_ALL_PURE -#WHICH_PURE_PRODUCTS=-DWITH_ALL_PURE -#PURE_INCLS=-I/usr/local/include -#PURE_STUBLIBS=-L/usr/local/lib -lpurify_stubs -lquantify_stubs -#pure puremodule.c $(WHICH_PURE_PRODUCTS) $(PURE_INCLS) $(PURE_STUBLIBS) - -# Uncommenting the following line tells makesetup that all following -# modules are to be built as shared libraries (see above for more -# detail; also note that *static* reverses this effect): - -#*shared* - -# GNU readline. Unlike previous Python incarnations, GNU readline is -# now incorporated in an optional module, configured in the Setup file -# instead of by a configure script switch. You may have to insert a -# -L option pointing to the directory where libreadline.* lives, -# and you may have to change -ltermcap to -ltermlib or perhaps remove -# it, depending on your system -- see the GNU readline instructions. -# It's okay for this to be a shared library, too. - -#readline readline.c -lreadline -ltermcap - - -# Modules that should always be present (non UNIX dependent): - -array arraymodule.c # array objects -cmath cmathmodule.c # -lm # complex math library functions -math mathmodule.c # -lm # math library functions, e.g. sin() -_struct _struct.c # binary structure packing/unpacking -time timemodule.c # -lm # time operations and variables -operator operator.c # operator.add() and similar goodies -_weakref _weakref.c # basic weak reference support -#_testcapi _testcapimodule.c # Python C API test module -_random _randommodule.c # Random number generator -_collections _collectionsmodule.c # Container types -itertools itertoolsmodule.c # Functions creating iterators for efficient looping -strop stropmodule.c # String manipulations -_functools _functoolsmodule.c # Tools for working with functions and callable objects -_elementtree -I$(srcdir)/Modules/expat -DHAVE_EXPAT_CONFIG_H -DUSE_PYEXPAT_CAPI _elementtree.c # elementtree accelerator -#_pickle _pickle.c # pickle accelerator -datetime datetimemodule.c # date/time type -_bisect _bisectmodule.c # Bisection algorithms - -#unicodedata unicodedata.c # static Unicode character database - -# access to ISO C locale support -#_locale _localemodule.c # -lintl - - -# Modules with some UNIX dependencies -- on by default: -# (If you have a really backward UNIX, select and socket may not be -# supported...) - -fcntl fcntlmodule.c # fcntl(2) and ioctl(2) -#spwd spwdmodule.c # spwd(3) -#grp grpmodule.c # grp(3) -select selectmodule.c # select(2); not on ancient System V - -# Memory-mapped files (also works on Win32). -#mmap mmapmodule.c - -# CSV file helper -#_csv _csv.c - -# Socket module helper for socket(2) -_socket socketmodule.c - -# Socket module helper for SSL support; you must comment out the other -# socket line above, and possibly edit the SSL variable: -#SSL=/usr/local/ssl -#_ssl _ssl.c \ -# -DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl \ -# -L$(SSL)/lib -lssl -lcrypto - -# The crypt module is now disabled by default because it breaks builds -# on many systems (where -lcrypt is needed), e.g. Linux (I believe). -# -# First, look at Setup.config; configure may have set this for you. - -#crypt cryptmodule.c # -lcrypt # crypt(3); needs -lcrypt on some systems - - -# Some more UNIX dependent modules -- off by default, since these -# are not supported by all UNIX systems: - -#nis nismodule.c -lnsl # Sun yellow pages -- not everywhere -#termios termios.c # Steen Lumholt's termios module -#resource resource.c # Jeremy Hylton's rlimit interface - - -# Multimedia modules -- off by default. -# These don't work for 64-bit platforms!!! -# #993173 says audioop works on 64-bit platforms, though. -# These represent audio samples or images as strings: - -#audioop audioop.c # Operations on audio samples -#imageop imageop.c # Operations on images - - -# Note that the _md5 and _sha modules are normally only built if the -# system does not have the OpenSSL libs containing an optimized version. - -# The _md5 module implements the RSA Data Security, Inc. MD5 -# Message-Digest Algorithm, described in RFC 1321. The necessary files -# md5.c and md5.h are included here. - -_md5 md5module.c md5.c - - -# The _sha module implements the SHA checksum algorithms. -# (NIST's Secure Hash Algorithms.) -_sha shamodule.c -_sha256 sha256module.c -_sha512 sha512module.c - - -# SGI IRIX specific modules -- off by default. - -# These module work on any SGI machine: - -# *** gl must be enabled higher up in this file *** -#fm fmmodule.c $(GLHACK) -lfm -lgl # Font Manager -#sgi sgimodule.c # sgi.nap() and a few more - -# This module requires the header file -# /usr/people/4Dgifts/iristools/include/izoom.h: -#imgfile imgfile.c -limage -lgutil -lgl -lm # Image Processing Utilities - - -# These modules require the Multimedia Development Option (I think): - -#al almodule.c -laudio # Audio Library -#cd cdmodule.c -lcdaudio -lds -lmediad # CD Audio Library -#cl clmodule.c -lcl -lawareaudio # Compression Library -#sv svmodule.c yuvconvert.c -lsvideo -lXext -lX11 # Starter Video - - -# The FORMS library, by Mark Overmars, implements user interface -# components such as dialogs and buttons using SGI's GL and FM -# libraries. You must ftp the FORMS library separately from -# ftp://ftp.cs.ruu.nl/pub/SGI/FORMS. It was tested with FORMS 2.2a. -# NOTE: if you want to be able to use FORMS and curses simultaneously -# (or both link them statically into the same binary), you must -# compile all of FORMS with the cc option "-Dclear=__GLclear". - -# The FORMS variable must point to the FORMS subdirectory of the forms -# toplevel directory: - -#FORMS=/ufs/guido/src/forms/FORMS -#fl flmodule.c -I$(FORMS) $(GLHACK) $(FORMS)/libforms.a -lfm -lgl - - -# SunOS specific modules -- off by default: - -#sunaudiodev sunaudiodev.c - - -# A Linux specific module -- off by default; this may also work on -# some *BSDs. - -#linuxaudiodev linuxaudiodev.c - - -# George Neville-Neil's timing module: - -#timing timingmodule.c - - -# The _tkinter module. -# -# The command for _tkinter is long and site specific. Please -# uncomment and/or edit those parts as indicated. If you don't have a -# specific extension (e.g. Tix or BLT), leave the corresponding line -# commented out. (Leave the trailing backslashes in! If you -# experience strange errors, you may want to join all uncommented -# lines and remove the backslashes -- the backslash interpretation is -# done by the shell's "read" command and it may not be implemented on -# every system. - -# *** Always uncomment this (leave the leading underscore in!): -# _tkinter _tkinter.c tkappinit.c -DWITH_APPINIT \ -# *** Uncomment and edit to reflect where your Tcl/Tk libraries are: -# -L/usr/local/lib \ -# *** Uncomment and edit to reflect where your Tcl/Tk headers are: -# -I/usr/local/include \ -# *** Uncomment and edit to reflect where your X11 header files are: -# -I/usr/X11R6/include \ -# *** Or uncomment this for Solaris: -# -I/usr/openwin/include \ -# *** Uncomment and edit for Tix extension only: -# -DWITH_TIX -ltix8.1.8.2 \ -# *** Uncomment and edit for BLT extension only: -# -DWITH_BLT -I/usr/local/blt/blt8.0-unoff/include -lBLT8.0 \ -# *** Uncomment and edit for PIL (TkImaging) extension only: -# (See http://www.pythonware.com/products/pil/ for more info) -# -DWITH_PIL -I../Extensions/Imaging/libImaging tkImaging.c \ -# *** Uncomment and edit for TOGL extension only: -# -DWITH_TOGL togl.c \ -# *** Uncomment and edit to reflect your Tcl/Tk versions: -# -ltk8.2 -ltcl8.2 \ -# *** Uncomment and edit to reflect where your X11 libraries are: -# -L/usr/X11R6/lib \ -# *** Or uncomment this for Solaris: -# -L/usr/openwin/lib \ -# *** Uncomment these for TOGL extension only: -# -lGL -lGLU -lXext -lXmu \ -# *** Uncomment for AIX: -# -lld \ -# *** Always uncomment this; X11 libraries to link with: -# -lX11 - -# Lance Ellinghaus's syslog module -#syslog syslogmodule.c # syslog daemon interface - - -# Curses support, requring the System V version of curses, often -# provided by the ncurses library. e.g. on Linux, link with -lncurses -# instead of -lcurses). -# -# First, look at Setup.config; configure may have set this for you. - -#_curses _cursesmodule.c -lcurses -ltermcap -# Wrapper for the panel library that's part of ncurses and SYSV curses. -#_curses_panel _curses_panel.c -lpanel -lncurses - - -# Generic (SunOS / SVR4) dynamic loading module. -# This is not needed for dynamic loading of Python modules -- -# it is a highly experimental and dangerous device for calling -# *arbitrary* C functions in *arbitrary* shared libraries: - -#dl dlmodule.c - - -# Modules that provide persistent dictionary-like semantics. You will -# probably want to arrange for at least one of them to be available on -# your machine, though none are defined by default because of library -# dependencies. The Python module anydbm.py provides an -# implementation independent wrapper for these; dumbdbm.py provides -# similar functionality (but slower of course) implemented in Python. - -# The standard Unix dbm module has been moved to Setup.config so that -# it will be compiled as a shared library by default. Compiling it as -# a built-in module causes conflicts with the pybsddb3 module since it -# creates a static dependency on an out-of-date version of db.so. -# -# First, look at Setup.config; configure may have set this for you. - -#dbm dbmmodule.c # dbm(3) may require -lndbm or similar - -# Anthony Baxter's gdbm module. GNU dbm(3) will require -lgdbm: -# -# First, look at Setup.config; configure may have set this for you. - -#gdbm gdbmmodule.c -I/usr/local/include -L/usr/local/lib -lgdbm - - -# Sleepycat Berkeley DB interface. -# -# This requires the Sleepycat DB code, see http://www.sleepycat.com/ -# The earliest supported version of that library is 3.0, the latest -# supported version is 4.0 (4.1 is specifically not supported, as that -# changes the semantics of transactional databases). A list of available -# releases can be found at -# -# http://www.sleepycat.com/update/index.html -# -# Edit the variables DB and DBLIBVERto point to the db top directory -# and the subdirectory of PORT where you built it. -#DB=/usr/local/BerkeleyDB.4.0 -#DBLIBVER=4.0 -#DBINC=$(DB)/include -#DBLIB=$(DB)/lib -#_bsddb _bsddb.c -I$(DBINC) -L$(DBLIB) -ldb-$(DBLIBVER) - -# Historical Berkeley DB 1.85 -# -# This module is deprecated; the 1.85 version of the Berkeley DB library has -# bugs that can cause data corruption. If you can, use later versions of the -# library instead, available from . - -#DB=/depot/sundry/src/berkeley-db/db.1.85 -#DBPORT=$(DB)/PORT/irix.5.3 -#bsddb185 bsddbmodule.c -I$(DBPORT)/include -I$(DBPORT) $(DBPORT)/libdb.a - - - -# Helper module for various ascii-encoders -binascii binascii.c - -# Fred Drake's interface to the Python parser -parser parsermodule.c - -# cStringIO and cPickle -cStringIO cStringIO.c -cPickle cPickle.c - - -# Lee Busby's SIGFPE modules. -# The library to link fpectl with is platform specific. -# Choose *one* of the options below for fpectl: - -# For SGI IRIX (tested on 5.3): -#fpectl fpectlmodule.c -lfpe - -# For Solaris with SunPro compiler (tested on Solaris 2.5 with SunPro C 4.2): -# (Without the compiler you don't have -lsunmath.) -#fpectl fpectlmodule.c -R/opt/SUNWspro/lib -lsunmath -lm - -# For other systems: see instructions in fpectlmodule.c. -#fpectl fpectlmodule.c ... - -# Test module for fpectl. No extra libraries needed. -#fpetest fpetestmodule.c - -# Andrew Kuchling's zlib module. -# This require zlib 1.1.3 (or later). -# See http://www.gzip.org/zlib/ -zlib zlibmodule.c -I$(prefix)/include -L$(exec_prefix)/lib -lz - -# Interface to the Expat XML parser -# -# Expat was written by James Clark and is now maintained by a group of -# developers on SourceForge; see www.libexpat.org for more -# information. The pyexpat module was written by Paul Prescod after a -# prototype by Jack Jansen. Source of Expat 1.95.2 is included in -# Modules/expat/. Usage of a system shared libexpat.so/expat.dll is -# not advised. -# -# More information on Expat can be found at www.libexpat.org. -# -pyexpat expat/xmlparse.c expat/xmlrole.c expat/xmltok.c pyexpat.c -I$(srcdir)/Modules/expat -DHAVE_EXPAT_CONFIG_H -DUSE_PYEXPAT_CAPI - - -# Hye-Shik Chang's CJKCodecs - -# multibytecodec is required for all the other CJK codec modules -#_multibytecodec cjkcodecs/multibytecodec.c - -#_codecs_cn cjkcodecs/_codecs_cn.c -#_codecs_hk cjkcodecs/_codecs_hk.c -#_codecs_iso2022 cjkcodecs/_codecs_iso2022.c -#_codecs_jp cjkcodecs/_codecs_jp.c -#_codecs_kr cjkcodecs/_codecs_kr.c -#_codecs_tw cjkcodecs/_codecs_tw.c - -# Example -- included for reference only: -# xx xxmodule.c - -# Another example -- the 'xxsubtype' module shows C-level subtyping in action -xxsubtype xxsubtype.c diff --git a/src/python-for-android/recipes/hostpython2/__init__.py b/src/python-for-android/recipes/hostpython2/__init__.py deleted file mode 100644 index 4679135..0000000 --- a/src/python-for-android/recipes/hostpython2/__init__.py +++ /dev/null @@ -1,69 +0,0 @@ - -from pythonforandroid.toolchain import Recipe, shprint, current_directory, info, warning -from os.path import join, exists -import os -import sh - - -class Hostpython2Recipe(Recipe): - """ - Overrides upstream hostpython2 recipe from: - https://github.com/kivy/python-for-android/blob/b3d3d45 - /pythonforandroid/recipes/hostpython2/__init__.py - Adds `--enable-unicode=ucs4` configure flag, refs: - https://github.com/AndreMiras/PyWallet/issues/136 - """ - version = '2.7.2' - url = 'https://python.org/ftp/python/{version}/Python-{version}.tar.bz2' - name = 'hostpython2' - - conflicts = ['hostpython3'] - - def get_build_container_dir(self, arch=None): - choices = self.check_recipe_choices() - dir_name = '-'.join([self.name] + choices) - return join(self.ctx.build_dir, 'other_builds', dir_name, 'desktop') - - def get_build_dir(self, arch=None): - return join(self.get_build_container_dir(), self.name) - - def prebuild_arch(self, arch): - # Override hostpython Setup? - shprint(sh.cp, join(self.get_recipe_dir(), 'Setup'), - join(self.get_build_dir(), 'Modules', 'Setup')) - - def build_arch(self, arch): - with current_directory(self.get_build_dir()): - - if exists('hostpython'): - info('hostpython already exists, skipping build') - self.ctx.hostpython = join(self.get_build_dir(), - 'hostpython') - self.ctx.hostpgen = join(self.get_build_dir(), - 'hostpgen') - return - - if 'LIBS' in os.environ: - os.environ.pop('LIBS') - configure = sh.Command('./configure') - - # shprint(configure) - shprint(configure, '--enable-unicode=ucs4') - shprint(sh.make, '-j5') - - shprint(sh.mv, join('Parser', 'pgen'), 'hostpgen') - - if exists('python.exe'): - shprint(sh.mv, 'python.exe', 'hostpython') - elif exists('python'): - shprint(sh.mv, 'python', 'hostpython') - else: - warning('Unable to find the python executable after ' - 'hostpython build! Exiting.') - exit(1) - - self.ctx.hostpython = join(self.get_build_dir(), 'hostpython') - self.ctx.hostpgen = join(self.get_build_dir(), 'hostpgen') - - -recipe = Hostpython2Recipe() diff --git a/src/python-for-android/recipes/libsecp256k1/__init__.py b/src/python-for-android/recipes/libsecp256k1/__init__.py deleted file mode 100644 index 69349d9..0000000 --- a/src/python-for-android/recipes/libsecp256k1/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -from pythonforandroid.toolchain import shprint, current_directory -from pythonforandroid.recipe import Recipe -from multiprocessing import cpu_count -from os.path import exists -import sh - - -class LibSecp256k1Recipe(Recipe): - - version = 'b0452e6' - url = 'https://github.com/bitcoin-core/secp256k1/archive/{version}.zip' - - def build_arch(self, arch): - super(LibSecp256k1Recipe, self).build_arch(arch) - env = self.get_recipe_env(arch) - with current_directory(self.get_build_dir(arch.arch)): - if not exists('configure'): - shprint(sh.Command('./autogen.sh'), _env=env) - shprint( - sh.Command('./configure'), - '--host=' + arch.toolchain_prefix, - '--prefix=' + self.ctx.get_python_install_dir(), - '--enable-shared', - '--enable-module-recovery', - '--enable-experimental', - '--enable-module-ecdh', - _env=env) - shprint(sh.make, '-j' + str(cpu_count()), _env=env) - libs = ['.libs/libsecp256k1.so'] - self.install_libs(arch, *libs) - - -recipe = LibSecp256k1Recipe() diff --git a/src/python-for-android/recipes/scrypt/__init__.py b/src/python-for-android/recipes/scrypt/__init__.py deleted file mode 100644 index 17a4ef5..0000000 --- a/src/python-for-android/recipes/scrypt/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -from pythonforandroid.recipe import CythonRecipe - - -class ScryptRecipe(CythonRecipe): - - version = '0.8.6' - url = 'https://bitbucket.org/mhallin/py-scrypt/get/v{version}.zip' - depends = [('python2', 'python3crystax'), 'setuptools', 'openssl'] - call_hostpython_via_targetpython = False - patches = ["remove_librt.patch"] - - def get_recipe_env(self, arch, with_flags_in_cc=True): - """ - Adds openssl recipe to include and library path. - """ - env = super(ScryptRecipe, self).get_recipe_env(arch, with_flags_in_cc) - openssl_build_dir = self.get_recipe( - 'openssl', self.ctx).get_build_dir(arch.arch) - env['CFLAGS'] += ' -I{}'.format(os.path.join(openssl_build_dir, 'include')) - env['LDFLAGS'] += ' -L{}'.format( - self.ctx.get_libs_dir(arch.arch) + - '-L{}'.format(self.ctx.libs_dir)) + ' -L{}'.format( - openssl_build_dir) - # required additional library and path for Crystax - if self.ctx.ndk == 'crystax': - # only keeps major.minor (discards patch) - python_version = self.ctx.python_recipe.version[0:3] - ndk_dir_python = os.path.join(self.ctx.ndk_dir, 'sources/python/', python_version) - env['LDFLAGS'] += ' -L{}'.format(os.path.join(ndk_dir_python, 'libs', arch.arch)) - env['LDFLAGS'] += ' -lpython{}m'.format(python_version) - # until `pythonforandroid/archs.py` gets merged upstream: - # https://github.com/kivy/python-for-android/pull/1250/files#diff-569e13021e33ced8b54385f55b49cbe6 - env['CFLAGS'] += ' -I{}/include/python/'.format(ndk_dir_python) - return env - - -recipe = ScryptRecipe() diff --git a/src/python-for-android/recipes/scrypt/remove_librt.patch b/src/python-for-android/recipes/scrypt/remove_librt.patch deleted file mode 100644 index 270bab2..0000000 --- a/src/python-for-android/recipes/scrypt/remove_librt.patch +++ /dev/null @@ -1,20 +0,0 @@ ---- a/setup.py 2018-05-06 23:25:08.757522119 +0200 -+++ b/setup.py 2018-05-06 23:25:30.269797365 +0200 -@@ -15,7 +15,6 @@ - - if sys.platform.startswith('linux'): - define_macros = [('HAVE_CLOCK_GETTIME', '1'), -- ('HAVE_LIBRT', '1'), - ('HAVE_POSIX_MEMALIGN', '1'), - ('HAVE_STRUCT_SYSINFO', '1'), - ('HAVE_STRUCT_SYSINFO_MEM_UNIT', '1'), -@@ -23,8 +22,7 @@ - ('HAVE_SYSINFO', '1'), - ('HAVE_SYS_SYSINFO_H', '1'), - ('_FILE_OFFSET_BITS', '64')] -- libraries = ['crypto', 'rt'] -- includes = ['/usr/local/include', '/usr/include'] -+ libraries = ['crypto'] - CFLAGS.append('-O2') - elif sys.platform.startswith('win32'): - define_macros = [('inline', '__inline')] From 214b0ccb022883ecf23051d822bbb9b74aa9dec6 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 11 May 2019 14:28:59 +0200 Subject: [PATCH 34/60] Adds missing Android requirements The app now starts, refs #146 --- buildozer.spec | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/buildozer.spec b/buildozer.spec index 1c587ea..dd0a337 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -39,21 +39,38 @@ version.filename = %(source.dir)s/version.py # comma seperated e.g. requirements = sqlite3,kivy requirements = android, + attrdict==2.0.0, + certifi==2018.10.15, cffi==1.11.5, + chardet==3.0.4, + cytoolz==0.9.0, + eth-abi==1.2.2, + eth-account==0.3.0, eth-hash==0.1.1, + eth-keyfile==0.5.1, + eth-keys==0.2.0b3, + eth-rlp==0.1.2, eth-utils==1.4.1, + eth-typing==2.0.0, gevent, + hexbytes==0.1.0, https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip, https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz, + idna==2.7, Kivy==90c86f8, + lru-dict==1.1.5, openssl, + parsimonious==0.8.1, Pillow==5.2.0, + pycryptodome==3.4.6, python3, qrcode==5.3, raven==6.1.0, requests==2.20.0, rlp==1.0.3, setuptools==40.9.0, + toolz==0.9.0, + urllib3==1.24.1, web3==4.8.1 # (str) Custom source folders for requirements From 2ee5d0f6bb1d080aa75403c1e16a8da3679e7d89 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 18 Jul 2019 22:35:29 +0200 Subject: [PATCH 35/60] Minor doc update, adb uninstall --- docs/Troubleshoot.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/Troubleshoot.md b/docs/Troubleshoot.md index 77910c5..76711c6 100644 --- a/docs/Troubleshoot.md +++ b/docs/Troubleshoot.md @@ -70,6 +70,12 @@ See upstream ticket: https://github.com/kivy/python-for-android/issues/1148 buildozer android p4a -- apk --private $PWD/src/ --local-recipes $PWD/src/python-for-android/recipes/ --package=com.github.andremiras --name PyWallet --version 0.1 --bootstrap=sdl2 --requirements=python2,kivy ``` + +### Uninstaling the app using adb +``` +buildozer android adb -- uninstall com.github.andremiras.pywallet +``` + ## Kivy ### Debugging widget sizes From f44fc22bccf04fa15fef21da86406166797e3e13 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 12:23:41 +0200 Subject: [PATCH 36/60] Cleaner store path patching --- src/tests/ui/test_ui_base.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/tests/ui/test_ui_base.py b/src/tests/ui/test_ui_base.py index 8f85817..33da80a 100644 --- a/src/tests/ui/test_ui_base.py +++ b/src/tests/ui/test_ui_base.py @@ -22,6 +22,12 @@ ADDRESS = "0xab5801a7d398351b8be11c439e05c5b3259aec9b" +def patch_get_store_path(temp_path): + store_path = os.path.join(temp_path, 'store.json') + return mock.patch( + 'pywallet.controller.Store.get_store_path', lambda: store_path) + + class Test(unittest.TestCase): def setUp(self): @@ -327,15 +333,7 @@ def helper_test_address_alias(self, app): address2 = '0x' + account2.address.hex() Controller = main.Controller - @staticmethod - def get_store_path(): - """ - Makes sure we don't mess up with actual store config file. - """ - os.environ['KEYSTORE_PATH'] = self.temp_path - store_path = os.path.join(self.temp_path, 'store.json') - return store_path - with mock.patch.object(Controller, 'get_store_path', get_store_path): + with patch_get_store_path(self.temp_path): # no alias by default with self.assertRaises(KeyError): Controller.get_account_alias(account1) From 16880e5884caf37c7d678373abfdafa7688ef68d Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 12:27:14 +0200 Subject: [PATCH 37/60] Migrates to dedicated store class --- src/pywallet/controller.py | 26 ++++---------------------- src/pywallet/store.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 22 deletions(-) create mode 100644 src/pywallet/store.py diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index 3ddf90c..31d351b 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -6,7 +6,6 @@ from kivy.core.window import Window from kivy.logger import Logger from kivy.properties import DictProperty, ObjectProperty -from kivy.storage.jsonstore import JsonStore from kivy.uix.floatlayout import FloatLayout from kivy.utils import platform from kivymd.bottomsheet import MDListBottomSheet @@ -18,6 +17,7 @@ from pywallet.flashqrcode import FlashQrCodeScreen from pywallet.managekeystore import ManageKeystoreScreen from pywallet.overview import OverviewScreen +from pywallet.store import Store from pywallet.switchaccount import SwitchAccountScreen from pywallet.utils import Dialog, load_kv_from_py, run_in_thread @@ -208,31 +208,13 @@ def get_keystore_path(cls): keystore_path = PyWalib.get_default_keystore_path() return keystore_path - @staticmethod - def get_store_path(): - """ - Returns the full user store path. - """ - user_data_dir = App.get_running_app().user_data_dir - store_path = os.path.join(user_data_dir, 'store.json') - return store_path - - @classmethod - def get_store(cls): - """ - Returns the full user Store object instance. - """ - store_path = cls.get_store_path() - store = JsonStore(store_path) - return store - @classmethod def delete_account_alias(cls, account): """ Deletes the alias for the given account. """ address = "0x" + account.address.hex() - store = cls.get_store() + store = Store.get_store() alias_dict = store['alias'] alias_dict.pop(address) store['alias'] = alias_dict @@ -252,7 +234,7 @@ def set_account_alias(cls, account, alias): pass return address = "0x" + account.address.hex() - store = cls.get_store() + store = Store.get_store() try: alias_dict = store['alias'] except KeyError: @@ -267,7 +249,7 @@ def get_address_alias(cls, address): """ Returns the alias of the given address string. """ - store = cls.get_store() + store = Store.get_store() return store.get('alias')[address] @classmethod diff --git a/src/pywallet/store.py b/src/pywallet/store.py new file mode 100644 index 0000000..3ffd505 --- /dev/null +++ b/src/pywallet/store.py @@ -0,0 +1,27 @@ +import os + +from kivy.app import App +from kivy.storage.jsonstore import JsonStore + + +class Store: + + @classmethod + def get_store_path(cls): + """ + Returns the full user store path. + On Android, the store is purposely not stored on the sdcard. + That way we don't need permission for handling user settings. + Also losing it is not critical. + """ + app = App.get_running_app() + return os.path.join(app.user_data_dir, 'store.json') + + @classmethod + def get_store(cls): + """ + Returns user Store object. + """ + store_path = cls.get_store_path() + store = JsonStore(store_path) + return store From be4741cc89b943c804bcdd35dcc82f8cf55bde2b Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 11:32:29 +0200 Subject: [PATCH 38/60] WIP settings code Imported from Etheroll v2019.0624 --- src/pywallet/settings.py | 136 ++++++++++++++++++++++++++++ src/tests/pywallet/test_settings.py | 88 ++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 src/pywallet/settings.py create mode 100644 src/tests/pywallet/test_settings.py diff --git a/src/pywallet/settings.py b/src/pywallet/settings.py new file mode 100644 index 0000000..c0ca7e8 --- /dev/null +++ b/src/pywallet/settings.py @@ -0,0 +1,136 @@ +import os + +from kivy.app import App +from kivy.utils import platform +from pyetheroll.constants import DEFAULT_GAS_PRICE_GWEI, ChainID + +from etheroll.constants import KEYSTORE_DIR_SUFFIX +from etheroll.store import Store + +NETWORK_SETTINGS = 'network' +GAS_PRICE_SETTINGS = 'gas_price' +PERSIST_KEYSTORE_SETTINGS = 'persist_keystore' + + +class Settings: + """ + Screen for configuring network, gas price... + """ + + @classmethod + def get_stored_network(cls): + """ + Retrieves last stored network value, defaults to Mainnet. + """ + store = Store.get_store() + try: + network_dict = store[NETWORK_SETTINGS] + except KeyError: + network_dict = {} + network_name = network_dict.get( + 'value', ChainID.MAINNET.name) + network = ChainID[network_name] + return network + + @classmethod + def set_stored_network(cls, network: ChainID): + """ + Persists network settings. + """ + store = Store.get_store() + store.put(NETWORK_SETTINGS, value=network.name) + + @classmethod + def is_stored_mainnet(cls): + network = cls.get_stored_network() + return network == ChainID.MAINNET + + @classmethod + def is_stored_testnet(cls): + network = cls.get_stored_network() + return network == ChainID.ROPSTEN + + @classmethod + def get_stored_gas_price(cls): + """ + Retrieves stored gas price value, defaults to DEFAULT_GAS_PRICE_GWEI. + """ + store = Store.get_store() + try: + gas_price_dict = store[GAS_PRICE_SETTINGS] + except KeyError: + gas_price_dict = {} + gas_price = gas_price_dict.get( + 'value', DEFAULT_GAS_PRICE_GWEI) + return gas_price + + @classmethod + def set_stored_gas_price(cls, gas_price: int): + """ + Persists gas price settings. + """ + store = Store.get_store() + store.put(GAS_PRICE_SETTINGS, value=gas_price) + + @classmethod + def is_persistent_keystore(cls): + """ + Retrieves the settings value regarding the keystore persistency. + Defaults to False. + """ + store = Store.get_store() + try: + persist_keystore_dict = store[PERSIST_KEYSTORE_SETTINGS] + except KeyError: + persist_keystore_dict = {} + persist_keystore = persist_keystore_dict.get( + 'value', False) + return persist_keystore + + @classmethod + def set_is_persistent_keystore(cls, persist_keystore: bool): + """ + Saves keystore persistency settings. + """ + store = Store.get_store() + store.put(PERSIST_KEYSTORE_SETTINGS, value=persist_keystore) + + @staticmethod + def get_persistent_keystore_path(): + app = App.get_running_app() + # TODO: hardcoded path, refs: + # https://github.com/AndreMiras/EtherollApp/issues/145 + return os.path.join('/sdcard', app.name) + + @staticmethod + def get_non_persistent_keystore_path(): + app = App.get_running_app() + return app.user_data_dir + + @classmethod + def _get_android_keystore_prefix(cls): + """ + Returns the Android keystore path prefix. + The location differs based on the persistency user settings. + """ + if cls.is_persistent_keystore(): + keystore_dir_prefix = cls.get_persistent_keystore_path() + else: + keystore_dir_prefix = cls.get_non_persistent_keystore_path() + return keystore_dir_prefix + + @classmethod + def get_keystore_path(cls): + """ + Returns the keystore directory path. + This can be overriden by the `KEYSTORE_PATH` environment variable. + """ + keystore_path = os.environ.get('KEYSTORE_PATH') + if keystore_path is not None: + return keystore_path + KEYSTORE_DIR_PREFIX = os.path.expanduser("~") + if platform == "android": + KEYSTORE_DIR_PREFIX = cls._get_android_keystore_prefix() + keystore_path = os.path.join( + KEYSTORE_DIR_PREFIX, KEYSTORE_DIR_SUFFIX) + return keystore_path diff --git a/src/tests/pywallet/test_settings.py b/src/tests/pywallet/test_settings.py new file mode 100644 index 0000000..c7c64e5 --- /dev/null +++ b/src/tests/pywallet/test_settings.py @@ -0,0 +1,88 @@ +import shutil +import unittest +from tempfile import mkdtemp +from unittest import mock + +from kivy.app import App +from pyetheroll.constants import ChainID + +from etheroll.settings import Settings +from service.main import EtherollApp + + +class TestSettings(unittest.TestCase): + """ + Unit tests Settings methods. + """ + @classmethod + def setUpClass(cls): + EtherollApp() + + def setUp(self): + """ + Creates a temporary user data dir for storing the user config. + """ + self.temp_path = mkdtemp(prefix='etheroll') + self.app = App.get_running_app() + self.app._user_data_dir = self.temp_path + + def tearDown(self): + """ + Deletes temporary user data dir. + """ + shutil.rmtree(self.temp_path, ignore_errors=True) + + def test_get_set_stored_network(self): + """ + Checks default stored network and set method. + """ + # checks default + assert Settings.get_stored_network() == ChainID.MAINNET + # checks set + Settings.set_stored_network(ChainID.ROPSTEN) + assert Settings.get_stored_network() == ChainID.ROPSTEN + + def test_is_stored_mainnet(self): + Settings.set_stored_network(ChainID.MAINNET) + assert Settings.is_stored_mainnet() is True + Settings.set_stored_network(ChainID.ROPSTEN) + assert Settings.is_stored_mainnet() is False + + def test_is_stored_testnet(self): + Settings.set_stored_network(ChainID.MAINNET) + assert Settings.is_stored_testnet() is False + Settings.set_stored_network(ChainID.ROPSTEN) + assert Settings.is_stored_testnet() is True + + def test_get_set_stored_gas_price(self): + """ + Checks default stored gas price and set method. + """ + # checks default + assert Settings.get_stored_gas_price() == 4 + # checks set + Settings.set_stored_gas_price(42) + assert Settings.get_stored_gas_price() == 42 + + def test_get_set_is_persistent_keystore(self): + """ + Checks default persist value and set method. + """ + # checks default + assert Settings.is_persistent_keystore() is False + # checks set + Settings.set_is_persistent_keystore(True) + assert Settings.is_persistent_keystore() is True + + def test_get_android_keystore_prefix(self): + """ + The keystore prefix should be the same as user_data_dir by default. + But it can also be persisted to the sdcard. + """ + assert Settings.is_persistent_keystore() is False + prefix = Settings._get_android_keystore_prefix() + assert prefix == self.app.user_data_dir + with mock.patch.object( + Settings, 'is_persistent_keystore', return_value=True): + prefix = Settings._get_android_keystore_prefix() + assert prefix == '/sdcard/etheroll' From f0249063a658830000469b9568dfec0b034225d0 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 11:48:37 +0200 Subject: [PATCH 39/60] Migrates settings to pywallet project --- src/pywalib.py | 3 ++- src/pywallet/settings.py | 3 +-- src/tests/pywallet/test_settings.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index b3d7bf8..7f56d4d 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -17,6 +17,7 @@ KEYSTORE_DIR_PREFIX = expanduser("~") # default pyethapp keystore path KEYSTORE_DIR_SUFFIX = ".config/pyethapp/keystore/" +DEFAULT_GAS_PRICE_GWEI = 5 class UnknownEtherscanException(Exception): @@ -173,7 +174,7 @@ def handle_web3_exception(exception: ValueError): raise UnknownEtherscanException(error) def transact(self, to, value=0, data='', sender=None, gas=25000, - gasprice=5 * (10 ** 9)): + gasprice=DEFAULT_GAS_PRICE_GWEI * (10 ** 9)): """ Signs and broadcasts a transaction. Returns transaction hash. diff --git a/src/pywallet/settings.py b/src/pywallet/settings.py index c0ca7e8..1fa8f5a 100644 --- a/src/pywallet/settings.py +++ b/src/pywallet/settings.py @@ -2,10 +2,9 @@ from kivy.app import App from kivy.utils import platform -from pyetheroll.constants import DEFAULT_GAS_PRICE_GWEI, ChainID -from etheroll.constants import KEYSTORE_DIR_SUFFIX from etheroll.store import Store +from pywalib import DEFAULT_GAS_PRICE_GWEI, KEYSTORE_DIR_SUFFIX, ChainID NETWORK_SETTINGS = 'network' GAS_PRICE_SETTINGS = 'gas_price' diff --git a/src/tests/pywallet/test_settings.py b/src/tests/pywallet/test_settings.py index c7c64e5..dda437f 100644 --- a/src/tests/pywallet/test_settings.py +++ b/src/tests/pywallet/test_settings.py @@ -4,9 +4,9 @@ from unittest import mock from kivy.app import App -from pyetheroll.constants import ChainID -from etheroll.settings import Settings +from pywalib import ChainID +from pywallet.settings import Settings from service.main import EtherollApp @@ -22,7 +22,7 @@ def setUp(self): """ Creates a temporary user data dir for storing the user config. """ - self.temp_path = mkdtemp(prefix='etheroll') + self.temp_path = mkdtemp(prefix='pywallet') self.app = App.get_running_app() self.app._user_data_dir = self.temp_path @@ -85,4 +85,4 @@ def test_get_android_keystore_prefix(self): with mock.patch.object( Settings, 'is_persistent_keystore', return_value=True): prefix = Settings._get_android_keystore_prefix() - assert prefix == '/sdcard/etheroll' + assert prefix == '/sdcard/pywallet' From 834ec0d1692d970a64b2fc62e579e7709e02ea5c Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 12:43:23 +0200 Subject: [PATCH 40/60] fixing tests post migration WIP --- src/pywalib.py | 2 +- src/pywallet/settings.py | 2 +- src/tests/pywallet/test_settings.py | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index 7f56d4d..9093d41 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -17,7 +17,7 @@ KEYSTORE_DIR_PREFIX = expanduser("~") # default pyethapp keystore path KEYSTORE_DIR_SUFFIX = ".config/pyethapp/keystore/" -DEFAULT_GAS_PRICE_GWEI = 5 +DEFAULT_GAS_PRICE_GWEI = 4 class UnknownEtherscanException(Exception): diff --git a/src/pywallet/settings.py b/src/pywallet/settings.py index 1fa8f5a..b15747e 100644 --- a/src/pywallet/settings.py +++ b/src/pywallet/settings.py @@ -3,7 +3,7 @@ from kivy.app import App from kivy.utils import platform -from etheroll.store import Store +from pywallet.store import Store from pywalib import DEFAULT_GAS_PRICE_GWEI, KEYSTORE_DIR_SUFFIX, ChainID NETWORK_SETTINGS = 'network' diff --git a/src/tests/pywallet/test_settings.py b/src/tests/pywallet/test_settings.py index dda437f..4ffbccb 100644 --- a/src/tests/pywallet/test_settings.py +++ b/src/tests/pywallet/test_settings.py @@ -7,7 +7,7 @@ from pywalib import ChainID from pywallet.settings import Settings -from service.main import EtherollApp +from main import PyWalletApp class TestSettings(unittest.TestCase): @@ -16,7 +16,10 @@ class TestSettings(unittest.TestCase): """ @classmethod def setUpClass(cls): - EtherollApp() + """ + Makes sure the `App.get_running_app` singleton gets created. + """ + PyWalletApp() def setUp(self): """ From 6e36a9a17cfb3f5eac2425139adff8acd9047543 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 12:56:58 +0200 Subject: [PATCH 41/60] Updates Kivy and KivyMD deps Also fixes tests --- buildozer.spec | 4 ++-- requirements/requirements.txt | 4 ++-- src/tests/test_pywalib.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index dd0a337..a0fd5d4 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -55,9 +55,9 @@ requirements = gevent, hexbytes==0.1.0, https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip, - https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz, + https://github.com/AndreMiras/KivyMD/archive/20181106.tar.gz, idna==2.7, - Kivy==90c86f8, + Kivy==1.11.1, lru-dict==1.1.5, openssl, parsimonious==0.8.1, diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0919628..ba88c5a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,8 +1,8 @@ eth-hash==0.1.1 eth-utils==1.4.1 https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip#egg=zbarcam -https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz#egg=kivymd -Kivy==1.10.1 +https://github.com/AndreMiras/KivyMD/archive/20181106.tar.gz#egg=kivymd +Kivy==1.11.1 Pillow==5.2.0 qrcode==5.3 raven==6.1.0 diff --git a/src/tests/test_pywalib.py b/src/tests/test_pywalib.py index 62a4bc4..ae235c6 100644 --- a/src/tests/test_pywalib.py +++ b/src/tests/test_pywalib.py @@ -343,7 +343,7 @@ def test_transact_no_sender(self): transaction = { 'chainId': 1, 'gas': 25000, - 'gasPrice': 5000000000, + 'gasPrice': 4000000000, 'nonce': 0, 'value': value_wei, } From c72339a31d7865cb8c16391a979823200caa1b87 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 12:58:20 +0200 Subject: [PATCH 42/60] isort fix --- src/pywallet/settings.py | 2 +- src/tests/pywallet/test_settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pywallet/settings.py b/src/pywallet/settings.py index b15747e..c10f07a 100644 --- a/src/pywallet/settings.py +++ b/src/pywallet/settings.py @@ -3,8 +3,8 @@ from kivy.app import App from kivy.utils import platform -from pywallet.store import Store from pywalib import DEFAULT_GAS_PRICE_GWEI, KEYSTORE_DIR_SUFFIX, ChainID +from pywallet.store import Store NETWORK_SETTINGS = 'network' GAS_PRICE_SETTINGS = 'gas_price' diff --git a/src/tests/pywallet/test_settings.py b/src/tests/pywallet/test_settings.py index 4ffbccb..1523060 100644 --- a/src/tests/pywallet/test_settings.py +++ b/src/tests/pywallet/test_settings.py @@ -5,9 +5,9 @@ from kivy.app import App +from main import PyWalletApp from pywalib import ChainID from pywallet.settings import Settings -from main import PyWalletApp class TestSettings(unittest.TestCase): From 4ab8bed5654281936fb953d71ded16a1a6624c71 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 15:03:28 +0200 Subject: [PATCH 43/60] pywalib property --- src/pywalib.py | 3 ++- src/pywallet/controller.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/pywalib.py b/src/pywalib.py index 9093d41..0df9a66 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -43,7 +43,8 @@ class PyWalib: def __init__(self, keystore_dir=None): if keystore_dir is None: keystore_dir = PyWalib.get_default_keystore_path() - self.account_utils = AccountUtils(keystore_dir=keystore_dir) + self.keystore_dir = keystore_dir + self.account_utils = AccountUtils(keystore_dir=self.keystore_dir) self.chain_id = ChainID.MAINNET self.provider = HTTPProvider('https://mainnet.infura.io') self.web3 = Web3(self.provider) diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index 31d351b..ed8d4d0 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -40,9 +40,8 @@ class Controller(FloatLayout): accounts_history = DictProperty({}) def __init__(self, **kwargs): - super(Controller, self).__init__(**kwargs) - keystore_path = Controller.get_keystore_path() - self.pywalib = PyWalib(keystore_path) + super().__init__(**kwargs) + self._pywalib = None self.screen_history = [] self.register_event_type('on_alias_updated') Clock.schedule_once(lambda dt: self.load_landing_page()) @@ -135,6 +134,18 @@ def toolbar(self): def screen_manager(self): return self.ids.screen_manager_id + @property + def pywalib(self): + """ + Gets or creates the PyWalib object. + Also recreates the object if the keystore_path changed. + """ + keystore_path = Controller.get_keystore_path() + if self._pywalib is None or \ + self._pywalib.keystore_dir != keystore_path: + self._pywalib = PyWalib(keystore_path) + return self._pywalib + def set_toolbar_title(self, title): self.toolbar.title_property = title From e3bde6d90c28613f660a1ed1503865642d80bd87 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 15:27:04 +0200 Subject: [PATCH 44/60] Use Settings.get_keystore_path() --- src/pywallet/controller.py | 14 ++------------ src/pywallet/managekeystore.py | 3 ++- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index ed8d4d0..95fc3c1 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -17,6 +17,7 @@ from pywallet.flashqrcode import FlashQrCodeScreen from pywallet.managekeystore import ManageKeystoreScreen from pywallet.overview import OverviewScreen +from pywallet.settings import Settings from pywallet.store import Store from pywallet.switchaccount import SwitchAccountScreen from pywallet.utils import Dialog, load_kv_from_py, run_in_thread @@ -140,7 +141,7 @@ def pywalib(self): Gets or creates the PyWalib object. Also recreates the object if the keystore_path changed. """ - keystore_path = Controller.get_keystore_path() + keystore_path = Settings.get_keystore_path() if self._pywalib is None or \ self._pywalib.keystore_dir != keystore_path: self._pywalib = PyWalib(keystore_path) @@ -208,17 +209,6 @@ def patch_keystore_path(): # uses kivy user_data_dir (/sdcard/) pywalib.KEYSTORE_DIR_PREFIX = App.get_running_app().user_data_dir - @classmethod - def get_keystore_path(cls): - """ - This is the Kivy default keystore path. - """ - keystore_path = os.environ.get('KEYSTORE_PATH') - if keystore_path is None: - Controller.patch_keystore_path() - keystore_path = PyWalib.get_default_keystore_path() - return keystore_path - @classmethod def delete_account_alias(cls, account): """ diff --git a/src/pywallet/managekeystore.py b/src/pywallet/managekeystore.py index b24363d..6c2a0dd 100644 --- a/src/pywallet/managekeystore.py +++ b/src/pywallet/managekeystore.py @@ -4,6 +4,7 @@ from kivy.uix.boxlayout import BoxLayout from kivy.uix.screenmanager import Screen +from pywallet.settings import Settings from pywallet.utils import Dialog, load_kv_from_py, run_in_thread load_kv_from_py(__file__) @@ -287,7 +288,7 @@ def __init__(self, **kwargs): def setup(self): self.controller = App.get_running_app().controller - self.keystore_path = self.controller.get_keystore_path() + self.keystore_path = Settings.get_keystore_path() accounts = self.controller.pywalib.get_account_list() if len(accounts) == 0: title = "No keystore found." From 07999147480af6c88d4bb5529f55c1a9d85e12f6 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 15:46:37 +0200 Subject: [PATCH 45/60] WIP load settings screen --- src/pywallet/controller.py | 15 +++++++++++++++ src/pywallet/navigation.kv | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index 95fc3c1..4216f1f 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -18,6 +18,7 @@ from pywallet.managekeystore import ManageKeystoreScreen from pywallet.overview import OverviewScreen from pywallet.settings import Settings +from pywallet.settings_screen import SettingsScreen from pywallet.store import Store from pywallet.switchaccount import SwitchAccountScreen from pywallet.utils import Dialog, load_kv_from_py, run_in_thread @@ -168,6 +169,7 @@ def screen_manager_current(self, current, direction=None, history=True): 'switch_account': SwitchAccountScreen, 'manage_keystores': ManageKeystoreScreen, 'flashqrcode': FlashQrCodeScreen, + 'settings_screen': SettingsScreen, 'about': AboutScreen, } screen_manager = self.screen_manager @@ -433,6 +435,19 @@ def load_flash_qr_code(self): # loads the flash QR Code screen self.screen_manager_current('flashqrcode', direction='left') + def load_settings_screen(self): + """ + Loads the settings screen. + """ + if SCREEN_SWITCH_DELAY: + Clock.schedule_once( + lambda dt: self.screen_manager_current( + 'settings_screen', direction='left'), + SCREEN_SWITCH_DELAY) + else: + self.screen_manager_current( + 'settings_screen', direction='left') + def load_about_screen(self): """ Loads the about screen. diff --git a/src/pywallet/navigation.kv b/src/pywallet/navigation.kv index 8dcec69..49a9040 100644 --- a/src/pywallet/navigation.kv +++ b/src/pywallet/navigation.kv @@ -13,6 +13,10 @@ icon: "key" text: "Account" on_release: app.controller.load_manage_keystores() + NavigationDrawerIconButton: + icon: "settings" + text: "Settings" + on_release: app.controller.load_settings_screen() NavigationDrawerIconButton: icon: "help" text: "About" From 1c6660c1b72d98f167340b95c1d826be6543a06b Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 16:26:08 +0200 Subject: [PATCH 46/60] Imported from Etheroll v2019.0624 --- src/pywallet/settings_screen.kv | 84 +++++++++++++++++ src/pywallet/settings_screen.py | 160 ++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 src/pywallet/settings_screen.kv create mode 100644 src/pywallet/settings_screen.py diff --git a/src/pywallet/settings_screen.kv b/src/pywallet/settings_screen.kv new file mode 100644 index 0000000..531c96e --- /dev/null +++ b/src/pywallet/settings_screen.kv @@ -0,0 +1,84 @@ +#:import MDCheckbox kivymd.selectioncontrols.MDCheckbox +#:import MDSlider kivymd.slider.MDSlider + + +: + on_pre_enter: + root.load_settings() + on_pre_leave: + root.store_settings() + name: 'settings_screen' + BoxLayoutMarginLayout: + BoxLayoutAddMargin: + orientation: 'vertical' + margin: 30, 10, 30, 10 + MDLabel: + text: 'Network' + font_style: 'Title' + theme_text_color: 'Primary' + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: mainnet_checkbox_id + group: 'network' + size_hint: None, None + size: dp(48), dp(48) + pos_hint: {'center_x': 0.5, 'center_y': 0.5} + active: root.is_stored_mainnet + MDLabel: + text: 'Mainnet' + theme_text_color: 'Primary' + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: testnet_checkbox_id + group: 'network' + size_hint: None, None + size: dp(48), dp(48) + pos_hint: {'center_x': 0.5, 'center_y': 0.5} + active: root.is_stored_testnet + MDLabel: + text: 'Testnet' + theme_text_color: 'Primary' + PushUp: + BoxLayout: + orientation: 'vertical' + MDLabel: + text: 'Gas price (gwei)' + font_style: 'Title' + theme_text_color: 'Primary' + BoxLayout: + orientation: 'horizontal' + MDLabel: + id: gas_price_label_id + text: "{}".format(int(gas_price_slider_id.value)) + font_size: dp(20) + width: dp(40) + size_hint_x: None + theme_text_color: 'Primary' + MDSlider: + id: gas_price_slider_id + range: 0, 50 + value: root.stored_gas_price + step: 1 + PushUp: + BoxLayout: + orientation: 'vertical' + MDLabel: + text: 'Persist keystore' + font_style: 'Title' + theme_text_color: 'Primary' + BoxLayout: + orientation: 'horizontal' + MDSwitch: + id: persist_keystore_switch_id + size_hint: None, None + size: dp(36), dp(48) + pos_hint: {'center_x': 0.75, 'center_y': 0.5} + on_active: root.check_request_write_permission() + MDLabel: + halign: 'right' + text: 'Keeps accounts even if the app is uninstalled' + theme_text_color: 'Primary' + + PushUp: diff --git a/src/pywallet/settings_screen.py b/src/pywallet/settings_screen.py new file mode 100644 index 0000000..df5944c --- /dev/null +++ b/src/pywallet/settings_screen.py @@ -0,0 +1,160 @@ +import os +import shutil + +from kivy.properties import BooleanProperty, NumericProperty +from pyetheroll.constants import ChainID + +from etheroll.constants import KEYSTORE_DIR_SUFFIX +from etheroll.settings import Settings +from etheroll.ui_utils import SubScreen, load_kv_from_py +from etheroll.utils import (check_request_write_permission, + check_write_permission) + +load_kv_from_py(__file__) + + +class SettingsScreen(SubScreen): + """ + Screen for configuring network, gas price... + """ + + is_stored_mainnet = BooleanProperty() + is_stored_testnet = BooleanProperty() + stored_gas_price = NumericProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def store_network(self): + """ + Saves selected network to the store. + """ + network = self.get_ui_network() + Settings.set_stored_network(network) + + def store_gas_price(self): + """ + Saves gas price value to the store. + """ + gas_price = self.get_ui_gas_price() + Settings.set_stored_gas_price(gas_price) + + def store_is_persistent_keystore(self): + """ + Saves the persistency option to the store. + Note that to save `True` we also check if we have write permissions. + """ + persist_keystore = self.is_ui_persistent_keystore() + persist_keystore = persist_keystore and check_write_permission() + persistency_toggled = ( + Settings.is_persistent_keystore() != persist_keystore) + if persistency_toggled: + self.sync_keystore(persist_keystore) + Settings.set_is_persistent_keystore(persist_keystore) + + def sync_to_directory(source_dir, destination_dir): + """ + Copy source dir content to the destination dir one. + Files already existing get overriden. + """ + os.makedirs(destination_dir, exist_ok=True) + files = os.listdir(source_dir) + for f in files: + source_file = os.path.join(source_dir, f) + # file path is given rather than the dir so it gets overriden + destination_file = os.path.join(destination_dir, f) + try: + shutil.copy(source_file, destination_file) + except PermissionError: + # `copymode()` may have fail, fallback to simple copy + shutil.copyfile(source_file, destination_file) + + @classmethod + def sync_keystore_to_persistent(cls): + """ + Copies keystore from non persistent to persistent storage. + """ + # TODO: handle dir doesn't exist + source_dir = os.path.join( + Settings.get_non_persistent_keystore_path(), + KEYSTORE_DIR_SUFFIX) + destination_dir = os.path.join( + Settings.get_persistent_keystore_path(), + KEYSTORE_DIR_SUFFIX) + cls.sync_to_directory(source_dir, destination_dir) + + @classmethod + def sync_keystore_to_non_persistent(cls): + """ + Copies keystore from persistent to non persistent storage. + """ + # TODO: handle dir doesn't exist + source_dir = os.path.join( + Settings.get_persistent_keystore_path(), + KEYSTORE_DIR_SUFFIX) + destination_dir = os.path.join( + Settings.get_non_persistent_keystore_path(), + KEYSTORE_DIR_SUFFIX) + cls.sync_to_directory(source_dir, destination_dir) + + @classmethod + def sync_keystore(cls, to_persistent): + if to_persistent: + cls.sync_keystore_to_persistent() + else: + cls.sync_keystore_to_non_persistent() + + def set_persist_keystore_switch_state(self, active): + """ + The MDSwitch UI look doesn't seem to be binded to its status. + Here the UI look will be updated depending on the "active" status. + """ + mdswitch = self.ids.persist_keystore_switch_id + if self.is_ui_persistent_keystore() != active: + mdswitch.ids.thumb.trigger_action() + + def load_settings(self): + """ + Load json store settings to UI properties. + """ + self.is_stored_mainnet = Settings.is_stored_mainnet() + self.is_stored_testnet = Settings.is_stored_testnet() + self.stored_gas_price = Settings.get_stored_gas_price() + is_persistent_keystore = ( + Settings.is_persistent_keystore() and check_write_permission()) + self.set_persist_keystore_switch_state(is_persistent_keystore) + + def store_settings(self): + """ + Stores settings to json store. + """ + self.store_gas_price() + self.store_network() + self.store_is_persistent_keystore() + + def get_ui_network(self): + """ + Retrieves network values from UI. + """ + if self.is_ui_mainnet(): + network = ChainID.MAINNET + else: + network = ChainID.ROPSTEN + return network + + def is_ui_mainnet(self): + return self.ids.mainnet_checkbox_id.active + + def is_ui_testnet(self): + return self.ids.testnet_checkbox_id.active + + def get_ui_gas_price(self): + return self.ids.gas_price_slider_id.value + + def is_ui_persistent_keystore(self): + return self.ids.persist_keystore_switch_id.active + + def check_request_write_permission(self): + # previous state before the toggle + if self.is_ui_persistent_keystore(): + check_request_write_permission() From 6de36ab15ef31f61c3145c2c4303c3e4ef06310d Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 16:47:28 +0200 Subject: [PATCH 47/60] New settings screen fixes --- buildozer.spec | 1 + requirements/requirements.txt | 1 + src/pywallet/settings_screen.py | 16 ++++++---------- src/pywallet/utils.py | 34 +++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index a0fd5d4..b0919c6 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -54,6 +54,7 @@ requirements = eth-typing==2.0.0, gevent, hexbytes==0.1.0, + https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.tar.gz, https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip, https://github.com/AndreMiras/KivyMD/archive/20181106.tar.gz, idna==2.7, diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ba88c5a..3511930 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,6 @@ eth-hash==0.1.1 eth-utils==1.4.1 +https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.tar.gz#egg=layoutmargin https://github.com/AndreMiras/garden.zbarcam/archive/20190303.zip#egg=zbarcam https://github.com/AndreMiras/KivyMD/archive/20181106.tar.gz#egg=kivymd Kivy==1.11.1 diff --git a/src/pywallet/settings_screen.py b/src/pywallet/settings_screen.py index df5944c..464a5ef 100644 --- a/src/pywallet/settings_screen.py +++ b/src/pywallet/settings_screen.py @@ -2,18 +2,17 @@ import shutil from kivy.properties import BooleanProperty, NumericProperty -from pyetheroll.constants import ChainID +from kivy.uix.screenmanager import Screen -from etheroll.constants import KEYSTORE_DIR_SUFFIX -from etheroll.settings import Settings -from etheroll.ui_utils import SubScreen, load_kv_from_py -from etheroll.utils import (check_request_write_permission, - check_write_permission) +from pywalib import KEYSTORE_DIR_SUFFIX, ChainID +from pywallet.settings import Settings +from pywallet.utils import (check_request_write_permission, + check_write_permission, load_kv_from_py) load_kv_from_py(__file__) -class SettingsScreen(SubScreen): +class SettingsScreen(Screen): """ Screen for configuring network, gas price... """ @@ -22,9 +21,6 @@ class SettingsScreen(SubScreen): is_stored_testnet = BooleanProperty() stored_gas_price = NumericProperty() - def __init__(self, **kwargs): - super().__init__(**kwargs) - def store_network(self): """ Saves selected network to the store. diff --git a/src/pywallet/utils.py b/src/pywallet/utils.py index 2b77e8f..d99bcb7 100644 --- a/src/pywallet/utils.py +++ b/src/pywallet/utils.py @@ -7,9 +7,12 @@ from kivy.clock import mainthread from kivy.lang import Builder from kivy.metrics import dp +from kivy.uix.boxlayout import BoxLayout +from kivy.utils import platform from kivymd.dialog import MDDialog from kivymd.label import MDLabel from kivymd.snackbar import Snackbar +from layoutmargin import AddMargin, MarginLayout def run_in_thread(fn): @@ -50,6 +53,29 @@ def load_kv_from_py(f): ) +def check_write_permission(): + """ + Android runtime storage permission check. + """ + if platform != "android": + return True + from android.permissions import Permission, check_permission + permission = Permission.WRITE_EXTERNAL_STORAGE + return check_permission(permission) + + +def check_request_write_permission(): + """ + Android runtime storage permission check & request. + """ + had_permission = check_write_permission() + if not had_permission: + from android.permissions import Permission, request_permission + permission = Permission.WRITE_EXTERNAL_STORAGE + request_permission(permission) + return had_permission + + class StringIOCBWrite(StringIO): """ Inherits StringIO, provides callback on write. @@ -195,3 +221,11 @@ def on_history_value_error(cls): body = "Couldn't not decode history data." dialog = cls.create_dialog(title, body) dialog.open() + + +class BoxLayoutMarginLayout(MarginLayout, BoxLayout): + pass + + +class BoxLayoutAddMargin(AddMargin, BoxLayout): + pass From c518fbc67d231690de1dd01cc2ee32376cdd46c1 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 19:49:13 +0200 Subject: [PATCH 48/60] Configurable gas price --- src/pywallet/send.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pywallet/send.py b/src/pywallet/send.py index eca2696..222009e 100644 --- a/src/pywallet/send.py +++ b/src/pywallet/send.py @@ -7,6 +7,7 @@ from pywalib import (ROUND_DIGITS, InsufficientFundsException, UnknownEtherscanException) from pywallet.passwordform import PasswordForm +from pywallet.settings import Settings from pywallet.utils import Dialog, load_kv_from_py, run_in_thread load_kv_from_py(__file__) @@ -87,6 +88,8 @@ def unlock_send_transaction(self): address = to_checksum_address(self.send_to_address) amount_eth = round(self.send_amount, ROUND_DIGITS) amount_wei = int(amount_eth * pow(10, 18)) + gas_price_gwei = Settings.get_stored_gas_price() + gas_price_wei = int(gas_price_gwei * (10 ** 9)) # TODO: not the main account, but the current account account = controller.pywalib.get_main_account() Dialog.snackbar_message("Unlocking account...") @@ -99,7 +102,9 @@ def unlock_send_transaction(self): Dialog.snackbar_message("Unlocked! Sending transaction...") sender = account.address try: - pywalib.transact(address, value=amount_wei, data='', sender=sender) + pywalib.transact( + address, value=amount_wei, data='', sender=sender, + gasprice=gas_price_wei) except InsufficientFundsException: Dialog.snackbar_message("Insufficient funds") return From 573aeb62127df755079909301c7206b7b352f8b3 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 19 Jul 2019 23:53:15 +0200 Subject: [PATCH 49/60] Update CHANGELOG.md --- src/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index c27bbfa..e1422d7 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -2,7 +2,9 @@ ## [Unreleased] - - Migration to Python3, refs #19 + - Migration to Python3, refs #19, #146 + - Handle dynamic permissions, refs #149 + - Configurable gas price ## [v20180729] From 158d5e29f53dff80843145601e62d124df212c46 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 20 Jul 2019 00:51:29 +0200 Subject: [PATCH 50/60] Ask for storage permissions on main screen, refs #149 Shows an explanation dialog asking for permissions when opening the app. --- src/pywallet/controller.py | 38 +++++++++++++++++++++++++++++++++++--- src/pywallet/utils.py | 10 ++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index 4216f1f..e50d54c 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -21,7 +21,9 @@ from pywallet.settings_screen import SettingsScreen from pywallet.store import Store from pywallet.switchaccount import SwitchAccountScreen -from pywallet.utils import Dialog, load_kv_from_py, run_in_thread +from pywallet.utils import (Dialog, check_request_write_permission, + check_write_permission, load_kv_from_py, + run_in_thread) # Time before loading the next screen. # The idea is to let the application render before trying to add child widget, @@ -280,9 +282,32 @@ def update_toolbar_title_balance(self, instance=None, value=None): title = "%s ETH" % (balance) self.set_toolbar_title(title) - def load_landing_page(self): + def show_storage_permissions_required_dialog(self): + title = "External storage permissions required" + body = "" + body += "In order to save your keystore, PyWallet requires access " + body += "to your device storage. " + body += "Please allow PyWallet to access it when prompted." + dialog = Dialog.create_dialog(title, body) + dialog.open() + return dialog + + def check_external_storage_permission(self, on_permission=None): """ - Loads the landing page. + Checks for external storage permissions and pops a dialog to ask for it + if needed. + """ + if check_write_permission(): + return + dialog = self.show_storage_permissions_required_dialog() + # TODO: on permission callback update settings and reload accounts + dialog.bind( + on_dismiss=lambda *x: check_request_write_permission( + on_permission)) + + def try_load_current_account(self): + """ + Load the main account or fallback to the create account screen. """ try: # will trigger account data fetching @@ -296,6 +321,13 @@ def load_landing_page(self): except IndexError: self.load_create_new_account() + def load_landing_page(self): + """ + Loads the landing page. + """ + self.check_external_storage_permission( + on_permission=lambda *x: self.try_load_current_account()) + @run_in_thread def fetch_balance(self): """ diff --git a/src/pywallet/utils.py b/src/pywallet/utils.py index d99bcb7..ee7af81 100644 --- a/src/pywallet/utils.py +++ b/src/pywallet/utils.py @@ -64,15 +64,17 @@ def check_write_permission(): return check_permission(permission) -def check_request_write_permission(): +def check_request_write_permission(callback=None): """ Android runtime storage permission check & request. """ had_permission = check_write_permission() if not had_permission: - from android.permissions import Permission, request_permission - permission = Permission.WRITE_EXTERNAL_STORAGE - request_permission(permission) + from android.permissions import Permission, request_permissions + permissions = [Permission.WRITE_EXTERNAL_STORAGE] + # TODO: add callback support refs: + # https://github.com/kivy/python-for-android/pull/1818 + request_permissions(permissions) return had_permission From 98e7e22131201ad1b8533e67778bb1c54a290503 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 20 Jul 2019 02:29:03 +0200 Subject: [PATCH 51/60] Fake on_permission callback, refs #151 Prepares the ground for #151 --- src/pywallet/controller.py | 8 ++++---- src/pywallet/overview.kv | 2 +- src/pywallet/utils.py | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index e50d54c..d35ca9d 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -292,18 +292,18 @@ def show_storage_permissions_required_dialog(self): dialog.open() return dialog - def check_external_storage_permission(self, on_permission=None): + def check_external_storage_permission(self, callback): """ Checks for external storage permissions and pops a dialog to ask for it if needed. """ if check_write_permission(): - return + return callback() dialog = self.show_storage_permissions_required_dialog() # TODO: on permission callback update settings and reload accounts dialog.bind( on_dismiss=lambda *x: check_request_write_permission( - on_permission)) + callback)) def try_load_current_account(self): """ @@ -326,7 +326,7 @@ def load_landing_page(self): Loads the landing page. """ self.check_external_storage_permission( - on_permission=lambda *x: self.try_load_current_account()) + callback=lambda *x: self.try_load_current_account()) @run_in_thread def fetch_balance(self): diff --git a/src/pywallet/overview.kv b/src/pywallet/overview.kv index 383b013..1a3d5eb 100644 --- a/src/pywallet/overview.kv +++ b/src/pywallet/overview.kv @@ -7,7 +7,7 @@ : orientation: 'vertical' AddressButton: - address_property: root.current_account_string + address_property: root.current_account_string or 'No account selected' on_release: app.controller.open_address_options() History: id: history_id diff --git a/src/pywallet/utils.py b/src/pywallet/utils.py index ee7af81..df9e2dd 100644 --- a/src/pywallet/utils.py +++ b/src/pywallet/utils.py @@ -4,7 +4,7 @@ import threading from io import StringIO -from kivy.clock import mainthread +from kivy.clock import Clock, mainthread from kivy.lang import Builder from kivy.metrics import dp from kivy.uix.boxlayout import BoxLayout @@ -75,6 +75,8 @@ def check_request_write_permission(callback=None): # TODO: add callback support refs: # https://github.com/kivy/python-for-android/pull/1818 request_permissions(permissions) + # for now the callback is simulated + Clock.schedule_once(lambda dt: callback(), 2) return had_permission From fdb1dca51d85f5e8bbe1ef4059d995d06b7224aa Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 21 Jul 2019 03:26:31 +0200 Subject: [PATCH 52/60] Use ChainID/network settings, fixes #1 --- src/CHANGELOG.md | 1 + src/pywalib.py | 46 +++++++++++++++++++++++++++--------- src/pywallet/controller.py | 10 +++++--- src/pywallet/history.py | 4 +++- src/tests/ui/test_ui_base.py | 2 +- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index e1422d7..79837c7 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -5,6 +5,7 @@ - Migration to Python3, refs #19, #146 - Handle dynamic permissions, refs #149 - Configurable gas price + - Configurable network/chainID, refs #1 ## [v20180729] diff --git a/src/pywalib.py b/src/pywalib.py index 0df9a66..bb83221 100755 --- a/src/pywalib.py +++ b/src/pywalib.py @@ -38,15 +38,38 @@ class ChainID(Enum): ROPSTEN = 3 +class HTTPProviderFactory: + + PROVIDER_URLS = { + # ChainID.MAINNET: 'https://api.myetherapi.com/eth', + ChainID.MAINNET: 'https://mainnet.infura.io', + # ChainID.ROPSTEN: 'https://api.myetherapi.com/rop', + ChainID.ROPSTEN: 'https://ropsten.infura.io', + } + + @classmethod + def create(cls, chain_id=ChainID.MAINNET) -> HTTPProvider: + url = cls.PROVIDER_URLS[chain_id] + return HTTPProvider(url) + + +def get_etherscan_prefix(chain_id=ChainID.MAINNET) -> str: + PREFIXES = { + ChainID.MAINNET: 'https://api.etherscan.io/api', + ChainID.ROPSTEN: 'https://api-ropsten.etherscan.io/api', + } + return PREFIXES[chain_id] + + class PyWalib: - def __init__(self, keystore_dir=None): + def __init__(self, keystore_dir=None, chain_id=ChainID.MAINNET): if keystore_dir is None: keystore_dir = PyWalib.get_default_keystore_path() self.keystore_dir = keystore_dir self.account_utils = AccountUtils(keystore_dir=self.keystore_dir) - self.chain_id = ChainID.MAINNET - self.provider = HTTPProvider('https://mainnet.infura.io') + self.chain_id = chain_id + self.provider = HTTPProviderFactory.create(self.chain_id) self.web3 = Web3(self.provider) @staticmethod @@ -64,13 +87,13 @@ def handle_etherscan_error(response_json): assert message == "OK" @staticmethod - def get_balance(address): + def get_balance(address, chain_id=ChainID.MAINNET): """ Retrieves the balance from etherscan.io. The balance is returned in ETH rounded to the second decimal. """ address = to_checksum_address(address) - url = 'https://api.etherscan.io/api' + url = get_etherscan_prefix(chain_id) url += '?module=account&action=balance' url += '&address=%s' % address url += '&tag=latest' @@ -96,12 +119,12 @@ def get_balance_web3(self, address): return balance_eth @staticmethod - def get_transaction_history(address): + def get_transaction_history(address, chain_id=ChainID.MAINNET): """ Retrieves the transaction history from etherscan.io. """ address = to_checksum_address(address) - url = 'https://api.etherscan.io/api' + url = get_etherscan_prefix(chain_id) url += '?module=account&action=txlist' url += '&sort=asc' url += '&address=%s' % address @@ -137,11 +160,11 @@ def get_transaction_history(address): return transactions @staticmethod - def get_out_transaction_history(address): + def get_out_transaction_history(address, chain_id=ChainID.MAINNET): """ Retrieves the outbound transaction history from Etherscan. """ - transactions = PyWalib.get_transaction_history(address) + transactions = PyWalib.get_transaction_history(address, chain_id) out_transactions = [] for transaction in transactions: if transaction['extra_dict']['sent']: @@ -150,13 +173,14 @@ def get_out_transaction_history(address): # TODO: can be removed since the migration to web3 @staticmethod - def get_nonce(address): + def get_nonce(address, chain_id=ChainID.MAINNET): """ Gets the nonce by counting the list of outbound transactions from Etherscan. """ try: - out_transactions = PyWalib.get_out_transaction_history(address) + out_transactions = PyWalib.get_out_transaction_history( + address, chain_id) except NoTransactionFoundException: out_transactions = [] nonce = len(out_transactions) diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index d35ca9d..2d49c50 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -145,9 +145,12 @@ def pywalib(self): Also recreates the object if the keystore_path changed. """ keystore_path = Settings.get_keystore_path() + chain_id = Settings.get_stored_network() if self._pywalib is None or \ - self._pywalib.keystore_dir != keystore_path: - self._pywalib = PyWalib(keystore_path) + self._pywalib.keystore_dir != keystore_path or \ + self._pywalib.chain_id != chain_id: + self._pywalib = PyWalib( + keystore_dir=keystore_path, chain_id=chain_id) return self._pywalib def set_toolbar_title(self, title): @@ -336,8 +339,9 @@ def fetch_balance(self): if self.current_account is None: return address = '0x' + self.current_account.address.hex() + chain_id = Settings.get_stored_network() try: - balance = PyWalib.get_balance(address) + balance = PyWalib.get_balance(address, chain_id) except ConnectionError: Dialog.on_balance_connection_error() Logger.warning('ConnectionError', exc_info=True) diff --git a/src/pywallet/history.py b/src/pywallet/history.py index 0de018a..58d009d 100644 --- a/src/pywallet/history.py +++ b/src/pywallet/history.py @@ -8,6 +8,7 @@ from pywalib import NoTransactionFoundException, PyWalib from pywallet.list import IconLeftWidget +from pywallet.settings import Settings from pywallet.utils import Dialog, load_kv_from_py, run_in_thread load_kv_from_py(__file__) @@ -91,9 +92,10 @@ def update_history_list(self, instance=None, value=None): def fetch_history(self): if self.current_account is None: return + chain_id = Settings.get_stored_network() address = '0x' + self.current_account.address.hex() try: - transactions = PyWalib.get_transaction_history(address) + transactions = PyWalib.get_transaction_history(address, chain_id) except ConnectionError: Dialog.on_history_connection_error() Logger.warning('ConnectionError', exc_info=True) diff --git a/src/tests/ui/test_ui_base.py b/src/tests/ui/test_ui_base.py index 33da80a..c594630 100644 --- a/src/tests/ui/test_ui_base.py +++ b/src/tests/ui/test_ui_base.py @@ -550,7 +550,7 @@ def helper_test_controller_fetch_balance(self, app): thread = controller.fetch_balance() thread.join() address = '0x' + account.address.hex() - mock_get_balance.assert_called_with(address) + mock_get_balance.assert_called_with(address, pywalib.ChainID.MAINNET) # and the balance updated self.assertEqual( controller.accounts_balance[address], balance) From c9ea0877b7f7ee81654094c3c39a0e8c6bbd3006 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 22 Jul 2019 14:56:34 +0200 Subject: [PATCH 53/60] Patches json store with temp path Fixes assert: ``` mock_get_balance.assert_called_with(address, pywalib.ChainID.MAINNET) ``` --- src/tests/ui/test_ui_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/ui/test_ui_base.py b/src/tests/ui/test_ui_base.py index c594630..4f98895 100644 --- a/src/tests/ui/test_ui_base.py +++ b/src/tests/ui/test_ui_base.py @@ -545,7 +545,8 @@ def helper_test_controller_fetch_balance(self, app): account = controller.current_account balance = 42 # 1) simple case, library PyWalib.get_balance() gets called - with mock.patch('pywalib.PyWalib.get_balance') as mock_get_balance: + with mock.patch('pywalib.PyWalib.get_balance') as mock_get_balance, \ + patch_get_store_path(self.temp_path): mock_get_balance.return_value = balance thread = controller.fetch_balance() thread.join() From c22da35e6df28e2bab756c8c3004eec8f39bc679 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 22 Jul 2019 15:57:48 +0200 Subject: [PATCH 54/60] Fixes empty send amount crashing the application, closes #152 --- src/pywallet/send.kv | 4 +--- src/pywallet/send.py | 7 +++++++ src/tests/ui/test_ui_base.py | 12 ++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/pywallet/send.kv b/src/pywallet/send.kv index c5e6af9..c705401 100644 --- a/src/pywallet/send.kv +++ b/src/pywallet/send.kv @@ -23,12 +23,10 @@ required: True write_tab: False text: str(round(root.send_amount, ROUND_DIGITS)) - on_text: root.send_amount = args[1] + on_text: root.on_send_amount_text(*args) AnchorLayout: MDRaisedButton: id: send_button_id text: "Send" on_release: root.on_send_click() PushUp: - - diff --git a/src/pywallet/send.py b/src/pywallet/send.py index 222009e..52a2772 100644 --- a/src/pywallet/send.py +++ b/src/pywallet/send.py @@ -70,6 +70,13 @@ def prompt_password_dialog(self): dialog, content.password)) return dialog + def on_send_amount_text(self, instance, value): + try: + self.send_amount = float(value) + except ValueError: + # e.g. value is empty, refs #152 + pass + def on_send_click(self): if not self.verify_fields(): Dialog.show_invalid_form_dialog() diff --git a/src/tests/ui/test_ui_base.py b/src/tests/ui/test_ui_base.py index 4f98895..a24229e 100644 --- a/src/tests/ui/test_ui_base.py +++ b/src/tests/ui/test_ui_base.py @@ -238,8 +238,8 @@ def helper_test_create_account_form(self, app): def helper_test_on_send_click(self, app): """ - This is a regression test for #63, verify clicking "Send" Ethers works - as expected, refs #63. + Verifies clicking "Send" Ethers works as expected, refs #63. + Also checks for the amount field, refs #152. """ controller = app.controller # TODO: use dispatch('on_release') on navigation drawer @@ -255,6 +255,14 @@ def helper_test_on_send_click(self, app): self.assertEqual(dialogs[1].title, 'Invalid form') Dialog.dismiss_all_dialogs() self.assertEqual(len(dialogs), 0) + # also checks for the amount field, refs #152 + send_amount_id = send.ids.send_amount_id + send_amount_id.text = '0.1' + # the send_amount property should get updated from the input + self.assertEqual(send.send_amount, 0.1) + # blank amount shouldn't crash the app, just get ignored + send_amount_id.text = '' + self.assertEqual(send.send_amount, 0.1) def helper_test_send(self, app): """ From 78295f4d01fc61698f895d023d590216235a5e8b Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 4 Aug 2019 22:14:57 +0200 Subject: [PATCH 55/60] Reloads accounts & settings on permissions callback On write permission callback, updates settings to reload accounts from persistent storage, fixes #151. --- buildozer.spec | 10 ++++++---- src/CHANGELOG.md | 2 +- src/pywallet/controller.py | 26 ++++++++++++++++++++++---- src/pywallet/utils.py | 8 ++------ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index b0919c6..ac23826 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -196,10 +196,6 @@ android.blacklist_src = blacklist.txt # bootstrap) #android.gradle_dependencies = -# (str) python-for-android branch to use, defaults to master -#p4a.branch = stable -p4a.branch = master - # (str) OUYA Console category. Should be one of GAME or APP # If you leave this blank, OUYA support will not be enabled #android.ouya.category = GAME @@ -240,6 +236,12 @@ android.arch = armeabi-v7a # Python for android (p4a) specific # +# (str) python-for-android fork to use, defaults to upstream (kivy) +p4a.fork = kivy + +# (str) python-for-android branch to use, defaults to master +p4a.branch = develop + # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) #p4a.source_dir = diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 79837c7..35c0778 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -3,7 +3,7 @@ ## [Unreleased] - Migration to Python3, refs #19, #146 - - Handle dynamic permissions, refs #149 + - Handle dynamic permissions, refs #149, #151 - Configurable gas price - Configurable network/chainID, refs #1 diff --git a/src/pywallet/controller.py b/src/pywallet/controller.py index 2d49c50..69990f9 100644 --- a/src/pywallet/controller.py +++ b/src/pywallet/controller.py @@ -299,14 +299,16 @@ def check_external_storage_permission(self, callback): """ Checks for external storage permissions and pops a dialog to ask for it if needed. + Returns True if the permission was already granted, otherwise prompts + for permissions dialog (async) and returns False. """ if check_write_permission(): - return callback() + return True dialog = self.show_storage_permissions_required_dialog() - # TODO: on permission callback update settings and reload accounts dialog.bind( on_dismiss=lambda *x: check_request_write_permission( callback)) + return False def try_load_current_account(self): """ @@ -328,8 +330,24 @@ def load_landing_page(self): """ Loads the landing page. """ - self.check_external_storage_permission( - callback=lambda *x: self.try_load_current_account()) + @mainthread + def on_permissions_callback(permissions, grant_results): + """ + On write permission callback, toggles loading account from + persistent keystore if granted. + Also loads the current account to the app. + This is called from the Java thread, hence the `@mainthread`. + Find out more on the p4a permissions callback in: + https://github.com/kivy/python-for-android/pull/1818 + """ + if all(grant_results): + Settings.set_is_persistent_keystore(True) + self.try_load_current_account() + # if no permission yet, the try_load_current_account() call will be + # async from the callback + if self.check_external_storage_permission( + callback=on_permissions_callback): + self.try_load_current_account() @run_in_thread def fetch_balance(self): diff --git a/src/pywallet/utils.py b/src/pywallet/utils.py index df9e2dd..d6218cb 100644 --- a/src/pywallet/utils.py +++ b/src/pywallet/utils.py @@ -4,7 +4,7 @@ import threading from io import StringIO -from kivy.clock import Clock, mainthread +from kivy.clock import mainthread from kivy.lang import Builder from kivy.metrics import dp from kivy.uix.boxlayout import BoxLayout @@ -72,11 +72,7 @@ def check_request_write_permission(callback=None): if not had_permission: from android.permissions import Permission, request_permissions permissions = [Permission.WRITE_EXTERNAL_STORAGE] - # TODO: add callback support refs: - # https://github.com/kivy/python-for-android/pull/1818 - request_permissions(permissions) - # for now the callback is simulated - Clock.schedule_once(lambda dt: callback(), 2) + request_permissions(permissions, callback) return had_permission From 6bd3637718db71eae60bbdeb5b54e3af1a23e884 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 4 Aug 2019 23:24:11 +0200 Subject: [PATCH 56/60] Updates Travis build to Python3 Fixes build for Linux and Android. --- .travis.yml | 4 +-- Makefile | 49 +++++++++++++------------------- dockerfiles/Dockerfile-android | 52 ++++++++++++++++++++-------------- src/CHANGELOG.md | 1 + 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/.travis.yml b/.travis.yml index a7603d2..d69ea72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: - TAG=pywallet-android DOCKERFILE=dockerfiles/Dockerfile-android COMMAND='buildozer android debug' install: - - docker build --tag=$TAG --file=$DOCKERFILE . + - docker build --tag=$TAG --file=$DOCKERFILE --build-arg TRAVIS . script: - - travis_wait docker run $TAG $COMMAND + - travis_wait 30 docker run $TAG $COMMAND diff --git a/Makefile b/Makefile index 913878e..aea0071 100644 --- a/Makefile +++ b/Makefile @@ -4,54 +4,45 @@ PIP=`. $(ACTIVATE_PATH); which pip` TOX=`which tox` GARDEN=$(VENV_NAME)/bin/garden PYTHON=$(VENV_NAME)/bin/python -SYSTEM_DEPENDENCIES=virtualenv build-essential libpython2.7-dev \ +SYSTEM_DEPENDENCIES=virtualenv build-essential libpython3.6-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ cmake python-numpy tox wget curl libssl-dev libzbar-dev \ xclip xsel OS=$(shell lsb_release -si) -OPENCV_VERSION=2.4.13.6 -OPENCV_BASENAME=opencv-$(OPENCV_VERSION) -OPENCV_BUILD=$(OPENCV_BASENAME)/build/lib/cv2.so -OPENCV_DEPLOY=$(VENV_NAME)/lib/python2.7/site-packages/cv2.so -NPROC=`grep -c '^processor' /proc/cpuinfo` +TMPDIR ?= /tmp +DOWNLOAD_DIR = $(TMPDIR)/downloads +KM_REPOSITORY=https://raw.githubusercontent.com/AndreMiras/km +KM_BRANCH=develop +OPENCV_MAKEFILE_NAME=Makefile.opencv +OPENCV_MAKEFILE_URL=$(KM_REPOSITORY)/$(KM_BRANCH)/attachments/$(OPENCV_MAKEFILE_NAME) -all: system_dependencies opencv virtualenv +all: system_dependencies virtualenv -virtualenv: - test -d venv || virtualenv -p python3 venv - . venv/bin/activate +$(VENV_NAME): + test -d $(VENV_NAME) || virtualenv -p python3 $(VENV_NAME) + . $(VENV_NAME)/bin/activate $(PIP) install Cython==0.26.1 $(PIP) install -r requirements/requirements.txt $(GARDEN) install qrcode $(GARDEN) install xcamera +virtualenv: $(VENV_NAME) + system_dependencies: ifeq ($(OS), Ubuntu) sudo apt install --yes --no-install-recommends $(SYSTEM_DEPENDENCIES) endif -$(OPENCV_BUILD): - curl --location https://github.com/opencv/opencv/archive/$(OPENCV_VERSION).tar.gz \ - --progress-bar --output $(OPENCV_BASENAME).tar.gz - tar -xf $(OPENCV_BASENAME).tar.gz - cmake \ - -D BUILD_DOCS=OFF -D BUILD_PACKAGE=OFF -D BUILD_PERF_TESTS=OFF \ - -D BUILD_TESTS=OFF -D BUILD_opencv_apps=OFF \ - -D BUILD_opencv_nonfree=OFF -D BUILD_opencv_stitching=OFF \ - -D BUILD_opencv_superres=OFF -D BUILD_opencv_ts=OFF \ - -D BUILD_WITH_DEBUG_INFO=OFF -D WITH_1394=OFF -D WITH_CUDA=OFF \ - -D WITH_CUFFT=OFF -D WITH_GIGEAPI=OFF -D WITH_JASPER=OFF \ - -D WITH_OPENEXR=OFF -D WITH_PVAPI=OFF -D WITH_GTK=OFF \ - -D BUILD_opencv_python=ON -B$(OPENCV_BASENAME)/build -H$(OPENCV_BASENAME) - cmake --build $(OPENCV_BASENAME)/build -- -j$(NPROC) - -opencv_build: $(OPENCV_BUILD) +opencv_build: + curl --location --progress-bar $(OPENCV_MAKEFILE_URL) \ + --output $(DOWNLOAD_DIR)/$(OPENCV_MAKEFILE_NAME) + make --file $(DOWNLOAD_DIR)/$(OPENCV_MAKEFILE_NAME) VENV_NAME=$(VENV_NAME) -$(OPENCV_DEPLOY): opencv_build virtualenv - cp $(OPENCV_BUILD) $(OPENCV_DEPLOY) +opencv_deploy: opencv_build virtualenv + make --file $(DOWNLOAD_DIR)/$(OPENCV_MAKEFILE_NAME) opencv_deploy VENV_NAME=$(VENV_NAME) -opencv: $(OPENCV_DEPLOY) +opencv: opencv_deploy clean: rm -rf $(VENV_NAME) .tox/ $(OPENCV_BASENAME) diff --git a/dockerfiles/Dockerfile-android b/dockerfiles/Dockerfile-android index 6086714..261b91f 100644 --- a/dockerfiles/Dockerfile-android +++ b/dockerfiles/Dockerfile-android @@ -9,16 +9,14 @@ # docker run -it --rm pywallet-android FROM ubuntu:18.04 +ARG TRAVIS=false ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}" \ PATH="${HOME_DIR}/.local/bin:${PATH}" -ENV DOCKERFILES_VERSION="master" \ +ENV DOCKERFILES_VERSION="develop" \ DOCKERFILES_URL="https://raw.githubusercontent.com/AndreMiras/dockerfiles" -ENV MAKEFILES_URL="${DOCKERFILES_URL}/${DOCKERFILES_VERSION}/buildozer_android_new" -# currently needed because buildozer still uses `tools/android` binary -# even though we have the new `tools/bin/sdkmanager` available -ENV USE_SDK_WRAPPER=true +ENV MAKEFILES_URL="${DOCKERFILES_URL}/${DOCKERFILES_VERSION}/buildozer_android" # configure locale @@ -29,15 +27,31 @@ ENV LANG="en_US.UTF-8" \ LANGUAGE="en_US.UTF-8" \ LC_ALL="en_US.UTF-8" -# install system dependencies (required to setup all the tools) -RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ - make curl ca-certificates xz-utils unzip openjdk-8-jdk sudo python-pip \ - python-setuptools - # install build dependencies (required to successfully build the project) -# TODO: should this go to a Makefile instead so it can be shared/reused? RUN apt install -qq --yes --no-install-recommends \ - file autoconf automake libtool gettext pkg-config libltdl-dev + autoconf \ + automake \ + ca-certificates \ + cmake \ + curl \ + gettext \ + libffi-dev \ + libltdl-dev \ + libpython2.7-dev \ + libpython3.6-dev \ + libtool \ + make \ + openjdk-8-jdk \ + pkg-config \ + python3-setuptools \ + python3.6 \ + python3-pip \ + python3-setuptools \ + python \ + sudo \ + unzip \ + xz-utils \ + zip # prepare non root env RUN useradd --create-home --shell /bin/bash ${USER} @@ -48,19 +62,13 @@ RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers USER ${USER} WORKDIR ${WORK_DIR} -# downloads and installs Android SDK -# makes sure to have an up to date build-tools version to workaround buildozer bug, see: -# https://github.com/kivy/buildozer/commit/83ad94d#r29065648 -RUN curl --location --progress-bar ${MAKEFILES_URL}/android_sdk.mk --output android_sdk.mk -RUN make -f android_sdk.mk - # install buildozer and dependencies RUN curl --location --progress-bar ${MAKEFILES_URL}/buildozer.mk --output buildozer.mk RUN make -f buildozer.mk - -# links SDK to where buildozer is expecting to see it -RUN mkdir -p ${HOME}/.buildozer/android/platform -RUN ln -sfn ${HOME}/.android ${HOME}/.buildozer/android/platform/android-sdk-20 +# enforces buildozer master (586152c) until next release +RUN pip3 install --upgrade https://github.com/kivy/buildozer/archive/586152c.zip COPY . ${WORK_DIR} +# limits the amount of logs for Travis +RUN if [ "${TRAVIS}" = "true" ]; then sed 's/log_level = [0-9]/log_level = 1/' -i buildozer.spec; fi ENTRYPOINT ["./dockerfiles/start.sh"] diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 35c0778..ef321ad 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -6,6 +6,7 @@ - Handle dynamic permissions, refs #149, #151 - Configurable gas price - Configurable network/chainID, refs #1 + - Fix broken Travis build, refs #148 ## [v20180729] From d27cba5032a585d19c2735accde1c02e663a1cd3 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 11 Aug 2019 11:24:34 +0200 Subject: [PATCH 57/60] Updates TODO.md - pydevp2p, pyethereum and pydevp2p deps were dropped - CI is already in place --- TODO.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/TODO.md b/TODO.md index c8b6011..afd96eb 100644 --- a/TODO.md +++ b/TODO.md @@ -18,29 +18,6 @@ * https://kivy.org/docs/guide/packaging-android.html#packaging-your-application-into-apk * https://python-for-android.readthedocs.io/en/latest/quickstart/#installing-dependencies * https://kivy.org/docs/guide/packaging-android.html - * Upstream - * pydevp2p - * fix broken tests: - * https://github.com/ethereum/pydevp2p/commit/8e1f2b2ef28ecba22bf27eac346bfa7007eaf0fe - * pyethereum - * take a look at the potential unlock wallet performance improvement: - https://github.com/ethereum/pyethereum/pull/777 - * python-for-android - * recipes - * secp256k1 - * `env['CFLAGS'] += ' -I' + join(libsecp256k1_dir, 'include')` # note the `+=` - * `env['INCLUDE_DIR'] = join(libsecp256k1_dir, 'include')` # see secp256k1-py README.md - * `env['LIB_DIR'] = join(libsecp256k1_dir, '.libs')` # see secp256k1-py README.md - * `env['LDFLAGS'] += ' -L' + join(libsecp256k1_dir, '.libs')` # note the `.libs` - * But in fact passing the library path ("LIB_DIR" and "LDFLAGS") may not be required - because libsecp256k1 recipe has `self.install_libs(arch, *libs)` - * libsecp256k1 - * remove java symbols from libsecp256k1 (configure `--enable-jni=no`) to reduce library size - * `should_build()` with `.libs/libsecp256k1.so` check - * Continuous integration - * https://github.com/kivy/python-for-android/issues/625 - * p4a apk --reauirements=kivy --private /tmp/p4a/ --package=org.package.testname --name=testname --boostrap=sdl2 - * buildozer android p4a -- apk ... * MISC * kill running threads on application leave so it doesn't hangs when you quite while the thread tries to connect From 229bdfbf1143714d14fda799596902374bd3cba7 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 11 Aug 2019 22:21:16 +0200 Subject: [PATCH 58/60] Fixes QRCode scan after migrating garden.zbarcam Recent garden.zbarcam update changed the API. Also makes sure this part of the UI is tested. --- Makefile | 5 ++++- buildozer.spec | 2 ++ src/CHANGELOG.md | 2 +- src/pywallet/flashqrcode.kv | 2 ++ src/pywallet/flashqrcode.py | 26 ++++++-------------------- src/tests/fixtures/one_qr_code.png | Bin 0 -> 1999 bytes src/tests/ui/test_ui_base.py | 22 ++++++++++++++++++++++ 7 files changed, 37 insertions(+), 22 deletions(-) create mode 100644 src/tests/fixtures/one_qr_code.png diff --git a/Makefile b/Makefile index aea0071..62f848f 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,10 @@ ifeq ($(OS), Ubuntu) sudo apt install --yes --no-install-recommends $(SYSTEM_DEPENDENCIES) endif -opencv_build: +$(DOWNLOAD_DIR): + mkdir --parents $(DOWNLOAD_DIR) + +opencv_build: $(DOWNLOAD_DIR) curl --location --progress-bar $(OPENCV_MAKEFILE_URL) \ --output $(DOWNLOAD_DIR)/$(OPENCV_MAKEFILE_NAME) make --file $(DOWNLOAD_DIR)/$(OPENCV_MAKEFILE_NAME) VENV_NAME=$(VENV_NAME) diff --git a/buildozer.spec b/buildozer.spec index ac23826..5b739af 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -59,12 +59,14 @@ requirements = https://github.com/AndreMiras/KivyMD/archive/20181106.tar.gz, idna==2.7, Kivy==1.11.1, + libzbar==0.10, lru-dict==1.1.5, openssl, parsimonious==0.8.1, Pillow==5.2.0, pycryptodome==3.4.6, python3, + pyzbar==0.1.8, qrcode==5.3, raven==6.1.0, requests==2.20.0, diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index ef321ad..9c990e7 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] - - Migration to Python3, refs #19, #146 + - Migration to Python3, refs #19, #146, #143 - Handle dynamic permissions, refs #149, #151 - Configurable gas price - Configurable network/chainID, refs #1 diff --git a/src/pywallet/flashqrcode.kv b/src/pywallet/flashqrcode.kv index 0b149be..cc16644 100644 --- a/src/pywallet/flashqrcode.kv +++ b/src/pywallet/flashqrcode.kv @@ -1,4 +1,5 @@ #:import platform kivy.utils.platform +#:import ZBarSymbol pyzbar.pyzbar.ZBarSymbol : @@ -10,3 +11,4 @@ self.unbind_on_symbols() ZBarCam: id: zbarcam_id + code_types: ZBarSymbol.QRCODE, diff --git a/src/pywallet/flashqrcode.py b/src/pywallet/flashqrcode.py index f2bfbbf..65eff1b 100644 --- a/src/pywallet/flashqrcode.py +++ b/src/pywallet/flashqrcode.py @@ -22,24 +22,9 @@ class FlashQrCodeScreen(Screen): - def __init__(self, **kwargs): - super(FlashQrCodeScreen, self).__init__(**kwargs) - self.setup() - - def setup(self): - """ - Configures scanner to handle only QRCodes. - """ - self.controller = App.get_running_app().controller - self.zbarcam = self.ids.zbarcam_id - # loads ZBarCam only when needed, refs: - # https://github.com/AndreMiras/PyWallet/issues/94 - import zbar - # enables QRCode scanning only - self.zbarcam.scanner.set_config( - zbar.Symbol.NONE, zbar.Config.ENABLE, 0) - self.zbarcam.scanner.set_config( - zbar.Symbol.QRCODE, zbar.Config.ENABLE, 1) + @property + def zbarcam(self): + return self.ids.zbarcam_id def bind_on_symbols(self): """ @@ -62,6 +47,7 @@ def on_symbols(self, instance, symbols): return symbol = symbols[0] # update Send screen address - self.controller.send.send_to_address = symbol.data + controller = App.get_running_app().controller + controller.send.send_to_address = symbol.data.decode('utf-8') self.zbarcam.play = False - self.controller.load_landing_page() + controller.load_landing_page() diff --git a/src/tests/fixtures/one_qr_code.png b/src/tests/fixtures/one_qr_code.png new file mode 100644 index 0000000000000000000000000000000000000000..4d9fc3149bafd7b92617a51b0082069a6baa9ae8 GIT binary patch literal 1999 zcmbtVdr%W+5I^966EK`ILP69EDgxzYqOCjce!G{0_I0nFM6UcZEfC7ZK&IDVf zcKrNQlI+`)Eq(5W2Q92~wWZ^ywE7c7j#L0A-cm605vRgH?s8_UJ;rgjKzk*V$&gc% z3mE-A=$k-zu~?~#qQvvFoutRoepQ>Iy~uQw^K+DR)#L`1PO$7!7xj4RZbO-6zLy2M zZ4@_{gEK?}r2k>x8CaqwCo1pj$#z!p-G|-pFr?CpLk!QauGG$mJ|*Y=$Wq8-ytCHX zG_9^8E>tyFT}&7gI471-gTCwBwUTvhICzbel%0RHLUE`cdsJA?O`5Q>_i34wnYP&p z(|Ch6sM0nMnJ~!SMs)#i_c7ibC30R<$^Ry#8!dZx8zg9zzY7@%Pi80o{lG>3`^|b* z)pe6|x3HX~S1axBm)oFbdfCI?3W)Rz!>pyB#?7ISFDzezSq+w5tM{3em>{>>eaqf0 zgdcwcV>S$oXxT{qcYPy%GWRd9EFajf`Hiv=87K9_Cpg((K_t zGgwK@Aeum_3r400kLUj|w)E>EnS%VpS-=S3R*QG?_V*7JAW(6BSEqzB3q-pj@0GPI z!H^J#Fh8xH27{qNDU3fVAq+`z!4GPEQJzLZITNh!r)zwOHwhpLgFb8o_v7V?!b$+c zb-ZLjlxu<;q15WZK2)SG*0!e+{UjzBhbq$|9An67!dsq;SRkBdZT{An3mXwwYyBYs z@F5`NReE}Q1|vdP(PNv}gV;764l74KTP zaf1m=dvXxZkByj8juVGPt~g&=5n>w6OLZF(whcm2++f#f8snTf z#r{3fBGvkGVs6I^B4>E7YqnfLHUVcmtK$hXsX}{F0GC}E_7CJU%v@VqN&>F}h7nU4UifAdP8N*65W0?k2JP2ojHk zxIa(@alPP0CC{~auhU08TdjEQnyAy=EQ1Ft=W5PEPVY~QL7`Zs-_mcKW3mda50c*v z-zOhxk(nBs7a^OER5tQ8l?rB=B}i~Y&l4!yy?Q7ueIz@`aRs(xUgnKB(ZSuVNuKbE zJ>YF$-OF`19k?>ap9Bv`vR>!_K9Tw{1Pq7lX`x<;5$i zaS;AKi>6~wQ>%F-_v<_C%am(k&v5c^`g86+aL`Fw7*5~Zt@C9@fCu&#gcX*@sc@3= z@?4j)?#=*CQ_Aa$-8^") + def helper_test_flashqrcode(self, app): + """ + Verifies the flash QRCode screen loads and can flash codes. + """ + controller = app.controller + controller.load_flash_qr_code() + self.advance_frames(1) + screen_manager = controller.screen_manager + self.assertEqual(screen_manager.current, 'flashqrcode') + flashqrcode_screen = screen_manager.get_screen('flashqrcode') + zbarcam = flashqrcode_screen.ids.zbarcam_id + fixture_path = os.path.join(FIXTURE_DIR, 'one_qr_code.png') + texture = Image(fixture_path).texture + camera = mock.Mock(texture=texture) + zbarcam._on_texture(camera) + send = controller.send + self.assertEqual(send.send_to_address, 'zbarlight test qr code') + # main test function def run_test(self, app, *args): Clock.schedule_interval(self.pause, 0.000001) @@ -673,6 +694,7 @@ def run_test(self, app, *args): self.helper_test_controller_fetch_balance(app) self.helper_test_delete_last_account(app) self.helper_test_about(app) + self.helper_test_flashqrcode(app) # Comment out if you are editing the test, it'll leave the # Window opened. app.stop() From 4c77823bb33f8876703ac2e13ea253175f381fad Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 11 Aug 2019 23:15:33 +0200 Subject: [PATCH 59/60] Configurable send from address, fixes #147 --- src/CHANGELOG.md | 1 + src/pywallet/send.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 9c990e7..ca0533c 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -7,6 +7,7 @@ - Configurable gas price - Configurable network/chainID, refs #1 - Fix broken Travis build, refs #148 + - Send from different accounts, refs #147 ## [v20180729] diff --git a/src/pywallet/send.py b/src/pywallet/send.py index 52a2772..8f6b10c 100644 --- a/src/pywallet/send.py +++ b/src/pywallet/send.py @@ -19,9 +19,6 @@ class Send(BoxLayout): send_to_address = StringProperty("") send_amount = NumericProperty(0) - def __init__(self, **kwargs): - super(Send, self).__init__(**kwargs) - def verify_to_address_field(self): title = "Input error" body = "Invalid address field" @@ -98,7 +95,7 @@ def unlock_send_transaction(self): gas_price_gwei = Settings.get_stored_gas_price() gas_price_wei = int(gas_price_gwei * (10 ** 9)) # TODO: not the main account, but the current account - account = controller.pywalib.get_main_account() + account = controller.current_account Dialog.snackbar_message("Unlocking account...") try: account.unlock(self.password) From 0c9cb0ac2e46586026d7d4313f0ad825acb48dee Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 12 Aug 2019 22:51:52 +0200 Subject: [PATCH 60/60] v20190812 --- src/CHANGELOG.md | 2 +- src/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index ca0533c..fa0a1aa 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [Unreleased] +## [v20190812] - Migration to Python3, refs #19, #146, #143 - Handle dynamic permissions, refs #149, #151 diff --git a/src/version.py b/src/version.py index 57202aa..2402883 100644 --- a/src/version.py +++ b/src/version.py @@ -1 +1 @@ -__version__ = '2018.0729' +__version__ = '2019.0812'