Move pytest fixtures to importable location, add allowance check for transfer from
This commit is contained in:
		
							parent
							
								
									18382a1f35
								
							
						
					
					
						commit
						6a45d5faa7
					
				| @ -204,6 +204,82 @@ class Api: | |||||||
| #        return t | #        return t | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |     def transfer_from(self, from_address, to_address, value, token_symbol, spender_address): | ||||||
|  |         """Executes a chain of celery tasks that performs a transfer of ERC20 tokens by one address on behalf of another address to a third party. | ||||||
|  | 
 | ||||||
|  |         :param from_address: Ethereum address of sender | ||||||
|  |         :type from_address: str, 0x-hex | ||||||
|  |         :param to_address: Ethereum address of recipient | ||||||
|  |         :type to_address: str, 0x-hex | ||||||
|  |         :param value: Estimated return from conversion | ||||||
|  |         :type  value: int | ||||||
|  |         :param token_symbol: ERC20 token symbol of token to send | ||||||
|  |         :type token_symbol: str | ||||||
|  |         :param spender_address: Ethereum address of recipient | ||||||
|  |         :type spender_address: str, 0x-hex | ||||||
|  |         :returns: uuid of root task | ||||||
|  |         :rtype: celery.Task | ||||||
|  |         """ | ||||||
|  |         s_check = celery.signature( | ||||||
|  |                 'cic_eth.admin.ctrl.check_lock', | ||||||
|  |                 [ | ||||||
|  |                     [token_symbol], | ||||||
|  |                     self.chain_spec.asdict(), | ||||||
|  |                     LockEnum.QUEUE, | ||||||
|  |                     from_address, | ||||||
|  |                     ], | ||||||
|  |                 queue=self.queue, | ||||||
|  |                 ) | ||||||
|  |         s_nonce = celery.signature( | ||||||
|  |                 'cic_eth.eth.nonce.reserve_nonce', | ||||||
|  |                 [ | ||||||
|  |                     self.chain_spec.asdict(), | ||||||
|  |                     from_address, | ||||||
|  |                     ], | ||||||
|  |                 queue=self.queue, | ||||||
|  |                 ) | ||||||
|  |         s_tokens = celery.signature( | ||||||
|  |                 'cic_eth.eth.erc20.resolve_tokens_by_symbol', | ||||||
|  |                 [ | ||||||
|  |                     self.chain_spec.asdict(), | ||||||
|  |                     ], | ||||||
|  |                 queue=self.queue, | ||||||
|  |                 ) | ||||||
|  |         s_allow = celery.signature( | ||||||
|  |                 'cic_eth.eth.erc20.check_allowance', | ||||||
|  |                 [ | ||||||
|  |                     from_address, | ||||||
|  |                     value, | ||||||
|  |                     self.chain_spec.asdict(), | ||||||
|  |                     spender_address, | ||||||
|  |                     ], | ||||||
|  |                 queue=self.queue, | ||||||
|  |                 ) | ||||||
|  |         s_transfer = celery.signature( | ||||||
|  |                 'cic_eth.eth.erc20.transfer_from', | ||||||
|  |                 [ | ||||||
|  |                     from_address, | ||||||
|  |                     to_address, | ||||||
|  |                     value, | ||||||
|  |                     self.chain_spec.asdict(), | ||||||
|  |                     spender_address, | ||||||
|  |                     ], | ||||||
|  |                 queue=self.queue, | ||||||
|  |                 ) | ||||||
|  |         s_tokens.link(s_allow) | ||||||
|  |         s_nonce.link(s_tokens) | ||||||
|  |         s_check.link(s_nonce) | ||||||
|  |         if self.callback_param != None: | ||||||
|  |             s_transfer.link(self.callback_success) | ||||||
|  |             s_allow.link(s_transfer).on_error(self.callback_error) | ||||||
|  |         else: | ||||||
|  |             s_allow.link(s_transfer) | ||||||
|  | 
 | ||||||
|  |         t = s_check.apply_async(queue=self.queue) | ||||||
|  |         return t | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     def transfer(self, from_address, to_address, value, token_symbol): |     def transfer(self, from_address, to_address, value, token_symbol): | ||||||
|         """Executes a chain of celery tasks that performs a transfer of ERC20 tokens from one address to another. |         """Executes a chain of celery tasks that performs a transfer of ERC20 tokens from one address to another. | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -80,3 +80,8 @@ class SignerError(SeppukuError): | |||||||
| class RoleAgencyError(SeppukuError): | class RoleAgencyError(SeppukuError): | ||||||
|     """Exception raise when a role cannot perform its function. This is a critical exception |     """Exception raise when a role cannot perform its function. This is a critical exception | ||||||
|     """ |     """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class YouAreBrokeError(Exception): | ||||||
|  |     """Exception raised when a value transfer is attempted without access to sufficient funds | ||||||
|  |     """ | ||||||
|  | |||||||
| @ -24,6 +24,7 @@ from cic_eth.error import ( | |||||||
|         TokenCountError, |         TokenCountError, | ||||||
|         PermanentTxError, |         PermanentTxError, | ||||||
|         OutOfGasError, |         OutOfGasError, | ||||||
|  |         YouAreBrokeError, | ||||||
|         ) |         ) | ||||||
| from cic_eth.queue.tx import register_tx | from cic_eth.queue.tx import register_tx | ||||||
| from cic_eth.eth.gas import ( | from cic_eth.eth.gas import ( | ||||||
| @ -71,6 +72,117 @@ def balance(tokens, holder_address, chain_spec_dict): | |||||||
|     return tokens |     return tokens | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @celery_app.task(bind=True) | ||||||
|  | def check_allowance(self, tokens, holder_address, value, chain_spec_dict, spender_address): | ||||||
|  |     """Best-effort verification that the allowance for a transfer from spend is sufficient. | ||||||
|  | 
 | ||||||
|  |     :raises YouAreBrokeError: If allowance is insufficient | ||||||
|  |      | ||||||
|  |     :param tokens: Token addresses  | ||||||
|  |     :type tokens: list of str, 0x-hex | ||||||
|  |     :param holder_address: Token holder address | ||||||
|  |     :type holder_address: str, 0x-hex | ||||||
|  |     :param value: Amount of token, in 'wei' | ||||||
|  |     :type value: int | ||||||
|  |     :param chain_str: Chain spec string representation | ||||||
|  |     :type chain_str: str | ||||||
|  |     :param spender_address: Address of account spending on behalf of holder | ||||||
|  |     :type spender_address: str, 0x-hex | ||||||
|  |     :return: Token list as passed to task | ||||||
|  |     :rtype: dict | ||||||
|  |     """ | ||||||
|  |     logg.debug('tokens {}'.format(tokens)) | ||||||
|  |     if len(tokens) != 1: | ||||||
|  |         raise TokenCountError | ||||||
|  |     t = tokens[0] | ||||||
|  |     chain_spec = ChainSpec.from_dict(chain_spec_dict) | ||||||
|  | 
 | ||||||
|  |     rpc = RPCConnection.connect(chain_spec, 'default') | ||||||
|  | 
 | ||||||
|  |     caller_address = ERC20Token.caller_address  | ||||||
|  |     c = ERC20(chain_spec) | ||||||
|  |     o = c.allowance(t['address'], holder_address, spender_address, sender_address=caller_address) | ||||||
|  |     r = rpc.do(o) | ||||||
|  |     allowance = c.parse_allowance(r) | ||||||
|  |     if allowance < value: | ||||||
|  |         errstr = 'allowance {} insufficent to transfer {} {} by {} on behalf of {}'.format(allowance, value, t['symbol'], spender_address, holder_address) | ||||||
|  |         logg.error(errstr) | ||||||
|  |         raise YouAreBrokeError(errstr) | ||||||
|  | 
 | ||||||
|  |     return tokens | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @celery_app.task(bind=True, base=CriticalSQLAlchemyAndSignerTask) | ||||||
|  | def transfer_from(self, tokens, holder_address, receiver_address, value, chain_spec_dict, spender_address): | ||||||
|  |     """Transfer ERC20 tokens between addresses | ||||||
|  | 
 | ||||||
|  |     First argument is a list of tokens, to enable the task to be chained to the symbol to token address resolver function. However, it accepts only one token as argument. | ||||||
|  | 
 | ||||||
|  |     :param tokens: Token addresses  | ||||||
|  |     :type tokens: list of str, 0x-hex | ||||||
|  |     :param holder_address: Token holder address | ||||||
|  |     :type holder_address: str, 0x-hex | ||||||
|  |     :param receiver_address: Token receiver address | ||||||
|  |     :type receiver_address: str, 0x-hex | ||||||
|  |     :param value: Amount of token, in 'wei' | ||||||
|  |     :type value: int | ||||||
|  |     :param chain_str: Chain spec string representation | ||||||
|  |     :type chain_str: str | ||||||
|  |     :param spender_address: Address of account spending on behalf of holder | ||||||
|  |     :type spender_address: str, 0x-hex | ||||||
|  |     :raises TokenCountError: Either none or more then one tokens have been passed as tokens argument | ||||||
|  |     :return: Transaction hash for tranfer operation | ||||||
|  |     :rtype: str, 0x-hex | ||||||
|  |     """ | ||||||
|  |     # we only allow one token, one transfer | ||||||
|  |     logg.debug('tokens {}'.format(tokens)) | ||||||
|  |     if len(tokens) != 1: | ||||||
|  |         raise TokenCountError | ||||||
|  |     t = tokens[0] | ||||||
|  |     chain_spec = ChainSpec.from_dict(chain_spec_dict) | ||||||
|  |     queue = self.request.delivery_info.get('routing_key') | ||||||
|  | 
 | ||||||
|  |     rpc = RPCConnection.connect(chain_spec, 'default') | ||||||
|  |     rpc_signer = RPCConnection.connect(chain_spec, 'signer') | ||||||
|  | 
 | ||||||
|  |     session = self.create_session() | ||||||
|  |     nonce_oracle = CustodialTaskNonceOracle(holder_address, self.request.root_id, session=session) | ||||||
|  |     gas_oracle = self.create_gas_oracle(rpc, MaxGasOracle.gas) | ||||||
|  |     c = ERC20(chain_spec, signer=rpc_signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) | ||||||
|  |     try: | ||||||
|  |         (tx_hash_hex, tx_signed_raw_hex) = c.transfer_from(t['address'], spender_address, holder_address, receiver_address, value, tx_format=TxFormat.RLP_SIGNED) | ||||||
|  |     except FileNotFoundError as e: | ||||||
|  |         raise SignerError(e) | ||||||
|  |     except ConnectionError as e: | ||||||
|  |         raise SignerError(e) | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  |     rpc_signer.disconnect() | ||||||
|  |     rpc.disconnect() | ||||||
|  | 
 | ||||||
|  |     cache_task = 'cic_eth.eth.erc20.cache_transfer_from_data' | ||||||
|  | 
 | ||||||
|  |     register_tx(tx_hash_hex, tx_signed_raw_hex, chain_spec, queue, cache_task=cache_task, session=session) | ||||||
|  |     session.commit() | ||||||
|  |     session.close() | ||||||
|  |      | ||||||
|  |     gas_pair = gas_oracle.get_gas(tx_signed_raw_hex) | ||||||
|  |     gas_budget = gas_pair[0] * gas_pair[1] | ||||||
|  |     logg.debug('transfer tx {} {} {}'.format(tx_hash_hex, queue, gas_budget)) | ||||||
|  | 
 | ||||||
|  |     s = create_check_gas_task( | ||||||
|  |              [tx_signed_raw_hex], | ||||||
|  |              chain_spec, | ||||||
|  |              holder_address, | ||||||
|  |              gas_budget, | ||||||
|  |              [tx_hash_hex], | ||||||
|  |              queue, | ||||||
|  |             ) | ||||||
|  |     s.apply_async() | ||||||
|  |     return tx_hash_hex | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @celery_app.task(bind=True, base=CriticalSQLAlchemyAndSignerTask) | @celery_app.task(bind=True, base=CriticalSQLAlchemyAndSignerTask) | ||||||
| def transfer(self, tokens, holder_address, receiver_address, value, chain_spec_dict): | def transfer(self, tokens, holder_address, receiver_address, value, chain_spec_dict): | ||||||
|     """Transfer ERC20 tokens between addresses |     """Transfer ERC20 tokens between addresses | ||||||
| @ -232,6 +344,7 @@ def resolve_tokens_by_symbol(self, token_symbols, chain_spec_dict): | |||||||
|         logg.debug('token {}'.format(token_address)) |         logg.debug('token {}'.format(token_address)) | ||||||
|         tokens.append({ |         tokens.append({ | ||||||
|             'address': token_address, |             'address': token_address, | ||||||
|  |             'symbol': token_symbol, | ||||||
|             'converters': [], |             'converters': [], | ||||||
|             }) |             }) | ||||||
|     rpc.disconnect() |     rpc.disconnect() | ||||||
| @ -279,6 +392,48 @@ def cache_transfer_data( | |||||||
|     return (tx_hash_hex, cache_id) |     return (tx_hash_hex, cache_id) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @celery_app.task(base=CriticalSQLAlchemyTask) | ||||||
|  | def cache_transfer_from_data( | ||||||
|  |     tx_hash_hex, | ||||||
|  |     tx_signed_raw_hex, | ||||||
|  |     chain_spec_dict, | ||||||
|  |         ): | ||||||
|  |     """Helper function for otx_cache_transfer_from | ||||||
|  | 
 | ||||||
|  |     :param tx_hash_hex: Transaction hash | ||||||
|  |     :type tx_hash_hex: str, 0x-hex | ||||||
|  |     :param tx: Signed raw transaction | ||||||
|  |     :type tx: str, 0x-hex | ||||||
|  |     :returns: Transaction hash and id of cache element in storage backend, respectively | ||||||
|  |     :rtype: tuple | ||||||
|  |     """ | ||||||
|  |     chain_spec = ChainSpec.from_dict(chain_spec_dict) | ||||||
|  |     tx_signed_raw_bytes = bytes.fromhex(strip_0x(tx_signed_raw_hex)) | ||||||
|  |     tx = unpack(tx_signed_raw_bytes, chain_spec) | ||||||
|  | 
 | ||||||
|  |     tx_data = ERC20.parse_transfer_from_request(tx['data']) | ||||||
|  |     spender_address = tx_data[0] | ||||||
|  |     recipient_address = tx_data[1] | ||||||
|  |     token_value = tx_data[2] | ||||||
|  | 
 | ||||||
|  |     session = SessionBase.create_session() | ||||||
|  |     tx_cache = TxCache( | ||||||
|  |         tx_hash_hex, | ||||||
|  |         tx['from'], | ||||||
|  |         recipient_address, | ||||||
|  |         tx['to'], | ||||||
|  |         tx['to'], | ||||||
|  |         token_value, | ||||||
|  |         token_value, | ||||||
|  |         session=session, | ||||||
|  |             ) | ||||||
|  |     session.add(tx_cache) | ||||||
|  |     session.commit() | ||||||
|  |     cache_id = tx_cache.id | ||||||
|  |     session.close() | ||||||
|  |     return (tx_hash_hex, cache_id) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @celery_app.task(base=CriticalSQLAlchemyTask) | @celery_app.task(base=CriticalSQLAlchemyTask) | ||||||
| def cache_approve_data( | def cache_approve_data( | ||||||
|     tx_hash_hex, |     tx_hash_hex, | ||||||
|  | |||||||
| @ -2,13 +2,13 @@ | |||||||
| import os | import os | ||||||
| import logging | import logging | ||||||
| 
 | 
 | ||||||
| # third-party imports | # external imports | ||||||
| import pytest | import pytest | ||||||
| import confini | import confini | ||||||
| 
 | 
 | ||||||
| script_dir = os.path.dirname(os.path.realpath(__file__)) | script_dir = os.path.dirname(os.path.realpath(__file__)) | ||||||
| root_dir = os.path.dirname(script_dir) | root_dir = os.path.dirname(os.path.dirname(script_dir)) | ||||||
| logg = logging.getLogger(__file__) | logg = logging.getLogger(__name__) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture(scope='session') | @pytest.fixture(scope='session') | ||||||
| @ -37,7 +37,8 @@ def init_database( | |||||||
|         database_engine, |         database_engine, | ||||||
|         ): |         ): | ||||||
| 
 | 
 | ||||||
|     rootdir = os.path.dirname(os.path.dirname(__file__)) |     script_dir = os.path.dirname(os.path.realpath(__file__)) | ||||||
|  |     rootdir = os.path.dirname(os.path.dirname(script_dir)) | ||||||
|     dbdir = os.path.join(rootdir, 'cic_eth', 'db') |     dbdir = os.path.join(rootdir, 'cic_eth', 'db') | ||||||
|     migrationsdir = os.path.join(dbdir, 'migrations', load_config.get('DATABASE_ENGINE')) |     migrationsdir = os.path.join(dbdir, 'migrations', load_config.get('DATABASE_ENGINE')) | ||||||
|     if not os.path.isdir(migrationsdir): |     if not os.path.isdir(migrationsdir): | ||||||
| @ -17,11 +17,11 @@ root_dir = os.path.dirname(script_dir) | |||||||
| sys.path.insert(0, root_dir) | sys.path.insert(0, root_dir) | ||||||
| 
 | 
 | ||||||
| # assemble fixtures | # assemble fixtures | ||||||
| from tests.fixtures_config import * | from cic_eth.pytest.fixtures_config import * | ||||||
| from tests.fixtures_database import * | from cic_eth.pytest.fixtures_celery import * | ||||||
| from tests.fixtures_celery import * | from cic_eth.pytest.fixtures_database import * | ||||||
| from tests.fixtures_role import * | from cic_eth.pytest.fixtures_role import * | ||||||
| from tests.fixtures_contract import * | from cic_eth.pytest.fixtures_contract import * | ||||||
| from chainlib.eth.pytest import * | from chainlib.eth.pytest import * | ||||||
| from eth_contract_registry.pytest import * | from eth_contract_registry.pytest import * | ||||||
| from cic_eth_registry.pytest.fixtures_contracts import * | from cic_eth_registry.pytest.fixtures_contracts import * | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user