diff --git a/apps/cic-eth/cic_eth/eth/token.py b/apps/cic-eth/cic_eth/eth/erc20.py similarity index 77% rename from apps/cic-eth/cic_eth/eth/token.py rename to apps/cic-eth/cic_eth/eth/erc20.py index dcf40683..f8b150f6 100644 --- a/apps/cic-eth/cic_eth/eth/token.py +++ b/apps/cic-eth/cic_eth/eth/erc20.py @@ -1,26 +1,27 @@ # standard imports import logging -# third-party imports +# external imports import celery import requests import web3 -from cic_registry import zero_address -from cic_registry.chain import ChainSpec -from hexathon import strip_0x +from chainlib.eth.constant import ZERO_ADDRESS +from chainlib.chain import ChainSpec from chainlib.status import Status as TxStatus +from chainlib.connection import RPCConnection +from chainlib.eth.erc20 import ERC20 +from cic_eth_registry.erc20 import ERC20Token +from hexathon import strip_0x -# platform imports +# local imports from cic_eth.registry import safe_registry from cic_eth.db.models.tx import TxCache from cic_eth.db.models.base import SessionBase from cic_eth.eth import RpcClient from cic_eth.error import TokenCountError, PermanentTxError, OutOfGasError, NotLocalTxError -from cic_eth.eth.task import ( - register_tx, - create_check_gas_task, - ) -from cic_eth.eth.factory import TxFactory +from cic_eth.queue.tx import register_tx +from cic_eth.eth.gas import create_check_gas_task +#from cic_eth.eth.factory import TxFactory from cic_eth.eth.util import unpack_signed_raw_tx from cic_eth.ext.address import translate_address from cic_eth.task import ( @@ -32,96 +33,96 @@ from cic_eth.task import ( celery_app = celery.current_app logg = logging.getLogger() -# TODO: fetch from cic-contracts instead when implemented -contract_function_signatures = { - 'transfer': 'a9059cbb', - 'approve': '095ea7b3', - 'transferfrom': '23b872dd', - } - - -class TokenTxFactory(TxFactory): - """Factory for creating ERC20 token transactions. - """ - def approve( - self, - token_address, - spender_address, - amount, - chain_spec, - uuid, - session=None, - ): - """Create an ERC20 "approve" transaction - - :param token_address: ERC20 contract address - :type token_address: str, 0x-hex - :param spender_address: Address to approve spending for - :type spender_address: str, 0x-hex - :param amount: Amount of tokens to approve - :type amount: int - :param chain_spec: Chain spec - :type chain_spec: cic_registry.chain.ChainSpec - :returns: Unsigned "approve" transaction in standard Ethereum format - :rtype: dict - """ - source_token = self.registry.get_address(chain_spec, token_address) - source_token_contract = source_token.contract - tx_approve_buildable = source_token_contract.functions.approve( - spender_address, - amount, - ) - source_token_gas = source_token.gas('transfer') - - tx_approve = tx_approve_buildable.buildTransaction({ - 'from': self.address, - 'gas': source_token_gas, - 'gasPrice': self.gas_price, - 'chainId': chain_spec.chain_id(), - 'nonce': self.next_nonce(uuid, session=session), - }) - return tx_approve - - - def transfer( - self, - token_address, - receiver_address, - value, - chain_spec, - uuid, - session=None, - ): - """Create an ERC20 "transfer" transaction - - :param token_address: ERC20 contract address - :type token_address: str, 0x-hex - :param receiver_address: Address to send tokens to - :type receiver_address: str, 0x-hex - :param amount: Amount of tokens to send - :type amount: int - :param chain_spec: Chain spec - :type chain_spec: cic_registry.chain.ChainSpec - :returns: Unsigned "transfer" transaction in standard Ethereum format - :rtype: dict - """ - source_token = self.registry.get_address(chain_spec, token_address) - source_token_contract = source_token.contract - transfer_buildable = source_token_contract.functions.transfer( - receiver_address, - value, - ) - source_token_gas = source_token.gas('transfer') - - tx_transfer = transfer_buildable.buildTransaction( - { - 'from': self.address, - 'gas': source_token_gas, - 'gasPrice': self.gas_price, - 'chainId': chain_spec.chain_id(), - 'nonce': self.next_nonce(uuid, session=session), - }) - return tx_transfer +## TODO: fetch from cic-contracts instead when implemented +#contract_function_signatures = { +# 'transfer': 'a9059cbb', +# 'approve': '095ea7b3', +# 'transferfrom': '23b872dd', +# } +# +# +#class TokenTxFactory(TxFactory): +# """Factory for creating ERC20 token transactions. +# """ +# def approve( +# self, +# token_address, +# spender_address, +# amount, +# chain_spec, +# uuid, +# session=None, +# ): +# """Create an ERC20 "approve" transaction +# +# :param token_address: ERC20 contract address +# :type token_address: str, 0x-hex +# :param spender_address: Address to approve spending for +# :type spender_address: str, 0x-hex +# :param amount: Amount of tokens to approve +# :type amount: int +# :param chain_spec: Chain spec +# :type chain_spec: cic_registry.chain.ChainSpec +# :returns: Unsigned "approve" transaction in standard Ethereum format +# :rtype: dict +# """ +# source_token = self.registry.get_address(chain_spec, token_address) +# source_token_contract = source_token.contract +# tx_approve_buildable = source_token_contract.functions.approve( +# spender_address, +# amount, +# ) +# source_token_gas = source_token.gas('transfer') +# +# tx_approve = tx_approve_buildable.buildTransaction({ +# 'from': self.address, +# 'gas': source_token_gas, +# 'gasPrice': self.gas_price, +# 'chainId': chain_spec.chain_id(), +# 'nonce': self.next_nonce(uuid, session=session), +# }) +# return tx_approve +# +# +# def transfer( +# self, +# token_address, +# receiver_address, +# value, +# chain_spec, +# uuid, +# session=None, +# ): +# """Create an ERC20 "transfer" transaction +# +# :param token_address: ERC20 contract address +# :type token_address: str, 0x-hex +# :param receiver_address: Address to send tokens to +# :type receiver_address: str, 0x-hex +# :param amount: Amount of tokens to send +# :type amount: int +# :param chain_spec: Chain spec +# :type chain_spec: cic_registry.chain.ChainSpec +# :returns: Unsigned "transfer" transaction in standard Ethereum format +# :rtype: dict +# """ +# source_token = self.registry.get_address(chain_spec, token_address) +# source_token_contract = source_token.contract +# transfer_buildable = source_token_contract.functions.transfer( +# receiver_address, +# value, +# ) +# source_token_gas = source_token.gas('transfer') +# +# tx_transfer = transfer_buildable.buildTransaction( +# { +# 'from': self.address, +# 'gas': source_token_gas, +# 'gasPrice': self.gas_price, +# 'chainId': chain_spec.chain_id(), +# 'nonce': self.next_nonce(uuid, session=session), +# }) +# return tx_transfer def unpack_transfer(data): @@ -189,7 +190,7 @@ def unpack_approve(data): @celery_app.task(base=CriticalWeb3Task) -def balance(tokens, holder_address, chain_str): +def balance(tokens, holder_address, chain_spec_dict): """Return token balances for a list of tokens for given address :param tokens: Token addresses @@ -201,14 +202,17 @@ def balance(tokens, holder_address, chain_str): :return: List of balances :rtype: list of int """ - #abi = ContractRegistry.abi('ERC20Token') - chain_spec = ChainSpec.from_chain_str(chain_str) - c = RpcClient(chain_spec) - registry = safe_registry(c.w3) + chain_spec = ChainSpec.from_dict(chain_spec_dict) + rpc = RPCConnection.connect(chain_spec, 'default') + caller_address = ERC20Token.caller_address + for t in tokens: - o = registry.get_address(chain_spec, t['address']).contract - b = o.functions.balanceOf(holder_address).call() - t['balance_network'] = b + address = t['address'] + token = ERC20Token(rpc, address) + c = ERC20() + o = c.balance_of(address, holder_address, sender_address=caller_address) + r = rpc.do(o) + t['balance_network'] = c.parse_balance(r) return tokens @@ -487,8 +491,8 @@ class ExtendedTx: self.recipient_label = None self.source_token_value = 0 self.destination_token_value = 0 - self.source_token = zero_address - self.destination_token = zero_address + self.source_token = ZERO_ADDRESS + self.destination_token = ZERO_ADDRESS self.source_token_symbol = '' self.destination_token_symbol = '' self.source_token_decimals = ExtendedTx._default_decimals diff --git a/apps/cic-eth/cic_eth/eth/factory.py b/apps/cic-eth/cic_eth/eth/factory.py deleted file mode 100644 index 55d358cb..00000000 --- a/apps/cic-eth/cic_eth/eth/factory.py +++ /dev/null @@ -1,42 +0,0 @@ -# standard imports -import logging - -# local imports -from cic_registry import CICRegistry -from cic_eth.eth.nonce import NonceOracle -from cic_eth.eth import RpcClient - -logg = logging.getLogger(__name__) - - -class TxFactory: - """Base class for transaction factory classes. - - :param from_address: Signer address to create transaction on behalf of - :type from_address: str, 0x-hex - :param rpc_client: RPC connection object to use to acquire account nonce if no record in nonce cache - :type rpc_client: cic_eth.eth.rpc.RpcClient - """ - - gas_price = 100 - """Gas price, updated between batches""" - - - def __init__(self, from_address, rpc_client, registry=CICRegistry): - self.address = from_address - self.registry = registry - - self.default_nonce = rpc_client.w3.eth.getTransactionCount(from_address, 'pending') - self.nonce_oracle = NonceOracle(from_address, self.default_nonce) - - TxFactory.gas_price = rpc_client.gas_price() - logg.debug('txfactory instance address {} gas price'.format(self.address, self.gas_price)) - - - def next_nonce(self, uuid, session=None): - """Returns the current reserved nonce value, and increments it for next transaction. - - :returns: Nonce - :rtype: number - """ - return self.nonce_oracle.next_by_task_uuid(uuid, session=session) diff --git a/apps/cic-eth/cic_eth/eth/tx.py b/apps/cic-eth/cic_eth/eth/tx.py index 5a68d0e3..e64bcb1a 100644 --- a/apps/cic-eth/cic_eth/eth/tx.py +++ b/apps/cic-eth/cic_eth/eth/tx.py @@ -215,8 +215,6 @@ def hashes_to_txs(self, tx_hashes): queue = self.request.delivery_info['routing_key'] - #otxs = ','.format("'{}'".format(tx_hash) for tx_hash in tx_hashes) - session = SessionBase.create_session() q = session.query(Otx.signed_tx) q = q.filter(Otx.tx_hash.in_(tx_hashes)) @@ -282,16 +280,7 @@ def send(self, txs, chain_spec_dict): o = raw(tx_hex) conn = RPCConnection.connect(chain_spec, 'default') - #try: - #r = c.w3.eth.send_raw_transaction(tx_hex) - #r = c.w3.eth.sendRawTransaction(tx_hex) conn.do(o) - #except requests.exceptions.ConnectionError as e: - # raise(e) -# except Exception as e: -# raiser = ParityNodeHandler(chain_spec, queue) -# (t, e, m) = raiser.handle(e, tx_hash_hex, tx_hex) -# raise e(m) s_set_sent.apply_async() tx_tail = txs[1:] @@ -320,8 +309,13 @@ def refill_gas(self, recipient_address, chain_spec_dict): :returns: Transaction hash. :rtype: str, 0x-hex """ + # essentials chain_spec = ChainSpec.from_dict(chain_spec_dict) + queue = self.request.delivery_info.get('routing_key') + # Determine value of gas tokens to send + # if an uncompleted gas refill for the same recipient already exists, we still need to spend the nonce + # however, we will perform a 0-value transaction instead zero_amount = False session = SessionBase.create_session() status_filter = StatusBits.FINAL | StatusBits.NODE_ERROR | StatusBits.NETWORK_ERROR | StatusBits.UNKNOWN_ERROR @@ -336,56 +330,32 @@ def refill_gas(self, recipient_address, chain_spec_dict): zero_amount = True session.flush() - queue = self.request.delivery_info.get('routing_key') - - #c = RpcClient(chain_spec) - rpc = RPCConnection.connect(chain_spec, 'default') - - gas_provider = AccountRole.get_address('GAS_GIFTER', session=session) - session.flush() - - # Get default nonce to use from network if no nonce has been set - # TODO: This step may be redundant as nonce entry is set at account creation time - #default_nonce = c.w3.eth.getTransactionCount(c.gas_provider(), 'pending') - #o = count_pending(gas_provider) - #default_nonce = conn.do(o) - - nonce_oracle = CustodialTaskNonceOracle(gas_provider, self.request.root_id, session=session) #, default_nonce) - #nonce = nonce_generator.next(session=session) - #nonce = nonce_generator.next_by_task_uuid(self.request.root_id, session=session) - rpc_signer = RPCConnection.connect(chain_spec, 'signer') - gas_oracle = self.create_gas_oracle(rpc) - c = Gas(signer=rpc_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_id=chain_spec.chain_id()) - #gas_price = c.gas_price() - #gas_limit = c.default_gas_limit + # finally determine the value to send refill_amount = 0 if not zero_amount: refill_amount = self.safe_gas_refill_amount - logg.debug('tx send gas amount {} from provider {} to {}'.format(refill_amount, gas_provider, recipient_address)) -# # create and sign transaction -# tx_send_gas = { -# 'from': c.gas_provider(), -# 'to': recipient_address, -# 'gas': gas_limit, -# 'gasPrice': gas_price, -# 'chainId': chain_spec.chain_id(), -# 'nonce': nonce, -# 'value': refill_amount, -# 'data': '', -# } -# #tx_send_gas_signed = c.w3.eth.sign_transaction(tx_send_gas) -# #tx_hash = web3.Web3.keccak(hexstr=tx_send_gas_signed['raw']) -# #tx_hash_hex = tx_hash.hex() -# (tx_hash_hex, tx_send_gas_signed) = sign_tx(tx_send_gas) - (tx_hash_hex, tx_signed_raw_hex) = c.create(gas_provider, recipient_address, refill_amount, tx_format=TxFormat.RLP_SIGNED) + # determine sender + gas_provider = AccountRole.get_address('GAS_GIFTER', session=session) + session.flush() - # TODO: route this through sign_and_register_tx instead + # set up evm RPC connection + rpc = RPCConnection.connect(chain_spec, 'default') + + # set up transaction builder + nonce_oracle = CustodialTaskNonceOracle(gas_provider, self.request.root_id, session=session) + gas_oracle = self.create_gas_oracle(rpc) + rpc_signer = RPCConnection.connect(chain_spec, 'signer') + c = Gas(signer=rpc_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_id=chain_spec.chain_id()) + + # build and add transaction + logg.debug('tx send gas amount {} from provider {} to {}'.format(refill_amount, gas_provider, recipient_address)) + (tx_hash_hex, tx_signed_raw_hex) = c.create(gas_provider, recipient_address, refill_amount, tx_format=TxFormat.RLP_SIGNED) logg.debug('adding queue refill gas tx {}'.format(tx_hash_hex)) - #cache_task = 'cic_eth.eth.tx.cache_gas_refill_data' cache_task = 'cic_eth.eth.tx.otx_cache_parse_tx' register_tx(tx_hash_hex, tx_signed_raw_hex, chain_spec, queue, cache_task=cache_task, session=session) + # add transaction to send queue s_status = celery.signature( 'cic_eth.queue.tx.set_ready', [ diff --git a/apps/cic-eth/tests/conftest.py b/apps/cic-eth/tests/conftest.py index a858a034..147943de 100644 --- a/apps/cic-eth/tests/conftest.py +++ b/apps/cic-eth/tests/conftest.py @@ -13,3 +13,4 @@ from tests.fixtures_role import * from chainlib.eth.pytest import * from contract_registry.pytest import * from cic_eth_registry.pytest.fixtures_contracts import * +from cic_eth_registry.pytest.fixtures_tokens import * diff --git a/apps/cic-eth/tests/fixtures_celery.py b/apps/cic-eth/tests/fixtures_celery.py index 1ad05846..257c3f30 100644 --- a/apps/cic-eth/tests/fixtures_celery.py +++ b/apps/cic-eth/tests/fixtures_celery.py @@ -13,7 +13,7 @@ logg = logging.getLogger() def celery_includes(): return [ # 'cic_eth.eth.bancor', -# 'cic_eth.eth.token', + 'cic_eth.eth.erc20', 'cic_eth.eth.tx', # 'cic_eth.ext.tx', 'cic_eth.queue.tx', diff --git a/apps/cic-eth/tests/task/test_account.py b/apps/cic-eth/tests/task/test_task_account.py similarity index 100% rename from apps/cic-eth/tests/task/test_account.py rename to apps/cic-eth/tests/task/test_task_account.py diff --git a/apps/cic-eth/tests/task/test_task_erc20.py b/apps/cic-eth/tests/task/test_task_erc20.py new file mode 100644 index 00000000..7de7b4a8 --- /dev/null +++ b/apps/cic-eth/tests/task/test_task_erc20.py @@ -0,0 +1,43 @@ +# external imports +import celery +from chainlib.eth.erc20 import ERC20 +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.tx import receipt + + +def test_erc20_balance( + default_chain_spec, + foo_token, + token_roles, + agent_roles, + eth_signer, + eth_rpc, + celery_worker, + ): + + nonce_oracle = RPCNonceOracle(token_roles['FOO_TOKEN_OWNER'], eth_rpc) + c = ERC20(signer=eth_signer, nonce_oracle=nonce_oracle) + transfer_value = 100 * (10**6) + (tx_hash_hex, o) = c.transfer(foo_token, token_roles['FOO_TOKEN_OWNER'], agent_roles['ALICE'], transfer_value) + eth_rpc.do(o) + + o = receipt(tx_hash_hex) + r = eth_rpc.do(o) + assert r['status'] == 1 + + token_object = { + 'address': foo_token, + } + s = celery.signature( + 'cic_eth.eth.erc20.balance', + [ + [token_object], + agent_roles['ALICE'], + default_chain_spec.asdict(), + ], + queue=None, + ) + t = s.apply_async() + r = t.get() + assert r[0]['balance_network'] == transfer_value +