Rehabilitate erc20 balance task

This commit is contained in:
nolash 2021-03-19 19:51:30 +01:00
parent 871cbdcaeb
commit c245a29a6b
Signed by untrusted user who does not match committer: lash
GPG Key ID: 21D2E7BB88C2A746
7 changed files with 181 additions and 205 deletions

View File

@ -1,26 +1,27 @@
# standard imports # standard imports
import logging import logging
# third-party imports # external imports
import celery import celery
import requests import requests
import web3 import web3
from cic_registry import zero_address from chainlib.eth.constant import ZERO_ADDRESS
from cic_registry.chain import ChainSpec from chainlib.chain import ChainSpec
from hexathon import strip_0x
from chainlib.status import Status as TxStatus 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.registry import safe_registry
from cic_eth.db.models.tx import TxCache from cic_eth.db.models.tx import TxCache
from cic_eth.db.models.base import SessionBase from cic_eth.db.models.base import SessionBase
from cic_eth.eth import RpcClient from cic_eth.eth import RpcClient
from cic_eth.error import TokenCountError, PermanentTxError, OutOfGasError, NotLocalTxError from cic_eth.error import TokenCountError, PermanentTxError, OutOfGasError, NotLocalTxError
from cic_eth.eth.task import ( from cic_eth.queue.tx import register_tx
register_tx, from cic_eth.eth.gas import create_check_gas_task
create_check_gas_task, #from cic_eth.eth.factory import TxFactory
)
from cic_eth.eth.factory import TxFactory
from cic_eth.eth.util import unpack_signed_raw_tx from cic_eth.eth.util import unpack_signed_raw_tx
from cic_eth.ext.address import translate_address from cic_eth.ext.address import translate_address
from cic_eth.task import ( from cic_eth.task import (
@ -32,96 +33,96 @@ from cic_eth.task import (
celery_app = celery.current_app celery_app = celery.current_app
logg = logging.getLogger() logg = logging.getLogger()
# TODO: fetch from cic-contracts instead when implemented ## TODO: fetch from cic-contracts instead when implemented
contract_function_signatures = { #contract_function_signatures = {
'transfer': 'a9059cbb', # 'transfer': 'a9059cbb',
'approve': '095ea7b3', # 'approve': '095ea7b3',
'transferfrom': '23b872dd', # 'transferfrom': '23b872dd',
} # }
#
#
class TokenTxFactory(TxFactory): #class TokenTxFactory(TxFactory):
"""Factory for creating ERC20 token transactions. # """Factory for creating ERC20 token transactions.
""" # """
def approve( # def approve(
self, # self,
token_address, # token_address,
spender_address, # spender_address,
amount, # amount,
chain_spec, # chain_spec,
uuid, # uuid,
session=None, # session=None,
): # ):
"""Create an ERC20 "approve" transaction # """Create an ERC20 "approve" transaction
#
:param token_address: ERC20 contract address # :param token_address: ERC20 contract address
:type token_address: str, 0x-hex # :type token_address: str, 0x-hex
:param spender_address: Address to approve spending for # :param spender_address: Address to approve spending for
:type spender_address: str, 0x-hex # :type spender_address: str, 0x-hex
:param amount: Amount of tokens to approve # :param amount: Amount of tokens to approve
:type amount: int # :type amount: int
:param chain_spec: Chain spec # :param chain_spec: Chain spec
:type chain_spec: cic_registry.chain.ChainSpec # :type chain_spec: cic_registry.chain.ChainSpec
:returns: Unsigned "approve" transaction in standard Ethereum format # :returns: Unsigned "approve" transaction in standard Ethereum format
:rtype: dict # :rtype: dict
""" # """
source_token = self.registry.get_address(chain_spec, token_address) # source_token = self.registry.get_address(chain_spec, token_address)
source_token_contract = source_token.contract # source_token_contract = source_token.contract
tx_approve_buildable = source_token_contract.functions.approve( # tx_approve_buildable = source_token_contract.functions.approve(
spender_address, # spender_address,
amount, # amount,
) # )
source_token_gas = source_token.gas('transfer') # source_token_gas = source_token.gas('transfer')
#
tx_approve = tx_approve_buildable.buildTransaction({ # tx_approve = tx_approve_buildable.buildTransaction({
'from': self.address, # 'from': self.address,
'gas': source_token_gas, # 'gas': source_token_gas,
'gasPrice': self.gas_price, # 'gasPrice': self.gas_price,
'chainId': chain_spec.chain_id(), # 'chainId': chain_spec.chain_id(),
'nonce': self.next_nonce(uuid, session=session), # 'nonce': self.next_nonce(uuid, session=session),
}) # })
return tx_approve # return tx_approve
#
#
def transfer( # def transfer(
self, # self,
token_address, # token_address,
receiver_address, # receiver_address,
value, # value,
chain_spec, # chain_spec,
uuid, # uuid,
session=None, # session=None,
): # ):
"""Create an ERC20 "transfer" transaction # """Create an ERC20 "transfer" transaction
#
:param token_address: ERC20 contract address # :param token_address: ERC20 contract address
:type token_address: str, 0x-hex # :type token_address: str, 0x-hex
:param receiver_address: Address to send tokens to # :param receiver_address: Address to send tokens to
:type receiver_address: str, 0x-hex # :type receiver_address: str, 0x-hex
:param amount: Amount of tokens to send # :param amount: Amount of tokens to send
:type amount: int # :type amount: int
:param chain_spec: Chain spec # :param chain_spec: Chain spec
:type chain_spec: cic_registry.chain.ChainSpec # :type chain_spec: cic_registry.chain.ChainSpec
:returns: Unsigned "transfer" transaction in standard Ethereum format # :returns: Unsigned "transfer" transaction in standard Ethereum format
:rtype: dict # :rtype: dict
""" # """
source_token = self.registry.get_address(chain_spec, token_address) # source_token = self.registry.get_address(chain_spec, token_address)
source_token_contract = source_token.contract # source_token_contract = source_token.contract
transfer_buildable = source_token_contract.functions.transfer( # transfer_buildable = source_token_contract.functions.transfer(
receiver_address, # receiver_address,
value, # value,
) # )
source_token_gas = source_token.gas('transfer') # source_token_gas = source_token.gas('transfer')
#
tx_transfer = transfer_buildable.buildTransaction( # tx_transfer = transfer_buildable.buildTransaction(
{ # {
'from': self.address, # 'from': self.address,
'gas': source_token_gas, # 'gas': source_token_gas,
'gasPrice': self.gas_price, # 'gasPrice': self.gas_price,
'chainId': chain_spec.chain_id(), # 'chainId': chain_spec.chain_id(),
'nonce': self.next_nonce(uuid, session=session), # 'nonce': self.next_nonce(uuid, session=session),
}) # })
return tx_transfer # return tx_transfer
def unpack_transfer(data): def unpack_transfer(data):
@ -189,7 +190,7 @@ def unpack_approve(data):
@celery_app.task(base=CriticalWeb3Task) @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 """Return token balances for a list of tokens for given address
:param tokens: Token addresses :param tokens: Token addresses
@ -201,14 +202,17 @@ def balance(tokens, holder_address, chain_str):
:return: List of balances :return: List of balances
:rtype: list of int :rtype: list of int
""" """
#abi = ContractRegistry.abi('ERC20Token') chain_spec = ChainSpec.from_dict(chain_spec_dict)
chain_spec = ChainSpec.from_chain_str(chain_str) rpc = RPCConnection.connect(chain_spec, 'default')
c = RpcClient(chain_spec) caller_address = ERC20Token.caller_address
registry = safe_registry(c.w3)
for t in tokens: for t in tokens:
o = registry.get_address(chain_spec, t['address']).contract address = t['address']
b = o.functions.balanceOf(holder_address).call() token = ERC20Token(rpc, address)
t['balance_network'] = b 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 return tokens
@ -487,8 +491,8 @@ class ExtendedTx:
self.recipient_label = None self.recipient_label = None
self.source_token_value = 0 self.source_token_value = 0
self.destination_token_value = 0 self.destination_token_value = 0
self.source_token = zero_address self.source_token = ZERO_ADDRESS
self.destination_token = zero_address self.destination_token = ZERO_ADDRESS
self.source_token_symbol = '' self.source_token_symbol = ''
self.destination_token_symbol = '' self.destination_token_symbol = ''
self.source_token_decimals = ExtendedTx._default_decimals self.source_token_decimals = ExtendedTx._default_decimals

View File

@ -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)

View File

@ -215,8 +215,6 @@ def hashes_to_txs(self, tx_hashes):
queue = self.request.delivery_info['routing_key'] queue = self.request.delivery_info['routing_key']
#otxs = ','.format("'{}'".format(tx_hash) for tx_hash in tx_hashes)
session = SessionBase.create_session() session = SessionBase.create_session()
q = session.query(Otx.signed_tx) q = session.query(Otx.signed_tx)
q = q.filter(Otx.tx_hash.in_(tx_hashes)) q = q.filter(Otx.tx_hash.in_(tx_hashes))
@ -282,16 +280,7 @@ def send(self, txs, chain_spec_dict):
o = raw(tx_hex) o = raw(tx_hex)
conn = RPCConnection.connect(chain_spec, 'default') 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) 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() s_set_sent.apply_async()
tx_tail = txs[1:] tx_tail = txs[1:]
@ -320,8 +309,13 @@ def refill_gas(self, recipient_address, chain_spec_dict):
:returns: Transaction hash. :returns: Transaction hash.
:rtype: str, 0x-hex :rtype: str, 0x-hex
""" """
# essentials
chain_spec = ChainSpec.from_dict(chain_spec_dict) 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 zero_amount = False
session = SessionBase.create_session() session = SessionBase.create_session()
status_filter = StatusBits.FINAL | StatusBits.NODE_ERROR | StatusBits.NETWORK_ERROR | StatusBits.UNKNOWN_ERROR 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 zero_amount = True
session.flush() session.flush()
queue = self.request.delivery_info.get('routing_key') # finally determine the value to send
#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
refill_amount = 0 refill_amount = 0
if not zero_amount: if not zero_amount:
refill_amount = self.safe_gas_refill_amount refill_amount = self.safe_gas_refill_amount
logg.debug('tx send gas amount {} from provider {} to {}'.format(refill_amount, gas_provider, recipient_address)) # determine sender
# # create and sign transaction gas_provider = AccountRole.get_address('GAS_GIFTER', session=session)
# tx_send_gas = { session.flush()
# '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)
# 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)) 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' 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) 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( s_status = celery.signature(
'cic_eth.queue.tx.set_ready', 'cic_eth.queue.tx.set_ready',
[ [

View File

@ -13,3 +13,4 @@ from tests.fixtures_role import *
from chainlib.eth.pytest import * from chainlib.eth.pytest import *
from contract_registry.pytest import * from contract_registry.pytest import *
from cic_eth_registry.pytest.fixtures_contracts import * from cic_eth_registry.pytest.fixtures_contracts import *
from cic_eth_registry.pytest.fixtures_tokens import *

View File

@ -13,7 +13,7 @@ logg = logging.getLogger()
def celery_includes(): def celery_includes():
return [ return [
# 'cic_eth.eth.bancor', # 'cic_eth.eth.bancor',
# 'cic_eth.eth.token', 'cic_eth.eth.erc20',
'cic_eth.eth.tx', 'cic_eth.eth.tx',
# 'cic_eth.ext.tx', # 'cic_eth.ext.tx',
'cic_eth.queue.tx', 'cic_eth.queue.tx',

View File

@ -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