Compare commits
2 Commits
master
...
lash/fixtu
Author | SHA1 | Date | |
---|---|---|---|
|
f951cc2f69 | ||
|
6a45d5faa7 |
@ -204,6 +204,82 @@ class Api:
|
||||
# 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):
|
||||
"""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):
|
||||
"""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,
|
||||
PermanentTxError,
|
||||
OutOfGasError,
|
||||
YouAreBrokeError,
|
||||
)
|
||||
from cic_eth.queue.tx import register_tx
|
||||
from cic_eth.eth.gas import (
|
||||
@ -71,6 +72,117 @@ def balance(tokens, holder_address, chain_spec_dict):
|
||||
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)
|
||||
def transfer(self, tokens, holder_address, receiver_address, value, chain_spec_dict):
|
||||
"""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))
|
||||
tokens.append({
|
||||
'address': token_address,
|
||||
'symbol': token_symbol,
|
||||
'converters': [],
|
||||
})
|
||||
rpc.disconnect()
|
||||
@ -279,6 +392,48 @@ def cache_transfer_data(
|
||||
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)
|
||||
def cache_approve_data(
|
||||
tx_hash_hex,
|
||||
|
@ -2,13 +2,13 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
# external imports
|
||||
import pytest
|
||||
import confini
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
root_dir = os.path.dirname(script_dir)
|
||||
logg = logging.getLogger(__file__)
|
||||
root_dir = os.path.dirname(os.path.dirname(script_dir))
|
||||
logg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
@ -37,7 +37,8 @@ def init_database(
|
||||
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')
|
||||
migrationsdir = os.path.join(dbdir, 'migrations', load_config.get('DATABASE_ENGINE'))
|
||||
if not os.path.isdir(migrationsdir):
|
@ -17,11 +17,11 @@ root_dir = os.path.dirname(script_dir)
|
||||
sys.path.insert(0, root_dir)
|
||||
|
||||
# assemble fixtures
|
||||
from tests.fixtures_config import *
|
||||
from tests.fixtures_database import *
|
||||
from tests.fixtures_celery import *
|
||||
from tests.fixtures_role import *
|
||||
from tests.fixtures_contract import *
|
||||
from cic_eth.pytest.fixtures_config import *
|
||||
from cic_eth.pytest.fixtures_celery import *
|
||||
from cic_eth.pytest.fixtures_database import *
|
||||
from cic_eth.pytest.fixtures_role import *
|
||||
from cic_eth.pytest.fixtures_contract import *
|
||||
from chainlib.eth.pytest import *
|
||||
from eth_contract_registry.pytest import *
|
||||
from cic_eth_registry.pytest.fixtures_contracts import *
|
||||
|
Loading…
Reference in New Issue
Block a user