From 67ff5e7e343ddce93c4344a933be41f6a124c364 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Tue, 30 Oct 2018 20:31:24 +0100 Subject: [PATCH 01/30] Switch to dedicated pyetheroll library, fixes #97 Uses freshly created `pyetheroll` standalone library: https://github.com/AndreMiras/pyetheroll --- buildozer.spec | 3 +- requirements.txt | 1 + src/CHANGELOG.md | 6 + src/ethereum_utils.py | 13 - src/etheroll/roll.py | 2 +- src/etheroll/roll_results.py | 2 +- src/etheroll/settings.py | 3 +- src/pyetheroll/__init__.py | 0 src/pyetheroll/constants.py | 10 - src/pyetheroll/etheroll.py | 497 ------------------ src/pyetheroll/etherscan_utils.py | 77 --- src/pyetheroll/transaction_debugger.py | 156 ------ src/pyetheroll/utils.py | 19 - src/service/main.py | 4 +- src/tests/pyetheroll/test_etheroll.py | 1 - .../pyetheroll/test_transaction_debugger.py | 3 +- src/tests/test_import.py | 4 + 17 files changed, 20 insertions(+), 781 deletions(-) delete mode 100644 src/pyetheroll/__init__.py delete mode 100644 src/pyetheroll/constants.py delete mode 100644 src/pyetheroll/etheroll.py delete mode 100644 src/pyetheroll/etherscan_utils.py delete mode 100644 src/pyetheroll/transaction_debugger.py delete mode 100644 src/pyetheroll/utils.py diff --git a/buildozer.spec b/buildozer.spec index 63f75b7..b89159a 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -100,7 +100,8 @@ requirements = raven==6.6.0, requests-cache==0.4.13, qrcode, - https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.zip + https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.zip, + https://github.com/AndreMiras/pyetheroll/archive/20181030.zip, # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes diff --git a/requirements.txt b/requirements.txt index 8927ba6..33bf816 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ https://github.com/mfranciszkiewicz/pyelliptic/archive/1.5.10.tar.gz#egg=pyellip https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.zip#egg=layoutmargin plyer==1.3.1 oscpy==0.3.0 +https://github.com/AndreMiras/pyetheroll/archive/20181030.zip#egg=pyetheroll diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 38550e0..041b541 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log +## [v20181028] + + - Split dedicated Etheroll library, refs #97 + + ## [v20181028] - Click notification to open the app, refs #114 @@ -44,6 +49,7 @@ - Speed up application loading, refs #91 - Optional Etherscan API key, refs #93 + ## [v20180517] - Show account balance, refs #8 diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py index 9d1db66..9ea515a 100644 --- a/src/ethereum_utils.py +++ b/src/ethereum_utils.py @@ -20,19 +20,6 @@ def get_account_list(self): accounts_service = self.app.services.accounts return accounts_service.accounts - @staticmethod - def get_private_key(wallet_path, wallet_password): - """ - Given wallet path and password, returns private key. - Made this way to workaround pyethapp slow account management: - https://github.com/ethereum/pyethapp/issues/292 - """ - # lazy loading - from web3.auto import w3 - encrypted_key = open(wallet_path).read() - private_key = w3.eth.account.decrypt(encrypted_key, wallet_password) - return private_key - def new_account(self, password): """ Creates an account on the disk and returns it. diff --git a/src/etheroll/roll.py b/src/etheroll/roll.py index c8af449..1d73cdd 100644 --- a/src/etheroll/roll.py +++ b/src/etheroll/roll.py @@ -5,10 +5,10 @@ from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.screenmanager import Screen +from pyetheroll.constants import ROUND_DIGITS from etheroll.ui_utils import Dialog, load_kv_from_py from etheroll.utils import run_in_thread -from pyetheroll.constants import ROUND_DIGITS load_kv_from_py(__file__) diff --git a/src/etheroll/roll_results.py b/src/etheroll/roll_results.py index 85d2a02..22a93ce 100644 --- a/src/etheroll/roll_results.py +++ b/src/etheroll/roll_results.py @@ -3,10 +3,10 @@ from kivy.properties import ListProperty from kivymd.label import MDLabel from kivymd.list import ILeftBody, ThreeLineAvatarListItem +from pyetheroll.constants import ROUND_DIGITS from etheroll.ui_utils import Dialog, SubScreen, load_kv_from_py from etheroll.utils import run_in_thread -from pyetheroll.constants import ROUND_DIGITS load_kv_from_py(__file__) diff --git a/src/etheroll/settings.py b/src/etheroll/settings.py index 92aa2e9..2dcf123 100644 --- a/src/etheroll/settings.py +++ b/src/etheroll/settings.py @@ -1,6 +1,7 @@ +from pyetheroll.constants import DEFAULT_GAS_PRICE_GWEI, ChainID + from etheroll.store import Store from etheroll.ui_utils import SubScreen, load_kv_from_py -from pyetheroll.constants import DEFAULT_GAS_PRICE_GWEI, ChainID load_kv_from_py(__file__) diff --git a/src/pyetheroll/__init__.py b/src/pyetheroll/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/pyetheroll/constants.py b/src/pyetheroll/constants.py deleted file mode 100644 index 0931e5e..0000000 --- a/src/pyetheroll/constants.py +++ /dev/null @@ -1,10 +0,0 @@ -from enum import Enum - -ROUND_DIGITS = 2 -DEFAULT_GAS_PRICE_GWEI = 4 - - -class ChainID(Enum): - MAINNET = 1 - MORDEN = 2 - ROPSTEN = 3 diff --git a/src/pyetheroll/etheroll.py b/src/pyetheroll/etheroll.py deleted file mode 100644 index 9dd89e9..0000000 --- a/src/pyetheroll/etheroll.py +++ /dev/null @@ -1,497 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Python Etheroll library. -""" -import json -from datetime import datetime - -import requests -import requests_cache -from ethereum.utils import checksum_encode -from etherscan.client import EmptyResponse -from hexbytes.main import HexBytes -from pyethapp.accounts import Account -from web3 import Web3 -from web3.auto import w3 -from web3.contract import Contract - -from ethereum_utils import AccountUtils -from pyetheroll.constants import DEFAULT_GAS_PRICE_GWEI, ROUND_DIGITS, ChainID -from pyetheroll.etherscan_utils import (ChainEtherscanAccountFactory, - ChainEtherscanContractFactory, - get_etherscan_api_key) -from pyetheroll.transaction_debugger import (HTTPProviderFactory, - TransactionDebugger) - -REQUESTS_CACHE_PARAMS = { - 'cache_name': 'requests_cache', - 'backend': 'sqlite', - 'fast_save': True, - # we cache most of the request for a pretty long period, but not forever - # as we still want some very outdate data to get wiped at some point - 'expire_after': 30*24*60*60, -} - - -class Etheroll: - - CONTRACT_ADDRESSES = { - ChainID.MAINNET: '0xA52e014B3f5Cc48287c2D483A3E026C32cc76E6d', - ChainID.ROPSTEN: '0xe12c6dEb59f37011d2D9FdeC77A6f1A8f3B8B1e8', - } - - def __init__(self, chain_id=ChainID.MAINNET, contract_address=None): - if contract_address is None: - contract_address = self.CONTRACT_ADDRESSES[chain_id] - self.contract_address = contract_address - self.chain_id = chain_id - self.contract_address = contract_address - # ethereum_tester = EthereumTester() - # self.provider = EthereumTesterProvider(ethereum_tester) - self.provider = HTTPProviderFactory.create(self.chain_id) - self.web3 = Web3(self.provider) - self.etherscan_api_key = get_etherscan_api_key() - ChainEtherscanContract = ChainEtherscanContractFactory.create( - self.chain_id) - self.ChainEtherscanAccount = ChainEtherscanAccountFactory.create( - self.chain_id) - # object construction needs to be within the context manager because - # the requests.Session object to be patched is initialized in the - # constructor - with requests_cache.enabled(**REQUESTS_CACHE_PARAMS): - self.etherscan_contract_api = ChainEtherscanContract( - address=self.contract_address, api_key=self.etherscan_api_key) - self.contract_abi = json.loads( - self.etherscan_contract_api.get_abi()) - # contract_factory_class = ConciseContract - contract_factory_class = Contract - self.contract = self.web3.eth.contract( - abi=self.contract_abi, address=self.contract_address, - ContractFactoryClass=contract_factory_class) - # retrieve signatures - self.events_signatures = self.get_events_signatures(self.contract_abi) - self.functions_signatures = self.get_functions_signatures( - self.contract_abi) - - def abi_definitions(self, contract_abi, typ): - """ - Returns only ABI definitions of matching type. - """ - return [a for a in contract_abi if a['type'] == typ] - - def definitions(self, contract_abi, typ): - """ - Returns all events definitions (built from ABI definition). - e.g. - >>> {"LogRefund": "LogRefund(bytes32,address,uint256)"} - """ - events_definitions = {} - abi_definitions = self.abi_definitions(contract_abi, typ) - for abi_definition in abi_definitions: - name = abi_definition['name'] - types = ','.join([x['type'] for x in abi_definition['inputs']]) - definition = "%s(%s)" % (name, types) - events_definitions.update({name: definition}) - return events_definitions - - def get_signatures(self, contract_abi, typ): - """ - Returns sha3 signature of methods or events. - e.g. - >>> {'LogResult': '0x6883...5c88', 'LogBet': '0x1cb5...75c4'} - """ - signatures = {} - definitions = self.definitions(contract_abi, typ) - for name in definitions: - definition = definitions[name] - signature = Web3.sha3(text=definition) - signatures.update({name: signature}) - return signatures - - def get_events_signatures(self, contract_abi=None): - """ - Returns sha3 signature of all events. - e.g. - >>> {'LogResult': '0x6883...5c88', 'LogBet': '0x1cb5...75c4'} - """ - return self.get_signatures(contract_abi, 'event') - - def get_functions_signatures(self, contract_abi=None): - """ - Returns sha3 signature of all functions. - """ - return self.get_signatures(contract_abi, 'function') - - def events_logs(self, event_list): - """ - Returns the logs of the given events. - """ - events_signatures = self.events_signatures - topics = [] - for event in event_list: - topics.append(events_signatures[event]) - event_filter = self.web3.eth.filter({ - "fromBlock": "earliest", - "toBlock": "latest", - "address": self.contract_address, - "topics": topics, - }) - events_logs = event_filter.get(False) - return events_logs - - def player_roll_dice( - self, bet_size_ether, chances, wallet_path, wallet_password, - gas_price_gwei=DEFAULT_GAS_PRICE_GWEI): - """ - Signs and broadcasts `playerRollDice` transaction. - Returns transaction hash. - """ - roll_under = chances - # `w3.toWei` one has some issues on Android, see: - # https://github.com/AndreMiras/EtherollApp/issues/77 - # value_wei = w3.toWei(bet_size_ether, 'ether') - value_wei = int(bet_size_ether * 1e18) - gas = 310000 - gas_price = w3.toWei(gas_price_gwei, 'gwei') - # since Account.load is hanging while decrypting the password - # we set password to None and use `w3.eth.account.decrypt` instead - account = Account.load(wallet_path, password=None) - from_address_normalized = checksum_encode(account.address) - nonce = self.web3.eth.getTransactionCount(from_address_normalized) - transaction = { - 'chainId': self.chain_id.value, - 'gas': gas, - 'gasPrice': gas_price, - 'nonce': nonce, - 'value': value_wei, - } - transaction = self.contract.functions.playerRollDice( - roll_under).buildTransaction(transaction) - private_key = AccountUtils.get_private_key( - wallet_path, wallet_password) - signed_tx = self.web3.eth.account.signTransaction( - transaction, private_key) - tx_hash = self.web3.eth.sendRawTransaction(signed_tx.rawTransaction) - return tx_hash - - def get_transaction_page( - self, address=None, page=1, offset=100, internal=False): - """ - Retrieves all transactions related to the given address. - """ - if address is None: - address = self.contract_address - # that one should not be cached, because we want the user to know - # realtime what's happening with his transaction - etherscan_account_api = self.ChainEtherscanAccount( - address=address, api_key=self.etherscan_api_key) - sort = 'desc' - try: - transactions = etherscan_account_api.get_transaction_page( - page=page, offset=offset, sort=sort, internal=internal) - except EmptyResponse: - transactions = [] - return transactions - - def get_player_roll_dice_tx(self, address, page=1, offset=100): - """ - Retrieves `address` last `playerRollDice` transactions associated with - the Etheroll contract. - """ - # last transactions from/to address - transactions = self.get_transaction_page( - address=address, page=page, offset=offset) - # keeps only transactions sent to Etheroll contract - transactions = filter( - lambda t: t['to'].lower() == self.contract_address.lower(), - transactions) - # keeps only transactions to `playerRollDice` methodID - method_id = self.functions_signatures['playerRollDice'].hex()[:10] - self.functions_signatures - transactions = filter( - lambda t: t['input'].lower().startswith(method_id), - transactions) - # let's not keep it as an iterator - transactions = list(transactions) - return transactions - - def get_last_bets_transactions(self, address=None, page=1, offset=100): - """ - Retrieves `address` last bets from transactions and returns the list - of bets infos. Does not return the actual roll result. - """ - bets = [] - transactions = self.get_player_roll_dice_tx( - address=address, page=page, offset=offset) - for transaction in transactions: - # from Wei to Ether - bet_size_ether = int(transaction['value']) / 1e18 - # `playerRollDice(uint256 rollUnder)`, rollUnder is 256 bits - # let's strip it from methodID and keep only 32 bytes - roll_under = transaction['input'][-2*32:] - roll_under = int(roll_under, 16) - block_number = transaction['blockNumber'] - timestamp = transaction['timeStamp'] - date_time = datetime.utcfromtimestamp(int(timestamp, 16)) - transaction_hash = transaction['hash'] - bet = { - 'bet_size_ether': bet_size_ether, - 'roll_under': roll_under, - 'block_number': block_number, - 'timestamp': timestamp, - 'datetime': date_time, - 'transaction_hash': transaction_hash, - } - bets.append(bet) - return bets - - def get_bets_logs(self, address, from_block, to_block='latest'): - """ - Retrieves `address` last bets from event logs and returns the list - of bets with decoded info. Does not return the actual roll result. - Least recent first (index 0), most recent last (index -1). - """ - bets = [] - bet_events = self.get_log_bet_events(address, from_block, to_block) - transaction_debugger = TransactionDebugger(self.contract_abi) - for bet_event in bet_events: - topics = [HexBytes(topic) for topic in bet_event['topics']] - log_data = bet_event['data'] - decoded_method = transaction_debugger.decode_method( - topics, log_data) - call = decoded_method['call'] - bet_id = call['BetID'].hex() - reward_value = call['RewardValue'] - reward_value_ether = round( - reward_value / 1e18, ROUND_DIGITS) - profit_value = call['ProfitValue'] - profit_value_ether = round( - profit_value / 1e18, ROUND_DIGITS) - bet_value = call['BetValue'] - bet_value_ether = round(bet_value / 1e18, ROUND_DIGITS) - roll_under = call['PlayerNumber'] - timestamp = bet_event['timeStamp'] - date_time = datetime.utcfromtimestamp(int(timestamp, 16)) - transaction_hash = bet_event['transactionHash'] - bet = { - 'bet_id': bet_id, - 'reward_value_ether': reward_value_ether, - 'profit_value_ether': profit_value_ether, - 'bet_value_ether': bet_value_ether, - 'roll_under': roll_under, - 'timestamp': timestamp, - 'datetime': date_time, - 'transaction_hash': transaction_hash, - } - bets.append(bet) - return bets - - def get_bet_results_logs(self, address, from_block, to_block='latest'): - """ - Retrieves `address` bet results from event logs and returns the list of - bet results with decoded info. - """ - results = [] - result_events = self.get_log_result_events( - address, from_block, to_block) - transaction_debugger = TransactionDebugger(self.contract_abi) - for result_event in result_events: - topics = [HexBytes(topic) for topic in result_event['topics']] - log_data = result_event['data'] - decoded_method = transaction_debugger.decode_method( - topics, log_data) - call = decoded_method['call'] - bet_id = call['BetID'].hex() - roll_under = call['PlayerNumber'] - dice_result = call['DiceResult'] - # not to be mistaken with what the user bet here, in this case it's - # what he will receive/loss as a result of his bet - bet_value = call['Value'] - bet_value_ether = round(bet_value / 1e18, ROUND_DIGITS) - timestamp = result_event['timeStamp'] - date_time = datetime.utcfromtimestamp(int(timestamp, 16)) - transaction_hash = result_event['transactionHash'] - bet = { - 'bet_id': bet_id, - 'roll_under': roll_under, - 'dice_result': dice_result, - 'bet_value_ether': bet_value_ether, - 'timestamp': timestamp, - 'datetime': date_time, - 'transaction_hash': transaction_hash, - } - results.append(bet) - return results - - def get_last_bets_blocks(self, address): - """ - Returns a block range containing the "last" bets. - """ - # retrieves recent `playerRollDice` transactions - transactions = self.get_player_roll_dice_tx(address) - if not transactions: - return None - # take the oldest block of the recent transactions - oldest_tx = transactions[-1] - from_block = int(oldest_tx['blockNumber']) - # makes sure this block is included in the search - from_block -= 1 - # take the most recent block of the recent transactions - last_tx = transactions[0] - to_block = int(last_tx['blockNumber']) - # the result for the last roll is included in later blocks - to_block += 100 - ret = { - 'from_block': from_block, - 'to_block': to_block, - } - return ret - - @staticmethod - def merge_logs(bet_logs, bet_results_logs): - """ - Merges bet logs (LogBet) with bet results logs (LogResult). - """ - merged_logs = [] - # per bet ID dictionary - bet_results_dict = {} - for bet_result in bet_results_logs: - bet_id = bet_result['bet_id'] - bet_results_dict.update({ - bet_id: bet_result - }) - for bet_log in bet_logs: - bet_id = bet_log['bet_id'] - bet_result = bet_results_dict.get(bet_id) - merged_log = { - 'bet_log': bet_log, - 'bet_result': bet_result, - } - merged_logs.append(merged_log) - return merged_logs - - def get_merged_logs(self, address): - """ - Returns the merged logs. - Least recent first (index 0), most recent last (index -1). - """ - last_bets_blocks = self.get_last_bets_blocks(address) - if last_bets_blocks is None: - return [] - from_block = last_bets_blocks['from_block'] - to_block = last_bets_blocks['to_block'] - bet_logs = self.get_bets_logs(address, from_block, to_block) - bet_results_logs = self.get_bet_results_logs( - address, from_block, to_block) - merged_logs = self.merge_logs(bet_logs, bet_results_logs) - return merged_logs - - def get_logs_url( - self, address, from_block, to_block='latest', - topic0=None, topic1=None, topic2=None, topic3=None, - topic_opr=None): - """ - Builds the Etherscan API URL call for the `getLogs` action. - """ - url = self.ChainEtherscanAccount.PREFIX - url += 'module=logs&action=getLogs&' - url += 'apikey={}&'.format(self.etherscan_api_key) - url += 'address={}&'.format(address) - url += 'fromBlock={}&'.format(from_block) - url += 'toBlock={}&'.format(to_block) - if topic0 is not None: - url += 'topic0={}&'.format(topic0) - if topic1 is not None: - url += 'topic1={}&'.format(topic1) - if topic2 is not None: - url += 'topic2={}&'.format(topic2) - if topic3 is not None: - url += 'topic3={}&'.format(topic3) - if topic_opr is not None: - topic0_1_opr = topic_opr.get('topic0_1_opr', '') - topic0_1_opr = 'topic0_1_opr={}&'.format(topic0_1_opr) \ - if topic0_1_opr else '' - topic1_2_opr = topic_opr.get('topic1_2_opr', '') - topic1_2_opr = 'topic1_2_opr={}&'.format(topic1_2_opr) \ - if topic1_2_opr else '' - topic2_3_opr = topic_opr.get('topic2_3_opr', '') - topic2_3_opr = 'topic2_3_opr={}&'.format(topic2_3_opr) \ - if topic2_3_opr else '' - topic0_2_opr = topic_opr.get('topic0_2_opr', '') - topic0_2_opr = 'topic0_2_opr={}&'.format(topic0_2_opr) \ - if topic0_2_opr else '' - topic0_3_opr = topic_opr.get('topic0_3_opr', '') - topic0_3_opr = 'topic0_3_opr={}&'.format(topic0_3_opr) \ - if topic0_3_opr else '' - topic1_3_opr = topic_opr.get('topic1_3_opr', '') - topic1_3_opr = 'topic1_3_opr={}&'.format(topic1_3_opr) \ - if topic1_3_opr else '' - url += ( - topic0_1_opr + topic1_2_opr + topic2_3_opr + topic0_2_opr + - topic0_3_opr + topic1_3_opr) - return url - - def get_logs( - self, address, from_block, to_block='latest', - topic0=None, topic1=None, topic2=None, topic3=None, - topic_opr=None): - """ - Currently py-etherscan-api doesn't provide support for event logs, see: - https://github.com/corpetty/py-etherscan-api/issues/26 - """ - url = self.get_logs_url( - address, from_block, to_block, topic0, topic1, topic2, topic3, - topic_opr) - response = requests.get(url) - response = response.json() - logs = response['result'] - return logs - - def get_log_bet_events( - self, player_address, from_block, to_block='latest'): - """ - Retrieves all `LogBet` events associated with `player_address` - between two blocks. - """ - address = self.contract_address - topic0 = self.events_signatures['LogBet'].hex() - # adds zero padding to match topic format (32 bytes) - topic2 = '0x' + player_address[2:].zfill(2*32) - topic_opr = { - 'topic0_2_opr': 'and', - } - logs = self.get_logs( - address, from_block, to_block, topic0, topic2=topic2, - topic_opr=topic_opr) - return logs - - def get_log_result_events( - self, player_address, from_block, to_block='latest'): - """ - Retrieves all `LogResult` events associated with `player_address` - between two blocks. - """ - log_result_signature = self.events_signatures['LogResult'].hex() - address = self.contract_address - topic0 = log_result_signature - # adds zero padding to match topic format (32 bytes) - topic3 = '0x' + player_address[2:].zfill(2*32) - topic_opr = { - 'topic0_3_opr': 'and', - } - logs = self.get_logs( - address, from_block, to_block, topic0, topic3=topic3, - topic_opr=topic_opr) - return logs - - def get_balance(self, address): - """ - Retrieves the Ether balance of the given account, refs: - https://github.com/AndreMiras/EtherollApp/issues/8 - """ - etherscan_account_api = self.ChainEtherscanAccount( - address=address, api_key=self.etherscan_api_key) - balance_wei = int(etherscan_account_api.get_balance()) - balance_eth = round(balance_wei / 1e18, ROUND_DIGITS) - return balance_eth diff --git a/src/pyetheroll/etherscan_utils.py b/src/pyetheroll/etherscan_utils.py deleted file mode 100644 index ffb5f3d..0000000 --- a/src/pyetheroll/etherscan_utils.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -import logging -import os - -from etherscan.accounts import Account as EtherscanAccount -from etherscan.contracts import Contract as EtherscanContract - -from pyetheroll.constants import ChainID - -logger = logging.getLogger(__name__) - - -def get_etherscan_api_key(): - """ - Tries to retrieve etherscan API key from environment or from file. - """ - ETHERSCAN_API_KEY = os.environ.get('ETHERSCAN_API_KEY') - if ETHERSCAN_API_KEY is None: - location = os.path.realpath( - os.path.join(os.getcwd(), os.path.dirname(__file__))) - api_key_path = str(os.path.join(location, 'api_key.json')) - try: - with open(api_key_path, mode='r') as key_file: - ETHERSCAN_API_KEY = json.loads(key_file.read())['key'] - except FileNotFoundError: - ETHERSCAN_API_KEY = 'YourApiKeyToken' - logger.warning( - 'Cannot get Etherscan API key. ' - 'File {} not found, defaulting to `{}`.'.format( - api_key_path, ETHERSCAN_API_KEY)) - return ETHERSCAN_API_KEY - - -class RopstenEtherscanContract(EtherscanContract): - """ - https://github.com/corpetty/py-etherscan-api/issues/24 - """ - PREFIX = 'https://api-ropsten.etherscan.io/api?' - - -class ChainEtherscanContractFactory: - """ - Creates Contract class type depending on the chain ID. - """ - - CONTRACTS = { - ChainID.MAINNET: EtherscanContract, - ChainID.ROPSTEN: RopstenEtherscanContract, - } - - @classmethod - def create(cls, chain_id=ChainID.MAINNET): - ChainEtherscanContract = cls.CONTRACTS[chain_id] - return ChainEtherscanContract - - -class RopstenEtherscanAccount(EtherscanAccount): - """ - https://github.com/corpetty/py-etherscan-api/issues/24 - """ - PREFIX = 'https://api-ropsten.etherscan.io/api?' - - -class ChainEtherscanAccountFactory: - """ - Creates Account class type depending on the chain ID. - """ - - ACCOUNTS = { - ChainID.MAINNET: EtherscanAccount, - ChainID.ROPSTEN: RopstenEtherscanAccount, - } - - @classmethod - def create(cls, chain_id=ChainID.MAINNET): - ChainEtherscanAccount = cls.ACCOUNTS[chain_id] - return ChainEtherscanAccount diff --git a/src/pyetheroll/transaction_debugger.py b/src/pyetheroll/transaction_debugger.py deleted file mode 100644 index 5e3a69c..0000000 --- a/src/pyetheroll/transaction_debugger.py +++ /dev/null @@ -1,156 +0,0 @@ -import json - -import eth_abi -from ethereum.abi import decode_abi -from ethereum.abi import method_id as get_abi_method_id -from ethereum.abi import normalize_name as normalize_abi_method_name -from ethereum.utils import decode_hex, encode_int, zpad -from web3 import HTTPProvider, Web3 - -from pyetheroll.constants import ChainID -from pyetheroll.etherscan_utils import (ChainEtherscanContractFactory, - get_etherscan_api_key) - - -def decode_contract_call(contract_abi: list, call_data: str): - """ - https://ethereum.stackexchange.com/a/33887/34898 - """ - call_data = call_data.lower().replace("0x", "") - call_data_bin = decode_hex(call_data) - method_signature = call_data_bin[:4] - for description in contract_abi: - if description.get('type') != 'function': - continue - method_name = normalize_abi_method_name(description['name']) - arg_types = [item['type'] for item in description['inputs']] - method_id = get_abi_method_id(method_name, arg_types) - if zpad(encode_int(method_id), 4) == method_signature: - try: - # TODO: ethereum.abi.decode_abi vs eth_abi.decode_abi - args = decode_abi(arg_types, call_data_bin[4:]) - except AssertionError: - # Invalid args - continue - return method_name, args - - -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): - url = cls.PROVIDER_URLS[chain_id] - return HTTPProvider(url) - - -class TransactionDebugger: - - def __init__(self, contract_abi): - self.contract_abi = contract_abi - self.methods_infos = None - - @staticmethod - def get_contract_abi(chain_id, contract_address): - """ - Given a contract address returns the contract ABI from Etherscan, - refs #2 - """ - key = get_etherscan_api_key() - ChainEtherscanContract = ChainEtherscanContractFactory.create(chain_id) - api = ChainEtherscanContract(address=contract_address, api_key=key) - json_abi = api.get_abi() - abi = json.loads(json_abi) - return abi - - @staticmethod - def get_methods_infos(contract_abi): - """ - List of infos for each events. - """ - methods_infos = {} - # only retrieves functions and events, other existing types are: - # "fallback" and "constructor" - types = ['function', 'event'] - methods = [a for a in contract_abi if a['type'] in types] - for description in methods: - method_name = description['name'] - types = ','.join([x['type'] for x in description['inputs']]) - event_definition = "%s(%s)" % (method_name, types) - event_sha3 = Web3.sha3(text=event_definition) - method_info = { - 'definition': event_definition, - 'sha3': event_sha3, - 'abi': description, - } - methods_infos.update({method_name: method_info}) - return methods_infos - - def decode_method(self, topics, log_data): - """ - Given a topic and log data, decode the event. - """ - topic = topics[0] - # each indexed field generates a new topics and is excluded from data - # hence we consider topics[1:] like data, assuming indexed fields - # always come first - # see https://codeburst.io/deep-dive-into-ethereum-logs-a8d2047c7371 - topics_log_data = b"".join(topics[1:]) - log_data = log_data.lower().replace("0x", "") - log_data = bytes.fromhex(log_data) - topics_log_data += log_data - if self.methods_infos is None: - self.methods_infos = self.get_methods_infos(self.contract_abi) - method_info = None - for event, info in self.methods_infos.items(): - if info['sha3'].lower() == topic.lower(): - method_info = info - event_inputs = method_info['abi']['inputs'] - types = [e_input['type'] for e_input in event_inputs] - # hot patching `bytes` type to replace it with bytes32 since the former - # is crashing with `InsufficientDataBytes` during `LogResult` decoding. - types = ['bytes32' if t == 'bytes' else t for t in types] - names = [e_input['name'] for e_input in event_inputs] - values = eth_abi.decode_abi(types, topics_log_data) - call = {name: value for name, value in zip(names, values)} - decoded_method = { - 'method_info': method_info, - 'call': call, - } - return decoded_method - - @classmethod - def decode_transaction_log(cls, chain_id, log): - """ - Given a transaction event log. - 1) downloads the ABI associated to the recipient address - 2) uses it to decode methods calls - """ - contract_address = log.address - contract_abi = cls.get_contract_abi(chain_id, contract_address) - transaction_debugger = cls(contract_abi) - topics = log.topics - log_data = log.data - decoded_method = transaction_debugger.decode_method(topics, log_data) - return decoded_method - - @classmethod - def decode_transaction_logs(cls, chain_id, transaction_hash): - """ - Given a transaction hash, reads and decode the event log. - """ - decoded_methods = [] - provider = HTTPProviderFactory.create(chain_id) - web3 = Web3(provider) - transaction_receipt = web3.eth.getTransactionReceipt( - transaction_hash) - logs = transaction_receipt.logs - for log in logs: - decoded_methods.append(cls.decode_transaction_log(chain_id, log)) - return decoded_methods diff --git a/src/pyetheroll/utils.py b/src/pyetheroll/utils.py deleted file mode 100644 index f8356f7..0000000 --- a/src/pyetheroll/utils.py +++ /dev/null @@ -1,19 +0,0 @@ -from pyetheroll.constants import ROUND_DIGITS - - -class EtherollUtils: - - @staticmethod - def compute_profit(bet_size, chances_win): - """ - Helper method to compute profit given a bet_size and chances_win. - """ - if chances_win <= 0 or chances_win >= 100: - return - house_edge = 1.0 / 100 - chances_loss = 100 - chances_win - payout = ((chances_loss / chances_win) * bet_size) + bet_size - payout *= (1 - house_edge) - profit = payout - bet_size - profit = round(profit, ROUND_DIGITS) - return profit diff --git a/src/service/main.py b/src/service/main.py index 9a90b9b..0373735 100755 --- a/src/service/main.py +++ b/src/service/main.py @@ -20,6 +20,8 @@ from kivy.logger import Logger from kivy.utils import platform from plyer import notification +from pyetheroll.constants import ROUND_DIGITS, ChainID +from pyetheroll.etheroll import Etheroll from raven import Client from ethereum_utils import AccountUtils @@ -27,8 +29,6 @@ from etheroll.patches import patch_find_library_android from etheroll.store import Store from osc.osc_app_client import OscAppClient -from pyetheroll.constants import ROUND_DIGITS, ChainID -from pyetheroll.etheroll import Etheroll from sentry_utils import configure_sentry patch_find_library_android() diff --git a/src/tests/pyetheroll/test_etheroll.py b/src/tests/pyetheroll/test_etheroll.py index a21c990..3682be2 100644 --- a/src/tests/pyetheroll/test_etheroll.py +++ b/src/tests/pyetheroll/test_etheroll.py @@ -9,7 +9,6 @@ from eth_account.internal.transactions import assert_valid_fields from ethereum.tools.keys import PBKDF2_CONSTANTS from pyethapp.accounts import Account - from pyetheroll.constants import ChainID from pyetheroll.etheroll import Etheroll diff --git a/src/tests/pyetheroll/test_transaction_debugger.py b/src/tests/pyetheroll/test_transaction_debugger.py index d16f237..18ca001 100644 --- a/src/tests/pyetheroll/test_transaction_debugger.py +++ b/src/tests/pyetheroll/test_transaction_debugger.py @@ -3,11 +3,10 @@ from unittest import mock from hexbytes.main import HexBytes -from web3.utils.datastructures import AttributeDict - from pyetheroll.constants import ChainID from pyetheroll.transaction_debugger import (TransactionDebugger, decode_contract_call) +from web3.utils.datastructures import AttributeDict class TestTransactionDebugger(unittest.TestCase): diff --git a/src/tests/test_import.py b/src/tests/test_import.py index 240a94c..cf1f560 100644 --- a/src/tests/test_import.py +++ b/src/tests/test_import.py @@ -42,6 +42,10 @@ def test_pyethapp(self): self.assertIsNotNone(account) self.assertIsNotNone(address) + def test_pyetheroll(self): + from pyetheroll.etheroll import Etheroll + self.assertTrue(hasattr(Etheroll, 'player_roll_dice')) + if __name__ == '__main__': unittest.main() From a6e4fb41f58c39e54e7a262836ee81e8bb621ab5 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Wed, 31 Oct 2018 00:48:28 +0100 Subject: [PATCH 02/30] Removes pyetheroll tests, refs 67ff5e7 Tests are also in the dedicated library, refs #97 --- src/tests/pyetheroll/__init__.py | 0 src/tests/pyetheroll/test_etheroll.py | 911 ------------------ src/tests/pyetheroll/test_etherscan_utils.py | 42 - .../pyetheroll/test_transaction_debugger.py | 388 -------- src/tests/pyetheroll/test_utils.py | 17 - 5 files changed, 1358 deletions(-) delete mode 100644 src/tests/pyetheroll/__init__.py delete mode 100644 src/tests/pyetheroll/test_etheroll.py delete mode 100644 src/tests/pyetheroll/test_etherscan_utils.py delete mode 100644 src/tests/pyetheroll/test_transaction_debugger.py delete mode 100644 src/tests/pyetheroll/test_utils.py diff --git a/src/tests/pyetheroll/__init__.py b/src/tests/pyetheroll/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/tests/pyetheroll/test_etheroll.py b/src/tests/pyetheroll/test_etheroll.py deleted file mode 100644 index 3682be2..0000000 --- a/src/tests/pyetheroll/test_etheroll.py +++ /dev/null @@ -1,911 +0,0 @@ -import json -import os -import shutil -import unittest -from datetime import datetime -from tempfile import mkdtemp -from unittest import mock - -from eth_account.internal.transactions import assert_valid_fields -from ethereum.tools.keys import PBKDF2_CONSTANTS -from pyethapp.accounts import Account -from pyetheroll.constants import ChainID -from pyetheroll.etheroll import Etheroll - - -class TestEtheroll(unittest.TestCase): - - log_bet_abi = { - 'inputs': [ - {'indexed': True, 'type': 'bytes32', 'name': 'BetID'}, - {'indexed': True, 'type': 'address', 'name': 'PlayerAddress'}, - {'indexed': True, 'type': 'uint256', 'name': 'RewardValue'}, - {'indexed': False, 'type': 'uint256', 'name': 'ProfitValue'}, - {'indexed': False, 'type': 'uint256', 'name': 'BetValue'}, - {'indexed': False, 'type': 'uint256', 'name': 'PlayerNumber'}, - {'indexed': False, 'type': 'uint256', 'name': 'RandomQueryID'}, - ], - 'type': 'event', 'name': 'LogBet', 'anonymous': False - } - - log_result_abi = { - 'name': 'LogResult', - 'inputs': [ - {'name': 'ResultSerialNumber', 'indexed': True, 'type': 'uint256'}, - {'name': 'BetID', 'indexed': True, 'type': 'bytes32'}, - {'name': 'PlayerAddress', 'indexed': True, 'type': 'address'}, - {'name': 'PlayerNumber', 'indexed': False, 'type': 'uint256'}, - {'name': 'DiceResult', 'indexed': False, 'type': 'uint256'}, - {'name': 'Value', 'indexed': False, 'type': 'uint256'}, - {'name': 'Status', 'indexed': False, 'type': 'int256'}, - {'name': 'Proof', 'indexed': False, 'type': 'bytes'}, - ], - 'anonymous': False, 'type': 'event', - } - - player_roll_dice_abi = { - 'constant': False, - 'inputs': [{'name': 'rollUnder', 'type': 'uint256'}], - 'name': 'playerRollDice', - 'outputs': [], - 'payable': True, - 'stateMutability': 'payable', - 'type': 'function', - } - - bet_logs = [ - { - 'bet_id': ( - '15e007148ec621d996c886de0f2b88a0' - '3af083aa819e851a51133dc17b6e0e5b'), - 'bet_value_ether': 0.45, - 'datetime': datetime(2018, 4, 7, 0, 17, 6), - 'profit_value_ether': 44.1, - 'reward_value_ether': 44.55, - 'roll_under': 2, - 'timestamp': '0x5ac80e02', - 'transaction_hash': ( - '0xf363906a9278c4dd300c50a3c9a2790' - '0bb85df60596c49f7833c232f2944d1cb') - }, - { - 'bet_id': ( - '14bae6b4711bdc5e3db19983307a9208' - '1e2e7c1d45161117bdf7b8b509d1abbe'), - 'bet_value_ether': 0.45, - 'datetime': datetime(2018, 4, 7, 0, 20, 14), - 'profit_value_ether': 6.97, - 'reward_value_ether': 7.42, - 'roll_under': 7, - 'timestamp': '0x5ac80ebe', - 'transaction_hash': ( - '0x0df8789552248edf1dd9d06a7a90726' - 'f1bc83a9c39f315b04efb6128f0d02146') - }, - { - # that one would not have been yet resolved (no `LogResult`) - 'bet_id': ( - 'c2997a1bad35841b2c30ca95eea9cb08' - 'c7b101bc14d5aa8b1b8a0facea793e05'), - 'bet_value_ether': 0.5, - 'datetime': datetime(2018, 4, 7, 0, 23, 46), - 'profit_value_ether': 3.31, - 'reward_value_ether': 3.81, - 'roll_under': 14, - 'timestamp': '0x5ac80f92', - 'transaction_hash': ( - '0x0440f1013a5eafd88f16be6b5612b6e' - '051a4eb1b0b91a160c680295e7fab5bfe') - } - ] - - bet_results_logs = [ - { - 'bet_id': ( - '15e007148ec621d996c886de0f2b88a0' - '3af083aa819e851a51133dc17b6e0e5b'), - 'bet_value_ether': 0.45, - 'datetime': datetime(2018, 4, 7, 0, 17, 55), - 'dice_result': 86, - 'roll_under': 2, - 'timestamp': '0x5ac80e33', - 'transaction_hash': ( - '0x3505de688dc20748eb5f6b3efd6e6d3' - '66ea7f0737b4ab17035c6b60ab4329f2a') - }, - { - 'bet_id': ( - '14bae6b4711bdc5e3db19983307a9208' - '1e2e7c1d45161117bdf7b8b509d1abbe'), - 'bet_value_ether': 0.45, - 'datetime': datetime(2018, 4, 7, 0, 20, 54), - 'dice_result': 51, - 'roll_under': 7, - 'timestamp': '0x5ac80ee6', - 'transaction_hash': ( - '0x42df3e3136957bcc64226206ed177d5' - '7ac9c31e116290c8778c97474226d3092') - }, - ] - - def setUp(self): - self.keystore_dir = mkdtemp() - - def tearDown(self): - shutil.rmtree(self.keystore_dir, ignore_errors=True) - - def test_init(self): - """ - Verifies object initializes properly and contract methods are callable. - """ - with mock.patch('etherscan.contracts.Contract.get_abi') \ - as m_get_abi: - m_get_abi.return_value = ( - '[{"constant":true,"inputs":[],"name":"minBet","outputs":[{"na' - 'me":"","type":"uint256"}],"payable":false,"stateMutability":"' - 'view","type":"function"}]') - etheroll = Etheroll() - self.assertIsNotNone(etheroll.contract) - - def create_account_helper(self, password): - # reduces key derivation iterations to speed up creation - PBKDF2_CONSTANTS['c'] = 1 - wallet_path = os.path.join(self.keystore_dir, 'wallet.json') - account = Account.new(password, path=wallet_path) - with open(account.path, 'w') as f: - f.write(account.dump()) - return account - - def test_player_roll_dice(self): - """ - Verifies the transaction is properly built and sent. - """ - # simplified contract ABI - contract_abi = [self.player_roll_dice_abi] - with mock.patch('etherscan.contracts.Contract.get_abi') \ - as m_get_abi: - m_get_abi.return_value = json.dumps(contract_abi) - etheroll = Etheroll() - bet_size_ether = 0.1 - chances = 50 - wallet_password = 'password' - account = self.create_account_helper(wallet_password) - wallet_path = account.path - with \ - mock.patch('web3.eth.Eth.sendRawTransaction') \ - as m_sendRawTransaction, mock.patch( - 'web3.eth.Eth.getTransactionCount' - ) as m_getTransactionCount, mock.patch( - 'eth_account.account.Account.signTransaction' - ) as m_signTransaction: - m_getTransactionCount.return_value = 0 - transaction = etheroll.player_roll_dice( - bet_size_ether, chances, wallet_path, wallet_password) - # the method should return a transaction hash - self.assertIsNotNone(transaction) - # a second one with custom gas (in gwei), refs #23 - gas_price_gwei = 12 - transaction = etheroll.player_roll_dice( - bet_size_ether, chances, wallet_path, wallet_password, - gas_price_gwei) - self.assertIsNotNone(transaction) - # the nonce was retrieved - self.assertTrue(m_getTransactionCount.called) - # the transaction was sent - self.assertTrue(m_sendRawTransaction.called) - # the transaction should be built that way - expected_transaction1 = { - 'nonce': 0, 'chainId': 1, - 'to': Etheroll.CONTRACT_ADDRESSES[ChainID.MAINNET], - 'data': ( - '0xdc6dd152000000000000000000000000000' - '0000000000000000000000000000000000032'), - 'gas': 310000, - 'value': 100000000000000000, 'gasPrice': 4000000000 - } - expected_transaction2 = expected_transaction1.copy() - expected_transaction2['gasPrice'] = 12*1e9 - expected_call1 = mock.call(expected_transaction1, account.privkey) - expected_call2 = mock.call(expected_transaction2, account.privkey) - # the method should have been called only once - expected_calls = [expected_call1, expected_call2] - self.assertEqual(m_signTransaction.call_args_list, expected_calls) - # also make sure the transaction dict is passing the validation - # e.g. scientific notation 1e+17 is not accepted - transaction_dict = m_signTransaction.call_args[0][0] - assert_valid_fields(transaction_dict) - # even though below values are equal - self.assertTrue(transaction_dict['value'] == 0.1 * 1e18 == 1e17) - # this is not accepted by `assert_valid_fields()` - transaction_dict['value'] = 0.1 * 1e18 - with self.assertRaises(TypeError): - assert_valid_fields(transaction_dict) - # because float are not accepted - self.assertEqual(type(transaction_dict['value']), float) - - def test_get_last_bets_transactions(self): - """ - Verifies `get_last_bets_transactions()` performs the correct calls to - underlying libraries, and verifies it handle their inputs correctly. - """ - # simplified contract ABI - contract_abi = [self.player_roll_dice_abi] - # we want this unit test to still pass even if the Etheroll contract - # address changes, so let's make it explicit - contract_address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - with mock.patch('etherscan.contracts.Contract.get_abi') as m_get_abi: - m_get_abi.return_value = json.dumps(contract_abi) - etheroll = Etheroll(contract_address=contract_address) - - # simplified version of `get_transaction_page()` return - transactions = [ - { - 'blockHash': ( - '0xd0e85045f06f2ac6419ce6a3edf51b0' - '3c67fc78ffe92594e29f4aeddeab67476'), - 'blockNumber': '5442078', - # this transactions has the correct `from` address, but is not - 'from': '0x46044beaa1e985c67767e04de58181de5daaa00f', - 'hash': ( - '0x6191c2f77e4dee0d9677c77613c2e8d' - '2785d43bc6082bf5b5b67cbd9e0eb2b54'), - 'input': '0x', - 'timeStamp': '1523753147', - # sent `to` Etheroll contract address - 'to': '0x00e695c5d7b2f6a2e83e1b34db1390f89e2741ef', - 'value': '197996051600000005' - }, - { - 'blockHash': ( - '0x9814be792821e5d98b639e211fbe8f4' - 'b1930b8f12fa28aeb9ecf4737e749626b'), - 'blockNumber': '5394094', - 'from': '0x46044beaa1e985c67767e04de58181de5daaa00f', - 'hash': ( - '0x0440f1013a5eafd88f16be6b5612b6e' - '051a4eb1b0b91a160c680295e7fab5bfe'), - 'input': ( - '0xdc6dd152000000000000000000000000000' - '000000000000000000000000000000000000e'), - 'timeStamp': '0x5ac80f92', - 'to': '0x048717ea892f23fb0126f00640e2b18072efd9d2', - 'value': '500000000000000000', - }, - { - 'blockHash': ( - '0xbf5776b12ee403b3a99c03d560cd709' - 'a389f8342b03133c4eb9ae8fa58b5acfe'), - 'blockNumber': '5394085', - 'from': '0x46044beaa1e985c67767e04de58181de5daaa00f', - 'hash': ( - '0x72def66d60ecc85268c714e71929953' - 'ef94fd4fae37632a5f56ea49bee44dd59'), - 'input': ( - '0xdc6dd152000000000000000000000000000' - '0000000000000000000000000000000000002'), - 'timeStamp': '0x5ac80f0e', - 'to': '0x048717ea892f23fb0126f00640e2b18072efd9d2', - 'value': '450000000000000000', - }, - ] - address = '0x46044beaa1e985c67767e04de58181de5daaa00f' - page = 1 - offset = 3 - with mock.patch('etherscan.accounts.Account.get_transaction_page') \ - as m_get_transaction_page: - m_get_transaction_page.return_value = transactions - bets = etheroll.get_last_bets_transactions( - address=address, page=page, offset=offset) - # we should have only two bets returns as the first transaction - # was not made to the Etheroll contract - self.assertEqual( - bets, - [ - { - 'bet_size_ether': 0.5, - 'roll_under': 14, - 'block_number': '5394094', - 'timestamp': '0x5ac80f92', - 'datetime': datetime(2018, 4, 7, 0, 23, 46), - 'transaction_hash': ( - '0x0440f1013a5eafd88f16be6b5612b6e' - '051a4eb1b0b91a160c680295e7fab5bfe'), - }, - { - 'bet_size_ether': 0.45, - 'roll_under': 2, - 'block_number': '5394085', - 'timestamp': '0x5ac80f0e', - 'datetime': datetime(2018, 4, 7, 0, 21, 34), - 'transaction_hash': ( - '0x72def66d60ecc85268c714e71929953' - 'ef94fd4fae37632a5f56ea49bee44dd59'), - }, - ] - ) - # makes sure underlying library was used properly - expected_call = mock.call( - internal=False, offset=3, page=1, sort='desc') - # the method should have been called only once - expected_calls = [expected_call] - self.assertEqual(m_get_transaction_page.call_args_list, expected_calls) - - def test_get_logs_url(self): - with \ - mock.patch( - 'etherscan.contracts.Contract.get_abi') as m_get_abi, \ - mock.patch( - 'pyetheroll.etheroll.get_etherscan_api_key') \ - as m_get_etherscan_api_key: - m_get_abi.return_value = '[]' - m_get_etherscan_api_key.return_value = 'apikey' - etheroll = Etheroll() - address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - from_block = 1 - logs_url = etheroll.get_logs_url( - address=address, from_block=from_block) - self.assertEqual( - logs_url, - ( - 'https://api.etherscan.io/api?' - 'module=logs&action=getLogs&apikey=apikey' - '&address=0x048717Ea892F23Fb0126F00640e2b18072efd9D2&' - 'fromBlock=1&toBlock=latest&' - ) - ) - # makes sure Testnet is also supported - with \ - mock.patch( - 'etherscan.contracts.Contract.get_abi') as m_get_abi, \ - mock.patch( - 'pyetheroll.etheroll.get_etherscan_api_key') \ - as m_get_etherscan_api_key: - m_get_abi.return_value = '[]' - m_get_etherscan_api_key.return_value = 'apikey' - etheroll = Etheroll(chain_id=ChainID.ROPSTEN) - address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - from_block = 1 - logs_url = etheroll.get_logs_url( - address=address, from_block=from_block) - self.assertEqual( - logs_url, - ( - 'https://api-ropsten.etherscan.io/api?' - 'module=logs&action=getLogs&apikey=apikey&' - 'address=0x048717Ea892F23Fb0126F00640e2b18072efd9D2&' - 'fromBlock=1&toBlock=latest&' - ) - ) - - def test_get_logs_url_topics(self): - """ - More advanced tests for topic support. - """ - with \ - mock.patch( - 'etherscan.contracts.Contract.get_abi') as m_get_abi, \ - mock.patch( - 'pyetheroll.etheroll.get_etherscan_api_key') \ - as m_get_etherscan_api_key: - m_get_abi.return_value = '[]' - m_get_etherscan_api_key.return_value = 'apikey' - etheroll = Etheroll() - address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - from_block = 1 - logs_url = etheroll.get_logs_url( - address=address, from_block=from_block) - self.assertEqual( - logs_url, - ( - 'https://api.etherscan.io/api?' - 'module=logs&action=getLogs&apikey=apikey' - '&address=0x048717Ea892F23Fb0126F00640e2b18072efd9D2&' - 'fromBlock=1&toBlock=latest&' - ) - ) - # makes sure Testnet is also supported - with \ - mock.patch( - 'etherscan.contracts.Contract.get_abi') as m_get_abi, \ - mock.patch( - 'pyetheroll.etheroll.get_etherscan_api_key') \ - as m_get_etherscan_api_key: - m_get_abi.return_value = '[]' - m_get_etherscan_api_key.return_value = 'apikey' - etheroll = Etheroll(chain_id=ChainID.ROPSTEN) - address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - from_block = 1 - topic0 = 'topic0' - topic1 = 'topic1' - topic2 = 'topic2' - topic3 = 'topic3' - topic_opr = { - 'topic0_1_opr': 'and', - 'topic1_2_opr': 'or', - 'topic2_3_opr': 'and', - 'topic0_2_opr': 'or', - } - logs_url = etheroll.get_logs_url( - address=address, from_block=from_block, - topic0=topic0, topic1=topic1, topic2=topic2, topic3=topic3, - topic_opr=topic_opr) - self.assertEqual( - logs_url, - ( - 'https://api-ropsten.etherscan.io/api?' - 'module=logs&action=getLogs&apikey=apikey&' - 'address=0x048717Ea892F23Fb0126F00640e2b18072efd9D2&' - 'fromBlock=1&toBlock=latest&' - 'topic0=topic0&topic1=topic1&topic2=topic2&topic3=topic3&' - 'topic0_1_opr=and&topic1_2_opr=or&topic2_3_opr=and&' - 'topic0_2_opr=or&' - ) - ) - - def test_get_log_bet_events(self): - """ - Makes sure the Etherscan getLogs API is called correctly for LogBet. - """ - contract_address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - # simplified contract ABI - contract_abi = [self.log_bet_abi] - with \ - mock.patch( - 'etherscan.contracts.Contract.get_abi') as m_get_abi, \ - mock.patch( - 'pyetheroll.etheroll.get_etherscan_api_key') \ - as m_get_etherscan_api_key: - m_get_abi.return_value = json.dumps(contract_abi) - m_get_etherscan_api_key.return_value = 'apikey' - etheroll = Etheroll(contract_address=contract_address) - player_address = '0x46044beaa1e985c67767e04de58181de5daaa00f' - from_block = 5394085 - to_block = 5442078 - with mock.patch('requests.get') as m_get: - etheroll.get_log_bet_events( - player_address, from_block, to_block) - expected_call = mock.call( - 'https://api.etherscan.io/api?module=logs&action=getLogs' - '&apikey=apikey' - '&address=0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - '&fromBlock=5394085&toBlock=5442078&topic0=0x' - '56b3f1a6cd856076d6f8adbf8170c43a0b0f532fc5696a2699a0e0cabc704163' - '&topic2=0x' - '00000000000000000000000046044beaa1e985c67767e04de58181de5daaa00f' - '&topic0_2_opr=and&') - expected_calls = [expected_call] - self.assertEqual(m_get.call_args_list, expected_calls) - - def test_get_log_result_events(self): - """ - Makes sure the Etherscan getLogs API is called correctly for LogBet. - """ - contract_address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - # simplified contract ABI - contract_abi = [self.log_result_abi] - with \ - mock.patch( - 'etherscan.contracts.Contract.get_abi') as m_get_abi, \ - mock.patch( - 'pyetheroll.etheroll.get_etherscan_api_key') \ - as m_get_etherscan_api_key: - m_get_abi.return_value = json.dumps(contract_abi) - m_get_etherscan_api_key.return_value = 'apikey' - etheroll = Etheroll(contract_address=contract_address) - player_address = '0x46044beaa1e985c67767e04de58181de5daaa00f' - from_block = 5394085 - to_block = 5442078 - with mock.patch('requests.get') as m_get: - etheroll.get_log_result_events( - player_address, from_block, to_block) - expected_call = mock.call( - 'https://api.etherscan.io/api?module=logs&action=getLogs' - '&apikey=apikey' - '&address=0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - '&fromBlock=5394085&toBlock=5442078' - '&topic0=0x' - '8dd0b145385d04711e29558ceab40b456976a2b9a7d648cc1bcd416161bf97b9' - '&topic3=0x' - '00000000000000000000000046044beaa1e985c67767e04de58181de5daaa00f' - '&topic0_3_opr=and&' - ) - expected_calls = [expected_call] - self.assertEqual(m_get.call_args_list, expected_calls) - - def test_get_bets_logs(self): - """ - Verifies `get_bets_logs()` can retrieve bet info out from the logs. - """ - # simplified contract ABI - contract_abi = [self.log_bet_abi] - # simplified (a bit) for tests - get_log_bet_events = [ - { - 'address': '0x048717ea892f23fb0126f00640e2b18072efd9d2', - 'blockNumber': '0x524e94', - 'data': ( - '0x0000000000000000000000000000000000000000000000026402ac5922ba000' - '0000000000000000000000000000000000000000000000000063eb89da4ed0000' - '00000000000000000000000000000000000000000000000000000000000000020' - '000000000000000000000000000000000000000000000000000000000002aea'), - 'logIndex': '0x2a', - 'timeStamp': '0x5ac80e02', - 'topics': [ - '56b3f1a6cd856076d6f8adbf8170c43a0b0f532fc5696a2699a0e0cabc704163', - '15e007148ec621d996c886de0f2b88a03af083aa819e851a51133dc17b6e0e5b', - '00000000000000000000000046044beaa1e985c67767e04de58181de5daaa00f', - '0000000000000000000000000000000000000000000000026a4164f6c7a70000', - ], - 'transactionHash': ( - '0xf363906a9278c4dd300c50a3c9a2790' - '0bb85df60596c49f7833c232f2944d1cb'), - }, - { - 'address': '0x048717ea892f23fb0126f00640e2b18072efd9d2', - 'blockNumber': '0x524eae', - 'data': ( - '0x0000000000000000000000000000000000000000000000002de748a1024ac4eb' - '00000000000000000000000000000000000000000000000006f05b59d3b2000000' - '0000000000000000000000000000000000000000000000000000000000000e0000' - '000000000000000000000000000000000000000000000000000000002af9'), - 'logIndex': '0x1c', - 'timeStamp': '0x5ac80f92', - 'topics': [ - '56b3f1a6cd856076d6f8adbf8170c43a0b0f532fc5696a2699a0e0cabc704163', - 'c2997a1bad35841b2c30ca95eea9cb08c7b101bc14d5aa8b1b8a0facea793e05', - '00000000000000000000000046044beaa1e985c67767e04de58181de5daaa00f', - '00000000000000000000000000000000000000000000000034d7a3fad5fcc4eb', - ], - 'transactionHash': ( - '0x0440f1013a5eafd88f16be6b5612b6e' - '051a4eb1b0b91a160c680295e7fab5bfe'), - } - ] - with mock.patch('etherscan.contracts.Contract.get_abi') \ - as m_get_abi: - m_get_abi.return_value = json.dumps(contract_abi) - etheroll = Etheroll() - address = '0x46044beAa1E985C67767E04dE58181de5DAAA00F' - from_block = 5394067 - to_block = 5394095 - with mock.patch('pyetheroll.etheroll.Etheroll.get_log_bet_events') \ - as m_get_log_bet_events: - m_get_log_bet_events.return_value = get_log_bet_events - logs = etheroll.get_bets_logs(address, from_block, to_block) - expected_logs = [ - { - 'bet_id': ( - '15e007148ec621d996c886de0f2b88a0' - '3af083aa819e851a51133dc17b6e0e5b'), - 'bet_value_ether': 0.45, - 'profit_value_ether': 44.1, - 'reward_value_ether': 44.55, - 'roll_under': 2, - 'timestamp': '0x5ac80e02', - 'datetime': datetime(2018, 4, 7, 0, 17, 6), - 'transaction_hash': ( - '0xf363906a9278c4dd300c50a3c9a2790' - '0bb85df60596c49f7833c232f2944d1cb'), - }, - { - 'bet_id': ( - 'c2997a1bad35841b2c30ca95eea9cb08' - 'c7b101bc14d5aa8b1b8a0facea793e05'), - 'bet_value_ether': 0.5, - 'profit_value_ether': 3.31, - 'reward_value_ether': 3.81, - 'roll_under': 14, - 'timestamp': '0x5ac80f92', - 'datetime': datetime(2018, 4, 7, 0, 23, 46), - 'transaction_hash': ( - '0x0440f1013a5eafd88f16be6b5612b6e' - '051a4eb1b0b91a160c680295e7fab5bfe'), - }, - ] - self.assertEqual(logs, expected_logs) - - def test_get_bet_results_logs(self): - """ - Checks `get_bet_results_logs()` can retrieve bet info from the logs. - """ - # simplified contract ABI - contract_abi = [self.log_result_abi] - # simplified (a bit) for tests - get_log_result_events = [ - { - 'address': '0x048717ea892f23fb0126f00640e2b18072efd9d2', - 'blockNumber': '0x524e97', - 'data': ( - '0x0000000000000000000000000000000000000000000000000000000000000002' - '000000000000000000000000000000000000000000000000000000000000005600' - '0000000000000000000000000000000000000000000000063eb89da4ed00000000' - '000000000000000000000000000000000000000000000000000000000000000000' - '00000000000000000000000000000000000000000000000000000000a000000000' - '0000000000000000000000000000000000000000000000000000002212209856f6' - '9aa7983168979d4b0c41978807202b14cc7ffc6e31a17d443f017fcdff00000000' - '0000000000000000000000000000000000000000000000000000'), - 'logIndex': '0xc', - 'timeStamp': '0x5ac80e33', - 'topics': [ - '8dd0b145385d04711e29558ceab40b456976a2b9a7d648cc1bcd416161bf97b9', - '000000000000000000000000000000000000000000000000000000000004511d', - '15e007148ec621d996c886de0f2b88a03af083aa819e851a51133dc17b6e0e5b', - '00000000000000000000000046044beaa1e985c67767e04de58181de5daaa00f', - ], - 'transactionHash': ( - '0x3505de688dc20748eb5f6b3efd6e6d3' - '66ea7f0737b4ab17035c6b60ab4329f2a'), - }, - { - 'address': '0x048717ea892f23fb0126f00640e2b18072efd9d2', - 'blockNumber': '0x524eaa', - 'data': ( - '0x0000000000000000000000000000000000000000000000000000000000000002' - '000000000000000000000000000000000000000000000000000000000000000400' - '0000000000000000000000000000000000000000000000063eb89da4ed00000000' - '000000000000000000000000000000000000000000000000000000000000000000' - '00000000000000000000000000000000000000000000000000000000a000000000' - '0000000000000000000000000000000000000000000000000000002212205a95b8' - '96176efeb912d5d4937c541ee511092aced04eb764eab4e9629c613c3c00000000' - '0000000000000000000000000000000000000000000000000000'), - 'logIndex': '0x4f', - 'timeStamp': '0x5ac80f38', - 'topics': [ - '8dd0b145385d04711e29558ceab40b456976a2b9a7d648cc1bcd416161bf97b9', - '000000000000000000000000000000000000000000000000000000000004512b', - 'f2fb7902894213d47c482fb155cafd9677286d930fba1a1434265be0dbe80e66', - '00000000000000000000000046044beaa1e985c67767e04de58181de5daaa00f', - ], - 'transactionHash': ( - '0x6123e2a19f649df79c6cf2dfbe99811' - '530d0770ade8e2c71488b8eb881ad20e9'), - } - ] - with mock.patch('etherscan.contracts.Contract.get_abi') \ - as m_get_abi: - m_get_abi.return_value = json.dumps(contract_abi) - etheroll = Etheroll() - address = '0x46044beAa1E985C67767E04dE58181de5DAAA00F' - from_block = 5394067 - to_block = 5394095 - with mock.patch('pyetheroll.etheroll.Etheroll.get_log_result_events') \ - as m_get_log_result_events: - m_get_log_result_events.return_value = get_log_result_events - results = etheroll.get_bet_results_logs( - address, from_block, to_block) - expected_results = [ - { - 'bet_id': ( - '15e007148ec621d996c886de0f2b88a0' - '3af083aa819e851a51133dc17b6e0e5b'), - 'bet_value_ether': 0.45, - 'dice_result': 86, - 'roll_under': 2, - 'timestamp': '0x5ac80e33', - 'datetime': datetime(2018, 4, 7, 0, 17, 55), - 'transaction_hash': ( - '0x3505de688dc20748eb5f6b3efd6e6d3' - '66ea7f0737b4ab17035c6b60ab4329f2a'), - }, - { - 'bet_id': ( - 'f2fb7902894213d47c482fb155cafd96' - '77286d930fba1a1434265be0dbe80e66'), - 'bet_value_ether': 0.45, - 'dice_result': 4, - 'roll_under': 2, - 'timestamp': '0x5ac80f38', - 'datetime': datetime(2018, 4, 7, 0, 22, 16), - 'transaction_hash': ( - '0x6123e2a19f649df79c6cf2dfbe99811' - '530d0770ade8e2c71488b8eb881ad20e9'), - }, - ] - self.assertEqual(results, expected_results) - - def test_get_last_bets_blocks(self): - transactions = [ - { - 'blockHash': ( - '0x9814be792821e5d98b639e211fbe8f4' - 'b1930b8f12fa28aeb9ecf4737e749626b'), - 'blockNumber': '5394094', - 'confirmations': '81252', - 'contractAddress': '', - 'cumulativeGasUsed': '2619957', - 'from': '0x46044beaa1e985c67767e04de58181de5daaa00f', - 'gas': '250000', - 'gasPrice': '2000000000', - 'gasUsed': '177773', - 'hash': ( - '0x0440f1013a5eafd88f16be6b5612b6e' - '051a4eb1b0b91a160c680295e7fab5bfe'), - 'input': ( - '0xdc6dd152000000000000000000000000000' - '000000000000000000000000000000000000e'), - 'isError': '0', - 'nonce': '9485', - 'timeStamp': '1523060626', - 'to': '0x048717ea892f23fb0126f00640e2b18072efd9d2', - 'transactionIndex': '42', - 'txreceipt_status': '1', - 'value': '500000000000000000'}, - { - 'blockHash': ( - '0x0f74b07fe04dd447b2a48c7aee6998a' - 'c97cf7c12b4fd46ef781f00652abe4642'), - 'blockNumber': '5394068', - 'confirmations': '81278', - 'contractAddress': '', - 'cumulativeGasUsed': '4388802', - 'from': '0x46044beaa1e985c67767e04de58181de5daaa00f', - 'gas': '250000', - 'gasPrice': '2200000000', - 'gasUsed': '177773', - 'hash': ( - '0xf363906a9278c4dd300c50a3c9a2790' - '0bb85df60596c49f7833c232f2944d1cb'), - 'input': ( - '0xdc6dd152000000000000000000000000000' - '0000000000000000000000000000000000002'), - 'isError': '0', - 'nonce': '9481', - 'timeStamp': '1523060226', - 'to': '0x048717ea892f23fb0126f00640e2b18072efd9d2', - 'transactionIndex': '150', - 'txreceipt_status': '1', - 'value': '450000000000000000' - } - ] - contract_address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - contract_abi = [] - address = '0x46044beAa1E985C67767E04dE58181de5DAAA00F' - with mock.patch('etherscan.contracts.Contract.get_abi') as m_get_abi: - m_get_abi.return_value = json.dumps(contract_abi) - etheroll = Etheroll(contract_address=contract_address) - with mock.patch( - 'pyetheroll.etheroll.Etheroll.get_player_roll_dice_tx') \ - as m_get_player_roll_dice_tx: - m_get_player_roll_dice_tx.return_value = transactions - last_bets_blocks = etheroll.get_last_bets_blocks(address) - self.assertEqual( - last_bets_blocks, {'from_block': 5394067, 'to_block': 5394194}) - - def test_merge_logs(self): - bet_logs = self.bet_logs - bet_results_logs = self.bet_results_logs - expected_merged_logs = [ - {'bet_log': bet_logs[0], 'bet_result': bet_results_logs[0]}, - {'bet_log': bet_logs[1], 'bet_result': bet_results_logs[1]}, - # not yet resolved (no `LogResult`) - {'bet_log': bet_logs[2], 'bet_result': None}, - ] - merged_logs = Etheroll.merge_logs(bet_logs, bet_results_logs) - self.assertEqual(merged_logs, expected_merged_logs) - - def test_get_merged_logs(self): - """ - Checking we can merge both `LogBet` and `LogResult` events. - We have 3 `LogBet` here and only 2 matching `LogResult` since the - last bet is not yet resolved. - """ - contract_abi = [ - self.log_bet_abi, self.log_result_abi, self.player_roll_dice_abi] - contract_address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - last_bets_blocks = {'from_block': 5394067, 'to_block': 5394194} - bet_logs = self.bet_logs - bet_results_logs = self.bet_results_logs - with mock.patch('etherscan.contracts.Contract.get_abi') as m_get_abi: - m_get_abi.return_value = json.dumps(contract_abi) - etheroll = Etheroll(contract_address=contract_address) - address = '0x46044beAa1E985C67767E04dE58181de5DAAA00F' - with \ - mock.patch( - 'pyetheroll.etheroll.Etheroll.get_last_bets_blocks') \ - as m_get_last_bets_blocks,\ - mock.patch('pyetheroll.etheroll.Etheroll.get_bets_logs') \ - as m_get_bets_logs,\ - mock.patch( - 'pyetheroll.etheroll.Etheroll.get_bet_results_logs') \ - as m_get_bet_results_logs: - m_get_last_bets_blocks.return_value = last_bets_blocks - m_get_bets_logs.return_value = bet_logs - m_get_bet_results_logs.return_value = bet_results_logs - merged_logs = etheroll.get_merged_logs(address) - expected_merged_logs = [ - {'bet_log': bet_logs[0], 'bet_result': bet_results_logs[0]}, - {'bet_log': bet_logs[1], 'bet_result': bet_results_logs[1]}, - # not yet resolved (no `LogResult`) - {'bet_log': bet_logs[2], 'bet_result': None}, - ] - self.assertEqual(merged_logs, expected_merged_logs) - - def test_get_merged_logs_empty_tx(self): - """ - Empty transaction history should not crash the application, refs: - https://github.com/AndreMiras/EtherollApp/issues/67 - """ - contract_abi = [self.player_roll_dice_abi] - contract_address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - with mock.patch('etherscan.contracts.Contract.get_abi') as m_get_abi: - m_get_abi.return_value = json.dumps(contract_abi) - etheroll = Etheroll(contract_address=contract_address) - address = '0x7aBE7DdD94DB8feb6BE426e53cA090b94F15d73E' - with mock.patch('requests.sessions.Session.get') as m_get: - # this is what etherscan.io would return on empty tx history - m_get.return_value.status_code = 200 - m_get.return_value.text = ( - '{"status":"0","message":"No transactions found","result":[]}') - m_get.return_value.json.return_value = json.loads( - m_get.return_value.text) - # the library should not crash but return an empty list - merged_logs = etheroll.get_merged_logs(address) - self.assertEqual(merged_logs, []) - - def test_get_merged_logs_no_matching_tx(self): - """ - Makes sure no matching transactions doesn't crash the app, refs: - https://github.com/AndreMiras/EtherollApp/issues/87 - """ - contract_abi = [self.player_roll_dice_abi] - contract_address = '0xe12c6dEb59f37011d2D9FdeC77A6f1A8f3B8B1e8' - with mock.patch('etherscan.contracts.Contract.get_abi') as m_get_abi: - m_get_abi.return_value = json.dumps(contract_abi) - etheroll = Etheroll( - chain_id=ChainID.ROPSTEN, contract_address=contract_address) - address = '0x4F4b934af9Bb3656daDD4c7C7d8dD348AC4f787A' - with mock.patch('requests.sessions.Session.get') as m_get: - # there's a transaction, but it's not matching the expected ones - m_get.return_value.status_code = 200 - m_get.return_value.text = ( - '{"status":"1","message":"OK","result":[{"blockNumber":"306526' - '5","timeStamp":"1524087170","hash":"0x93bf3cff2c334d15e96b07e' - '362240e09255b9e8728d855741e5970110d5a8a6d","nonce":"13","bloc' - 'kHash":"0x9625ece7c2bbca90c15628a970d962b8d0f1e57221e1f3ded6c' - '24f25df834d62","transactionIndex":"51","from":"0x66d4bacfe61d' - 'f23be813089a7a6d1a749a5c936a","to":"0x4f4b934af9bb3656dadd4c7' - 'c7d8dd348ac4f787a","value":"2000000000000000000","gas":"21000' - '","gasPrice":"1000000000","isError":"0","txreceipt_status":"1' - '","input":"0x","contractAddress":"","cumulativeGasUsed":"1871' - '849","gasUsed":"21000","confirmations":"161389"}]}') - m_get.return_value.json.return_value = json.loads( - m_get.return_value.text) - merged_logs = etheroll.get_merged_logs(address) - # merged logs should simply be empty - self.assertEqual(merged_logs, []) - - def test_get_balance(self): - """ - Makes sure proper Etherscan API call is made. - https://github.com/AndreMiras/EtherollApp/issues/67 - """ - contract_abi = [] - contract_address = '0x048717Ea892F23Fb0126F00640e2b18072efd9D2' - with mock.patch('etherscan.contracts.Contract.get_abi') as m_get_abi, \ - mock.patch( - 'pyetheroll.etheroll.get_etherscan_api_key') \ - as m_get_etherscan_api_key: - m_get_abi.return_value = json.dumps(contract_abi) - m_get_etherscan_api_key.return_value = 'apikey' - etheroll = Etheroll(contract_address=contract_address) - address = '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B' - with mock.patch('requests.sessions.Session.get') as m_get: - # this is what etherscan.io would return on empty tx history - m_get.return_value.status_code = 200 - m_get.return_value.text = ( - '{"status":"1","message":"OK",' - '"result":"365003278106457867877843"}') - m_get.return_value.json.return_value = json.loads( - m_get.return_value.text) - # but this crashes the library with an exit - balance = etheroll.get_balance(address) - # makes sure the Etherscan API was called and parsed properly - expected_url = ( - 'https://api.etherscan.io/api?module=account' - '&address=0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B' - '&tag=latest&apikey=apikey' - '&action=balance') - expected_call = mock.call(expected_url) - expected_calls = [expected_call] - self.assertEqual(m_get.call_args_list, expected_calls) - self.assertEqual(balance, 365003.28) diff --git a/src/tests/pyetheroll/test_etherscan_utils.py b/src/tests/pyetheroll/test_etherscan_utils.py deleted file mode 100644 index bd0ae5b..0000000 --- a/src/tests/pyetheroll/test_etherscan_utils.py +++ /dev/null @@ -1,42 +0,0 @@ -import unittest -from unittest import mock - -from pyetheroll.etherscan_utils import get_etherscan_api_key - - -class TestEtherscanlUtils(unittest.TestCase): - - def test_get_etherscan_api_key(self): - """ - Verifies the key can be retrieved from either: - 1) environment - 2) file - 3) or fallbacks on default key - """ - expected_key = '0102030405060708091011121314151617' - # 1) environment - with mock.patch.dict( - 'os.environ', {'ETHERSCAN_API_KEY': expected_key}): - actual_key = get_etherscan_api_key() - self.assertEqual(actual_key, expected_key) - # 2) file - read_data = '{ "key" : "%s" }' % (expected_key) - with mock.patch('builtins.open', mock.mock_open(read_data=read_data)) \ - as m_open: - actual_key = get_etherscan_api_key() - self.assertEqual(expected_key, actual_key) - # verifies the file was read - self.assertTrue( - m_open.call_args_list[0][0][0].endswith( - '/pyetheroll/api_key.json')) - self.assertEqual(m_open.call_args_list[0][1], {'mode': 'r'}) - # 3) or fallbacks on default key - with mock.patch('builtins.open') as m_open, \ - mock.patch('pyetheroll.etherscan_utils.logger') as m_logger: - m_open.side_effect = FileNotFoundError - actual_key = get_etherscan_api_key() - self.assertEqual('YourApiKeyToken', actual_key) - # verifies the fallback warning was logged - self.assertTrue( - 'Cannot get Etherscan API key.' - in m_logger.warning.call_args_list[0][0][0]) diff --git a/src/tests/pyetheroll/test_transaction_debugger.py b/src/tests/pyetheroll/test_transaction_debugger.py deleted file mode 100644 index 18ca001..0000000 --- a/src/tests/pyetheroll/test_transaction_debugger.py +++ /dev/null @@ -1,388 +0,0 @@ -import json -import unittest -from unittest import mock - -from hexbytes.main import HexBytes -from pyetheroll.constants import ChainID -from pyetheroll.transaction_debugger import (TransactionDebugger, - decode_contract_call) -from web3.utils.datastructures import AttributeDict - - -class TestTransactionDebugger(unittest.TestCase): - - def test_decode_method_log1(self): - """ - Trying to decode a `Log1()` event call. - """ - # simplified contract ABI for tests - contract_abi = [ - {'inputs': [], 'type': 'constructor', 'payable': False}, - {'payable': False, 'type': 'fallback'}, - {'inputs': [ - {'indexed': False, 'type': 'address', 'name': 'sender'}, - {'indexed': False, 'type': 'bytes32', 'name': 'cid'}, - {'indexed': False, 'type': 'uint256', 'name': 'timestamp'}, - {'indexed': False, 'type': 'string', 'name': 'datasource'}, - {'indexed': False, 'type': 'string', 'name': 'arg'}, - {'indexed': False, 'type': 'uint256', 'name': 'gaslimit'}, - {'indexed': False, 'type': 'bytes1', 'name': 'proofType'}, - {'indexed': False, 'type': 'uint256', 'name': 'gasPrice'}], - 'type': 'event', 'name': 'Log1', 'anonymous': False}, - {'inputs': [ - {'indexed': False, 'type': 'address', 'name': 'sender'}, - {'indexed': False, 'type': 'bytes32', 'name': 'cid'}, - {'indexed': False, 'type': 'uint256', 'name': 'timestamp'}, - {'indexed': False, 'type': 'string', 'name': 'datasource'}, - {'indexed': False, 'type': 'string', 'name': 'arg1'}, - {'indexed': False, 'type': 'string', 'name': 'arg2'}, - {'indexed': False, 'type': 'uint256', 'name': 'gaslimit'}, - {'indexed': False, 'type': 'bytes1', 'name': 'proofType'}, - {'indexed': False, 'type': 'uint256', 'name': 'gasPrice'}], - 'type': 'event', 'name': 'Log2', 'anonymous': False}, - {'inputs': [ - {'indexed': False, 'type': 'address', 'name': 'sender'}, - {'indexed': False, 'type': 'bytes32', 'name': 'cid'}, - {'indexed': False, 'type': 'uint256', 'name': 'timestamp'}, - {'indexed': False, 'type': 'string', 'name': 'datasource'}, - {'indexed': False, 'type': 'bytes', 'name': 'args'}, - {'indexed': False, 'type': 'uint256', 'name': 'gaslimit'}, - {'indexed': False, 'type': 'bytes1', 'name': 'proofType'}, - {'indexed': False, 'type': 'uint256', 'name': 'gasPrice'}], - 'type': 'event', 'name': 'LogN', 'anonymous': False}] - topics = [HexBytes( - 'b76d0edd90c6a07aa3ff7a222d7f5933e29c6acc660c059c97837f05c4ca1a84' - )] - log_data = ( - "0x" - "000000000000000000000000fe8a5f3a7bb446e1cb4566717691cd3139289ed4" - "b0230ab70b78e47050766089ea333f2ff7ad41c6f31e8bed8c2acfcb8e911841" - "0000000000000000000000000000000000000000000000000000000000000000" - "0000000000000000000000000000000000000000000000000000000000000100" - "0000000000000000000000000000000000000000000000000000000000000140" - "00000000000000000000000000000000000000000000000000000000000395f8" - "1100000000000000000000000000000000000000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000004a817c800" - "0000000000000000000000000000000000000000000000000000000000000006" - "6e65737465640000000000000000000000000000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000001b4" - "5b55524c5d205b276a736f6e2868747470733a2f2f6170692e72616e646f6d2e" - "6f72672f6a736f6e2d7270632f312f696e766f6b65292e726573756c742e7261" - "6e646f6d5b2273657269616c4e756d626572222c2264617461225d272c20275c" - "6e7b226a736f6e727063223a22322e30222c226d6574686f64223a2267656e65" - "726174655369676e6564496e746567657273222c22706172616d73223a7b2261" - "70694b6579223a247b5b646563727970745d20424b6733544373376c6b7a4e72" - "316b523670786a50434d32534f656a63466f6a55504d544f73426b432f343748" - "485066317350326f78564c546a4e42752b736c523953675a797144746a564f56" - "35597a67313269556b62756270304470636a434564654a54486e477743366744" - "3732394755566f47766f393668757877526f5a6c436a594f3830725771325747" - "596f522f4c433357616d704475767632426f3d7d2c226e223a312c226d696e22" - "3a312c226d6178223a3130302c227265706c6163656d656e74223a747275652c" - "2262617365223a3130247b5b6964656e746974795d20227d227d2c226964223a" - "31247b5b6964656e746974795d20227d227d275d000000000000000000000000") - transaction_debugger = TransactionDebugger(contract_abi) - decoded_method = transaction_debugger.decode_method(topics, log_data) - # TODO: simplify that arg call for unit testing - self.assertEqual( - decoded_method['call'], - {'arg': bytes( - '[URL] [\'json(https://api.random.org/json-rpc/1/invoke).resul' - 't.random["serialNumber","data"]\', \'\\n{"jsonrpc":"2.0","met' - 'hod":"generateSignedIntegers","params":{"apiKey":${[decrypt] ' - 'BKg3TCs7lkzNr1kR6pxjPCM2SOejcFojUPMTOsBkC/47HHPf1sP2oxVLTjNBu' - '+slR9SgZyqDtjVOV5Yzg12iUkbubp0DpcjCEdeJTHnGwC6gD729GUVoGvo96h' - 'uxwRoZlCjYO80rWq2WGYoR/LC3WampDuvv2Bo=},"n":1,"min":1,"max":1' - '00,"replacement":true,"base":10${[identity] "}"},"id":1${[ide' - 'ntity] "}"}\']', "utf8"), - 'cid': ( - b'\xb0#\n\xb7\x0bx\xe4pPv`\x89\xea3?/\xf7\xadA\xc6\xf3\x1e' - b'\x8b\xed\x8c*\xcf\xcb\x8e\x91\x18A'), - 'datasource': b'nested', - 'gasPrice': 20000000000, - 'gaslimit': 235000, - 'proofType': b'\x11', - 'sender': '0xfe8a5f3a7bb446e1cb4566717691cd3139289ed4', - 'timestamp': 0} - ) - self.assertEqual( - decoded_method['method_info']['definition'], - 'Log1(address,bytes32,uint256,string,string,uint256,bytes1,uint256)') - self.assertEqual( - decoded_method['method_info']['sha3'].hex(), - '0xb76d0edd90c6a07aa3ff7a222d7f5933e29c6acc660c059c97837f05c4ca1a84') - - def test_decode_method_log_bet(self): - """ - Trying to decode a `LogBet()` event call. - """ - # simplified contract ABI - contract_abi = [ - { - 'inputs': [{'type': 'uint256', 'name': 'newMaxProfitAsPercent'}], - 'constant': False, 'name': 'ownerSetMaxProfitAsPercentOfHouse', - 'outputs': [], 'stateMutability': 'nonpayable', - 'payable': False, 'type': 'function'}, - { - 'inputs': [], 'constant': True, 'name': 'treasury', - 'outputs': [{'type': 'address', 'name': ''}], - 'stateMutability': 'view', 'payable': False, 'type': 'function'}, - { - 'inputs': [], 'constant': True, 'name': 'totalWeiWagered', - 'outputs': [{'type': 'uint256', 'name': ''}], - 'stateMutability': 'view', 'payable': False, 'type': 'function'}, - { - 'inputs': [{'type': 'uint256', 'name': 'newMinimumBet'}], - 'constant': False, 'name': 'ownerSetMinBet', - 'outputs': [], 'stateMutability': 'nonpayable', - 'payable': False, 'type': 'function' - }, - { - 'stateMutability': 'nonpayable', - 'inputs': [], - 'type': 'constructor', - 'payable': False - }, - {'stateMutability': 'payable', 'payable': True, 'type': 'fallback'}, - { - 'inputs': [ - {'indexed': True, 'type': 'bytes32', 'name': 'BetID'}, - {'indexed': True, 'type': 'address', 'name': 'PlayerAddress'}, - {'indexed': True, 'type': 'uint256', 'name': 'RewardValue'}, - {'indexed': False, 'type': 'uint256', 'name': 'ProfitValue'}, - {'indexed': False, 'type': 'uint256', 'name': 'BetValue'}, - {'indexed': False, 'type': 'uint256', 'name': 'PlayerNumber'}], - 'type': 'event', 'name': 'LogBet', 'anonymous': False}, - ] - topics = [ - HexBytes( - '1cb5bfc4e69cbacf65c8e05bdb84d7a327bd6bb4c034ff82359aefd7443775c4' - ), - HexBytes( - 'b0230ab70b78e47050766089ea333f2ff7ad41c6f31e8bed8c2acfcb8e911841' - ), - HexBytes( - '00000000000000000000000066d4bacfe61df23be813089a7a6d1a749a5c936a' - ), - HexBytes( - '000000000000000000000000000000000000000000000000016a98b78c556c34' - ), - ] - log_data = ( - '0x' - '0000000000000000000000000000000000000000000000000007533f2ecb6c34' - '000000000000000000000000000000000000000000000000016345785d8a0000' - '0000000000000000000000000000000000000000000000000000000000000062') - transaction_debugger = TransactionDebugger(contract_abi) - decoded_method = transaction_debugger.decode_method(topics, log_data) - self.assertEqual( - decoded_method['call'], - {'BetID': ( - b'\xb0#\n\xb7\x0bx\xe4pPv`\x89\xea3?/\xf7\xadA\xc6\xf3\x1e\x8b' - b'\xed\x8c*\xcf\xcb\x8e\x91\x18A'), - 'BetValue': 100000000000000000, - 'PlayerAddress': - '0x66d4bacfe61df23be813089a7a6d1a749a5c936a', - 'PlayerNumber': 98, - 'ProfitValue': 2061855670103092, - 'RewardValue': 102061855670103092}) - self.assertEqual( - decoded_method['method_info']['definition'], - 'LogBet(bytes32,address,uint256,uint256,uint256,uint256)') - self.assertEqual( - decoded_method['method_info']['sha3'].hex(), - '0x1cb5bfc4e69cbacf65c8e05bdb84d7a327bd6bb4c034ff82359aefd7443775c4') - - def test_decode_contract_call(self): - """ - Uses actual data from: - https://etherscan.io/tx/ - 0xf7b7196ca9eab6e4fb6e7bce81aeb25a4edf04330e57b3c15bece9d260577e2b - In its simplified form for tests. - """ - json_abi = ( - '[{"constant":false,"inputs":[{"name":"_to","type":"address"},{"na' - 'me":"_value","type":"uint256"}],"name":"transfer","outputs":[{"na' - 'me":"success","type":"bool"}],"payable":false,"type":"function"}]' - ) - contract_abi = json.loads(json_abi) - call_data = ( - 'a9059cbb00000000000000000000000067fa2c06c9c6d4332f330e14a66bdf18' - '73ef3d2b0000000000000000000000000000000000000000000000000de0b6b3' - 'a7640000') - method_name, args = decode_contract_call(contract_abi, call_data) - self.assertEqual(method_name, 'transfer') - self.assertEqual( - args, - ['0x67fa2c06c9c6d4332f330e14a66bdf1873ef3d2b', 1000000000000000000] - ) - - def test_decode_contract_call_callback(self): - """ - Decode `__callback()` method call. - Uses actual data from: - https://etherscan.io/tx/ - 0xf6d291b2de12db618aafc5fd9f40a37384b4a7ac41d14463a1d707a4f43137c3 - In its simplified form for tests. - """ - contract_abi = [ - { - "constant": False, - "inputs": [ - {"name": "myid", "type": "bytes32"}, - {"name": "result", "type": "string"} - ], - "name": "__callback", "outputs": [], "payable": False, - "stateMutability": "nonpayable", "type": "function" - }, - { - "constant": False, - "inputs": [ - {"name": "myid", "type": "bytes32"}, - {"name": "result", "type": "string"}, - {"name": "proof", "type": "bytes"} - ], - "name": "__callback", "outputs": [], "payable": False, - "stateMutability": "nonpayable", "type": "function" - } - ] - call_data = ( - '38bbfa5010369b11d06269122229ec4088d4bf42fbf629b0d40432ffc40cc638' - 'd938f1e800000000000000000000000000000000000000000000000000000000' - '0000006000000000000000000000000000000000000000000000000000000000' - '0000008000000000000000000000000000000000000000000000000000000000' - '0000000000000000000000000000000000000000000000000000000000000000' - '000000221220ba7237d9ed277fdd4bf2b358049b1c5e971b2bc5fa0edd47b334' - '5d3890e415fc0000000000000000000000000000000000000000000000000000' - '00000000') - method_name, args = decode_contract_call(contract_abi, call_data) - self.assertEqual(method_name, '__callback') - myid = bytes.fromhex( - '10369b11d06269122229ec4088d4bf42fbf629b0d40432ffc40cc638d938f1e8') - result = b'' - proof = bytes.fromhex( - '1220ba7237d9ed277fdd4bf2b358049b1c5e971b2bc5fa0edd47b3345d3890e4' - '15fc') - self.assertEqual( - args, - [ - myid, - result, - proof, - ] - ) - - def m_get_abi(self, instance): - """ - Mocked version of `web3.contract.Contract.get_abi()`. - """ - # retrieves the original contract address - address = instance.url_dict[instance.ADDRESS] - abi1 = ( - '[{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","t' - 'ype":"address"},{"indexed":false,"name":"cid","type":"bytes32"},{' - '"indexed":false,"name":"timestamp","type":"uint256"},{"indexed":f' - 'alse,"name":"datasource","type":"string"},{"indexed":false,"name"' - ':"arg","type":"string"},{"indexed":false,"name":"gaslimit","type"' - ':"uint256"},{"indexed":false,"name":"proofType","type":"bytes1"},' - '{"indexed":false,"name":"gasPrice","type":"uint256"}],"name":"Log' - '1","type":"event"}]') - abi2 = ( - '[{"anonymous":false,"inputs":[{"indexed":true,"name":"BetID","typ' - 'e":"bytes32"},{"indexed":true,"name":"PlayerAddress","type":"addr' - 'ess"},{"indexed":true,"name":"RewardValue","type":"uint256"},{"in' - 'dexed":false,"name":"ProfitValue","type":"uint256"},{"indexed":fa' - 'lse,"name":"BetValue","type":"uint256"},{"indexed":false,"name":"' - 'PlayerNumber","type":"uint256"}],"name":"LogBet","type":"event"}]' - ) - if address.lower() == '0xcbf1735aad8c4b337903cd44b419efe6538aab40': - return abi1 - elif address.lower() == '0xfe8a5f3a7bb446e1cb4566717691cd3139289ed4': - return abi2 - return None - - def test_decode_transaction_logs(self): - """ - Mocking `web3.eth.Eth.getTransactionReceipt()` response and verifies - decoding transaction works as expected. - """ - mocked_logs = [ - AttributeDict({ - 'address': '0xCBf1735Aad8C4B337903cD44b419eFE6538aaB40', - 'topics': [ - HexBytes( - 'b76d0edd90c6a07aa3ff7a222d7f5933' - 'e29c6acc660c059c97837f05c4ca1a84' - ) - ], - 'data': - '000000000000000000000000fe8a5f3a7bb446e1cb4566717691cd3139289ed4' - 'b0230ab70b78e47050766089ea333f2ff7ad41c6f31e8bed8c2acfcb8e911841' - '0000000000000000000000000000000000000000000000000000000000000000' - '0000000000000000000000000000000000000000000000000000000000000100' - '0000000000000000000000000000000000000000000000000000000000000140' - '00000000000000000000000000000000000000000000000000000000000395f8' - '1100000000000000000000000000000000000000000000000000000000000000' - '00000000000000000000000000000000000000000000000000000004a817c800' - '0000000000000000000000000000000000000000000000000000000000000006' - '6e65737465640000000000000000000000000000000000000000000000000000' - '00000000000000000000000000000000000000000000000000000000000001b4' - '5b55524c5d205b276a736f6e2868747470733a2f2f6170692e72616e646f6d2e' - '6f72672f6a736f6e2d7270632f312f696e766f6b65292e726573756c742e7261' - '6e646f6d5b2273657269616c4e756d626572222c2264617461225d272c20275c' - '6e7b226a736f6e727063223a22322e30222c226d6574686f64223a2267656e65' - '726174655369676e6564496e746567657273222c22706172616d73223a7b2261' - '70694b6579223a247b5b646563727970745d20424b6733544373376c6b7a4e72' - '316b523670786a50434d32534f656a63466f6a55504d544f73426b432f343748' - '485066317350326f78564c546a4e42752b736c523953675a797144746a564f56' - '35597a67313269556b62756270304470636a434564654a54486e477743366744' - '3732394755566f47766f393668757877526f5a6c436a594f3830725771325747' - '596f522f4c433357616d704475767632426f3d7d2c226e223a312c226d696e22' - '3a312c226d6178223a3130302c227265706c6163656d656e74223a747275652c' - '2262617365223a3130247b5b6964656e746974795d20227d227d2c226964223a' - '31247b5b6964656e746974795d20227d227d275d000000000000000000000000', - }), - AttributeDict({ - 'address': '0xFE8a5f3a7Bb446e1cB4566717691cD3139289ED4', - 'topics': [ - HexBytes( - '1cb5bfc4e69cbacf65c8e05bdb84d7a3' - '27bd6bb4c034ff82359aefd7443775c4'), - HexBytes( - 'b0230ab70b78e47050766089ea333f2f' - 'f7ad41c6f31e8bed8c2acfcb8e911841'), - HexBytes( - '00000000000000000000000066d4bacf' - 'e61df23be813089a7a6d1a749a5c936a'), - HexBytes( - '00000000000000000000000000000000' - '0000000000000000016a98b78c556c34') - ], - 'data': - '0000000000000000000000000000000000000000000000000007533f2ecb6c34' - '000000000000000000000000000000000000000000000000016345785d8a0000' - '0000000000000000000000000000000000000000000000000000000000000062', - }) - ] - chain_id = ChainID.ROPSTEN - transaction_hash = ( - "0x330df22df6543c9816d80e582a4213b1fc11992f317be71775f49c3d853ed5be") - with \ - mock.patch('web3.eth.Eth.getTransactionReceipt') \ - as m_getTransactionReceipt, \ - mock.patch( - 'etherscan.contracts.Contract.get_abi', - side_effect=self.m_get_abi, autospec=True): - m_getTransactionReceipt.return_value.logs = mocked_logs - decoded_methods = TransactionDebugger.decode_transaction_logs( - chain_id, transaction_hash) - self.assertEqual(len(decoded_methods), 2) - decoded_method = decoded_methods[0] - self.assertEqual( - decoded_method['method_info']['definition'], - 'Log1(address,bytes32,uint256,string,string,uint256,bytes1,uint256)' - ) - decoded_method = decoded_methods[1] - self.assertEqual( - decoded_method['method_info']['definition'], - 'LogBet(bytes32,address,uint256,uint256,uint256,uint256)' - ) diff --git a/src/tests/pyetheroll/test_utils.py b/src/tests/pyetheroll/test_utils.py deleted file mode 100644 index 425784d..0000000 --- a/src/tests/pyetheroll/test_utils.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest - -from pyetheroll.utils import EtherollUtils - - -class TestEtherollUtils(unittest.TestCase): - - def test_compute_profit(self): - bet_size = 0.10 - chances_win = 34 - payout = EtherollUtils.compute_profit(bet_size, chances_win) - self.assertEqual(payout, 0.19) - bet_size = 0.10 - # chances of winning must be less than 100% - chances_win = 100 - payout = EtherollUtils.compute_profit(bet_size, chances_win) - self.assertEqual(payout, None) From abec32a30f56c366a6fd029012195445b9d06bea Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 1 Nov 2018 13:42:30 +0100 Subject: [PATCH 03/30] Migrates to pyetheroll==20181031 And fixes ui tests --- buildozer.spec | 2 +- requirements.txt | 2 +- src/tests/ui/test_etheroll_ui.py | 6 +-- src/tests/utils.py | 83 ++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 src/tests/utils.py diff --git a/buildozer.spec b/buildozer.spec index b89159a..ae208f9 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -101,7 +101,7 @@ requirements = requests-cache==0.4.13, qrcode, https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.zip, - https://github.com/AndreMiras/pyetheroll/archive/20181030.zip, + https://github.com/AndreMiras/pyetheroll/archive/20181031.zip # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes diff --git a/requirements.txt b/requirements.txt index 33bf816..1fc2df9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,4 @@ https://github.com/mfranciszkiewicz/pyelliptic/archive/1.5.10.tar.gz#egg=pyellip https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.zip#egg=layoutmargin plyer==1.3.1 oscpy==0.3.0 -https://github.com/AndreMiras/pyetheroll/archive/20181030.zip#egg=pyetheroll +https://github.com/AndreMiras/pyetheroll/archive/20181031.zip#egg=pyetheroll diff --git a/src/tests/ui/test_etheroll_ui.py b/src/tests/ui/test_etheroll_ui.py index b9f48dc..a16a897 100644 --- a/src/tests/ui/test_etheroll_ui.py +++ b/src/tests/ui/test_etheroll_ui.py @@ -13,7 +13,7 @@ from etheroll.controller import EtherollApp from etheroll.ui_utils import Dialog -from tests.pyetheroll import test_etheroll +from tests.utils import PyEtherollTestUtils class UITestCase(unittest.TestCase): @@ -347,8 +347,8 @@ def helper_test_roll_history(self, app): """ Roll history screen should display recent rolls, refs #61. """ - bet_results_logs = test_etheroll.TestEtheroll.bet_results_logs - bet_logs = test_etheroll.TestEtheroll.bet_logs + bet_results_logs = PyEtherollTestUtils.bet_results_logs + bet_logs = PyEtherollTestUtils.bet_logs merged_logs = [ {'bet_log': bet_logs[0], 'bet_result': bet_results_logs[0]}, {'bet_log': bet_logs[0], 'bet_result': bet_results_logs[0]}, diff --git a/src/tests/utils.py b/src/tests/utils.py new file mode 100644 index 0000000..77a641a --- /dev/null +++ b/src/tests/utils.py @@ -0,0 +1,83 @@ +from datetime import datetime + + +class PyEtherollTestUtils: + """ + Copy of some helper members from: + https://github.com/AndreMiras/pyetheroll/blob/20181031/ + tests/test_etheroll.py#L18 + """ + bet_logs = [ + { + 'bet_id': ( + '15e007148ec621d996c886de0f2b88a0' + '3af083aa819e851a51133dc17b6e0e5b'), + 'bet_value_ether': 0.45, + 'datetime': datetime(2018, 4, 7, 0, 17, 6), + 'profit_value_ether': 44.1, + 'reward_value_ether': 44.55, + 'roll_under': 2, + 'timestamp': '0x5ac80e02', + 'transaction_hash': ( + '0xf363906a9278c4dd300c50a3c9a2790' + '0bb85df60596c49f7833c232f2944d1cb') + }, + { + 'bet_id': ( + '14bae6b4711bdc5e3db19983307a9208' + '1e2e7c1d45161117bdf7b8b509d1abbe'), + 'bet_value_ether': 0.45, + 'datetime': datetime(2018, 4, 7, 0, 20, 14), + 'profit_value_ether': 6.97, + 'reward_value_ether': 7.42, + 'roll_under': 7, + 'timestamp': '0x5ac80ebe', + 'transaction_hash': ( + '0x0df8789552248edf1dd9d06a7a90726' + 'f1bc83a9c39f315b04efb6128f0d02146') + }, + { + # that one would not have been yet resolved (no `LogResult`) + 'bet_id': ( + 'c2997a1bad35841b2c30ca95eea9cb08' + 'c7b101bc14d5aa8b1b8a0facea793e05'), + 'bet_value_ether': 0.5, + 'datetime': datetime(2018, 4, 7, 0, 23, 46), + 'profit_value_ether': 3.31, + 'reward_value_ether': 3.81, + 'roll_under': 14, + 'timestamp': '0x5ac80f92', + 'transaction_hash': ( + '0x0440f1013a5eafd88f16be6b5612b6e' + '051a4eb1b0b91a160c680295e7fab5bfe') + } + ] + + bet_results_logs = [ + { + 'bet_id': ( + '15e007148ec621d996c886de0f2b88a0' + '3af083aa819e851a51133dc17b6e0e5b'), + 'bet_value_ether': 0.45, + 'datetime': datetime(2018, 4, 7, 0, 17, 55), + 'dice_result': 86, + 'roll_under': 2, + 'timestamp': '0x5ac80e33', + 'transaction_hash': ( + '0x3505de688dc20748eb5f6b3efd6e6d3' + '66ea7f0737b4ab17035c6b60ab4329f2a') + }, + { + 'bet_id': ( + '14bae6b4711bdc5e3db19983307a9208' + '1e2e7c1d45161117bdf7b8b509d1abbe'), + 'bet_value_ether': 0.45, + 'datetime': datetime(2018, 4, 7, 0, 20, 54), + 'dice_result': 51, + 'roll_under': 7, + 'timestamp': '0x5ac80ee6', + 'transaction_hash': ( + '0x42df3e3136957bcc64226206ed177d5' + '7ac9c31e116290c8778c97474226d3092') + }, + ] From 89f37b92359c7a7dba36a786ec6fdca5791eb64a Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 2 Nov 2018 21:51:15 +0100 Subject: [PATCH 04/30] Removes/updates legacy dependencies, fixes #122 Drops unmaintained pyethereum and pyethapp dependencies. Replaced by modulars deps eth-abi, eth-account, eth-hash, eth-key, eth-typing and eth-utils. Also updated some other requirements to more recent versions. High level account management is not yet available in eth-account, so some of pyethapp is imported and adapted to the needs. Removed duplicated Sentry utils code. Added `websockets` (dummy) and `distutils` (local copy) modules. --- blacklist.txt | 1 - buildozer.spec | 90 ++--- requirements.txt | 31 +- src/CHANGELOG.md | 3 +- src/distutils/README.md | 3 + src/distutils/__init__.py | 0 src/distutils/version.py | 348 ++++++++++++++++++ src/ethereum_utils.py | 68 ++-- src/etheroll/constants.py | 6 + src/etheroll/controller.py | 3 +- src/etheroll/utils.py | 55 --- src/pyethapp_accounts.py | 207 +++++++++++ .../recipes/eth-hash/__init__.py | 11 + .../disable-setuptools-markdown.patch | 19 + .../recipes/eth-typing/__init__.py | 11 + .../recipes/eth-typing/setup.patch | 18 + .../recipes/web3/__init__.py | 11 + .../recipes/web3/setup.patch | 21 ++ src/service/main.py | 4 +- src/tests/test_ethereum_utils.py | 28 +- src/tests/test_import.py | 32 -- src/tests/test_pyethapp_accounts.py | 178 +++++++++ src/testsuite.py | 5 +- src/websockets.py | 4 + 24 files changed, 939 insertions(+), 218 deletions(-) create mode 100644 src/distutils/README.md create mode 100644 src/distutils/__init__.py create mode 100644 src/distutils/version.py create mode 100644 src/pyethapp_accounts.py create mode 100644 src/python-for-android/recipes/eth-hash/__init__.py create mode 100644 src/python-for-android/recipes/eth-hash/disable-setuptools-markdown.patch create mode 100644 src/python-for-android/recipes/eth-typing/__init__.py create mode 100644 src/python-for-android/recipes/eth-typing/setup.patch create mode 100644 src/python-for-android/recipes/web3/__init__.py create mode 100644 src/python-for-android/recipes/web3/setup.patch create mode 100644 src/tests/test_pyethapp_accounts.py create mode 100644 src/websockets.py diff --git a/blacklist.txt b/blacklist.txt index f059284..f8a5302 100644 --- a/blacklist.txt +++ b/blacklist.txt @@ -1,5 +1,4 @@ lib-dynload/_csv.so -distutils/* xml/* pdb.{doc,pyo} profile.{doc,pyo} diff --git a/buildozer.spec b/buildozer.spec index ae208f9..da8e62f 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -38,70 +38,46 @@ version.filename = %(source.dir)s/version.py # (list) Application requirements # comma seperated e.g. requirements = sqlite3,kivy requirements = - hostpython3crystax==3.6, - python3crystax==3.6, - setuptools, - kivy==1.10.1, - plyer==1.3.1, - oscpy==0.3.0, android, - gevent, + attrdict==2.0.0, + certifi==2018.10.15, cffi, - https://github.com/AndreMiras/KivyMD/archive/9b2206a.tar.gz, - openssl, - pyelliptic==1.5.7, - asn1crypto==0.24.0, - coincurve==7.1.0, - bitcoin==1.1.42, - devp2p==0.9.3, - pycryptodome==3.4.6, - pbkdf2==1.3, - py-ecc==1.4.2, - pysha3==1.0.2, - pyyaml==3.12, - scrypt==0.8.6, - ethereum==2.1.1, - ptyprocess==0.5.2, - pexpect==4.4.0, - Pygments==2.2.0, - decorator==4.2.1, - ipython-genutils==0.2.0, - traitlets==4.3.2, - ipython==5.5.0, - click==6.7, - pickleshare==0.7.4, - simplegeneric==0.8.1, - wcwidth==0.1.7, - prompt-toolkit==1.0.15, - https://github.com/ethereum/pyethapp/archive/8406f32.zip, - idna==2.6, - typing==3.6.4, - eth-keys==0.2.0b3, + chardet==3.0.4, + cytoolz==0.9.0, + eth-abi==1.2.2, + eth-account==0.3.0, + eth-hash==0.2.0, eth-keyfile==0.5.1, - rlp==0.6.0, + eth-keys==0.2.0b3, eth-rlp==0.1.2, - attrdict==2.0.0, - eth-account==0.2.2, + eth-typing==2.0.0, + eth-utils==1.2.1, + gevent, hexbytes==0.1.0, + hostpython3crystax==3.6, + https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.tar.gz, + https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz, + https://github.com/AndreMiras/pyetheroll/archive/884805b.tar.gz, + https://github.com/corpetty/py-etherscan-api/archive/cb91fb3.tar.gz, + idna==2.7, + kivy==1.10.1, lru-dict==1.1.5, - web3==4.0.0b11, - certifi==2018.1.18, - chardet==3.0.4, - urllib3==1.22, - requests==2.18.4, - https://github.com/corpetty/py-etherscan-api/archive/cb91fb3.zip, - eth-testrpc==1.3.3, - eth-hash==0.1.1, - pyethash==0.1.27, - cytoolz==0.9.0, - toolz==0.9.0, - eth-abi==1.0.0, - eth-utils==1.0.1, - raven==6.6.0, + openssl, + oscpy==0.3.0, + parsimonious==0.8.1, + plyer==1.3.1, + pycryptodome==3.4.6, + Pygments==2.2.0, + python3crystax==3.6, + qrcode==6.0, + raven==6.9.0, + requests==2.20.0, requests-cache==0.4.13, - qrcode, - https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.zip, - https://github.com/AndreMiras/pyetheroll/archive/20181031.zip + rlp==1.0.3, + setuptools, + toolz==0.9.0, + urllib3==1.24.1, + web3==4.8.1 # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes diff --git a/requirements.txt b/requirements.txt index 1fc2df9..7162c49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,16 @@ -web3==4.0.0b11 -eth-testrpc==1.3.3 -https://github.com/corpetty/py-etherscan-api/archive/cb91fb3.zip#egg=py-etherscan-api +eth-utils==1.2.1 flake8 +future +https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.tar.gz#egg=layoutmargin +https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz#egg=kivymd +https://github.com/AndreMiras/pyetheroll/archive/884805b.tar.gz#egg=pyetheroll +https://github.com/corpetty/py-etherscan-api/archive/cb91fb3.tar.gz#egg=py-etherscan-api isort -Kivy==1.10.1 -https://github.com/AndreMiras/KivyMD/archive/9b2206a.tar.gz#egg=kivymd -ethereum==2.1.1 -# 2017/10/30 develop -https://github.com/ethereum/pyethapp/archive/8406f32.zip#egg=pyethapp -raven==6.6.0 -requests-cache +kivy==1.10.1 kivyunittest==0.1.8 -qrcode==5.3 -eth-rlp==0.1.2 -rlp==0.6.0 -# compatible with libcrypto.so.1.1 -# https://github.com/golemfactory/golem-messages/pull/112/files -https://github.com/mfranciszkiewicz/pyelliptic/archive/1.5.10.tar.gz#egg=pyelliptic -https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.zip#egg=layoutmargin -plyer==1.3.1 oscpy==0.3.0 -https://github.com/AndreMiras/pyetheroll/archive/20181031.zip#egg=pyetheroll +plyer==1.3.1 +qrcode==6.0 +raven==6.9.0 +requests-cache +web3==4.8.1 diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 041b541..25e4238 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,9 +1,10 @@ # Change Log -## [v20181028] +## [Unreleased] - Split dedicated Etheroll library, refs #97 + - Remove legacy dependencies, refs #112 ## [v20181028] diff --git a/src/distutils/README.md b/src/distutils/README.md new file mode 100644 index 0000000..523061c --- /dev/null +++ b/src/distutils/README.md @@ -0,0 +1,3 @@ +# README + +This is being embedded in the repository because python3crystax seems to blacklist it. diff --git a/src/distutils/__init__.py b/src/distutils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/distutils/version.py b/src/distutils/version.py new file mode 100644 index 0000000..4984fad --- /dev/null +++ b/src/distutils/version.py @@ -0,0 +1,348 @@ +# flake8: noqa +# CrystaX seems to be blacklisting `distutils` module somehow. +# Since LooseVersion is needed by web3 module, we embed our own. + +# +# distutils/version.py +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# $Id$ +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +import re + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__ (self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__ (self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# _cmp (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion (Version): + + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + + The following are examples of invalid version numbers: + + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', + re.VERBOSE | re.ASCII) + + + def parse (self, vstring): + match = self.version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + (major, minor, patch, prerelease, prerelease_num) = \ + match.group(1, 2, 4, 5, 6) + + if patch: + self.version = tuple(map(int, [major, minor, patch])) + else: + self.version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + self.prerelease = (prerelease[0], int(prerelease_num)) + else: + self.prerelease = None + + + def __str__ (self): + + if self.version[2] == 0: + vstring = '.'.join(map(str, self.version[0:2])) + else: + vstring = '.'.join(map(str, self.version)) + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) + + return vstring + + + def _cmp (self, other): + if isinstance(other, str): + other = StrictVersion(other) + + if self.version != other.version: + # numeric versions don't match + # prerelease stuff doesn't matter + if self.version < other.version: + return -1 + else: + return 1 + + # have to compare prerelease + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if (not self.prerelease and not other.prerelease): + return 0 + elif (self.prerelease and not other.prerelease): + return -1 + elif (not self.prerelease and other.prerelease): + return 1 + elif (self.prerelease and other.prerelease): + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 + else: + assert False, "never get here" + +# end class StrictVersion + + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + +class LooseVersion (Version): + + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + + def __init__ (self, vstring=None): + if vstring: + self.parse(vstring) + + + def parse (self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) + if x and x != '.'] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + + def __str__ (self): + return self.vstring + + + def __repr__ (self): + return "LooseVersion ('%s')" % str(self) + + + def _cmp (self, other): + if isinstance(other, str): + other = LooseVersion(other) + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + + +# end class LooseVersion diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py index 9ea515a..f0be32a 100644 --- a/src/ethereum_utils.py +++ b/src/ethereum_utils.py @@ -1,55 +1,47 @@ import os +from pyethapp_accounts import Account -class AccountUtils(): + +class AccountUtils: def __init__(self, keystore_dir): - # must be imported after `patch_find_library_android()` - from devp2p.app import BaseApp - from pyethapp.accounts import AccountsService self.keystore_dir = keystore_dir - self.app = BaseApp( - config=dict(accounts=dict(keystore_dir=self.keystore_dir))) - AccountsService.register_with_app(self.app) - self.patch_ethereum_tools_keys() + self._accounts = None def get_account_list(self): """ Returns the Account list. """ - accounts_service = self.app.services.accounts - return accounts_service.accounts + if self._accounts is not None: + return self._accounts + 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) + for keyfile in keyfiles: + account = Account.load(path=keyfile) + self._accounts.append(account) + return self._accounts - def new_account(self, password): + def new_account(self, password, iterations=None): """ Creates an account on the disk and returns it. """ - # lazy loading - from pyethapp.accounts import Account - account = Account.new(password, uuid=None) - account.path = os.path.join( - self.app.services.accounts.keystore_dir, - account.address.hex()) - self.app.services.accounts.add_account(account) + account = Account.new(password, uuid=None, iterations=iterations) + account.path = os.path.join(self.keystore_dir, account.address.hex()) + self.add_account(account) return account - @staticmethod - def patch_ethereum_tools_keys(): - """ - Patches `make_keystore_json()` to use `create_keyfile_json()`, see: - https://github.com/ethereum/pyethapp/issues/292 - """ - # lazy loading - import eth_keyfile - from ethereum.tools import keys - from ethereum.utils import is_string, to_string - keys.make_keystore_json = eth_keyfile.create_keyfile_json - - def decode_keyfile_json(raw_keyfile_json, password): - if not is_string(password): - password = to_string(password) - return eth_keyfile.decode_keyfile_json(raw_keyfile_json, password) - keys.decode_keystore_json = decode_keyfile_json + 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): @@ -80,7 +72,7 @@ def delete_account(self, account): """ # lazy loading import shutil - keystore_dir = self.app.services.accounts.keystore_dir + 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): @@ -90,6 +82,4 @@ def delete_account(self, account): 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 - accounts_service = self.app.services.accounts - accounts_service.accounts.remove(account) + self._accounts.remove(account) diff --git a/src/etheroll/constants.py b/src/etheroll/constants.py index b777bb5..b09711f 100644 --- a/src/etheroll/constants.py +++ b/src/etheroll/constants.py @@ -1,2 +1,8 @@ +import os + +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + # default pyethapp keystore path KEYSTORE_DIR_SUFFIX = ".config/pyethapp/keystore/" + +API_KEY_PATH = os.path.join(BASE_DIR, 'api_key.json') diff --git a/src/etheroll/controller.py b/src/etheroll/controller.py index 0597c86..365fcb7 100755 --- a/src/etheroll/controller.py +++ b/src/etheroll/controller.py @@ -10,6 +10,7 @@ from raven import Client from requests.exceptions import ConnectionError +from etheroll.constants import API_KEY_PATH from etheroll.patches import patch_find_library_android from etheroll.settings import SettingsScreen from etheroll.switchaccount import SwitchAccountScreen @@ -78,7 +79,7 @@ def pyetheroll(self): from pyetheroll.etheroll import Etheroll chain_id = SettingsScreen.get_stored_network() if self._pyetheroll is None or self._pyetheroll.chain_id != chain_id: - self._pyetheroll = Etheroll(chain_id) + self._pyetheroll = Etheroll(API_KEY_PATH, chain_id) return self._pyetheroll @property diff --git a/src/etheroll/utils.py b/src/etheroll/utils.py index 761b62d..cae8e8a 100644 --- a/src/etheroll/utils.py +++ b/src/etheroll/utils.py @@ -1,14 +1,6 @@ import threading from io import StringIO -from kivy.logger import LOG_LEVELS -from kivy.utils import platform -from raven import Client -from raven.conf import setup_logging -from raven.handlers.logging import SentryHandler - -from version import __version__ - def run_in_thread(fn): """ @@ -35,53 +27,6 @@ def run(*k, **kw): return run -def configure_sentry(in_debug=False): - """ - Configure the Raven client, or create a dummy one if `in_debug` is `True`. - """ - key = 'b290ecc8934f4cb599e6fa6af6cc5cc2' - # the public DSN URL is not available on the Python client - # so we're exposing the secret and will be revoking it on abuse - # https://github.com/getsentry/raven-python/issues/569 - secret = '0ae02bcb5a75467d9b4431042bb98cb9' - project_id = '1111738' - dsn = 'https://{key}:{secret}@sentry.io/{project_id}'.format( - key=key, secret=secret, project_id=project_id) - if in_debug: - client = DebugRavenClient() - else: - client = Client(dsn=dsn, release=__version__) - # adds context for Android devices - if platform == 'android': - from jnius import autoclass - Build = autoclass("android.os.Build") - VERSION = autoclass('android.os.Build$VERSION') - android_os_build = { - 'model': Build.MODEL, - 'brand': Build.BRAND, - 'device': Build.DEVICE, - 'manufacturer': Build.MANUFACTURER, - 'version_release': VERSION.RELEASE, - } - client.user_context({'android_os_build': android_os_build}) - # Logger.error() to Sentry - # https://docs.sentry.io/clients/python/integrations/logging/ - handler = SentryHandler(client) - handler.setLevel(LOG_LEVELS.get('error')) - setup_logging(handler) - return client - - -class DebugRavenClient(object): - """ - The DebugRavenClient should be used in debug mode, it just raises - the exception rather than capturing it. - """ - - def captureException(self): - raise - - class StringIOCBWrite(StringIO): """ Inherits StringIO, provides callback on write. 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/python-for-android/recipes/eth-hash/__init__.py b/src/python-for-android/recipes/eth-hash/__init__.py new file mode 100644 index 0000000..86aaa82 --- /dev/null +++ b/src/python-for-android/recipes/eth-hash/__init__.py @@ -0,0 +1,11 @@ +from pythonforandroid.recipe import PythonRecipe + + +class EthHashRecipe(PythonRecipe): + version = '0.2.0' + url = 'https://github.com/ethereum/eth-hash/archive/v{version}.tar.gz' + depends = [('python2', 'python3crystax'), 'setuptools'] + patches = ['disable-setuptools-markdown.patch'] + + +recipe = EthHashRecipe() diff --git a/src/python-for-android/recipes/eth-hash/disable-setuptools-markdown.patch b/src/python-for-android/recipes/eth-hash/disable-setuptools-markdown.patch new file mode 100644 index 0000000..e9ea788 --- /dev/null +++ b/src/python-for-android/recipes/eth-hash/disable-setuptools-markdown.patch @@ -0,0 +1,19 @@ +diff --git a/setup.py b/setup.py +index 8680508..8e9fd09 100644 +--- a/setup.py ++++ b/setup.py +@@ -46,14 +46,12 @@ setup( + # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme + version='0.2.0', + description="""eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3""", +- long_description_markdown_filename='README.md', + author='Jason Carver', + author_email='ethcalibur+pip@gmail.com', + url='https://github.com/ethereum/eth-hash', + include_package_data=True, + install_requires=[ + ], +- setup_requires=['setuptools-markdown'], + python_requires='>=3.5, <4', + extras_require=extras_require, + py_modules=['eth_hash'], diff --git a/src/python-for-android/recipes/eth-typing/__init__.py b/src/python-for-android/recipes/eth-typing/__init__.py new file mode 100644 index 0000000..84d98d0 --- /dev/null +++ b/src/python-for-android/recipes/eth-typing/__init__.py @@ -0,0 +1,11 @@ +from pythonforandroid.recipe import PythonRecipe + + +class EthTypingRecipe(PythonRecipe): + version = '2.0.0' + url = 'https://github.com/ethereum/eth-typing/archive/v{version}.tar.gz' + depends = [('python2', 'python3crystax'), 'setuptools'] + patches = ['setup.patch'] + + +recipe = EthTypingRecipe() diff --git a/src/python-for-android/recipes/eth-typing/setup.patch b/src/python-for-android/recipes/eth-typing/setup.patch new file mode 100644 index 0000000..0e09754 --- /dev/null +++ b/src/python-for-android/recipes/eth-typing/setup.patch @@ -0,0 +1,18 @@ +diff --git a/setup.py b/setup.py +index 52bf1c1..02be4d0 100644 +--- a/setup.py ++++ b/setup.py +@@ -40,13 +40,10 @@ setup( + # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme + version='2.0.0', + description="""eth-typing: Common type annotations for ethereum python packages""", +- long_description_markdown_filename='README.md', + author='The eth-typing contributors', + author_email='eth-typing@ethereum.org', + url='https://github.com/ethereum/eth-typing', + include_package_data=True, +- setup_requires=['setuptools-markdown'], +- python_requires='>=3.5, <4', + extras_require=extras_require, + py_modules=['eth_typing'], + license="MIT", diff --git a/src/python-for-android/recipes/web3/__init__.py b/src/python-for-android/recipes/web3/__init__.py new file mode 100644 index 0000000..bb1bc39 --- /dev/null +++ b/src/python-for-android/recipes/web3/__init__.py @@ -0,0 +1,11 @@ +from pythonforandroid.recipe import PythonRecipe + + +class Web3Recipe(PythonRecipe): + version = '4.8.1' + url = 'https://github.com/ethereum/web3.py/archive/v{version}.tar.gz' + depends = [('python2', 'python3crystax'), 'setuptools'] + patches = ['setup.patch'] + + +recipe = Web3Recipe() diff --git a/src/python-for-android/recipes/web3/setup.patch b/src/python-for-android/recipes/web3/setup.patch new file mode 100644 index 0000000..a9c0130 --- /dev/null +++ b/src/python-for-android/recipes/web3/setup.patch @@ -0,0 +1,21 @@ +diff --git a/setup.py b/setup.py +index fc78289..16e422b 100644 +--- a/setup.py ++++ b/setup.py +@@ -62,7 +62,6 @@ setup( + # *IMPORTANT*: Don't manually change the version here. Use the 'bumpversion' utility. + version='4.7.1', + description="""Web3.py""", +- long_description_markdown_filename='README.md', + author='Piper Merriam', + author_email='pipermerriam@gmail.com', + url='https://github.com/ethereum/web3.py', +@@ -80,8 +79,6 @@ setup( + "websockets>=6.0.0,<7.0.0", + "pypiwin32>=223;platform_system=='Windows'", + ], +- setup_requires=['setuptools-markdown'], +- python_requires='>=3.5.3,<4', + extras_require=extras_require, + py_modules=['web3', 'ens'], + license="MIT", diff --git a/src/service/main.py b/src/service/main.py index 0373735..0de9b59 100755 --- a/src/service/main.py +++ b/src/service/main.py @@ -25,7 +25,7 @@ from raven import Client from ethereum_utils import AccountUtils -from etheroll.constants import KEYSTORE_DIR_SUFFIX +from etheroll.constants import API_KEY_PATH, KEYSTORE_DIR_SUFFIX from etheroll.patches import patch_find_library_android from etheroll.store import Store from osc.osc_app_client import OscAppClient @@ -86,7 +86,7 @@ def pyetheroll(self): """ chain_id = self.get_stored_network() if self._pyetheroll is None or self._pyetheroll.chain_id != chain_id: - self._pyetheroll = Etheroll(chain_id) + self._pyetheroll = Etheroll(API_KEY_PATH, chain_id) return self._pyetheroll @staticmethod diff --git a/src/tests/test_ethereum_utils.py b/src/tests/test_ethereum_utils.py index 78d32eb..8bf39ec 100644 --- a/src/tests/test_ethereum_utils.py +++ b/src/tests/test_ethereum_utils.py @@ -25,13 +25,11 @@ def test_new_account(self): 3) tries to unlock the account """ # 1) verifies the current account list is empty - account_list = self.account_utils.get_account_list() - self.assertEqual(len(account_list), 0) + 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) - account_list = self.account_utils.get_account_list() - self.assertEqual(len(account_list), 1) + 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 @@ -42,6 +40,22 @@ def test_new_account(self): 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_deleted_account_dir(self): """ The deleted_account_dir() helper method should be working @@ -67,7 +81,7 @@ def test_delete_account(self): Then verify we can load the account from the backup/trash location. """ password = PASSWORD - account = self.account_utils.new_account(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 @@ -93,7 +107,7 @@ def test_delete_account_already_exists(self): https://github.com/AndreMiras/PyWallet/issues/88 """ password = PASSWORD - account = self.account_utils.new_account(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( diff --git a/src/tests/test_import.py b/src/tests/test_import.py index cf1f560..23ac22c 100644 --- a/src/tests/test_import.py +++ b/src/tests/test_import.py @@ -6,41 +6,9 @@ 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 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_pyethereum(self): - from ethereum import compress, utils - self.assertIsNotNone(compress) - self.assertIsNotNone(utils) - - def test_pyethapp(self): - from pyethapp.accounts import Account - from ethereum_utils import AccountUtils - AccountUtils.patch_ethereum_tools_keys() - password = "foobar" - uuid = None - account = Account.new(password, uuid=uuid) - # restore iterations - address = account.address.hex() - self.assertIsNotNone(account) - self.assertIsNotNone(address) def test_pyetheroll(self): from pyetheroll.etheroll import Etheroll diff --git a/src/tests/test_pyethapp_accounts.py b/src/tests/test_pyethapp_accounts.py new file mode 100644 index 0000000..d1f409c --- /dev/null +++ b/src/tests/test_pyethapp_accounts.py @@ -0,0 +1,178 @@ +""" +Adapted version of: +https://github.com/ethereum/pyethapp/blob/7fdec62/ +pyethapp/tests/test_accounts.py +""" +import json +import unittest +from builtins import str +from uuid import uuid4 + +from eth_keys import keys +from eth_utils import remove_0x_prefix +from past.utils import old_div + +from pyethapp_accounts import Account + + +class TestAccountUtils(unittest.TestCase): + + privkey = None + password = None + uuid = None + account = None + keystore = None + + @classmethod + def setUpClass(cls): + cls.privkey = bytes.fromhex( + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + cls.password = 'secret' + cls.uuid = str(uuid4()) + # keystore generation takes a while, so make this module scoped + cls.account = Account.new( + cls.password, cls.privkey, cls.uuid, iterations=1) + # `account.keystore` might not contain address and id + cls.keystore = json.loads(cls.account.dump()) + + def test_account_creation(self): + account = self.account + privkey = self.privkey + uuid = self.uuid + assert not account.locked + assert account.privkey == privkey + pk = keys.PrivateKey(privkey) + assert account.address.hex() == remove_0x_prefix( + pk.public_key.to_address()) + assert account.uuid == uuid + + def test_locked(self): + keystore = self.keystore + uuid = self.uuid + account = Account(keystore) + assert account.locked + assert account.address.hex() == remove_0x_prefix(keystore['address']) + assert account.privkey is None + assert account.pubkey is None + assert account.uuid == uuid + keystore2 = keystore.copy() + keystore2.pop('address') + account = Account(keystore2) + assert account.locked + assert account.address is None + assert account.privkey is None + assert account.pubkey is None + assert account.uuid == uuid + + def test_unlock(self): + keystore = self.keystore + password = self.password + privkey = self.privkey + account = Account(keystore) + assert account.locked + account.unlock(password) + assert not account.locked + assert account.privkey == privkey + pk = keys.PrivateKey(privkey) + assert account.address.hex() == remove_0x_prefix( + pk.public_key.to_address()) + + def test_unlock_wrong(self): + keystore = self.keystore + password = self.password + account = Account(keystore) + assert account.locked + with self.assertRaises(ValueError): + account.unlock(password + '1234') + assert account.locked + with self.assertRaises(ValueError): + account.unlock('4321' + password) + assert account.locked + with self.assertRaises(ValueError): + account.unlock(password[:old_div(len(password), 2)]) + assert account.locked + account.unlock(password) + assert not account.locked + account.unlock(password + 'asdf') + assert not account.locked + account.unlock(password + '1234') + assert not account.locked + + def test_lock(self): + account = self.account + password = self.password + privkey = self.privkey + assert not account.locked + pk = keys.PrivateKey(privkey) + assert account.address.hex() == remove_0x_prefix( + pk.public_key.to_address()) + assert account.privkey == privkey + assert account.pubkey is not None + account.unlock(password + 'fdsa') + account.lock() + assert account.locked + pk = keys.PrivateKey(privkey) + assert account.address.hex() == remove_0x_prefix( + pk.public_key.to_address()) + assert account.privkey is None + assert account.pubkey is None + with self.assertRaises(ValueError): + account.unlock(password + 'fdsa') + account.unlock(password) + + def test_address(self): + keystore = self.keystore + password = self.password + privkey = self.privkey + keystore_wo_address = keystore.copy() + keystore_wo_address.pop('address') + account = Account(keystore_wo_address) + assert account.address is None + account.unlock(password) + account.lock() + pk = keys.PrivateKey(privkey) + assert account.address.hex() == remove_0x_prefix( + pk.public_key.to_address()) + + def test_dump(self): + account = self.account + keystore = json.loads( + account.dump(include_address=True, include_id=True)) + required_keys = set(['crypto', 'version']) + assert set(keystore.keys()) == required_keys | set(['address', 'id']) + assert remove_0x_prefix(keystore['address']) == account.address.hex() + assert keystore['id'] == account.uuid + keystore = json.loads( + account.dump(include_address=False, include_id=True)) + assert set(keystore.keys()) == required_keys | set(['id']) + assert keystore['id'] == account.uuid + keystore = json.loads( + account.dump(include_address=True, include_id=False)) + assert set(keystore.keys()) == required_keys | set(['address']) + assert remove_0x_prefix(keystore['address']) == account.address.hex() + keystore = json.loads( + account.dump(include_address=False, include_id=False)) + assert set(keystore.keys()) == required_keys + + def test_uuid_setting(self): + account = self.account + uuid = account.uuid + account.uuid = 'asdf' + assert account.uuid == 'asdf' + account.uuid = None + assert account.uuid is None + assert 'id' not in account.keystore + account.uuid = uuid + assert account.uuid == uuid + assert account.keystore['id'] == uuid + + # TODO: not yet migrated + # def test_sign(account, password): + # from ethereum.transactions import Transaction + # tx = Transaction(1, 0, 10**6, account.address, 0, '') + # account.sign_tx(tx) + # assert tx.sender == account.address + # account.lock() + # with pytest.raises(ValueError): + # account.sign_tx(tx) + # account.unlock(password) diff --git a/src/testsuite.py b/src/testsuite.py index 5d763dd..ca753b2 100644 --- a/src/testsuite.py +++ b/src/testsuite.py @@ -1,14 +1,11 @@ import unittest from tests import test_ethereum_utils, test_import -from tests.pyetheroll import (test_etheroll, test_etherscan_utils, - test_transaction_debugger, test_utils) def suite(): modules = [ - test_ethereum_utils, test_import, test_etheroll, test_etherscan_utils, - test_transaction_debugger, test_utils + test_ethereum_utils, test_import, ] test_suite = unittest.TestSuite() for module in modules: diff --git a/src/websockets.py b/src/websockets.py new file mode 100644 index 0000000..de97bf0 --- /dev/null +++ b/src/websockets.py @@ -0,0 +1,4 @@ +""" +Dummy module so we don't need to install websockets package. +This is being used by the web3 module. +""" From 0ac868cfa26e9c942852d09d2c4a5103f1de6b34 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 30 Nov 2018 20:24:02 +0100 Subject: [PATCH 05/30] Uses updated upstream recipes, follow up refs #122 Also drops no longer used recipes. --- buildozer.spec | 1 + docs/Troubleshoot.md | 6 + src/CHANGELOG.md | 5 +- .../recipes/cffi/__init__.py | 43 --- .../recipes/cffi/disable-pkg-config.patch | 29 -- .../recipes/coincurve/__init__.py | 32 --- .../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/decorator/__init__.py | 11 - .../recipes/eth-account/__init__.py | 11 - .../disable-setuptools-markdown.patch | 20 -- .../recipes/eth-hash/__init__.py | 11 - .../disable-setuptools-markdown.patch | 19 -- .../recipes/eth-rlp/__init__.py | 11 - .../eth-rlp/disable-setuptools-markdown.patch | 20 -- .../recipes/eth-typing/__init__.py | 11 - .../recipes/eth-typing/setup.patch | 18 -- .../recipes/greenlet/__init__.py | 22 -- .../recipes/hexbytes/__init__.py | 11 - .../disable-setuptools-markdown.patch | 20 -- .../recipes/hostpython3crystax/__init__.py | 42 --- .../recipes/idna/__init__.py | 14 - .../recipes/pycryptodome/__init__.py | 17 -- .../recipes/pycryptodome/setup.py.patch | 27 -- .../recipes/pyethash/__init__.py | 25 -- .../recipes/pysha3/__init__.py | 24 -- .../recipes/python3crystax/__init__.py | 257 ------------------ .../python3crystax/patch_python3.6.patch | 89 ------ .../recipes/pyyaml/__init__.py | 11 - .../recipes/requests/__init__.py | 10 - .../recipes/scrypt/__init__.py | 35 --- .../recipes/scrypt/remove_librt.patch | 20 -- .../recipes/secp256k1/__init__.py | 39 --- .../recipes/secp256k1/cross_compile.patch | 12 - .../secp256k1/drop_setup_requires.patch | 12 - .../recipes/secp256k1/find_lib.patch | 13 - .../recipes/secp256k1/no-download.patch | 13 - .../recipes/secp256k1/pkg-config.patch | 28 -- .../recipes/web3/__init__.py | 11 - .../recipes/web3/setup.patch | 21 -- 42 files changed, 10 insertions(+), 1061 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/decorator/__init__.py delete mode 100644 src/python-for-android/recipes/eth-account/__init__.py delete mode 100644 src/python-for-android/recipes/eth-account/disable-setuptools-markdown.patch delete mode 100644 src/python-for-android/recipes/eth-hash/__init__.py delete mode 100644 src/python-for-android/recipes/eth-hash/disable-setuptools-markdown.patch delete mode 100644 src/python-for-android/recipes/eth-rlp/__init__.py delete mode 100644 src/python-for-android/recipes/eth-rlp/disable-setuptools-markdown.patch delete mode 100644 src/python-for-android/recipes/eth-typing/__init__.py delete mode 100644 src/python-for-android/recipes/eth-typing/setup.patch delete mode 100644 src/python-for-android/recipes/greenlet/__init__.py delete mode 100644 src/python-for-android/recipes/hexbytes/__init__.py delete mode 100644 src/python-for-android/recipes/hexbytes/disable-setuptools-markdown.patch delete mode 100644 src/python-for-android/recipes/hostpython3crystax/__init__.py delete mode 100644 src/python-for-android/recipes/idna/__init__.py delete mode 100644 src/python-for-android/recipes/pycryptodome/__init__.py delete mode 100644 src/python-for-android/recipes/pycryptodome/setup.py.patch delete mode 100644 src/python-for-android/recipes/pyethash/__init__.py delete mode 100644 src/python-for-android/recipes/pysha3/__init__.py delete mode 100644 src/python-for-android/recipes/python3crystax/__init__.py delete mode 100644 src/python-for-android/recipes/python3crystax/patch_python3.6.patch delete mode 100644 src/python-for-android/recipes/pyyaml/__init__.py delete mode 100644 src/python-for-android/recipes/requests/__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 delete mode 100644 src/python-for-android/recipes/secp256k1/__init__.py delete mode 100644 src/python-for-android/recipes/secp256k1/cross_compile.patch delete mode 100644 src/python-for-android/recipes/secp256k1/drop_setup_requires.patch delete mode 100644 src/python-for-android/recipes/secp256k1/find_lib.patch delete mode 100644 src/python-for-android/recipes/secp256k1/no-download.patch delete mode 100644 src/python-for-android/recipes/secp256k1/pkg-config.patch delete mode 100644 src/python-for-android/recipes/web3/__init__.py delete mode 100644 src/python-for-android/recipes/web3/setup.patch diff --git a/buildozer.spec b/buildozer.spec index da8e62f..07a09fc 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -138,6 +138,7 @@ android.permissions = INTERNET # (int) Minimum API required #android.minapi = 9 +android.minapi = 19 # (int) Android SDK version to use #android.sdk = 20 diff --git a/docs/Troubleshoot.md b/docs/Troubleshoot.md index edd501d..aefadd9 100644 --- a/docs/Troubleshoot.md +++ b/docs/Troubleshoot.md @@ -103,6 +103,12 @@ if not is_gradle_build: ``` +Uninstaling the app using adb: +``` +buildozer android adb -- uninstall com.github.andremiras.etheroll +``` + + ## Kivy diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 25e4238..27f7e3d 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -5,6 +5,7 @@ - Split dedicated Etheroll library, refs #97 - Remove legacy dependencies, refs #112 + - Migrate to upstream recipes ## [v20181028] @@ -12,8 +13,8 @@ - Click notification to open the app, refs #114 - Bets 2nd decimal place precision, refs #116 - Platform agnostic notification service, refs #120 - - Updates balance on roll, refs #115 - - Removes typing patch, refs #72 + - Update balance on roll, refs #115 + - Remove typing patch, refs #72 ## [v20180918] 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 21ba4b6..0000000 --- a/src/python-for-android/recipes/cffi/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -from pythonforandroid.recipe import CompiledComponentsPythonRecipe - - -class CffiRecipe(CompiledComponentsPythonRecipe): - name = 'cffi' - version = '1.4.2' - 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): - env = super(CffiRecipe, self).get_recipe_env(arch) - 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'], - ]) - 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) - 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 6803bda..0000000 --- a/src/python-for-android/recipes/coincurve/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -from pythonforandroid.recipe import PythonRecipe - - -class CoincurveRecipe(PythonRecipe): - 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"] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(CoincurveRecipe, self).get_recipe_env(arch, with_flags_in_cc) - libsecp256k1 = self.get_recipe('libsecp256k1', self.ctx) - libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch) - env['CFLAGS'] += ' -I' + os.path.join(libsecp256k1_dir, 'include') - # 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) - 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/decorator/__init__.py b/src/python-for-android/recipes/decorator/__init__.py deleted file mode 100644 index 2dbda4e..0000000 --- a/src/python-for-android/recipes/decorator/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - -class DecoratorPyRecipe(PythonRecipe): - version = '4.2.1' - url = 'https://pypi.python.org/packages/source/d/decorator/decorator-{version}.tar.gz' - url = 'https://github.com/micheles/decorator/archive/{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - site_packages_name = 'decorator' - call_hostpython_via_targetpython = False - -recipe = DecoratorPyRecipe() diff --git a/src/python-for-android/recipes/eth-account/__init__.py b/src/python-for-android/recipes/eth-account/__init__.py deleted file mode 100644 index b97c276..0000000 --- a/src/python-for-android/recipes/eth-account/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - - -class EthRlpRecipe(PythonRecipe): - version = '0.2.2' - url = 'https://github.com/ethereum/eth-account/archive/v{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - patches = ['disable-setuptools-markdown.patch'] - - -recipe = EthRlpRecipe() diff --git a/src/python-for-android/recipes/eth-account/disable-setuptools-markdown.patch b/src/python-for-android/recipes/eth-account/disable-setuptools-markdown.patch deleted file mode 100644 index 93f082b..0000000 --- a/src/python-for-android/recipes/eth-account/disable-setuptools-markdown.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/setup.py b/setup.py -index 63a6624..c0d742f 100644 ---- a/setup.py -+++ b/setup.py -@@ -39,7 +39,6 @@ setup( - # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme - version='0.2.2', - description="""eth-account: Sign Ethereum transactions and messages with local private keys""", -- long_description_markdown_filename='README.md', - author='Jason Carver', - author_email='ethcalibur+pip@gmail.com', - url='https://github.com/ethereum/eth-account', -@@ -52,7 +51,6 @@ setup( - "hexbytes>=0.1.0,<1", - "eth-rlp>=0.1.2,<1", - ], -- setup_requires=['setuptools-markdown'], - python_requires='>=3.5, <4', - extras_require=extras_require, - py_modules=['eth_account'], diff --git a/src/python-for-android/recipes/eth-hash/__init__.py b/src/python-for-android/recipes/eth-hash/__init__.py deleted file mode 100644 index 86aaa82..0000000 --- a/src/python-for-android/recipes/eth-hash/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - - -class EthHashRecipe(PythonRecipe): - version = '0.2.0' - url = 'https://github.com/ethereum/eth-hash/archive/v{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - patches = ['disable-setuptools-markdown.patch'] - - -recipe = EthHashRecipe() diff --git a/src/python-for-android/recipes/eth-hash/disable-setuptools-markdown.patch b/src/python-for-android/recipes/eth-hash/disable-setuptools-markdown.patch deleted file mode 100644 index e9ea788..0000000 --- a/src/python-for-android/recipes/eth-hash/disable-setuptools-markdown.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/setup.py b/setup.py -index 8680508..8e9fd09 100644 ---- a/setup.py -+++ b/setup.py -@@ -46,14 +46,12 @@ setup( - # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme - version='0.2.0', - description="""eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3""", -- long_description_markdown_filename='README.md', - author='Jason Carver', - author_email='ethcalibur+pip@gmail.com', - url='https://github.com/ethereum/eth-hash', - include_package_data=True, - install_requires=[ - ], -- setup_requires=['setuptools-markdown'], - python_requires='>=3.5, <4', - extras_require=extras_require, - py_modules=['eth_hash'], diff --git a/src/python-for-android/recipes/eth-rlp/__init__.py b/src/python-for-android/recipes/eth-rlp/__init__.py deleted file mode 100644 index 2eef8b8..0000000 --- a/src/python-for-android/recipes/eth-rlp/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - - -class EthRlpRecipe(PythonRecipe): - version = '0.1.2' - url = 'https://github.com/ethereum/eth-rlp/archive/v{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - patches = ['disable-setuptools-markdown.patch'] - - -recipe = EthRlpRecipe() diff --git a/src/python-for-android/recipes/eth-rlp/disable-setuptools-markdown.patch b/src/python-for-android/recipes/eth-rlp/disable-setuptools-markdown.patch deleted file mode 100644 index 982e409..0000000 --- a/src/python-for-android/recipes/eth-rlp/disable-setuptools-markdown.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/setup.py b/setup.py -index 1be66e6..5dd51a4 100644 ---- a/setup.py -+++ b/setup.py -@@ -40,7 +40,6 @@ setup( - # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme - version='0.1.2', - description="""eth-rlp: RLP definitions for common Ethereum objects in Python""", -- long_description_markdown_filename='README.md', - author='Jason Carver', - author_email='ethcalibur+pip@gmail.com', - url='https://github.com/ethereum/eth-rlp', -@@ -50,7 +49,6 @@ setup( - "hexbytes>=0.1.0,<1", - "rlp>=0.6.0,<2", - ], -- setup_requires=['setuptools-markdown'], - python_requires='>=3.5, <4', - extras_require=extras_require, - py_modules=['eth_rlp'], diff --git a/src/python-for-android/recipes/eth-typing/__init__.py b/src/python-for-android/recipes/eth-typing/__init__.py deleted file mode 100644 index 84d98d0..0000000 --- a/src/python-for-android/recipes/eth-typing/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - - -class EthTypingRecipe(PythonRecipe): - version = '2.0.0' - url = 'https://github.com/ethereum/eth-typing/archive/v{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - patches = ['setup.patch'] - - -recipe = EthTypingRecipe() diff --git a/src/python-for-android/recipes/eth-typing/setup.patch b/src/python-for-android/recipes/eth-typing/setup.patch deleted file mode 100644 index 0e09754..0000000 --- a/src/python-for-android/recipes/eth-typing/setup.patch +++ /dev/null @@ -1,18 +0,0 @@ -diff --git a/setup.py b/setup.py -index 52bf1c1..02be4d0 100644 ---- a/setup.py -+++ b/setup.py -@@ -40,13 +40,10 @@ setup( - # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme - version='2.0.0', - description="""eth-typing: Common type annotations for ethereum python packages""", -- long_description_markdown_filename='README.md', - author='The eth-typing contributors', - author_email='eth-typing@ethereum.org', - url='https://github.com/ethereum/eth-typing', - include_package_data=True, -- setup_requires=['setuptools-markdown'], -- python_requires='>=3.5, <4', - extras_require=extras_require, - py_modules=['eth_typing'], - license="MIT", diff --git a/src/python-for-android/recipes/greenlet/__init__.py b/src/python-for-android/recipes/greenlet/__init__.py deleted file mode 100644 index 65c9c4d..0000000 --- a/src/python-for-android/recipes/greenlet/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -from pythonforandroid.recipe import PythonRecipe - - -class GreenletRecipe(PythonRecipe): - version = '0.4.9' - url = 'https://pypi.python.org/packages/source/g/greenlet/greenlet-{version}.tar.gz' - depends = [('python2', 'python3crystax')] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(GreenletRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # 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) - return env - - -recipe = GreenletRecipe() diff --git a/src/python-for-android/recipes/hexbytes/__init__.py b/src/python-for-android/recipes/hexbytes/__init__.py deleted file mode 100644 index d42f2b7..0000000 --- a/src/python-for-android/recipes/hexbytes/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - - -class HexbytesRecipe(PythonRecipe): - version = '0.1.0' - url = 'https://github.com/carver/hexbytes/archive/v{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - patches = ['disable-setuptools-markdown.patch'] - - -recipe = HexbytesRecipe() diff --git a/src/python-for-android/recipes/hexbytes/disable-setuptools-markdown.patch b/src/python-for-android/recipes/hexbytes/disable-setuptools-markdown.patch deleted file mode 100644 index eee3e52..0000000 --- a/src/python-for-android/recipes/hexbytes/disable-setuptools-markdown.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/setup.py b/setup.py -index 22b4c5f..76fa946 100644 ---- a/setup.py -+++ b/setup.py -@@ -41,7 +41,6 @@ setup( - # *IMPORTANT*: Don't manually change the version here. Use `make bump`, as described in readme - version='0.1.0', - description="""hexbytes: Python `bytes` subclass that decodes hex, with a readable console output""", -- long_description_markdown_filename='README.md', - author='Jason Carver', - author_email='ethcalibur+pip@gmail.com', - url='https://github.com/ethereum/hexbytes', -@@ -49,7 +48,6 @@ setup( - install_requires=[ - 'eth-utils>=1.0.1,<2.0.0', - ], -- setup_requires=['setuptools-markdown'], - extras_require=extras_require, - py_modules=['hexbytes'], - license="MIT", diff --git a/src/python-for-android/recipes/hostpython3crystax/__init__.py b/src/python-for-android/recipes/hostpython3crystax/__init__.py deleted file mode 100644 index 4c4a90c..0000000 --- a/src/python-for-android/recipes/hostpython3crystax/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -from pythonforandroid.recipe import Recipe, shprint -from os.path import join -import sh - - -class Hostpython3Recipe(Recipe): - version = '3.5' - # url = 'http://python.org/ftp/python/{version}/Python-{version}.tgz' - # url = 'https://github.com/crystax/android-vendor-python-3-5/archive/master.zip' - name = 'hostpython3crystax' - - conflicts = ['hostpython2'] - - 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 prebuild_armeabi(self): - # # Override hostpython Setup? - # shprint(sh.cp, join(self.get_recipe_dir(), 'Setup'), - # join(self.get_build_dir('armeabi'), 'Modules', 'Setup')) - - def get_build_dir(self, arch=None): - return join(self.get_build_container_dir(), self.name) - - def build_arch(self, arch): - """ - Creates expected build and symlinks system Python version. - """ - self.ctx.hostpython = '/usr/bin/false' - self.ctx.hostpgen = '/usr/bin/false' - # creates the sub buildir (used by other recipes) - # https://github.com/kivy/python-for-android/issues/1154 - sub_build_dir = join(self.get_build_dir(), 'build') - shprint(sh.mkdir, '-p', sub_build_dir) - system_python = sh.which("python" + self.version) - link_dest = join(self.get_build_dir(), 'hostpython') - shprint(sh.ln, '-sf', system_python, link_dest) - - -recipe = Hostpython3Recipe() diff --git a/src/python-for-android/recipes/idna/__init__.py b/src/python-for-android/recipes/idna/__init__.py deleted file mode 100644 index bb57bac..0000000 --- a/src/python-for-android/recipes/idna/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - - -class IdnaRecipe(PythonRecipe): - name = 'idna' - version = '2.6' - url = 'https://github.com/kjd/idna/archive/v{version}.tar.gz' - - depends = [('python2', 'python3crystax'), 'setuptools'] - - call_hostpython_via_targetpython = False - - -recipe = IdnaRecipe() diff --git a/src/python-for-android/recipes/pycryptodome/__init__.py b/src/python-for-android/recipes/pycryptodome/__init__.py deleted file mode 100644 index 894e5db..0000000 --- a/src/python-for-android/recipes/pycryptodome/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -from pythonforandroid.recipe import PythonRecipe - - -class PycryptodomeRecipe(PythonRecipe): - version = '3.4.6' - url = 'https://github.com/Legrandin/pycryptodome/archive/v{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools', 'cffi'] - patches = ['setup.py.patch'] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(PycryptodomeRecipe, self).get_recipe_env(arch, with_flags_in_cc) - env['LDFLAGS'] += ' -L{}'.format(os.path.join(self.ctx.bootstrap.build_dir, 'libs', arch.arch)) - return env - - -recipe = PycryptodomeRecipe() diff --git a/src/python-for-android/recipes/pycryptodome/setup.py.patch b/src/python-for-android/recipes/pycryptodome/setup.py.patch deleted file mode 100644 index 1273e61..0000000 --- a/src/python-for-android/recipes/pycryptodome/setup.py.patch +++ /dev/null @@ -1,27 +0,0 @@ -diff --git a/setup.py b/setup.py -index b022d2c..ef302e4 100644 ---- a/setup.py -+++ b/setup.py -@@ -251,6 +251,22 @@ class PCTBuildExt (build_ext): - - aesni_mod_names = "Crypto.Cipher._raw_aesni", - -+ def finalize_options(self): -+ """ -+ Removes system include and library paths to avoid conflicts with cross-compilation. -+ """ -+ build_ext.finalize_options(self) -+ py_include = distutils.sysconfig.get_python_inc() -+ try: -+ self.include_dirs.remove(py_include) -+ except ValueError: -+ pass -+ libdir = distutils.sysconfig.get_config_var('LIBDIR') -+ try: -+ self.library_dirs.remove(libdir) -+ except ValueError: -+ pass -+ - def run(self): - build_ext.run(self) - diff --git a/src/python-for-android/recipes/pyethash/__init__.py b/src/python-for-android/recipes/pyethash/__init__.py deleted file mode 100644 index 6877ffe..0000000 --- a/src/python-for-android/recipes/pyethash/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -from pythonforandroid.recipe import PythonRecipe - - -class PyethashRecipe(PythonRecipe): - version = '0.1.27' - url = 'https://pypi.python.org/packages/source/p/pyethash/pyethash-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(PyethashRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # CFLAGS may only be used to specify C compiler flags, for macro definitions use CPPFLAGS - env['CPPFLAGS'] = env['CFLAGS'] - env['CFLAGS'] = '' - # LDFLAGS may only be used to specify linker flags, for libraries use LIBS - env['LDFLAGS'] = env['LDFLAGS'].replace('-lm', '').replace('-lcrystax', '') - env['LDFLAGS'] += ' -L{}'.format(os.path.join(self.ctx.bootstrap.build_dir, 'libs', arch.arch)) - env['LIBS'] = ' -lm' - if self.ctx.ndk == 'crystax': - env['LIBS'] += ' -lcrystax -lpython{}m'.format(self.ctx.python_recipe.version[0:3]) - env['LDSHARED'] += env['LIBS'] - return env - - -recipe = PyethashRecipe() diff --git a/src/python-for-android/recipes/pysha3/__init__.py b/src/python-for-android/recipes/pysha3/__init__.py deleted file mode 100644 index c3120dd..0000000 --- a/src/python-for-android/recipes/pysha3/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -from pythonforandroid.recipe import PythonRecipe - - -class Pysha3Recipe(PythonRecipe): - version = '1.0.2' - url = 'https://github.com/tiran/pysha3/archive/{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(Pysha3Recipe, self).get_recipe_env(arch, with_flags_in_cc) - # CFLAGS may only be used to specify C compiler flags, for macro definitions use CPPFLAGS - env['CPPFLAGS'] = env['CFLAGS'] - # LDFLAGS may only be used to specify linker flags, for libraries use LIBS - env['LDFLAGS'] = env['LDFLAGS'].replace('-lm', '').replace('-lcrystax', '') - env['LDFLAGS'] += ' -L{}'.format(os.path.join(self.ctx.bootstrap.build_dir, 'libs', arch.arch)) - env['LIBS'] = ' -lm' - if self.ctx.ndk == 'crystax': - env['LIBS'] += ' -lcrystax -lpython{}m'.format(self.ctx.python_recipe.version[0:3]) - env['LDSHARED'] += env['LIBS'] - return env - - -recipe = Pysha3Recipe() diff --git a/src/python-for-android/recipes/python3crystax/__init__.py b/src/python-for-android/recipes/python3crystax/__init__.py deleted file mode 100644 index feeec96..0000000 --- a/src/python-for-android/recipes/python3crystax/__init__.py +++ /dev/null @@ -1,257 +0,0 @@ -from pythonforandroid.recipe import TargetPythonRecipe -from pythonforandroid.toolchain import shprint -from pythonforandroid.logger import info, error -from pythonforandroid.util import ensure_dir, temp_directory -from os.path import exists, join -import os -import glob -import sh -from sh import Command - -# This is the content of opensslconf.h taken from -# ndkdir/build/tools/build-target-openssl.sh -OPENSSLCONF = """#if defined(__ARM_ARCH_5TE__) -#include "opensslconf_armeabi.h" -#elif defined(__ARM_ARCH_7A__) && !defined(__ARM_PCS_VFP) -#include "opensslconf_armeabi_v7a.h" -#elif defined(__ARM_ARCH_7A__) && defined(__ARM_PCS_VFP) -#include "opensslconf_armeabi_v7a_hard.h" -#elif defined(__aarch64__) -#include "opensslconf_arm64_v8a.h" -#elif defined(__i386__) -#include "opensslconf_x86.h" -#elif defined(__x86_64__) -#include "opensslconf_x86_64.h" -#elif defined(__mips__) && !defined(__mips64) -#include "opensslconf_mips.h" -#elif defined(__mips__) && defined(__mips64) -#include "opensslconf_mips64.h" -#else -#error "Unsupported ABI" -#endif -""" -LATEST_FULL_VERSION = { - '3.5': '3.5.1', - '3.6': '3.6.4' -} - -def realpath(fname): - """ - Own implementation of os.realpath which may be broken in some python versions - Returns: the absolute path o - - """ - - if not os.path.islink(fname): - return os.path.abspath(fname) - - abs_path = os.path.abspath(fname).split(os.sep)[:-1] - rel_path = os.readlink(fname) - - if os.path.abspath(rel_path) == rel_path: - return rel_path - - rel_path = rel_path.split(os.sep) - for folder in rel_path: - if folder == '..': - abs_path.pop() - else: - abs_path.append(folder) - return os.sep.join(abs_path) - -class Python3Recipe(TargetPythonRecipe): - version = '3.5' - url = '' - name = 'python3crystax' - - depends = ['hostpython3crystax'] - conflicts = ['python2', 'python3'] - - from_crystax = True - - def download_if_necessary(self): - if 'openssl' in self.ctx.recipe_build_order or self.version == '3.6': - full_version = LATEST_FULL_VERSION[self.version] - Python3Recipe.url = 'https://www.python.org/ftp/python/{0}.{1}.{2}/Python-{0}.{1}.{2}.tgz'.format(*full_version.split('.')) - super(Python3Recipe, self).download_if_necessary() - - def get_dir_name(self): - name = super(Python3Recipe, self).get_dir_name() - name += '-version{}'.format(self.version) - return name - - def copy_include_dir(self, source, target): - ensure_dir(target) - for fname in os.listdir(source): - sh.ln('-sf', realpath(join(source, fname)), join(target, fname)) - - def _patch_dev_defaults(self, fp, target_ver): - for line in fp: - if 'OPENSSL_VERSIONS=' in line: - versions = line.split('"')[1].split(' ') - if versions[0] == target_ver: - raise ValueError('Patch not needed') - - if target_ver in versions: - versions.remove(target_ver) - - versions.insert(0, target_ver) - - yield 'OPENSSL_VERSIONS="{}"\n'.format(' '.join(versions)) - else: - yield line - - def patch_dev_defaults(self, ssl_recipe): - def_fname = join(self.ctx.ndk_dir, 'build', 'tools', 'dev-defaults.sh') - try: - with open(def_fname, 'r') as fp: - s = ''.join(self._patch_dev_defaults(fp, - str(ssl_recipe.version))) - with open(def_fname, 'w') as fp: - fp.write(s) - - except ValueError: - pass - - def check_for_sslso(self, ssl_recipe, arch): - # type: (Recipe, str) - dynlib_dir = join(self.ctx.ndk_dir, 'sources', 'python', self.version, - 'libs', arch.arch, 'modules') - - if os.path.exists(join(dynlib_dir, '_ssl.so')): - return 10, 'Shared object exists in ndk' - - # find out why _ssl.so is missing - - source_dir = join(self.ctx.ndk_dir, 'sources', 'openssl', ssl_recipe.version) - if not os.path.exists(source_dir): - return 0, 'Openssl version not present' - - # these two path checks are lifted straight from: - # crystax-ndk/build/tools/build-target-python.sh - if not os.path.exists(join(source_dir, 'Android.mk')): - return 1.1, 'Android.mk is missing in openssl source' - - include_dir = join(source_dir, 'include','openssl') - if not os.path.exists(join(include_dir, 'opensslconf.h')): - return 1.2, 'Openssl include dir missing' - - under_scored_arch = arch.arch.replace('-', '_') - if not os.path.lexists(join(include_dir, - 'opensslconf_{}.h'.format(under_scored_arch))): - return 1.3, 'Opensslconf arch header missing from include' - - - - # lastly a check to see whether shared objects for the correct arch - # is present in the ndk - if not os.path.exists(join(source_dir, 'libs', arch.arch)): - return 2, 'Openssl libs for this arch is missing in ndk' - - return 5, 'Ready to recompile python' - - def find_Android_mk(self): - openssl_dir = join(self.ctx.ndk_dir, 'sources', 'openssl') - for version in os.listdir(openssl_dir): - mk_path = join(openssl_dir, version, 'Android.mk') - if os.path.exists(mk_path): - return mk_path - - def prebuild_arch(self, arch): - super(Python3Recipe, self).prebuild_arch(arch) - if self.version == '3.6': - Python3Recipe.patches = ['patch_python3.6.patch'] - build_dir = self.get_build_dir(arch.arch) - shprint(sh.ln, '-sf', - realpath(join(build_dir, 'Lib/site-packages/README.txt')), - join(build_dir, 'Lib/site-packages/README')) - python_build_files = ['android.mk', 'config.c', 'interpreter.c'] - ndk_build_tools_python_dir = join(self.ctx.ndk_dir, 'build', 'tools', 'build-target-python') - for python_build_file in python_build_files: - shprint(sh.cp, join(ndk_build_tools_python_dir, python_build_file+'.3.5'), - join(ndk_build_tools_python_dir, python_build_file+'.3.6')) - ndk_sources_python_dir = join(self.ctx.ndk_dir, 'sources', 'python') - if not os.path.exists(join(ndk_sources_python_dir, '3.6')): - os.mkdir(join(ndk_sources_python_dir, '3.6')) - sh.sed('s#3.5#3.6#', - join(ndk_sources_python_dir, '3.5/Android.mk'), - _out=join(ndk_sources_python_dir, '3.6/Android.mk')) - - def build_arch(self, arch): - # If openssl is needed we may have to recompile cPython to get the - # ssl.py module working properly - if self.from_crystax and 'openssl' in self.ctx.recipe_build_order: - info('Openssl and crystax-python combination may require ' - 'recompilation of python...') - ssl_recipe = self.get_recipe('openssl', self.ctx) - stage, msg = self.check_for_sslso(ssl_recipe, arch) - stage = 0 if stage < 5 else stage - info(msg) - openssl_build_dir = ssl_recipe.get_build_dir(arch.arch) - openssl_ndk_dir = join(self.ctx.ndk_dir, 'sources', 'openssl', - ssl_recipe.version) - - if stage < 2: - info('Copying openssl headers and Android.mk to ndk') - ensure_dir(openssl_ndk_dir) - if stage < 1.2: - # copy include folder and Android.mk to ndk - mk_path = self.find_Android_mk() - if mk_path is None: - raise IOError('Android.mk file could not be found in ' - 'any versions in ndk->sources->openssl') - shprint(sh.cp, mk_path, openssl_ndk_dir) - - include_dir = join(openssl_build_dir, 'include') - if stage < 1.3: - ndk_include_dir = join(openssl_ndk_dir, 'include', 'openssl') - self.copy_include_dir(join(include_dir, 'openssl'), ndk_include_dir) - - target_conf = join(openssl_ndk_dir, 'include', 'openssl', - 'opensslconf.h') - shprint(sh.rm, '-f', target_conf) - # overwrite opensslconf.h - with open(target_conf, 'w') as fp: - fp.write(OPENSSLCONF) - - if stage < 1.4: - # move current conf to arch specific conf in ndk - under_scored_arch = arch.arch.replace('-', '_') - shprint(sh.ln, '-sf', - realpath(join(include_dir, 'openssl', 'opensslconf.h')), - join(openssl_ndk_dir, 'include', 'openssl', - 'opensslconf_{}.h'.format(under_scored_arch)) - ) - - if stage < 3: - info('Copying openssl libs to ndk') - arch_ndk_lib = join(openssl_ndk_dir, 'libs', arch.arch) - ensure_dir(arch_ndk_lib) - shprint(sh.ln, '-sf', - realpath(join(openssl_build_dir, 'libcrypto{}.so'.format(ssl_recipe.version))), - join(openssl_build_dir, 'libcrypto.so')) - shprint(sh.ln, '-sf', - realpath(join(openssl_build_dir, 'libssl{}.so'.format(ssl_recipe.version))), - join(openssl_build_dir, 'libssl.so')) - libs = ['libcrypto.a', 'libcrypto.so', 'libssl.a', 'libssl.so'] - cmd = [join(openssl_build_dir, lib) for lib in libs] + [arch_ndk_lib] - shprint(sh.cp, '-f', *cmd) - - if stage < 10: - info('Recompiling python-crystax') - self.patch_dev_defaults(ssl_recipe) - build_script = join(self.ctx.ndk_dir, 'build', 'tools', - 'build-target-python.sh') - - shprint(Command(build_script), - '--ndk-dir={}'.format(self.ctx.ndk_dir), - '--abis={}'.format(arch.arch), - '-j5', '--verbose', - self.get_build_dir(arch.arch)) - - info('Extracting CrystaX python3 from NDK package') - dirn = self.ctx.get_python_install_dir() - ensure_dir(dirn) - self.ctx.hostpython = 'python{}'.format(self.version) - -recipe = Python3Recipe() diff --git a/src/python-for-android/recipes/python3crystax/patch_python3.6.patch b/src/python-for-android/recipes/python3crystax/patch_python3.6.patch deleted file mode 100644 index 4402d1b..0000000 --- a/src/python-for-android/recipes/python3crystax/patch_python3.6.patch +++ /dev/null @@ -1,89 +0,0 @@ -diff --git a/Modules/expat/xmlparse.c b/Modules/expat/xmlparse.c ---- a/Modules/expat/xmlparse.c -+++ b/Modules/expat/xmlparse.c -@@ -84,6 +84,8 @@ - # define LOAD_LIBRARY_SEARCH_SYSTEM32 0x00000800 - #endif - -+#define XML_POOR_ENTROPY 1 -+ - #if !defined(HAVE_GETRANDOM) && !defined(HAVE_SYSCALL_GETRANDOM) \ - && !defined(HAVE_ARC4RANDOM_BUF) && !defined(HAVE_ARC4RANDOM) \ - && !defined(XML_DEV_URANDOM) \ -diff --git a/Modules/getpath.c b/Modules/getpath.c ---- a/Modules/getpath.c -+++ b/Modules/getpath.c -@@ -101,8 +101,35 @@ - #endif - - --#if !defined(PREFIX) || !defined(EXEC_PREFIX) || !defined(VERSION) || !defined(VPATH) --#error "PREFIX, EXEC_PREFIX, VERSION, and VPATH must be constant defined" -+ /* These variables were set this way in old versions of Python, but -+ changed somewhere between 3.5.0 and 3.5.3. Here we just force -+ the old way again. A better solution would be to work out where -+ they should be defined, and make the CrystaX build scripts do -+ so. */ -+ -+/* #if !defined(PREFIX) || !defined(EXEC_PREFIX) || !defined(VERSION) || !defined(VPATH) */ -+/* #error "PREFIX, EXEC_PREFIX, VERSION, and VPATH must be constant defined" */ -+/* #endif */ -+ -+#ifndef VERSION -+#define VERSION "2.1" -+#endif -+ -+#ifndef VPATH -+#define VPATH "." -+#endif -+ -+#ifndef PREFIX -+# define PREFIX "/usr/local" -+#endif -+ -+#ifndef EXEC_PREFIX -+#define EXEC_PREFIX PREFIX -+#endif -+ -+#ifndef PYTHONPATH -+#define PYTHONPATH PREFIX "/lib/python" VERSION ":" \ -+ EXEC_PREFIX "/lib/python" VERSION "/lib-dynload" - #endif - - #ifndef LANDMARK -diff --git a/Modules/timemodule.c b/Modules/timemodule.c ---- a/Modules/timemodule.c -+++ b/Modules/timemodule.c -@@ -358,18 +358,20 @@ time_gmtime(PyObject *self, PyObject *args) - #endif - } - --#ifndef HAVE_TIMEGM --static time_t --timegm(struct tm *p) --{ -- /* XXX: the following implementation will not work for tm_year < 1970. -- but it is likely that platforms that don't have timegm do not support -- negative timestamps anyways. */ -- return p->tm_sec + p->tm_min*60 + p->tm_hour*3600 + p->tm_yday*86400 + -- (p->tm_year-70)*31536000 + ((p->tm_year-69)/4)*86400 - -- ((p->tm_year-1)/100)*86400 + ((p->tm_year+299)/400)*86400; --} --#endif -+/* In the Android build, HAVE_TIMEGM apparently should be defined but isn't. A better fix would be to work out why and fix that. */ -+ -+/* #ifndef HAVE_TIMEGM */ -+/* static time_t */ -+/* timegm(struct tm *p) */ -+/* { */ -+/* /\* XXX: the following implementation will not work for tm_year < 1970. */ -+/* but it is likely that platforms that don't have timegm do not support */ -+/* negative timestamps anyways. *\/ */ -+/* return p->tm_sec + p->tm_min*60 + p->tm_hour*3600 + p->tm_yday*86400 + */ -+/* (p->tm_year-70)*31536000 + ((p->tm_year-69)/4)*86400 - */ -+/* ((p->tm_year-1)/100)*86400 + ((p->tm_year+299)/400)*86400; */ -+/* } */ -+/* #endif */ - - PyDoc_STRVAR(gmtime_doc, - "gmtime([seconds]) -> (tm_year, tm_mon, tm_mday, tm_hour, tm_min,\n\ diff --git a/src/python-for-android/recipes/pyyaml/__init__.py b/src/python-for-android/recipes/pyyaml/__init__.py deleted file mode 100644 index 4ad8279..0000000 --- a/src/python-for-android/recipes/pyyaml/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - - -class PyYamlRecipe(PythonRecipe): - version = "3.12" - url = 'http://pyyaml.org/download/pyyaml/PyYAML-{version}.tar.gz' - depends = [('python2', 'python3crystax'), "setuptools"] - site_packages_name = 'pyyaml' - - -recipe = PyYamlRecipe() diff --git a/src/python-for-android/recipes/requests/__init__.py b/src/python-for-android/recipes/requests/__init__.py deleted file mode 100644 index 3e0ec66..0000000 --- a/src/python-for-android/recipes/requests/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - -class RequestsRecipe(PythonRecipe): - version = '2.13.0' - url = 'https://github.com/kennethreitz/requests/archive/v{version}.tar.gz' - depends = [('hostpython2', 'hostpython3crystax'), 'setuptools'] - site_packages_name = 'requests' - call_hostpython_via_targetpython = False - -recipe = RequestsRecipe() 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 0058639..0000000 --- a/src/python-for-android/recipes/scrypt/__init__.py +++ /dev/null @@ -1,35 +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) - 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')] diff --git a/src/python-for-android/recipes/secp256k1/__init__.py b/src/python-for-android/recipes/secp256k1/__init__.py deleted file mode 100644 index 5b74c86..0000000 --- a/src/python-for-android/recipes/secp256k1/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -from pythonforandroid.recipe import PythonRecipe - - -class Secp256k1Recipe(PythonRecipe): - - url = 'https://github.com/ludbb/secp256k1-py/archive/master.zip' - - call_hostpython_via_targetpython = False - - depends = [ - 'openssl', ('hostpython2', 'hostpython3crystax'), - ('python2', 'python3crystax'), 'setuptools', - 'libffi', 'cffi', 'libsecp256k1'] - - patches = [ - "cross_compile.patch", "drop_setup_requires.patch", - "pkg-config.patch", "find_lib.patch", "no-download.patch"] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(Secp256k1Recipe, self).get_recipe_env(arch, with_flags_in_cc) - libsecp256k1 = self.get_recipe('libsecp256k1', self.ctx) - libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch) - env['LDFLAGS'] += ' -L{}'.format(libsecp256k1_dir) - 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) - else: - env['LDFLAGS'] += " -lpython{}".format(python_version) - env['LDFLAGS'] += " -lsecp256k1" - return env - - -recipe = Secp256k1Recipe() diff --git a/src/python-for-android/recipes/secp256k1/cross_compile.patch b/src/python-for-android/recipes/secp256k1/cross_compile.patch deleted file mode 100644 index bfef228..0000000 --- a/src/python-for-android/recipes/secp256k1/cross_compile.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/setup.py b/setup.py -index bba4bce..b86b369 100644 ---- a/setup.py -+++ b/setup.py -@@ -191,6 +192,7 @@ class build_clib(_build_clib): - "--disable-dependency-tracking", - "--with-pic", - "--enable-module-recovery", -+ "--host=%s" % os.environ['TOOLCHAIN_PREFIX'], - "--prefix", - os.path.abspath(self.build_clib), - ] diff --git a/src/python-for-android/recipes/secp256k1/drop_setup_requires.patch b/src/python-for-android/recipes/secp256k1/drop_setup_requires.patch deleted file mode 100644 index 3be0293..0000000 --- a/src/python-for-android/recipes/secp256k1/drop_setup_requires.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/setup.py b/setup.py -index bba4bce..bfffbbc 100644 ---- a/setup.py -+++ b/setup.py -@@ -263,7 +263,6 @@ setup( - author_email='lud@tutanota.com', - license='MIT', - -- setup_requires=['cffi>=1.3.0', 'pytest-runner==2.6.2'], - install_requires=['cffi>=1.3.0'], - tests_require=['pytest==2.8.7'], - diff --git a/src/python-for-android/recipes/secp256k1/find_lib.patch b/src/python-for-android/recipes/secp256k1/find_lib.patch deleted file mode 100644 index 87997d5..0000000 --- a/src/python-for-android/recipes/secp256k1/find_lib.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/setup_support.py b/setup_support.py -index 68a2a7f..b84f420 100644 ---- a/setup_support.py -+++ b/setup_support.py -@@ -68,6 +68,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/secp256k1/no-download.patch b/src/python-for-android/recipes/secp256k1/no-download.patch deleted file mode 100644 index e905a39..0000000 --- a/src/python-for-android/recipes/secp256k1/no-download.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/setup.py b/setup.py -index bba4bce..5ea0228 100644 ---- a/setup.py -+++ b/setup.py -@@ -55,6 +55,8 @@ except OSError: - - - 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/secp256k1/pkg-config.patch b/src/python-for-android/recipes/secp256k1/pkg-config.patch deleted file mode 100644 index bb1e344..0000000 --- a/src/python-for-android/recipes/secp256k1/pkg-config.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/setup.py b/setup.py -index bba4bce..609481c 100644 ---- a/setup.py -+++ b/setup.py -@@ -48,10 +48,7 @@ if [int(i) for i in setuptools_version.split('.')] < [3, 3]: - try: - subprocess.check_call(['pkg-config', '--version']) - except OSError: -- raise SystemExit( -- "'pkg-config' is required to install this package. " -- "Please see the README for details." -- ) -+ pass - - - def download_library(command): -diff --git a/setup_support.py b/setup_support.py -index 68a2a7f..ccbafac 100644 ---- a/setup_support.py -+++ b/setup_support.py -@@ -40,6 +40,7 @@ def absolute(*paths): - - def build_flags(library, type_, path): - """Return separated build flags from pkg-config output""" -+ return [] - - pkg_config_path = [path] - if "PKG_CONFIG_PATH" in os.environ: diff --git a/src/python-for-android/recipes/web3/__init__.py b/src/python-for-android/recipes/web3/__init__.py deleted file mode 100644 index bb1bc39..0000000 --- a/src/python-for-android/recipes/web3/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import PythonRecipe - - -class Web3Recipe(PythonRecipe): - version = '4.8.1' - url = 'https://github.com/ethereum/web3.py/archive/v{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - patches = ['setup.patch'] - - -recipe = Web3Recipe() diff --git a/src/python-for-android/recipes/web3/setup.patch b/src/python-for-android/recipes/web3/setup.patch deleted file mode 100644 index a9c0130..0000000 --- a/src/python-for-android/recipes/web3/setup.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/setup.py b/setup.py -index fc78289..16e422b 100644 ---- a/setup.py -+++ b/setup.py -@@ -62,7 +62,6 @@ setup( - # *IMPORTANT*: Don't manually change the version here. Use the 'bumpversion' utility. - version='4.7.1', - description="""Web3.py""", -- long_description_markdown_filename='README.md', - author='Piper Merriam', - author_email='pipermerriam@gmail.com', - url='https://github.com/ethereum/web3.py', -@@ -80,8 +79,6 @@ setup( - "websockets>=6.0.0,<7.0.0", - "pypiwin32>=223;platform_system=='Windows'", - ], -- setup_requires=['setuptools-markdown'], -- python_requires='>=3.5.3,<4', - extras_require=extras_require, - py_modules=['web3', 'ens'], - license="MIT", From 257335486bf5b3cf307c95c542b8ce721c13bda4 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 12 Jan 2019 17:56:40 +0100 Subject: [PATCH 06/30] Migrating from python3crystax to pure python3 Updates API and NDK, refs #123 --- buildozer.spec | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index 07a09fc..3b1bdb5 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -54,7 +54,6 @@ requirements = eth-utils==1.2.1, gevent, hexbytes==0.1.0, - hostpython3crystax==3.6, https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.tar.gz, https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz, https://github.com/AndreMiras/pyetheroll/archive/884805b.tar.gz, @@ -68,7 +67,7 @@ requirements = plyer==1.3.1, pycryptodome==3.4.6, Pygments==2.2.0, - python3crystax==3.6, + python3==3.6.8, qrcode==6.0, raven==6.9.0, requests==2.20.0, @@ -135,24 +134,28 @@ android.permissions = INTERNET # (int) Android API to use #android.api = 19 +android.api = 27 -# (int) Minimum API required +# (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value. #android.minapi = 9 -android.minapi = 19 +android.minapi = 27 # (int) Android SDK version to use #android.sdk = 20 # (str) Android NDK version to use #android.ndk = 9c -# android.ndk = 10 +android.ndk = 17c + +# (int) Android NDK API to use (optional). This is the minimum API your app will support. +#android.ndk_api = 19 +android.ndk_api = 27 # (bool) Use --private data storage (True) or --dir public storage (False) #android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) #android.ndk_path = -android.ndk_path = ~/.buildozer/crystax-ndk # (str) Android SDK directory (if empty, it will be automatically downloaded.) #android.sdk_path = From ff242388af87e6065d4618eb717736d137e48789 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 14 Jan 2019 19:24:42 +0100 Subject: [PATCH 07/30] Some recipe don't compile and service is broken --- buildozer.spec | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index 3b1bdb5..01ddacb 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -37,13 +37,16 @@ version.filename = %(source.dir)s/version.py # (list) Application requirements # comma seperated e.g. requirements = sqlite3,kivy +# TODO: the recipe below don't compile yet for various reasons +# cffi, +# cytoolz==0.9.0, +# gevent, +# lru-dict==1.1.5, requirements = android, attrdict==2.0.0, certifi==2018.10.15, - cffi, chardet==3.0.4, - cytoolz==0.9.0, eth-abi==1.2.2, eth-account==0.3.0, eth-hash==0.2.0, @@ -52,7 +55,6 @@ requirements = eth-rlp==0.1.2, eth-typing==2.0.0, eth-utils==1.2.1, - gevent, hexbytes==0.1.0, https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.tar.gz, https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz, @@ -60,7 +62,6 @@ requirements = https://github.com/corpetty/py-etherscan-api/archive/cb91fb3.tar.gz, idna==2.7, kivy==1.10.1, - lru-dict==1.1.5, openssl, oscpy==0.3.0, parsimonious==0.8.1, @@ -99,7 +100,8 @@ orientation = portrait # (list) List of service to declare #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY -services = service:service/main.py +# TODO: currently broken after p4a refactoring +# services = service:service/main.py # # OSX Specific From c7e61c32a75bb1ff8d31041c6afe92213cbb22de Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 14 Jan 2019 22:17:41 +0100 Subject: [PATCH 08/30] Update cytoolz recipe --- .../recipes/cytoolz/__init__.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/python-for-android/recipes/cytoolz/__init__.py b/src/python-for-android/recipes/cytoolz/__init__.py index 3454376..2f8f2a9 100644 --- a/src/python-for-android/recipes/cytoolz/__init__.py +++ b/src/python-for-android/recipes/cytoolz/__init__.py @@ -1,23 +1,10 @@ import os -from pythonforandroid.recipe import PythonRecipe +from pythonforandroid.recipe import CythonRecipe -# TODO: CompiledComponentsPythonRecipe -class CytoolzRecipe(PythonRecipe): +class CytoolzRecipe(CythonRecipe): version = '0.9.0' url = 'https://github.com/pytoolz/cytoolz/archive/{version}.tar.gz' - depends = [('python2', 'python3crystax')] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(CytoolzRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # 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) - return env - + depends = ['setuptools'] recipe = CytoolzRecipe() From 248657868ae28da2e862771f342ea6a43a70825c Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 21 Jan 2019 16:28:25 +0100 Subject: [PATCH 09/30] fixing missing recipes WIP --- buildozer.spec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index 01ddacb..ed2d39f 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -38,15 +38,13 @@ version.filename = %(source.dir)s/version.py # (list) Application requirements # comma seperated e.g. requirements = sqlite3,kivy # TODO: the recipe below don't compile yet for various reasons -# cffi, -# cytoolz==0.9.0, -# gevent, -# lru-dict==1.1.5, requirements = android, attrdict==2.0.0, certifi==2018.10.15, + cffi, chardet==3.0.4, + cytoolz==0.9.0, eth-abi==1.2.2, eth-account==0.3.0, eth-hash==0.2.0, @@ -55,6 +53,7 @@ requirements = eth-rlp==0.1.2, eth-typing==2.0.0, eth-utils==1.2.1, + gevent, hexbytes==0.1.0, https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.tar.gz, https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz, @@ -62,13 +61,14 @@ requirements = https://github.com/corpetty/py-etherscan-api/archive/cb91fb3.tar.gz, idna==2.7, kivy==1.10.1, + lru-dict==1.1.5, openssl, oscpy==0.3.0, parsimonious==0.8.1, plyer==1.3.1, pycryptodome==3.4.6, Pygments==2.2.0, - python3==3.6.8, + python3==3.7.1, qrcode==6.0, raven==6.9.0, requests==2.20.0, From 8a8048fba4aeb7015bbb097cbdd1a19453c7a0a8 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 21 Jan 2019 16:31:06 +0100 Subject: [PATCH 10/30] Upgrades lru-dict to python3 --- .../recipes/lru-dict/__init__.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/python-for-android/recipes/lru-dict/__init__.py b/src/python-for-android/recipes/lru-dict/__init__.py index ca88d66..0a81542 100644 --- a/src/python-for-android/recipes/lru-dict/__init__.py +++ b/src/python-for-android/recipes/lru-dict/__init__.py @@ -1,22 +1,12 @@ import os -from pythonforandroid.recipe import PythonRecipe +from pythonforandroid.recipe import CompiledComponentsPythonRecipe -class LruDictRecipe(PythonRecipe): +class LruDictRecipe(CompiledComponentsPythonRecipe): version = '1.1.5' url = 'https://github.com/amitdev/lru-dict/archive/v{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(LruDictRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # 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) - return env + depends = ['setuptools'] + call_hostpython_via_targetpython = False recipe = LruDictRecipe() From 84d0a034c7aee95766f6a0f759965d17cc2e06a2 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 21 Jan 2019 16:32:29 +0100 Subject: [PATCH 11/30] Greenlet recipe Until upstream gets merged: https://github.com/kivy/python-for-android/pull/1603 --- src/python-for-android/recipes/greenlet/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/python-for-android/recipes/greenlet/__init__.py diff --git a/src/python-for-android/recipes/greenlet/__init__.py b/src/python-for-android/recipes/greenlet/__init__.py new file mode 100644 index 0000000..3f2043d --- /dev/null +++ b/src/python-for-android/recipes/greenlet/__init__.py @@ -0,0 +1,11 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class GreenletRecipe(CompiledComponentsPythonRecipe): + version = '0.4.15' + url = 'https://pypi.python.org/packages/source/g/greenlet/greenlet-{version}.tar.gz' + depends = ['setuptools'] + call_hostpython_via_targetpython = False + + +recipe = GreenletRecipe() From 9cb61e42521281140c5a05467f37d39c6e48d51e Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 21 Jan 2019 16:33:47 +0100 Subject: [PATCH 12/30] Gevent recipe migration to python3 Until upstream gets merged: https://github.com/kivy/python-for-android/pull/1600 --- .../recipes/gevent/__init__.py | 49 +++++++++++++++++++ .../recipes/gevent/cross_compiling.patch | 26 ++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/python-for-android/recipes/gevent/__init__.py create mode 100644 src/python-for-android/recipes/gevent/cross_compiling.patch diff --git a/src/python-for-android/recipes/gevent/__init__.py b/src/python-for-android/recipes/gevent/__init__.py new file mode 100644 index 0000000..9b09ad7 --- /dev/null +++ b/src/python-for-android/recipes/gevent/__init__.py @@ -0,0 +1,49 @@ +import re +import os +import sh +from pythonforandroid.logger import info, shprint +from pythonforandroid.recipe import CythonRecipe + + +class GeventRecipe(CythonRecipe): + version = '1.3.7' + url = 'https://pypi.python.org/packages/source/g/gevent/gevent-{version}.tar.gz' + depends = ['greenlet'] + patches = ["cross_compiling.patch"] + + def build_cython_components(self, arch): + """ + Hack to make it link properly to librt, inserted automatically by the + installer (Note: the librt doesn't exist in android but it is + integrated into libc, so we create a symbolic link which we will + remove when our build finishes) + """ + link_c = os.path.join(self.ctx.ndk_platform, 'usr', 'lib', 'libc') + link_rt = os.path.join(self.ctx.ndk_platform, 'usr', 'lib', 'librt') + shprint(sh.ln, '-sf', link_c + '.so', link_rt + '.so') + shprint(sh.ln, '-sf', link_c + '.a', link_rt + '.a') + super(GeventRecipe, self).build_cython_components(arch) + shprint(sh.rm, link_rt + '.so') + shprint(sh.rm, link_rt + '.a') + + def get_recipe_env(self, arch=None, with_flags_in_cc=True): + """ + - Moves all -I -D from CFLAGS to CPPFLAGS environment. + - Moves all -l from LDFLAGS to LIBS environment. + - Fixes linker name (use cross compiler) and flags (appends LIBS) + """ + env = super(GeventRecipe, self).get_recipe_env(arch, with_flags_in_cc) + # CFLAGS may only be used to specify C compiler flags, for macro definitions use CPPFLAGS + regex = re.compile(r'(?:\s|^)-[DI][\S]+') + env['CPPFLAGS'] = ''.join(re.findall(regex, env['CFLAGS'])).strip() + env['CFLAGS'] = re.sub(regex, '', env['CFLAGS']) + info('Moved "{}" from CFLAGS to CPPFLAGS.'.format(env['CPPFLAGS'])) + # LDFLAGS may only be used to specify linker flags, for libraries use LIBS + regex = re.compile(r'(?:\s|^)-l[\w\.]+') + env['LIBS'] = ''.join(re.findall(regex, env['LDFLAGS'])).strip() + env['LDFLAGS'] = re.sub(regex, '', env['LDFLAGS']) + info('Moved "{}" from LDFLAGS to LIBS.'.format(env['LIBS'])) + return env + + +recipe = GeventRecipe() diff --git a/src/python-for-android/recipes/gevent/cross_compiling.patch b/src/python-for-android/recipes/gevent/cross_compiling.patch new file mode 100644 index 0000000..01e55d8 --- /dev/null +++ b/src/python-for-android/recipes/gevent/cross_compiling.patch @@ -0,0 +1,26 @@ +diff --git a/_setupares.py b/_setupares.py +index dd184de6..bb16bebe 100644 +--- a/_setupares.py ++++ b/_setupares.py +@@ -43,7 +43,7 @@ else: + ares_configure_command = ' '.join([ + "(cd ", quoted_dep_abspath('c-ares'), + " && if [ -r ares_build.h ]; then cp ares_build.h ares_build.h.orig; fi ", +- " && sh ./configure --disable-dependency-tracking " + _m32 + "CONFIG_COMMANDS= ", ++ " && sh ./configure --host={} --disable-dependency-tracking ".format(os.environ['TOOLCHAIN_PREFIX']) + _m32 + "CONFIG_COMMANDS= ", + " && cp ares_config.h ares_build.h \"$OLDPWD\" ", + " && cat ares_build.h ", + " && if [ -r ares_build.h.orig ]; then mv ares_build.h.orig ares_build.h; fi)", +diff --git a/_setuplibev.py b/_setuplibev.py +index 2a5841bf..b6433c94 100644 +--- a/_setuplibev.py ++++ b/_setuplibev.py +@@ -31,7 +31,7 @@ LIBEV_EMBED = should_embed('libev') + # and the PyPy branch will clean it up. + libev_configure_command = ' '.join([ + "(cd ", quoted_dep_abspath('libev'), +- " && sh ./configure ", ++ " && sh ./configure --host={} ".format(os.environ['TOOLCHAIN_PREFIX']), + " && cp config.h \"$OLDPWD\"", + ")", + '> configure-output.txt' From 148aca01c948f3593be33abbadc63c1cef0908ed Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 21 Jan 2019 19:06:07 +0100 Subject: [PATCH 13/30] lowered API --- buildozer.spec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index ed2d39f..9c4acde 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -136,11 +136,11 @@ android.permissions = INTERNET # (int) Android API to use #android.api = 19 -android.api = 27 +android.api = 21 # (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value. #android.minapi = 9 -android.minapi = 27 +android.minapi = 21 # (int) Android SDK version to use #android.sdk = 20 @@ -151,7 +151,7 @@ android.ndk = 17c # (int) Android NDK API to use (optional). This is the minimum API your app will support. #android.ndk_api = 19 -android.ndk_api = 27 +android.ndk_api = 21 # (bool) Use --private data storage (True) or --dir public storage (False) #android.private_storage = True From f962394287a68efafe14575fd6f2dfeb778f64b5 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 21 Jan 2019 19:35:00 +0100 Subject: [PATCH 14/30] sqlite3 module should be placed before python3 https://github.com/kivy/python-for-android/issues/1564 --- buildozer.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildozer.spec b/buildozer.spec index 9c4acde..4f21bfc 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -37,8 +37,8 @@ version.filename = %(source.dir)s/version.py # (list) Application requirements # comma seperated e.g. requirements = sqlite3,kivy -# TODO: the recipe below don't compile yet for various reasons requirements = + sqlite3, android, attrdict==2.0.0, certifi==2018.10.15, From ead6d22b90eef69510184f51eb2a6540c539d044 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 21 Jan 2019 19:37:34 +0100 Subject: [PATCH 15/30] Enable service again Fixed upstream: https://github.com/kivy/python-for-android/pull/1586 --- buildozer.spec | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index 4f21bfc..45d3d83 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -100,8 +100,7 @@ orientation = portrait # (list) List of service to declare #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY -# TODO: currently broken after p4a refactoring -# services = service:service/main.py +services = service:service/main.py # # OSX Specific From cd0f456dba565e1f2e8e453112195944799031e2 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Mon, 21 Jan 2019 20:51:01 +0100 Subject: [PATCH 16/30] Drops CrystaX from Docker --- dockerfiles/Dockerfile-android | 14 -------------- src/CHANGELOG.md | 1 + 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/dockerfiles/Dockerfile-android b/dockerfiles/Dockerfile-android index bdb1b9e..6a6b73a 100644 --- a/dockerfiles/Dockerfile-android +++ b/dockerfiles/Dockerfile-android @@ -49,23 +49,9 @@ RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers USER ${USER} WORKDIR ${WORK_DIR} -# downloads and installs CrystaX NDK -RUN curl --location --progress-bar ${MAKEFILES_URL}/crystax_ndk.mk --output crystax_ndk.mk -# using `bsdtar` since `tar` is failing extracting it on Travis -RUN make -f crystax_ndk.mk TAR=`which bsdtar` - -# downloads and installs Android SDK -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 and NDK to where buildozer is expecting to see them -RUN mkdir -p ${HOME}/.buildozer/android/platform -RUN ln -sfn ${HOME}/.android ${HOME}/.buildozer/android/platform/android-sdk-20 -RUN ln -sfn ${HOME}/.android/crystax-ndk ${HOME}/.buildozer/ - COPY . ${WORK_DIR} ENTRYPOINT ["./dockerfiles/start.sh"] diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 27f7e3d..76451c2 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -6,6 +6,7 @@ - Split dedicated Etheroll library, refs #97 - Remove legacy dependencies, refs #112 - Migrate to upstream recipes + - Move to python3 recipe ## [v20181028] From 9fcec6d7144c648da56c86ab751691cac33572c2 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Wed, 30 Jan 2019 14:29:38 +0100 Subject: [PATCH 17/30] Minor CHANGELOG update --- src/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 76451c2..c2729db 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -6,7 +6,7 @@ - Split dedicated Etheroll library, refs #97 - Remove legacy dependencies, refs #112 - Migrate to upstream recipes - - Move to python3 recipe + - Move to python3 recipe, refs #123 ## [v20181028] From a1876a53b0e3fe0438d861ec802cdc24549d16f7 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 31 Jan 2019 12:44:43 +0100 Subject: [PATCH 18/30] Updates to buildozer, kivy and p4a master --- buildozer.spec | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index 45d3d83..4ae9c41 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -60,7 +60,7 @@ requirements = https://github.com/AndreMiras/pyetheroll/archive/884805b.tar.gz, https://github.com/corpetty/py-etherscan-api/archive/cb91fb3.tar.gz, idna==2.7, - kivy==1.10.1, + kivy==90c86f8, lru-dict==1.1.5, openssl, oscpy==0.3.0, @@ -130,26 +130,21 @@ fullscreen = 0 #android.presplash_color = #FFFFFF # (list) Permissions -#android.permissions = INTERNET android.permissions = INTERNET -# (int) Android API to use -#android.api = 19 -android.api = 21 +# (int) Target Android API, should be as high as possible. +android.api = 27 -# (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value. -#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 (optional). This is the minimum API your app will support. -#android.ndk_api = 19 +# (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) From 97b47194f2466445ce0b1f6a72c64b60ed00cdf3 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 1 Feb 2019 19:25:33 +0100 Subject: [PATCH 19/30] Migrate to last buildozer master --- buildozer.spec | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/buildozer.spec b/buildozer.spec index 4ae9c41..18ee424 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -164,6 +164,12 @@ android.ndk_api = 21 # 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 From e826d091c71ce83e55ab8a3001cd747f666061a6 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 2 Feb 2019 14:19:15 +0100 Subject: [PATCH 20/30] Migrating to last p4a release and buildozer master --- .travis.yml | 2 +- buildozer.spec | 2 +- dockerfiles/Dockerfile-android | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd3869c..418f06c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ env: - TAG=etheroll-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 diff --git a/buildozer.spec b/buildozer.spec index 18ee424..602d551 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -204,7 +204,7 @@ android.blacklist_src = blacklist.txt # (str) python-for-android branch to use, defaults to master #p4a.branch = stable -p4a.branch = master +p4a.branch = 0.7.0 # (str) OUYA Console category. Should be one of GAME or APP # If you leave this blank, OUYA support will not be enabled diff --git a/dockerfiles/Dockerfile-android b/dockerfiles/Dockerfile-android index 6a6b73a..f832c98 100644 --- a/dockerfiles/Dockerfile-android +++ b/dockerfiles/Dockerfile-android @@ -9,6 +9,7 @@ # docker run -it --rm etheroll-android FROM ubuntu:18.04 +ARG TRAVIS=false ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}" \ @@ -16,9 +17,6 @@ ENV WORK_DIR="${HOME_DIR}" \ ENV DOCKERFILES_VERSION="master" \ 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 # configure locale @@ -38,7 +36,7 @@ RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends # TODO: should this go to a Makefile instead so it can be shared/reused? RUN apt install -qq --yes --no-install-recommends \ python3.6 libpython3.6-dev python3-setuptools \ - autoconf automake libtool libltdl-dev libffi-dev bsdtar + autoconf automake libtool libltdl-dev libffi-dev bsdtar zip # prepare non root env RUN useradd --create-home --shell /bin/bash ${USER} @@ -52,6 +50,10 @@ WORKDIR ${WORK_DIR} # install buildozer and dependencies RUN curl --location --progress-bar ${MAKEFILES_URL}/buildozer.mk --output buildozer.mk RUN make -f buildozer.mk +# enforces buildozer master (afebba5) until next release +RUN pip install --upgrade https://github.com/kivy/buildozer/archive/afebba5.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"] From 5655c685355ac5a4350a31007bed165a775b9f0f Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 10 Feb 2019 15:23:45 +0100 Subject: [PATCH 21/30] Use p4a upstream recipe and simplified Dockerfile --- dockerfiles/Dockerfile-android | 2 +- .../recipes/gevent/__init__.py | 49 ------------------- .../recipes/gevent/cross_compiling.patch | 26 ---------- .../recipes/greenlet/__init__.py | 11 ----- 4 files changed, 1 insertion(+), 87 deletions(-) delete mode 100644 src/python-for-android/recipes/gevent/__init__.py delete mode 100644 src/python-for-android/recipes/gevent/cross_compiling.patch delete mode 100644 src/python-for-android/recipes/greenlet/__init__.py diff --git a/dockerfiles/Dockerfile-android b/dockerfiles/Dockerfile-android index f832c98..1fdac9f 100644 --- a/dockerfiles/Dockerfile-android +++ b/dockerfiles/Dockerfile-android @@ -28,7 +28,7 @@ ENV LANG="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 \ +RUN apt install -qq --yes --no-install-recommends \ make curl ca-certificates xz-utils unzip openjdk-8-jdk sudo python-pip \ python-setuptools diff --git a/src/python-for-android/recipes/gevent/__init__.py b/src/python-for-android/recipes/gevent/__init__.py deleted file mode 100644 index 9b09ad7..0000000 --- a/src/python-for-android/recipes/gevent/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import re -import os -import sh -from pythonforandroid.logger import info, shprint -from pythonforandroid.recipe import CythonRecipe - - -class GeventRecipe(CythonRecipe): - version = '1.3.7' - url = 'https://pypi.python.org/packages/source/g/gevent/gevent-{version}.tar.gz' - depends = ['greenlet'] - patches = ["cross_compiling.patch"] - - def build_cython_components(self, arch): - """ - Hack to make it link properly to librt, inserted automatically by the - installer (Note: the librt doesn't exist in android but it is - integrated into libc, so we create a symbolic link which we will - remove when our build finishes) - """ - link_c = os.path.join(self.ctx.ndk_platform, 'usr', 'lib', 'libc') - link_rt = os.path.join(self.ctx.ndk_platform, 'usr', 'lib', 'librt') - shprint(sh.ln, '-sf', link_c + '.so', link_rt + '.so') - shprint(sh.ln, '-sf', link_c + '.a', link_rt + '.a') - super(GeventRecipe, self).build_cython_components(arch) - shprint(sh.rm, link_rt + '.so') - shprint(sh.rm, link_rt + '.a') - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - """ - - Moves all -I -D from CFLAGS to CPPFLAGS environment. - - Moves all -l from LDFLAGS to LIBS environment. - - Fixes linker name (use cross compiler) and flags (appends LIBS) - """ - env = super(GeventRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # CFLAGS may only be used to specify C compiler flags, for macro definitions use CPPFLAGS - regex = re.compile(r'(?:\s|^)-[DI][\S]+') - env['CPPFLAGS'] = ''.join(re.findall(regex, env['CFLAGS'])).strip() - env['CFLAGS'] = re.sub(regex, '', env['CFLAGS']) - info('Moved "{}" from CFLAGS to CPPFLAGS.'.format(env['CPPFLAGS'])) - # LDFLAGS may only be used to specify linker flags, for libraries use LIBS - regex = re.compile(r'(?:\s|^)-l[\w\.]+') - env['LIBS'] = ''.join(re.findall(regex, env['LDFLAGS'])).strip() - env['LDFLAGS'] = re.sub(regex, '', env['LDFLAGS']) - info('Moved "{}" from LDFLAGS to LIBS.'.format(env['LIBS'])) - return env - - -recipe = GeventRecipe() diff --git a/src/python-for-android/recipes/gevent/cross_compiling.patch b/src/python-for-android/recipes/gevent/cross_compiling.patch deleted file mode 100644 index 01e55d8..0000000 --- a/src/python-for-android/recipes/gevent/cross_compiling.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/_setupares.py b/_setupares.py -index dd184de6..bb16bebe 100644 ---- a/_setupares.py -+++ b/_setupares.py -@@ -43,7 +43,7 @@ else: - ares_configure_command = ' '.join([ - "(cd ", quoted_dep_abspath('c-ares'), - " && if [ -r ares_build.h ]; then cp ares_build.h ares_build.h.orig; fi ", -- " && sh ./configure --disable-dependency-tracking " + _m32 + "CONFIG_COMMANDS= ", -+ " && sh ./configure --host={} --disable-dependency-tracking ".format(os.environ['TOOLCHAIN_PREFIX']) + _m32 + "CONFIG_COMMANDS= ", - " && cp ares_config.h ares_build.h \"$OLDPWD\" ", - " && cat ares_build.h ", - " && if [ -r ares_build.h.orig ]; then mv ares_build.h.orig ares_build.h; fi)", -diff --git a/_setuplibev.py b/_setuplibev.py -index 2a5841bf..b6433c94 100644 ---- a/_setuplibev.py -+++ b/_setuplibev.py -@@ -31,7 +31,7 @@ LIBEV_EMBED = should_embed('libev') - # and the PyPy branch will clean it up. - libev_configure_command = ' '.join([ - "(cd ", quoted_dep_abspath('libev'), -- " && sh ./configure ", -+ " && sh ./configure --host={} ".format(os.environ['TOOLCHAIN_PREFIX']), - " && cp config.h \"$OLDPWD\"", - ")", - '> configure-output.txt' diff --git a/src/python-for-android/recipes/greenlet/__init__.py b/src/python-for-android/recipes/greenlet/__init__.py deleted file mode 100644 index 3f2043d..0000000 --- a/src/python-for-android/recipes/greenlet/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pythonforandroid.recipe import CompiledComponentsPythonRecipe - - -class GreenletRecipe(CompiledComponentsPythonRecipe): - version = '0.4.15' - url = 'https://pypi.python.org/packages/source/g/greenlet/greenlet-{version}.tar.gz' - depends = ['setuptools'] - call_hostpython_via_targetpython = False - - -recipe = GreenletRecipe() From 27a9b48fd8a3098d666219dc05c242a1fe8d4845 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 10 Feb 2019 19:29:05 +0100 Subject: [PATCH 22/30] Makes sure account list is not cached too early --- src/ethereum_utils.py | 4 +++- src/tests/test_ethereum_utils.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/ethereum_utils.py b/src/ethereum_utils.py index f0be32a..368c030 100644 --- a/src/ethereum_utils.py +++ b/src/ethereum_utils.py @@ -15,12 +15,14 @@ def get_account_list(self): """ if self._accounts is not None: return self._accounts - 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) diff --git a/src/tests/test_ethereum_utils.py b/src/tests/test_ethereum_utils.py index 8bf39ec..59a006f 100644 --- a/src/tests/test_ethereum_utils.py +++ b/src/tests/test_ethereum_utils.py @@ -2,8 +2,10 @@ import shutil import unittest from tempfile import mkdtemp +from unittest import mock from ethereum_utils import AccountUtils +from pyethapp_accounts import Account PASSWORD = "password" @@ -56,6 +58,27 @@ def test_get_account_list(self): 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_deleted_account_dir(self): """ The deleted_account_dir() helper method should be working From 6b86e4ce4fd28f2947c9c48bf5c8497470bc42f9 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 10 Feb 2019 20:28:23 +0100 Subject: [PATCH 23/30] Keeps old app store behavior --- src/etheroll/store.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/etheroll/store.py b/src/etheroll/store.py index c175eb3..13466a6 100644 --- a/src/etheroll/store.py +++ b/src/etheroll/store.py @@ -37,6 +37,11 @@ def get_user_data_dir(app=None): """ if app is None: app = App.get_running_app() + # keeps the old behavior prior kivy==1.11.0 change, refs: + # https://github.com/kivy/kivy/pull/6095 + # we may still want to upgrade later in the future + if platform == "android": + return os.path.join('/sdcard', app.name) return App.user_data_dir.fget(app) @classmethod From 15e6dfb9551ca1261a82bb0755ba68d687378405 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 10 Feb 2019 20:30:05 +0100 Subject: [PATCH 24/30] Handles write storage permission, fixes #125 --- buildozer.spec | 2 +- src/etheroll/switchaccount.py | 32 +++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/buildozer.spec b/buildozer.spec index 602d551..7412771 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -130,7 +130,7 @@ fullscreen = 0 #android.presplash_color = #FFFFFF # (list) Permissions -android.permissions = INTERNET +android.permissions = INTERNET,WRITE_EXTERNAL_STORAGE # (int) Target Android API, should be as high as possible. android.api = 27 diff --git a/src/etheroll/switchaccount.py b/src/etheroll/switchaccount.py index 1fde50a..cf72f99 100644 --- a/src/etheroll/switchaccount.py +++ b/src/etheroll/switchaccount.py @@ -2,6 +2,7 @@ from kivy.clock import Clock, mainthread from kivy.properties import ObjectProperty from kivy.uix.boxlayout import BoxLayout +from kivy.utils import platform from kivymd.list import OneLineListItem from etheroll.ui_utils import Dialog, SubScreen, load_kv_from_py @@ -50,6 +51,19 @@ def update_account_list(self, accounts): list_item = self.create_item(account) account_list_id.add_widget(list_item) + @staticmethod + def check_request_permission(): + """ + Android runtime storage permission check. + """ + if platform != "android": + return + from android.permissions import ( + Permission, request_permission, check_permission) + permission = Permission.WRITE_EXTERNAL_STORAGE + if not check_permission(permission): + request_permission(permission) + @run_in_thread def load_account_list(self): """ @@ -59,16 +73,28 @@ def load_account_list(self): """ self.controller = App.get_running_app().root self.toggle_spinner(show=True) - accounts = self.controller.account_utils.get_account_list() + accounts = [] + self.check_request_permission() + try: + accounts = self.controller.account_utils.get_account_list() + self.update_account_list(accounts) + except PermissionError as exception: + self.on_permission_error(exception) self.toggle_spinner(show=False) - self.update_account_list(accounts) @staticmethod def on_empty_account_list(): controller = App.get_running_app().root keystore_dir = controller.account_utils.keystore_dir title = "No account found" - body = "No account found in:\n%s" % keystore_dir + body = f"No account found in:\n{keystore_dir}" + dialog = Dialog.create_dialog(title, body) + dialog.open() + + @staticmethod + def on_permission_error(exception): + title = "Permission denied" + body = str(exception.args) dialog = Dialog.create_dialog(title, body) dialog.open() From fd165b6a9b3ebc44ba81558e7059540779e75f23 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 10 Feb 2019 21:00:41 +0100 Subject: [PATCH 25/30] Service side write storage permission, fixes #125 --- src/service/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/service/main.py b/src/service/main.py index 0de9b59..2620a99 100755 --- a/src/service/main.py +++ b/src/service/main.py @@ -151,7 +151,13 @@ def pull_account_rolls(self, account): self.last_roll_activity = time() def pull_accounts_rolls(self): - accounts = self.account_utils.get_account_list() + accounts = [] + try: + accounts = self.account_utils.get_account_list() + except PermissionError: + # happens in e.g. Android runtime permission check, refs #125 + pass + print(f'accounts: {accounts}') for account in accounts: self.pull_account_rolls(account) From 02578643802d4e99da6ea8ad4ccd2728cf2d27dc Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 10 Feb 2019 21:51:08 +0100 Subject: [PATCH 26/30] Drops patch_find_library_android This is now working upstream since p4a 0.7.0 --- src/etheroll/controller.py | 2 -- src/etheroll/patches.py | 34 ---------------------------------- src/service/main.py | 2 -- 3 files changed, 38 deletions(-) delete mode 100644 src/etheroll/patches.py diff --git a/src/etheroll/controller.py b/src/etheroll/controller.py index 365fcb7..f1d033f 100755 --- a/src/etheroll/controller.py +++ b/src/etheroll/controller.py @@ -11,7 +11,6 @@ from requests.exceptions import ConnectionError from etheroll.constants import API_KEY_PATH -from etheroll.patches import patch_find_library_android from etheroll.settings import SettingsScreen from etheroll.switchaccount import SwitchAccountScreen from etheroll.ui_utils import Dialog, load_kv_from_py @@ -20,7 +19,6 @@ from sentry_utils import configure_sentry from service.utils import start_roll_pulling_service -patch_find_library_android() load_kv_from_py(__file__) diff --git a/src/etheroll/patches.py b/src/etheroll/patches.py deleted file mode 100644 index 5ea0a99..0000000 --- a/src/etheroll/patches.py +++ /dev/null @@ -1,34 +0,0 @@ -import ctypes.util -import os -from ctypes.util import find_library as original_find_library - -from kivy.utils import platform - - -def find_library(name): - """ - Looks in the right places on Android, see: - https://github.com/kivy/python-for-android/blob/0.6.0/ - pythonforandroid/recipes/python2/patches/ctypes-find-library-updated.patch - """ - # Check the user app lib dir - app_root = os.path.abspath('../../').split(os.path.sep) - lib_search = os.path.sep.join(app_root) + os.path.sep + 'lib' - for filename in os.listdir(lib_search): - if filename.endswith('.so') and name in filename: - return lib_search + os.path.sep + filename - # Check the normal Android system libraries - for filename in os.listdir('/system/lib'): - if filename.endswith('.so') and name in filename: - return lib_search + os.path.sep + filename - # fallback on the original find_library() - return original_find_library(name) - - -def patch_find_library_android(): - """ - Monkey patches find_library() to first try to find libraries on Android. - https://github.com/AndreMiras/EtherollApp/issues/30 - """ - if platform == 'android': - ctypes.util.find_library = find_library diff --git a/src/service/main.py b/src/service/main.py index 2620a99..2b1ddad 100755 --- a/src/service/main.py +++ b/src/service/main.py @@ -26,12 +26,10 @@ from ethereum_utils import AccountUtils from etheroll.constants import API_KEY_PATH, KEYSTORE_DIR_SUFFIX -from etheroll.patches import patch_find_library_android from etheroll.store import Store from osc.osc_app_client import OscAppClient from sentry_utils import configure_sentry -patch_find_library_android() PULL_FREQUENCY_SECONDS = 10 # time before the service shuts down if no roll activity NO_ROLL_ACTIVITY_PERDIOD_SECONDS = 5 * 60 From b0c3ed1b5843723bbb90c9a59859a98906f1cf5f Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 10 Feb 2019 21:30:42 +0100 Subject: [PATCH 27/30] UI testing in Travis via xvfb, fixes #65 --- .travis.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 418f06c..211b7b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,22 @@ services: - docker env: - - TAG=etheroll-linux DOCKERFILE=dockerfiles/Dockerfile-linux COMMAND='make test' - - TAG=etheroll-android DOCKERFILE=dockerfiles/Dockerfile-android COMMAND='buildozer android debug' + global: + - DISPLAY=:99.0 + matrix: + - TAG=etheroll-linux DOCKERFILE=dockerfiles/Dockerfile-linux COMMAND='make test' + - TAG=etheroll-linux DOCKERFILE=dockerfiles/Dockerfile-linux COMMAND='make uitest' + - TAG=etheroll-android DOCKERFILE=dockerfiles/Dockerfile-android COMMAND='buildozer android debug' + +before_install: + - sudo apt update -qq > /dev/null + - sudo apt install --yes --no-install-recommends xvfb install: - docker build --tag=$TAG --file=$DOCKERFILE --build-arg TRAVIS . +before_script: + - sh -e /etc/init.d/xvfb start + script: - - travis_wait docker run $TAG $COMMAND + - travis_wait docker run -e DISPLAY=unix$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix $TAG $COMMAND From 6bd8822de60b86974091e4c2bd2ef6de69f83c94 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 10 Feb 2019 23:36:09 +0100 Subject: [PATCH 28/30] Non root Docker user, fixes #127 --- dockerfiles/Dockerfile-linux | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/dockerfiles/Dockerfile-linux b/dockerfiles/Dockerfile-linux index 61fbadf..3a9d414 100644 --- a/dockerfiles/Dockerfile-linux +++ b/dockerfiles/Dockerfile-linux @@ -7,11 +7,17 @@ # docker run etheroll-linux 'make test' # Or for interactive shell: # docker run -it --rm etheroll-linux +# For running UI: +# xhost +"local:docker@" +# docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix etheroll-linux 'make uitest' # TODO: # - delete archives to keep small the container small -# - setup caching (for apt, and pip) FROM ubuntu:18.04 +ENV USER="user" +ENV HOME_DIR="/home/${USER}" +ENV WORK_DIR="${HOME_DIR}" + # configure locale RUN apt update -qq > /dev/null && apt install --yes --no-install-recommends \ locales && \ @@ -30,7 +36,17 @@ RUN apt update -qq > /dev/null && apt install --yes --no-install-recommends \ RUN apt install --yes --no-install-recommends \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev -WORKDIR /app -COPY . /app -RUN make system_dependencies +# prepare non root env +RUN useradd --create-home --shell /bin/bash ${USER} +# with sudo access and no password +RUN usermod -append --groups sudo ${USER} +RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + +USER ${USER} +WORKDIR ${WORK_DIR} +COPY . ${WORK_DIR} + +RUN sudo make system_dependencies +# required by Kivy `App.user_data_dir` +RUN mkdir ~/.config ENTRYPOINT ["./dockerfiles/start.sh"] From b69f8395301812586335f9deff0486b371c1bc1a Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 17 Feb 2019 20:04:52 +0100 Subject: [PATCH 29/30] Update CHANGELOG --- src/CHANGELOG.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index c2729db..e207671 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -3,10 +3,13 @@ ## [Unreleased] - - Split dedicated Etheroll library, refs #97 - - Remove legacy dependencies, refs #112 - Migrate to upstream recipes + - UI testing in Travis via xvfb, refs #65 + - Split dedicated Etheroll library, refs #97 + - Remove legacy dependencies, refs #122 - Move to python3 recipe, refs #123 + - Handles write storage permission, refs #125 + - Non root Docker user, refs #127 ## [v20181028] @@ -22,8 +25,8 @@ - Notify when roll processed, refs #57, #103, #106 - Recipes LDSHARED & CFLAGS cleaning, refs #104 - - Updates broken Mainnet node, refs #111 - - Upgrades to Kivy==1.10.1, refs #100 + - Update broken Mainnet node, refs #111 + - Upgrade to Kivy==1.10.1, refs #100 ## [v20180911] From 89f95c417af1bc4a3bf0d322e4e30892f5565ce9 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 17 Feb 2019 20:19:56 +0100 Subject: [PATCH 30/30] v20190217 --- README.md | 2 +- src/CHANGELOG.md | 2 +- src/version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 95f4908..29d2692 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://secure.travis-ci.org/AndreMiras/EtherollApp.png?branch=develop)](http://travis-ci.org/AndreMiras/EtherollApp) - + Provably fair dice game running on the [Ethereum blockchain](https://etheroll.com/#/smart-contract). Built with Python, [Kivy](https://github.com/kivy/kivy) and love. diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index e207671..0871796 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,7 +1,7 @@ # Change Log -## [Unreleased] +## [v20190217] - Migrate to upstream recipes - UI testing in Travis via xvfb, refs #65 diff --git a/src/version.py b/src/version.py index 07ade9e..e40e841 100644 --- a/src/version.py +++ b/src/version.py @@ -1 +1 @@ -__version__ = '2018.1028' +__version__ = '2019.0217'