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

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']
#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',
[

View File

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

View File

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

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