adding cic-eth as sub dir
This commit is contained in:
16
apps/cic-eth/cic_eth/eth/__init__.py
Normal file
16
apps/cic-eth/cic_eth/eth/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Ethereum batch functions and utilities
|
||||
|
||||
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
|
||||
|
||||
"""
|
||||
# standard imports
|
||||
import os
|
||||
|
||||
# local imports
|
||||
from .rpc import RpcClient
|
||||
|
||||
registry_extra_identifiers = {
|
||||
'Faucet': '0x{:0<64s}'.format(b'Faucet'.hex()),
|
||||
'TransferApproval': '0x{:0<64s}'.format(b'TransferApproval'.hex()),
|
||||
}
|
||||
|
||||
365
apps/cic-eth/cic_eth/eth/account.py
Normal file
365
apps/cic-eth/cic_eth/eth/account.py
Normal file
@@ -0,0 +1,365 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
import web3
|
||||
import celery
|
||||
from cic_registry import CICRegistry
|
||||
from cic_registry.chain import ChainSpec
|
||||
from erc20_single_shot_faucet import Faucet
|
||||
from cic_registry import zero_address
|
||||
|
||||
# local import
|
||||
from cic_eth.eth import RpcClient
|
||||
from cic_eth.eth import registry_extra_identifiers
|
||||
from cic_eth.eth.task import sign_and_register_tx
|
||||
from cic_eth.eth.task import create_check_gas_and_send_task
|
||||
from cic_eth.eth.factory import TxFactory
|
||||
from cic_eth.db.models.nonce import Nonce
|
||||
from cic_eth.db.models.base import SessionBase
|
||||
from cic_eth.db.models.role import AccountRole
|
||||
from cic_eth.db.models.tx import TxCache
|
||||
from cic_eth.eth.util import unpack_signed_raw_tx
|
||||
from cic_eth.error import RoleMissingError
|
||||
|
||||
#logg = logging.getLogger(__name__)
|
||||
logg = logging.getLogger()
|
||||
celery_app = celery.current_app
|
||||
|
||||
|
||||
class AccountTxFactory(TxFactory):
|
||||
"""Factory for creating account index contract transactions
|
||||
"""
|
||||
def add(
|
||||
self,
|
||||
address,
|
||||
chain_spec,
|
||||
):
|
||||
"""Register an Ethereum account address with the on-chain account registry
|
||||
|
||||
:param address: Ethereum account address to add
|
||||
:type address: str, 0x-hex
|
||||
:param chain_spec: Chain to build transaction for
|
||||
:type chain_spec: cic_registry.chain.ChainSpec
|
||||
:returns: Unsigned "AccountRegistry.add" transaction in standard Ethereum format
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
c = CICRegistry.get_contract(chain_spec, 'AccountRegistry')
|
||||
f = c.function('add')
|
||||
tx_add_buildable = f(
|
||||
address,
|
||||
)
|
||||
gas = c.gas('add')
|
||||
tx_add = tx_add_buildable.buildTransaction({
|
||||
'from': self.address,
|
||||
'gas': gas,
|
||||
'gasPrice': self.gas_price,
|
||||
'chainId': chain_spec.chain_id(),
|
||||
'nonce': self.next_nonce(),
|
||||
'value': 0,
|
||||
})
|
||||
return tx_add
|
||||
|
||||
|
||||
def gift(
|
||||
self,
|
||||
address,
|
||||
chain_spec,
|
||||
):
|
||||
"""Trigger the on-chain faucet to disburse tokens to the provided Ethereum account
|
||||
|
||||
:param address: Ethereum account address to gift to
|
||||
:type address: str, 0x-hex
|
||||
:param chain_spec: Chain to build transaction for
|
||||
:type chain_spec: cic_registry.chain.ChainSpec
|
||||
:returns: Unsigned "Faucet.giveTo" transaction in standard Ethereum format
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
c = CICRegistry.get_contract(chain_spec, 'Faucet')
|
||||
f = c.function('giveTo')
|
||||
tx_add_buildable = f(address)
|
||||
gas = c.gas('add')
|
||||
tx_add = tx_add_buildable.buildTransaction({
|
||||
'from': self.address,
|
||||
'gas': gas,
|
||||
'gasPrice': self.gas_price,
|
||||
'chainId': chain_spec.chain_id(),
|
||||
'nonce': self.next_nonce(),
|
||||
'value': 0,
|
||||
})
|
||||
return tx_add
|
||||
|
||||
|
||||
def unpack_register(data):
|
||||
"""Verifies that a transaction is an "AccountRegister.add" transaction, and extracts call parameters from it.
|
||||
|
||||
:param data: Raw input data from Ethereum transaction.
|
||||
:type data: str, 0x-hex
|
||||
:raises ValueError: Function signature does not match AccountRegister.add
|
||||
:returns: Parsed parameters
|
||||
:rtype: dict
|
||||
"""
|
||||
f = data[2:10]
|
||||
if f != '0a3b0a4f':
|
||||
raise ValueError('Invalid account index register data ({})'.format(f))
|
||||
|
||||
d = data[10:]
|
||||
return {
|
||||
'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
|
||||
}
|
||||
|
||||
|
||||
def unpack_gift(data):
|
||||
"""Verifies that a transaction is a "Faucet.giveTo" transaction, and extracts call parameters from it.
|
||||
|
||||
:param data: Raw input data from Ethereum transaction.
|
||||
:type data: str, 0x-hex
|
||||
:raises ValueError: Function signature does not match AccountRegister.add
|
||||
:returns: Parsed parameters
|
||||
:rtype: dict
|
||||
"""
|
||||
f = data[2:10]
|
||||
if f != '63e4bff4':
|
||||
raise ValueError('Invalid account index register data ({})'.format(f))
|
||||
|
||||
d = data[10:]
|
||||
return {
|
||||
'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
|
||||
}
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def create(password, chain_str):
|
||||
"""Creates and stores a new ethereum account in the keystore.
|
||||
|
||||
The password is passed on to the wallet backend, no encryption is performed in the task worker.
|
||||
|
||||
:param password: Password to encrypt private key with
|
||||
:type password: str
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Ethereum address of newly created account
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
c = RpcClient(chain_spec)
|
||||
a = c.w3.eth.personal.new_account(password)
|
||||
logg.debug('created account {}'.format(a))
|
||||
|
||||
# Initialize nonce provider record for account
|
||||
n = c.w3.eth.getTransactionCount(a, 'pending')
|
||||
session = SessionBase.create_session()
|
||||
o = session.query(Nonce).filter(Nonce.address_hex==a).first()
|
||||
if o == None:
|
||||
o = Nonce()
|
||||
o.address_hex = a
|
||||
o.nonce = n
|
||||
session.add(o)
|
||||
session.commit()
|
||||
session.close()
|
||||
return a
|
||||
|
||||
|
||||
@celery_app.task(bind=True, throws=(RoleMissingError,))
|
||||
def register(self, account_address, chain_str, writer_address=None):
|
||||
"""Creates a transaction to add the given address to the accounts index.
|
||||
|
||||
:param account_address: Ethereum address to add
|
||||
:type account_address: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:param writer_address: Specify address in keystore to sign transaction. Overrides local accounts role setting.
|
||||
:type writer_address: str, 0x-hex
|
||||
:raises RoleMissingError: Writer address not set and writer role not found.
|
||||
:returns: The account_address input param
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
if writer_address == None:
|
||||
writer_address = AccountRole.get_address('ACCOUNTS_INDEX_WRITER')
|
||||
|
||||
if writer_address == zero_address:
|
||||
raise RoleMissingError(account_address)
|
||||
|
||||
|
||||
logg.debug('adding account address {} to index; writer {}'.format(account_address, writer_address))
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
c = RpcClient(chain_spec, holder_address=writer_address)
|
||||
txf = AccountTxFactory(writer_address, c)
|
||||
|
||||
tx_add = txf.add(account_address, chain_spec)
|
||||
(tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_add, chain_str, queue, 'cic_eth.eth.account.cache_account_data')
|
||||
|
||||
gas_budget = tx_add['gas'] * tx_add['gasPrice']
|
||||
|
||||
logg.debug('register user tx {}'.format(tx_hash_hex))
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_signed_raw_hex],
|
||||
chain_str,
|
||||
writer_address,
|
||||
gas_budget,
|
||||
tx_hashes_hex=[tx_hash_hex],
|
||||
queue=queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return account_address
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def gift(self, account_address, chain_str):
|
||||
"""Creates a transaction to invoke the faucet contract for the given address.
|
||||
|
||||
:param account_address: Ethereum address to give to
|
||||
:type account_address: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Raw signed transaction
|
||||
:rtype: list with transaction as only element
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
logg.debug('gift account address {} to index'.format(account_address))
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
c = RpcClient(chain_spec, holder_address=account_address)
|
||||
txf = AccountTxFactory(account_address, c)
|
||||
|
||||
tx_add = txf.gift(account_address, chain_spec)
|
||||
(tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_add, chain_str, queue, 'cic_eth.eth.account.cache_gift_data')
|
||||
|
||||
gas_budget = tx_add['gas'] * tx_add['gasPrice']
|
||||
|
||||
logg.debug('register user tx {}'.format(tx_hash_hex))
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_signed_raw_hex],
|
||||
chain_str,
|
||||
account_address,
|
||||
gas_budget,
|
||||
[tx_hash_hex],
|
||||
queue=queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return [tx_signed_raw_hex]
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def have(self, account, chain_str):
|
||||
"""Check whether the given account exists in keystore
|
||||
|
||||
:param account: Account to check
|
||||
:type account: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Account, or None if not exists
|
||||
:rtype: Varies
|
||||
"""
|
||||
c = RpcClient(account)
|
||||
try:
|
||||
c.w3.eth.sign(account, text='2a')
|
||||
return account
|
||||
except Exception as e:
|
||||
logg.debug('cannot sign with {}: {}'.format(account, e))
|
||||
return None
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def role(self, account, chain_str):
|
||||
"""Return account role for address
|
||||
|
||||
:param account: Account to check
|
||||
:type account: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Account, or None if not exists
|
||||
:rtype: Varies
|
||||
"""
|
||||
return AccountRole.role_for(account)
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def cache_gift_data(
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
):
|
||||
"""Generates and commits transaction cache metadata for a Faucet.giveTo transaction
|
||||
|
||||
:param tx_hash_hex: Transaction hash
|
||||
:type tx_hash_hex: str, 0x-hex
|
||||
:param tx_signed_raw_hex: Raw signed transaction
|
||||
:type tx_signed_raw_hex: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||
:rtype: tuple
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
c = RpcClient(chain_spec)
|
||||
|
||||
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||
tx_data = unpack_gift(tx['data'])
|
||||
|
||||
tx_cache = TxCache(
|
||||
tx_hash_hex,
|
||||
tx['from'],
|
||||
tx['to'],
|
||||
zero_address,
|
||||
zero_address,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
session = SessionBase.create_session()
|
||||
session.add(tx_cache)
|
||||
session.commit()
|
||||
cache_id = tx_cache.id
|
||||
session.close()
|
||||
return (tx_hash_hex, cache_id)
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def cache_account_data(
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
):
|
||||
"""Generates and commits transaction cache metadata for an AccountsIndex.add transaction
|
||||
|
||||
:param tx_hash_hex: Transaction hash
|
||||
:type tx_hash_hex: str, 0x-hex
|
||||
:param tx_signed_raw_hex: Raw signed transaction
|
||||
:type tx_signed_raw_hex: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||
:rtype: tuple
|
||||
"""
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
c = RpcClient(chain_spec)
|
||||
|
||||
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||
tx_data = unpack_register(tx['data'])
|
||||
|
||||
tx_cache = TxCache(
|
||||
tx_hash_hex,
|
||||
tx['from'],
|
||||
tx['to'],
|
||||
zero_address,
|
||||
zero_address,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
session = SessionBase.create_session()
|
||||
session.add(tx_cache)
|
||||
session.commit()
|
||||
cache_id = tx_cache.id
|
||||
session.close()
|
||||
return (tx_hash_hex, cache_id)
|
||||
385
apps/cic-eth/cic_eth/eth/bancor.py
Normal file
385
apps/cic-eth/cic_eth/eth/bancor.py
Normal file
@@ -0,0 +1,385 @@
|
||||
# standard imports
|
||||
import os
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
import celery
|
||||
import web3
|
||||
from cic_registry import CICRegistry
|
||||
from cic_registry.chain import ChainSpec
|
||||
|
||||
# local imports
|
||||
from cic_eth.db import SessionBase
|
||||
from cic_eth.db.models.convert import TxConvertTransfer
|
||||
from cic_eth.db.models.otx import Otx
|
||||
from cic_eth.db.models.tx import TxCache
|
||||
from cic_eth.eth.task import sign_and_register_tx
|
||||
from cic_eth.eth.task import create_check_gas_and_send_task
|
||||
from cic_eth.eth.token import TokenTxFactory
|
||||
from cic_eth.eth.factory import TxFactory
|
||||
from cic_eth.eth.util import unpack_signed_raw_tx
|
||||
from cic_eth.eth.rpc import RpcClient
|
||||
|
||||
celery_app = celery.current_app
|
||||
#logg = celery_app.log.get_default_logger()
|
||||
logg = logging.getLogger()
|
||||
|
||||
contract_function_signatures = {
|
||||
'convert': 'f3898a97',
|
||||
'convert2': '569706eb',
|
||||
}
|
||||
|
||||
|
||||
class BancorTxFactory(TxFactory):
|
||||
|
||||
"""Factory for creating Bancor network transactions.
|
||||
"""
|
||||
def convert(
|
||||
self,
|
||||
source_token_address,
|
||||
destination_token_address,
|
||||
reserve_address,
|
||||
source_amount,
|
||||
minimum_return,
|
||||
chain_spec,
|
||||
fee_beneficiary='0x0000000000000000000000000000000000000000',
|
||||
fee_ppm=0,
|
||||
):
|
||||
"""Create a BancorNetwork "convert" transaction.
|
||||
|
||||
:param source_token_address: ERC20 contract address for token to convert from
|
||||
:type source_token_address: str, 0x-hex
|
||||
:param destination_token_address: ERC20 contract address for token to convert to
|
||||
:type destination_token_address: str, 0x-hex
|
||||
:param reserve_address: ERC20 contract address of Common reserve token
|
||||
:type reserve_address: str, 0x-hex
|
||||
:param source_amount: Amount of source tokens to convert
|
||||
:type source_amount: int
|
||||
:param minimum_return: Minimum amount of destination tokens to accept as result for conversion
|
||||
:type source_amount: int
|
||||
:return: Unsigned "convert" transaction in standard Ethereum format
|
||||
:rtype: dict
|
||||
"""
|
||||
network_contract = CICRegistry.get_contract(chain_spec, 'BancorNetwork')
|
||||
network_gas = network_contract.gas('convert')
|
||||
tx_convert_buildable = network_contract.contract.functions.convert2(
|
||||
[
|
||||
source_token_address,
|
||||
source_token_address,
|
||||
reserve_address,
|
||||
destination_token_address,
|
||||
destination_token_address,
|
||||
],
|
||||
source_amount,
|
||||
minimum_return,
|
||||
fee_beneficiary,
|
||||
fee_ppm,
|
||||
)
|
||||
tx_convert = tx_convert_buildable.buildTransaction({
|
||||
'from': self.address,
|
||||
'gas': network_gas,
|
||||
'gasPrice': self.gas_price,
|
||||
'chainId': chain_spec.chain_id(),
|
||||
'nonce': self.next_nonce(),
|
||||
})
|
||||
return tx_convert
|
||||
|
||||
|
||||
def unpack_convert(data):
|
||||
f = data[2:10]
|
||||
if f != contract_function_signatures['convert2']:
|
||||
raise ValueError('Invalid convert data ({})'.format(f))
|
||||
|
||||
d = data[10:]
|
||||
path = d[384:]
|
||||
source = path[64-40:64]
|
||||
destination = path[-40:]
|
||||
|
||||
amount = int(d[64:128], 16)
|
||||
min_return = int(d[128:192], 16)
|
||||
fee_recipient = d[192:256]
|
||||
fee = int(d[256:320], 16)
|
||||
return {
|
||||
'amount': amount,
|
||||
'min_return': min_return,
|
||||
'source_token': web3.Web3.toChecksumAddress('0x' + source),
|
||||
'destination_token': web3.Web3.toChecksumAddress('0x' + destination),
|
||||
'fee_recipient': fee_recipient,
|
||||
'fee': fee,
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Kept for historical reference, it unpacks a convert call without fee parameters
|
||||
#def _unpack_convert_mint(data):
|
||||
# f = data[2:10]
|
||||
# if f != contract_function_signatures['convert2']:
|
||||
# raise ValueError('Invalid convert data ({})'.format(f))
|
||||
#
|
||||
# d = data[10:]
|
||||
# path = d[256:]
|
||||
# source = path[64-40:64]
|
||||
# destination = path[-40:]
|
||||
#
|
||||
# amount = int(d[64:128], 16)
|
||||
# min_return = int(d[128:192], 16)
|
||||
# return {
|
||||
# 'amount': amount,
|
||||
# 'min_return': min_return,
|
||||
# 'source_token': web3.Web3.toChecksumAddress('0x' + source),
|
||||
# 'destination_token': web3.Web3.toChecksumAddress('0x' + destination),
|
||||
# }
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def convert_with_default_reserve(self, tokens, from_address, source_amount, minimum_return, to_address, chain_str):
|
||||
"""Performs a conversion between two liquid tokens using Bancor network.
|
||||
|
||||
:param tokens: Token pair, source and destination respectively
|
||||
:type tokens: list of str, 0x-hex
|
||||
:param from_address: Ethereum address of sender
|
||||
:type from_address: str, 0x-hex
|
||||
:param source_amount: Amount of source tokens to convert
|
||||
:type source_amount: int
|
||||
:param minimum_return: Minimum about of destination tokens to receive
|
||||
:type minimum_return: int
|
||||
"""
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
c = RpcClient(chain_spec, holder_address=from_address)
|
||||
|
||||
cr = CICRegistry.get_contract(chain_spec, 'BancorNetwork')
|
||||
source_token = CICRegistry.get_address(chain_spec, tokens[0]['address'])
|
||||
reserve_address = CICRegistry.get_contract(chain_spec, 'BNTToken', 'ERC20').address()
|
||||
|
||||
tx_factory = TokenTxFactory(from_address, c)
|
||||
|
||||
tx_approve_zero = tx_factory.approve(source_token.address(), cr.address(), 0, chain_spec)
|
||||
(tx_approve_zero_hash_hex, tx_approve_zero_signed_hex) = sign_and_register_tx(tx_approve_zero, chain_str, queue, 'cic_eth.eth.token.otx_cache_approve')
|
||||
|
||||
tx_approve = tx_factory.approve(source_token.address(), cr.address(), source_amount, chain_spec)
|
||||
(tx_approve_hash_hex, tx_approve_signed_hex) = sign_and_register_tx(tx_approve, chain_str, queue, 'cic_eth.eth.token.otx_cache_approve')
|
||||
|
||||
tx_factory = BancorTxFactory(from_address, c)
|
||||
tx_convert = tx_factory.convert(
|
||||
tokens[0]['address'],
|
||||
tokens[1]['address'],
|
||||
reserve_address,
|
||||
source_amount,
|
||||
minimum_return,
|
||||
chain_spec,
|
||||
)
|
||||
(tx_convert_hash_hex, tx_convert_signed_hex) = sign_and_register_tx(tx_convert, chain_str, queue, 'cic_eth.eth.bancor.otx_cache_convert')
|
||||
|
||||
# TODO: consider moving save recipient to async task / chain it before the tx send
|
||||
if to_address != None:
|
||||
save_convert_recipient(tx_convert_hash_hex, to_address, chain_str)
|
||||
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_approve_zero_signed_hex, tx_approve_signed_hex, tx_convert_signed_hex],
|
||||
chain_str,
|
||||
from_address,
|
||||
tx_approve_zero['gasPrice'] * tx_approve_zero['gas'],
|
||||
tx_hashes_hex=[tx_approve_hash_hex],
|
||||
queue=queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return tx_convert_hash_hex
|
||||
|
||||
|
||||
#@celery_app.task()
|
||||
#def process_approval(tx_hash_hex):
|
||||
# t = session.query(TxConvertTransfer).query(TxConvertTransfer.approve_tx_hash==tx_hash_hex).first()
|
||||
# c = session.query(Otx).query(Otx.tx_hash==t.convert_tx_hash)
|
||||
# gas_limit = 8000000
|
||||
# gas_price = GasOracle.gas_price()
|
||||
#
|
||||
# # TODO: use celery group instead
|
||||
# s_queue = celery.signature(
|
||||
# 'cic_eth.queue.tx.create',
|
||||
# [
|
||||
# nonce,
|
||||
# c['address'], # TODO: check that this is in fact sender address
|
||||
# c['tx_hash'],
|
||||
# c['signed_tx'],
|
||||
# ]
|
||||
# )
|
||||
# s_queue.apply_async()
|
||||
#
|
||||
# s_check_gas = celery.signature(
|
||||
# 'cic_eth.eth.tx.check_gas',
|
||||
# [
|
||||
# c['address'],
|
||||
# [c['signed_tx']],
|
||||
# gas_limit * gas_price,
|
||||
# ]
|
||||
# )
|
||||
# s_send = celery.signature(
|
||||
# 'cic_eth.eth.tx.send',
|
||||
# [],
|
||||
# )
|
||||
#
|
||||
# s_set_sent = celery.signature(
|
||||
# 'cic_eth.queue.tx.set_sent_status',
|
||||
# [False],
|
||||
# )
|
||||
# s_send.link(s_set_sent)
|
||||
# s_check_gas.link(s_send)
|
||||
# s_check_gas.apply_async()
|
||||
# return tx_hash_hex
|
||||
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def save_convert_recipient(convert_hash, recipient_address, chain_str):
|
||||
"""Registers the recipient target for a convert-and-transfer operation.
|
||||
|
||||
:param convert_hash: Transaction hash of convert operation
|
||||
:type convert_hash: str, 0x-hex
|
||||
:param recipient_address: Address of consequtive transfer recipient
|
||||
:type recipient_address: str, 0x-hex
|
||||
"""
|
||||
session = SessionBase.create_session()
|
||||
t = TxConvertTransfer(convert_hash, recipient_address, chain_str)
|
||||
session.add(t)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def save_convert_transfer(convert_hash, transfer_hash):
|
||||
"""Registers that the transfer part of a convert-and-transfer operation has been executed.
|
||||
|
||||
:param convert_hash: Transaction hash of convert operation
|
||||
:type convert_hash: str, 0x-hex
|
||||
:param convert_hash: Transaction hash of transfer operation
|
||||
:type convert_hash: str, 0x-hex
|
||||
:returns: transfer_hash,
|
||||
:rtype: list, single str, 0x-hex
|
||||
"""
|
||||
session = SessionBase.create_session()
|
||||
t = TxConvertTransfer.get(convert_hash)
|
||||
t.transfer(transfer_hash)
|
||||
session.add(t)
|
||||
session.commit()
|
||||
session.close()
|
||||
return [transfer_hash]
|
||||
|
||||
|
||||
# TODO: seems unused, consider removing
|
||||
@celery_app.task()
|
||||
def resolve_converters_by_tokens(tokens, chain_str):
|
||||
"""Return converters for a list of tokens.
|
||||
|
||||
:param tokens: Token addresses to look up
|
||||
:type tokens: list of str, 0x-hex
|
||||
:return: Addresses of matching converters
|
||||
:rtype: list of str, 0x-hex
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
for t in tokens:
|
||||
c = CICRegistry.get_contract(chain_spec, 'ConverterRegistry')
|
||||
fn = c.function('getConvertersByAnchors')
|
||||
try:
|
||||
converters = fn([t['address']]).call()
|
||||
except Exception as e:
|
||||
raise e
|
||||
t['converters'] = converters
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def transfer_converted(self, tokens, holder_address, receiver_address, value, tx_convert_hash_hex, chain_str):
|
||||
"""Execute the ERC20 transfer of a convert-and-transfer operation.
|
||||
|
||||
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 holder_address: Token receiver address
|
||||
:type holder_address: str, 0x-hex
|
||||
:param value: Amount of token, in 'wei'
|
||||
:type value: int
|
||||
:raises TokenCountError: Either none or more then one tokens have been passed as tokens argument
|
||||
:return: Transaction hash
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
# we only allow one token, one transfer
|
||||
if len(tokens) != 1:
|
||||
raise TokenCountError
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
c = RpcClient(chain_spec, holder_address=holder_address)
|
||||
|
||||
# get transaction parameters
|
||||
gas_price = c.gas_price()
|
||||
tx_factory = TokenTxFactory(holder_address, c)
|
||||
|
||||
token_address = tokens[0]['address']
|
||||
tx_transfer = tx_factory.transfer(
|
||||
token_address,
|
||||
receiver_address,
|
||||
value,
|
||||
chain_spec,
|
||||
)
|
||||
(tx_transfer_hash_hex, tx_transfer_signed_hex) = sign_and_register_tx(tx_transfer, chain_str, queue, 'cic_eth.eth.token.otx_cache_transfer')
|
||||
|
||||
# send transaction
|
||||
logg.info('transfer converted token {} from {} to {} value {} {}'.format(token_address, holder_address, receiver_address, value, tx_transfer_signed_hex))
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_transfer_signed_hex],
|
||||
chain_str,
|
||||
holder_address,
|
||||
tx_transfer['gasPrice'] * tx_transfer['gas'],
|
||||
None,
|
||||
queue,
|
||||
)
|
||||
s_save = celery.signature(
|
||||
'cic_eth.eth.bancor.save_convert_transfer',
|
||||
[
|
||||
tx_convert_hash_hex,
|
||||
tx_transfer_hash_hex,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
s_save.link(s)
|
||||
s_save.apply_async()
|
||||
return tx_transfer_hash_hex
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def otx_cache_convert(
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
):
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||
tx_data = unpack_convert(tx['data'])
|
||||
logg.debug('tx data {}'.format(tx_data))
|
||||
|
||||
session = TxCache.create_session()
|
||||
tx_cache = TxCache(
|
||||
tx_hash_hex,
|
||||
tx['from'],
|
||||
tx['from'],
|
||||
tx_data['source_token'],
|
||||
tx_data['destination_token'],
|
||||
tx_data['amount'],
|
||||
tx_data['amount'],
|
||||
)
|
||||
session.add(tx_cache)
|
||||
session.commit()
|
||||
session.close()
|
||||
return tx_hash_hex
|
||||
|
||||
41
apps/cic-eth/cic_eth/eth/factory.py
Normal file
41
apps/cic-eth/cic_eth/eth/factory.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# 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):
|
||||
self.address = from_address
|
||||
|
||||
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):
|
||||
"""Returns the current cached nonce value, and increments it for next transaction.
|
||||
|
||||
:returns: Nonce
|
||||
:rtype: number
|
||||
"""
|
||||
return self.nonce_oracle.next()
|
||||
71
apps/cic-eth/cic_eth/eth/gas.py
Normal file
71
apps/cic-eth/cic_eth/eth/gas.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# local imports
|
||||
from cic_eth.db.models.role import AccountRole
|
||||
from cic_eth.db.models.base import SessionBase
|
||||
|
||||
logg = logging.getLogger()
|
||||
|
||||
|
||||
class GasOracle():
|
||||
"""Provides gas pricing for transactions.
|
||||
|
||||
:param w3: Web3 object
|
||||
:type w3: web3.Web3
|
||||
"""
|
||||
|
||||
__safe_threshold_amount_value = 2000000000 * 60000 * 3
|
||||
__refill_amount_value = __safe_threshold_amount_value * 5
|
||||
default_gas_limit = 21000
|
||||
|
||||
def __init__(self, w3):
|
||||
self.w3 = w3
|
||||
self.gas_price_current = w3.eth.gas_price()
|
||||
|
||||
|
||||
def safe_threshold_amount(self):
|
||||
"""The gas balance threshold under which a new gas refill transaction should be initiated.
|
||||
|
||||
:returns: Gas token amount
|
||||
:rtype: number
|
||||
"""
|
||||
g = GasOracle.__safe_threshold_amount_value
|
||||
logg.warning('gas safe threshold is currently hardcoded to {}'.format(g))
|
||||
return g
|
||||
|
||||
|
||||
def refill_amount(self):
|
||||
"""The amount of gas tokens to send in a gas refill transaction.
|
||||
|
||||
:returns: Gas token amount
|
||||
:rtype: number
|
||||
"""
|
||||
g = GasOracle.__refill_amount_value
|
||||
logg.warning('gas refill amount is currently hardcoded to {}'.format(g))
|
||||
return g
|
||||
|
||||
|
||||
def gas_provider(self):
|
||||
"""Gas provider address.
|
||||
|
||||
:returns: Etheerum account address
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
return AccountRole.get_address('GAS_GIFTER')
|
||||
|
||||
|
||||
def gas_price(self, category='safe'):
|
||||
"""Get projected gas price to use for a transaction at the current moment.
|
||||
|
||||
When the category parameter is implemented, it can be used to control the priority of a transaction in the network.
|
||||
|
||||
:param category: Bid level category to return price for. Currently has no effect.
|
||||
:type category: str
|
||||
:returns: Gas price
|
||||
:rtype: number
|
||||
"""
|
||||
#logg.warning('gas price hardcoded to category "safe"')
|
||||
#g = 100
|
||||
#return g
|
||||
return self.gas_price_current
|
||||
23
apps/cic-eth/cic_eth/eth/nonce.py
Normal file
23
apps/cic-eth/cic_eth/eth/nonce.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# local imports
|
||||
from cic_eth.db.models.nonce import Nonce
|
||||
|
||||
class NonceOracle():
|
||||
"""Ensures atomic nonce increments for all transactions across all tasks and threads.
|
||||
|
||||
:param address: Address to generate nonces for
|
||||
:type address: str, 0x-hex
|
||||
:param default_nonce: Initial nonce value to use if no nonce cache entry already exists
|
||||
:type default_nonce: number
|
||||
"""
|
||||
def __init__(self, address, default_nonce):
|
||||
self.address = address
|
||||
self.default_nonce = default_nonce
|
||||
|
||||
|
||||
def next(self):
|
||||
"""Get next unique nonce.
|
||||
|
||||
:returns: Nonce
|
||||
:rtype: number
|
||||
"""
|
||||
return Nonce.next(self.address, self.default_nonce)
|
||||
194
apps/cic-eth/cic_eth/eth/request.py
Normal file
194
apps/cic-eth/cic_eth/eth/request.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
import web3
|
||||
import celery
|
||||
from erc20_approval_escrow import TransferApproval
|
||||
from cic_registry import CICRegistry
|
||||
from cic_registry.chain import ChainSpec
|
||||
|
||||
# local imports
|
||||
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.eth.factory import TxFactory
|
||||
from cic_eth.eth.task import sign_and_register_tx
|
||||
from cic_eth.eth.util import unpack_signed_raw_tx
|
||||
from cic_eth.eth.task import create_check_gas_and_send_task
|
||||
from cic_eth.error import TokenCountError
|
||||
|
||||
celery_app = celery.current_app
|
||||
logg = logging.getLogger()
|
||||
|
||||
contract_function_signatures = {
|
||||
'request': 'b0addede',
|
||||
}
|
||||
|
||||
|
||||
class TransferRequestTxFactory(TxFactory):
|
||||
"""Factory for creating Transfer request transactions using the TransferApproval contract backend
|
||||
"""
|
||||
def request(
|
||||
self,
|
||||
token_address,
|
||||
beneficiary_address,
|
||||
amount,
|
||||
chain_spec,
|
||||
):
|
||||
"""Create a new TransferApproval.request transaction
|
||||
|
||||
:param token_address: Token to create transfer request for
|
||||
:type token_address: str, 0x-hex
|
||||
:param beneficiary_address: Beneficiary of token transfer
|
||||
:type beneficiary_address: str, 0x-hex
|
||||
:param amount: Amount of tokens to transfer
|
||||
:type amount: number
|
||||
:param chain_spec: Chain spec
|
||||
:type chain_spec: cic_registry.chain.ChainSpec
|
||||
:returns: Transaction in standard Ethereum format
|
||||
:rtype: dict
|
||||
"""
|
||||
transfer_approval = CICRegistry.get_contract(chain_spec, 'TransferApproval', 'TransferAuthorization')
|
||||
fn = transfer_approval.function('createRequest')
|
||||
tx_approval_buildable = fn(beneficiary_address, token_address, amount)
|
||||
transfer_approval_gas = transfer_approval.gas('createRequest')
|
||||
|
||||
tx_approval = tx_approval_buildable.buildTransaction({
|
||||
'from': self.address,
|
||||
'gas': transfer_approval_gas,
|
||||
'gasPrice': self.gas_price,
|
||||
'chainId': chain_spec.chain_id(),
|
||||
'nonce': self.next_nonce(),
|
||||
})
|
||||
return tx_approval
|
||||
|
||||
|
||||
def unpack_transfer_approval_request(data):
|
||||
"""Verifies that a transaction is an "TransferApproval.request" transaction, and extracts call parameters from it.
|
||||
|
||||
:param data: Raw input data from Ethereum transaction.
|
||||
:type data: str, 0x-hex
|
||||
:raises ValueError: Function signature does not match AccountRegister.add
|
||||
:returns: Parsed parameters
|
||||
:rtype: dict
|
||||
"""
|
||||
f = data[2:10]
|
||||
if f != contract_function_signatures['request']:
|
||||
raise ValueError('Invalid transfer request data ({})'.format(f))
|
||||
|
||||
d = data[10:]
|
||||
return {
|
||||
'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
|
||||
'token': web3.Web3.toChecksumAddress('0x' + d[128-40:128]),
|
||||
'amount': int(d[128:], 16)
|
||||
}
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def transfer_approval_request(self, tokens, holder_address, receiver_address, value, chain_str):
|
||||
"""Creates a new transfer approval
|
||||
|
||||
:param tokens: Token to generate transfer request for
|
||||
:type tokens: list with single token spec as dict
|
||||
:param holder_address: Address to generate transfer on behalf of
|
||||
:type holder_address: str, 0x-hex
|
||||
:param receiver_address: Address to transfser tokens to
|
||||
:type receiver_address: str, 0x-hex
|
||||
:param value: Amount of tokens to transfer
|
||||
:type value: number
|
||||
:param chain_spec: Chain spec string representation
|
||||
:type chain_spec: str
|
||||
:raises cic_eth.error.TokenCountError: More than one token in tokens argument
|
||||
:returns: Raw signed transaction
|
||||
:rtype: list with transaction as only element
|
||||
"""
|
||||
|
||||
if len(tokens) != 1:
|
||||
raise TokenCountError
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
t = tokens[0]
|
||||
|
||||
c = RpcClient(holder_address)
|
||||
|
||||
txf = TransferRequestTxFactory(holder_address, c)
|
||||
|
||||
tx_transfer = txf.request(t['address'], receiver_address, value, chain_spec)
|
||||
(tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_transfer, chain_str, queue, 'cic_eth.eth.request.otx_cache_transfer_approval_request')
|
||||
|
||||
gas_budget = tx_transfer['gas'] * tx_transfer['gasPrice']
|
||||
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_signed_raw_hex],
|
||||
chain_str,
|
||||
holder_address,
|
||||
gas_budget,
|
||||
[tx_hash_hex],
|
||||
queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return [tx_signed_raw_hex]
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def otx_cache_transfer_approval_request(
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
):
|
||||
"""Generates and commits transaction cache metadata for an TransferApproval.request transaction
|
||||
|
||||
:param tx_hash_hex: Transaction hash
|
||||
:type tx_hash_hex: str, 0x-hex
|
||||
:param tx_signed_raw_hex: Raw signed transaction
|
||||
:type tx_signed_raw_hex: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||
:rtype: tuple
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||
logg.debug('in otx acche transfer approval request')
|
||||
(txc, cache_id) = cache_transfer_approval_request_data(tx_hash_hex, tx)
|
||||
return txc
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def cache_transfer_approval_request_data(
|
||||
tx_hash_hex,
|
||||
tx,
|
||||
):
|
||||
"""Helper function for otx_cache_transfer_approval_request
|
||||
|
||||
: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
|
||||
"""
|
||||
tx_data = unpack_transfer_approval_request(tx['data'])
|
||||
logg.debug('tx approval request data {}'.format(tx_data))
|
||||
logg.debug('tx approval request {}'.format(tx))
|
||||
|
||||
session = SessionBase.create_session()
|
||||
tx_cache = TxCache(
|
||||
tx_hash_hex,
|
||||
tx['from'],
|
||||
tx_data['to'],
|
||||
tx_data['token'],
|
||||
tx_data['token'],
|
||||
tx_data['amount'],
|
||||
tx_data['amount'],
|
||||
)
|
||||
session.add(tx_cache)
|
||||
session.commit()
|
||||
cache_id = tx_cache.id
|
||||
session.close()
|
||||
return (tx_hash_hex, cache_id)
|
||||
39
apps/cic-eth/cic_eth/eth/rpc.py
Normal file
39
apps/cic-eth/cic_eth/eth/rpc.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# local imports
|
||||
from cic_eth.eth.gas import GasOracle
|
||||
|
||||
logg = logging.getLogger()
|
||||
|
||||
|
||||
class RpcClient(GasOracle):
|
||||
"""RPC wrapper for web3 enabling gas calculation helpers and signer middleware.
|
||||
|
||||
:param chain_spec: Chain spec
|
||||
:type chain_spec: cic_registry.chain.ChainSpec
|
||||
:param holder_address: DEPRECATED Address of subject of the session.
|
||||
:type holder_address: str, 0x-hex
|
||||
"""
|
||||
|
||||
signer_ipc_path = None
|
||||
"""Unix socket path to JSONRPC signer and keystore"""
|
||||
|
||||
web3_constructor = None
|
||||
"""Custom function to build a web3 object with middleware plugins"""
|
||||
|
||||
|
||||
def __init__(self, chain_spec, holder_address=None):
|
||||
(self.provider, w3) = RpcClient.web3_constructor()
|
||||
super(RpcClient, self).__init__(w3)
|
||||
self.chain_spec = chain_spec
|
||||
if holder_address != None:
|
||||
self.holder_address = holder_address
|
||||
logg.info('gasprice {}'.format(self.gas_price()))
|
||||
|
||||
|
||||
@staticmethod
|
||||
def set_constructor(web3_constructor):
|
||||
"""Sets the constructor to use for building the web3 object.
|
||||
"""
|
||||
RpcClient.web3_constructor = web3_constructor
|
||||
136
apps/cic-eth/cic_eth/eth/task.py
Normal file
136
apps/cic-eth/cic_eth/eth/task.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
import celery
|
||||
from cic_registry.chain import ChainSpec
|
||||
|
||||
# local imports
|
||||
from cic_eth.eth import RpcClient
|
||||
from cic_eth.queue.tx import create as queue_create
|
||||
|
||||
celery_app = celery.current_app
|
||||
logg = celery_app.log.get_default_logger()
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def sign_tx(tx, chain_str):
|
||||
"""Sign a single transaction against the given chain specification.
|
||||
|
||||
:param tx: Transaction in standard Ethereum format
|
||||
:type tx: dict
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Transaction hash and raw signed transaction, respectively
|
||||
:rtype: tuple
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
c = RpcClient(chain_spec)
|
||||
tx_transfer_signed = c.w3.eth.sign_transaction(tx)
|
||||
logg.debug('tx_transfer_signed {}'.format(tx_transfer_signed))
|
||||
tx_hash = c.w3.keccak(hexstr=tx_transfer_signed['raw'])
|
||||
tx_hash_hex = tx_hash.hex()
|
||||
return (tx_hash_hex, tx_transfer_signed['raw'],)
|
||||
|
||||
|
||||
def sign_and_register_tx(tx, chain_str, queue, cache_task=None):
|
||||
"""Signs the provided transaction, and adds it to the transaction queue cache (with status PENDING).
|
||||
|
||||
:param tx: Standard ethereum transaction data
|
||||
:type tx: dict
|
||||
:param chain_str: Chain spec, string representation
|
||||
:type chain_str: str
|
||||
:param queue: Task queue
|
||||
:type queue: str
|
||||
:param cache_task: Cache task to call with signed transaction. If None, no task will be called.
|
||||
:type cache_task: str
|
||||
:returns: Tuple; Transaction hash, signed raw transaction data
|
||||
:rtype: tuple
|
||||
"""
|
||||
(tx_hash_hex, tx_signed_raw_hex) = sign_tx(tx, chain_str)
|
||||
|
||||
logg.debug('adding queue tx {}'.format(tx_hash_hex))
|
||||
|
||||
# s = celery.signature(
|
||||
# 'cic_eth.queue.tx.create',
|
||||
# [
|
||||
# tx['nonce'],
|
||||
# tx['from'],
|
||||
# tx_hash_hex,
|
||||
# tx_signed_raw_hex,
|
||||
# chain_str,
|
||||
# ],
|
||||
# queue=queue,
|
||||
# )
|
||||
|
||||
# TODO: consider returning this as a signature that consequtive tasks can be linked to
|
||||
queue_create(
|
||||
tx['nonce'],
|
||||
tx['from'],
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
)
|
||||
|
||||
if cache_task != None:
|
||||
logg.debug('adding cache task {} tx {}'.format(cache_task, tx_hash_hex))
|
||||
s_cache = celery.signature(
|
||||
cache_task,
|
||||
[
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
s_cache.apply_async()
|
||||
|
||||
return (tx_hash_hex, tx_signed_raw_hex,)
|
||||
|
||||
|
||||
# TODO: rename as we will not be sending task in the chain, this is the responsibility of the dispatcher
|
||||
def create_check_gas_and_send_task(tx_signed_raws_hex, chain_str, holder_address, gas, tx_hashes_hex=None, queue=None):
|
||||
"""Creates a celery task signature for a check_gas task that adds the task to the outgoing queue to be processed by the dispatcher.
|
||||
|
||||
If tx_hashes_hex is not spefified, a preceding task chained to check_gas must supply the transaction hashes as its return value.
|
||||
|
||||
:param tx_signed_raws_hex: Raw signed transaction data
|
||||
:type tx_signed_raws_hex: list of str, 0x-hex
|
||||
:param chain_str: Chain spec, string representation
|
||||
:type chain_str: str
|
||||
:param holder_address: Address sending the transactions
|
||||
:type holder_address: str, 0x-hex
|
||||
:param gas: Gas budget hint for transactions
|
||||
:type gas: int
|
||||
:param tx_hashes_hex: Transaction hashes
|
||||
:type tx_hashes_hex: list of str, 0x-hex
|
||||
:param queue: Task queue
|
||||
:type queue: str
|
||||
:returns: Signature of task chain
|
||||
:rtype: celery.Signature
|
||||
"""
|
||||
s_check_gas = None
|
||||
if tx_hashes_hex != None:
|
||||
s_check_gas = celery.signature(
|
||||
'cic_eth.eth.tx.check_gas',
|
||||
[
|
||||
tx_hashes_hex,
|
||||
chain_str,
|
||||
tx_signed_raws_hex,
|
||||
holder_address,
|
||||
gas,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
else:
|
||||
s_check_gas = celery.signature(
|
||||
'cic_eth.eth.tx.check_gas',
|
||||
[
|
||||
chain_str,
|
||||
tx_signed_raws_hex,
|
||||
holder_address,
|
||||
gas,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
return s_check_gas
|
||||
447
apps/cic-eth/cic_eth/eth/token.py
Normal file
447
apps/cic-eth/cic_eth/eth/token.py
Normal file
@@ -0,0 +1,447 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
import celery
|
||||
import requests
|
||||
import web3
|
||||
from cic_registry import CICRegistry
|
||||
from cic_registry.chain import ChainSpec
|
||||
|
||||
# platform imports
|
||||
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 sign_and_register_tx
|
||||
from cic_eth.eth.task import create_check_gas_and_send_task
|
||||
from cic_eth.eth.factory import TxFactory
|
||||
from cic_eth.eth.util import unpack_signed_raw_tx
|
||||
|
||||
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,
|
||||
):
|
||||
"""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 = CICRegistry.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(),
|
||||
})
|
||||
return tx_approve
|
||||
|
||||
|
||||
def transfer(
|
||||
self,
|
||||
token_address,
|
||||
receiver_address,
|
||||
value,
|
||||
chain_spec,
|
||||
):
|
||||
"""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 = CICRegistry.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(),
|
||||
})
|
||||
return tx_transfer
|
||||
|
||||
|
||||
def unpack_transfer(data):
|
||||
"""Verifies that a transaction is an "ERC20.transfer" transaction, and extracts call parameters from it.
|
||||
|
||||
:param data: Raw input data from Ethereum transaction.
|
||||
:type data: str, 0x-hex
|
||||
:raises ValueError: Function signature does not match AccountRegister.add
|
||||
:returns: Parsed parameters
|
||||
:rtype: dict
|
||||
"""
|
||||
f = data[2:10]
|
||||
if f != contract_function_signatures['transfer']:
|
||||
raise ValueError('Invalid transfer data ({})'.format(f))
|
||||
|
||||
d = data[10:]
|
||||
return {
|
||||
'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
|
||||
'amount': int(d[64:], 16)
|
||||
}
|
||||
|
||||
|
||||
def unpack_transferfrom(data):
|
||||
"""Verifies that a transaction is an "ERC20.transferFrom" transaction, and extracts call parameters from it.
|
||||
|
||||
:param data: Raw input data from Ethereum transaction.
|
||||
:type data: str, 0x-hex
|
||||
:raises ValueError: Function signature does not match AccountRegister.add
|
||||
:returns: Parsed parameters
|
||||
:rtype: dict
|
||||
"""
|
||||
f = data[2:10]
|
||||
if f != contract_function_signatures['transferfrom']:
|
||||
raise ValueError('Invalid transferFrom data ({})'.format(f))
|
||||
|
||||
d = data[10:]
|
||||
return {
|
||||
'from': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
|
||||
'to': web3.Web3.toChecksumAddress('0x' + d[128-40:128]),
|
||||
'amount': int(d[128:], 16)
|
||||
}
|
||||
|
||||
|
||||
def unpack_approve(data):
|
||||
"""Verifies that a transaction is an "ERC20.approve" transaction, and extracts call parameters from it.
|
||||
|
||||
:param data: Raw input data from Ethereum transaction.
|
||||
:type data: str, 0x-hex
|
||||
:raises ValueError: Function signature does not match AccountRegister.add
|
||||
:returns: Parsed parameters
|
||||
:rtype: dict
|
||||
"""
|
||||
f = data[2:10]
|
||||
if f != contract_function_signatures['approve']:
|
||||
raise ValueError('Invalid approval data ({})'.format(f))
|
||||
|
||||
d = data[10:]
|
||||
return {
|
||||
'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
|
||||
'amount': int(d[64:], 16)
|
||||
}
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def balance(tokens, holder_address, chain_str):
|
||||
"""Return token balances for a list of tokens for given address
|
||||
|
||||
:param tokens: Token addresses
|
||||
:type tokens: list of str, 0x-hex
|
||||
:param holder_address: Token holder address
|
||||
:type holder_address: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:return: List of balances
|
||||
:rtype: list of int
|
||||
"""
|
||||
#abi = ContractRegistry.abi('ERC20Token')
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
balances = []
|
||||
c = RpcClient(chain_spec)
|
||||
for t in tokens:
|
||||
#token = CICRegistry.get_address(t['address'])
|
||||
#abi = token.abi()
|
||||
#o = c.w3.eth.contract(abi=abi, address=t['address'])
|
||||
o = CICRegistry.get_address(chain_spec, t['address']).contract
|
||||
b = o.functions.balanceOf(holder_address).call()
|
||||
logg.debug('balance {} for {}: {}'.format(t['address'], holder_address, b))
|
||||
balances.append(b)
|
||||
return b
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def transfer(self, tokens, holder_address, receiver_address, value, chain_str):
|
||||
"""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.
|
||||
|
||||
:raises TokenCountError: Either none or more then one tokens have been passed as tokens 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
|
||||
:raises TokenCountError: More than one token is passed in tokens list
|
||||
:return: Transaction hash for tranfer operation
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
# we only allow one token, one transfer
|
||||
if len(tokens) != 1:
|
||||
raise TokenCountError
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
# retrieve the token interface
|
||||
t = tokens[0]
|
||||
|
||||
c = RpcClient(chain_spec, holder_address=holder_address)
|
||||
|
||||
txf = TokenTxFactory(holder_address, c)
|
||||
|
||||
tx_transfer = txf.transfer(t['address'], receiver_address, value, chain_spec)
|
||||
(tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_transfer, chain_str, queue, cache_task='cic_eth.eth.token.otx_cache_transfer')
|
||||
|
||||
gas_budget = tx_transfer['gas'] * tx_transfer['gasPrice']
|
||||
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_signed_raw_hex],
|
||||
chain_str,
|
||||
holder_address,
|
||||
gas_budget,
|
||||
[tx_hash_hex],
|
||||
queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return tx_hash_hex
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def approve(self, tokens, holder_address, spender_address, value, chain_str):
|
||||
"""Approve ERC20 transfer on behalf of holder address
|
||||
|
||||
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.
|
||||
|
||||
:raises TokenCountError: Either none or more then one tokens have been passed as tokens 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
|
||||
:raises TokenCountError: More than one token is passed in tokens list
|
||||
:return: Transaction hash for tranfer operation
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
# we only allow one token, one transfer
|
||||
if len(tokens) != 1:
|
||||
raise TokenCountError
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
# retrieve the token interface
|
||||
t = tokens[0]
|
||||
|
||||
c = RpcClient(chain_spec, holder_address=holder_address)
|
||||
|
||||
txf = TokenTxFactory(holder_address, c)
|
||||
|
||||
tx_transfer = txf.approve(t['address'], spender_address, value, chain_spec)
|
||||
(tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_transfer, chain_str, queue, cache_task='cic_eth.eth.token.otx_cache_approve')
|
||||
|
||||
gas_budget = tx_transfer['gas'] * tx_transfer['gasPrice']
|
||||
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_signed_raw_hex],
|
||||
chain_str,
|
||||
holder_address,
|
||||
gas_budget,
|
||||
[tx_hash_hex],
|
||||
queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return tx_hash_hex
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def resolve_tokens_by_symbol(token_symbols, chain_str):
|
||||
"""Returns contract addresses of an array of ERC20 token symbols
|
||||
|
||||
:param token_symbols: Token symbols to resolve
|
||||
:type token_symbols: list of str
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
|
||||
:return: Respective token contract addresses
|
||||
:rtype: list of str, 0x-hex
|
||||
"""
|
||||
tokens = []
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
for token_symbol in token_symbols:
|
||||
token = CICRegistry.get_token(chain_spec, token_symbol)
|
||||
tokens.append({
|
||||
'address': token.address(),
|
||||
#'converters': [],
|
||||
})
|
||||
return tokens
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def otx_cache_transfer(
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
):
|
||||
"""Generates and commits transaction cache metadata for an ERC20.transfer or ERC20.transferFrom transaction
|
||||
|
||||
:param tx_hash_hex: Transaction hash
|
||||
:type tx_hash_hex: str, 0x-hex
|
||||
:param tx_signed_raw_hex: Raw signed transaction
|
||||
:type tx_signed_raw_hex: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||
:rtype: tuple
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||
(txc, cache_id) = cache_transfer_data(tx_hash_hex, tx)
|
||||
return txc
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def cache_transfer_data(
|
||||
tx_hash_hex,
|
||||
tx,
|
||||
):
|
||||
"""Helper function for otx_cache_transfer
|
||||
|
||||
: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
|
||||
"""
|
||||
tx_data = unpack_transfer(tx['data'])
|
||||
logg.debug('tx data {}'.format(tx_data))
|
||||
logg.debug('tx {}'.format(tx))
|
||||
|
||||
session = SessionBase.create_session()
|
||||
tx_cache = TxCache(
|
||||
tx_hash_hex,
|
||||
tx['from'],
|
||||
tx_data['to'],
|
||||
tx['to'],
|
||||
tx['to'],
|
||||
tx_data['amount'],
|
||||
tx_data['amount'],
|
||||
)
|
||||
session.add(tx_cache)
|
||||
session.commit()
|
||||
cache_id = tx_cache.id
|
||||
session.close()
|
||||
return (tx_hash_hex, cache_id)
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def otx_cache_approve(
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
):
|
||||
"""Generates and commits transaction cache metadata for an ERC20.approve transaction
|
||||
|
||||
:param tx_hash_hex: Transaction hash
|
||||
:type tx_hash_hex: str, 0x-hex
|
||||
:param tx_signed_raw_hex: Raw signed transaction
|
||||
:type tx_signed_raw_hex: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||
:rtype: tuple
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||
(txc, cache_id) = cache_approve_data(tx_hash_hex, tx)
|
||||
return txc
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def cache_approve_data(
|
||||
tx_hash_hex,
|
||||
tx,
|
||||
):
|
||||
"""Helper function for otx_cache_approve
|
||||
|
||||
: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
|
||||
"""
|
||||
tx_data = unpack_approve(tx['data'])
|
||||
logg.debug('tx data {}'.format(tx_data))
|
||||
logg.debug('tx {}'.format(tx))
|
||||
|
||||
session = SessionBase.create_session()
|
||||
tx_cache = TxCache(
|
||||
tx_hash_hex,
|
||||
tx['from'],
|
||||
tx_data['to'],
|
||||
tx['to'],
|
||||
tx['to'],
|
||||
tx_data['amount'],
|
||||
tx_data['amount'],
|
||||
)
|
||||
session.add(tx_cache)
|
||||
session.commit()
|
||||
cache_id = tx_cache.id
|
||||
session.close()
|
||||
return (tx_hash_hex, cache_id)
|
||||
681
apps/cic-eth/cic_eth/eth/tx.py
Normal file
681
apps/cic-eth/cic_eth/eth/tx.py
Normal file
@@ -0,0 +1,681 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# third-party imports
|
||||
import celery
|
||||
import requests
|
||||
import web3
|
||||
from cic_registry import zero_address
|
||||
from cic_registry.chain import ChainSpec
|
||||
|
||||
# local imports
|
||||
from .rpc import RpcClient
|
||||
from cic_eth.db import Otx, SessionBase
|
||||
from cic_eth.db.models.tx import TxCache
|
||||
from cic_eth.db.models.lock import Lock
|
||||
from cic_eth.db.enum import LockEnum
|
||||
from cic_eth.error import PermanentTxError
|
||||
from cic_eth.error import TemporaryTxError
|
||||
from cic_eth.error import NotLocalTxError
|
||||
from cic_eth.queue.tx import create as queue_create
|
||||
from cic_eth.queue.tx import get_tx
|
||||
from cic_eth.queue.tx import get_nonce_tx
|
||||
from cic_eth.error import OutOfGasError
|
||||
from cic_eth.error import LockedError
|
||||
from cic_eth.eth.util import unpack_signed_raw_tx
|
||||
from cic_eth.eth.task import sign_and_register_tx, create_check_gas_and_send_task
|
||||
from cic_eth.eth.task import sign_tx
|
||||
from cic_eth.eth.nonce import NonceOracle
|
||||
from cic_eth.error import AlreadyFillingGasError
|
||||
from cic_eth.eth.util import tx_hex_string
|
||||
from cic_eth.admin.ctrl import lock_send
|
||||
|
||||
celery_app = celery.current_app
|
||||
logg = logging.getLogger()
|
||||
|
||||
MAX_NONCE_ATTEMPTS = 3
|
||||
|
||||
|
||||
# TODO this function is too long
|
||||
@celery_app.task(bind=True, throws=(OutOfGasError))
|
||||
def check_gas(self, tx_hashes, chain_str, txs=[], address=None, gas_required=None):
|
||||
"""Check the gas level of the sender address of a transaction.
|
||||
|
||||
If the account balance is not sufficient for the required gas, gas refill is requested and OutOfGasError raiser.
|
||||
|
||||
If account balance is sufficient, but level of gas before spend is below "safe" threshold, gas refill is requested, and execution continues normally.
|
||||
|
||||
:param tx_hashes: Transaction hashes due to be submitted
|
||||
:type tx_hashes: list of str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:param txs: Signed raw transaction data, corresponding to tx_hashes
|
||||
:type txs: list of str, 0x-hex
|
||||
:param address: Sender address
|
||||
:type address: str, 0x-hex
|
||||
:param gas_required: Gas limit * gas price for transaction, (optional, if not set will be retrived from transaction data)
|
||||
:type gas_required: int
|
||||
:return: Signed raw transaction data list
|
||||
:rtype: param txs, unchanged
|
||||
"""
|
||||
if len(txs) == 0:
|
||||
for i in range(len(tx_hashes)):
|
||||
o = get_tx(tx_hashes[i])
|
||||
txs.append(o['signed_tx'])
|
||||
logg.debug('ooooo {}'.format(o))
|
||||
if address == None:
|
||||
address = o['address']
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
#c = RpcClient(chain_spec, holder_address=address)
|
||||
c = RpcClient(chain_spec)
|
||||
|
||||
# TODO: it should not be necessary to pass address explicitly, if not passed should be derived from the tx
|
||||
balance = c.w3.eth.getBalance(address)
|
||||
logg.debug('check gas txs {}'.format(tx_hashes))
|
||||
logg.debug('address {} has gas {} needs {}'.format(address, balance, gas_required))
|
||||
|
||||
if gas_required > balance:
|
||||
s_refill_gas = celery.signature(
|
||||
'cic_eth.eth.tx.refill_gas',
|
||||
[
|
||||
address,
|
||||
chain_str,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
s_refill_gas.apply_async()
|
||||
wait_tasks = []
|
||||
for tx_hash in tx_hashes:
|
||||
s = celery.signature(
|
||||
'cic_eth.queue.tx.set_waitforgas',
|
||||
[
|
||||
tx_hash,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
wait_tasks.append(s)
|
||||
celery.group(wait_tasks)()
|
||||
raise OutOfGasError('need to fill gas, required {}, had {}'.format(gas_required, balance))
|
||||
|
||||
safe_gas = c.safe_threshold_amount()
|
||||
if balance < safe_gas:
|
||||
s_refill_gas = celery.signature(
|
||||
'cic_eth.eth.tx.refill_gas',
|
||||
[
|
||||
address,
|
||||
chain_str,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
s_refill_gas.apply_async()
|
||||
logg.debug('requested refill from {} to {}'.format(c.gas_provider(), address))
|
||||
ready_tasks = []
|
||||
for tx_hash in tx_hashes:
|
||||
s = celery.signature(
|
||||
'cic_eth.queue.tx.set_ready',
|
||||
[
|
||||
tx_hash,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
ready_tasks.append(s)
|
||||
logg.debug('tasks {}'.format(ready_tasks))
|
||||
celery.group(ready_tasks)()
|
||||
|
||||
return txs
|
||||
|
||||
|
||||
# TODO: chain chainable transactions that use hashes as inputs may be chained to this function to output signed txs instead.
|
||||
@celery_app.task(bind=True)
|
||||
def hashes_to_txs(self, tx_hashes):
|
||||
"""Return a list of raw signed transactions from the local transaction queue corresponding to a list of transaction hashes.
|
||||
|
||||
:param tx_hashes: Transaction hashes
|
||||
:type tx_hashes: list of str, 0x-hex
|
||||
:raises ValueError: Empty input list
|
||||
:returns: Signed raw transactions
|
||||
:rtype: list of str, 0x-hex
|
||||
"""
|
||||
#logg = celery_app.log.get_default_logger()
|
||||
if len(tx_hashes) == 0:
|
||||
raise ValueError('no transaction to send')
|
||||
|
||||
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))
|
||||
tx_tuples = q.all()
|
||||
session.close()
|
||||
|
||||
def __head(x):
|
||||
return x[0]
|
||||
|
||||
txs = []
|
||||
for f in map(__head, tx_tuples):
|
||||
txs.append(f)
|
||||
|
||||
return txs
|
||||
|
||||
|
||||
# TODO: Move this and send to subfolder submodule
|
||||
class ParityNodeHandler:
|
||||
def __init__(self, chain_spec, queue):
|
||||
self.chain_spec = chain_spec
|
||||
self.chain_str = str(chain_spec)
|
||||
self.queue = queue
|
||||
|
||||
def handle(self, exception, tx_hash_hex, tx_hex):
|
||||
meth = self.handle_default
|
||||
if isinstance(exception, (ValueError)):
|
||||
# s_debug = celery.signature(
|
||||
# 'cic_eth.admin.debug.out_tmp',
|
||||
# [tx_hash_hex, '{}: {}'.format(tx_hash_hex, exception)],
|
||||
# queue=queue,
|
||||
# )
|
||||
# s_debug.apply_async()
|
||||
earg = exception.args[0]
|
||||
if earg['code'] == -32010:
|
||||
logg.debug('skipping lock for code {}'.format(earg['code']))
|
||||
meth = self.handle_invalid_parameters
|
||||
elif earg['code'] == -32602:
|
||||
meth = self.handle_invalid_encoding
|
||||
else:
|
||||
meth = self.handle_invalid
|
||||
elif isinstance(exception, (requests.exceptions.ConnectionError)):
|
||||
meth = self.handle_connection
|
||||
(t, e_fn, message) = meth(tx_hash_hex, tx_hex)
|
||||
return (t, e_fn, '{} {}'.format(message, exception))
|
||||
|
||||
|
||||
def handle_connection(self, tx_hash_hex, tx_hex):
|
||||
s_set_sent = celery.signature(
|
||||
'cic_eth.queue.tx.set_sent_status',
|
||||
[
|
||||
tx_hash_hex,
|
||||
True,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
t = s_set_sent.apply_async()
|
||||
return (t, TemporaryTxError, 'Sendfail {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||
|
||||
|
||||
def handle_invalid_encoding(self, tx_hash_hex, tx_hex):
|
||||
tx_bytes = bytes.fromhex(tx_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_bytes, self.chain_spec.chain_id())
|
||||
s_lock = celery.signature(
|
||||
'cic_eth.admin.ctrl.lock_send',
|
||||
[
|
||||
tx_hash_hex,
|
||||
self.chain_str,
|
||||
tx['from'],
|
||||
tx_hash_hex,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_set_reject = celery.signature(
|
||||
'cic_eth.queue.tx.set_rejected',
|
||||
[],
|
||||
queue=self.queue,
|
||||
)
|
||||
nonce_txs = get_nonce_tx(tx['nonce'], tx['from'], self.chain_spec.chain_id())
|
||||
attempts = len(nonce_txs)
|
||||
if attempts < MAX_NONCE_ATTEMPTS:
|
||||
logg.debug('nonce {} address {} retries {} < {}'.format(tx['nonce'], tx['from'], attempts, MAX_NONCE_ATTEMPTS))
|
||||
s_resend = celery.signature(
|
||||
'cic_eth.eth.tx.resend_with_higher_gas',
|
||||
[
|
||||
self.chain_str,
|
||||
None,
|
||||
1.01,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_unlock = celery.signature(
|
||||
'cic_eth.admin.ctrl.unlock_send',
|
||||
[
|
||||
self.chain_str,
|
||||
tx['from'],
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_resend.link(s_unlock)
|
||||
s_set_reject.link(s_resend)
|
||||
|
||||
s_lock.link(s_set_reject)
|
||||
t = s_lock.apply_async()
|
||||
return (t, PermanentTxError, 'Reject invalid encoding {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||
|
||||
|
||||
def handle_invalid_parameters(self, tx_hash_hex, tx_hex):
|
||||
s_sync = celery.signature(
|
||||
'cic_eth.eth.tx.sync_tx',
|
||||
[
|
||||
tx_hash_hex,
|
||||
self.chain_str,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
t = s_sync.apply_async()
|
||||
return (t, PermanentTxError, 'Reject invalid parameters {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||
|
||||
|
||||
def handle_invalid(self, tx_hash_hex, tx_hex):
|
||||
tx_bytes = bytes.fromhex(tx_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_bytes, self.chain_spec.chain_id())
|
||||
s_lock = celery.signature(
|
||||
'cic_eth.admin.ctrl.lock_send',
|
||||
[
|
||||
tx_hash_hex,
|
||||
self.chain_str,
|
||||
tx['from'],
|
||||
tx_hash_hex,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_set_reject = celery.signature(
|
||||
'cic_eth.queue.tx.set_rejected',
|
||||
[],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_lock.link(s_set_reject)
|
||||
t = s_lock.apply_async()
|
||||
return (t, PermanentTxError, 'Reject invalid {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||
|
||||
|
||||
def handle_default(self, tx_hash_hex, tx_hex):
|
||||
tx_bytes = bytes.fromhex(tx_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_bytes, self.chain_spec.chain_id())
|
||||
s_lock = celery.signature(
|
||||
'cic_eth.admin.ctrl.lock_send',
|
||||
[
|
||||
tx_hash_hex,
|
||||
self.chain_str,
|
||||
tx['from'],
|
||||
tx_hash_hex,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_set_fubar = celery.signature(
|
||||
'cic_eth.queue.tx.set_fubar',
|
||||
[],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_lock.link(s_set_fubar)
|
||||
t = s_lock.apply_async()
|
||||
return (t, PermanentTxError, 'Fubar {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def send(self, txs, chain_str):
|
||||
"""Send transactions to the network.
|
||||
|
||||
If more than one transaction is passed to the task, it will spawn a new send task with the remaining transaction(s) after the first in the list has been processed.
|
||||
|
||||
Updates the outgoing transaction queue entry to SENT on successful send.
|
||||
|
||||
If a temporary error occurs, the queue entry is set to SENDFAIL.
|
||||
|
||||
If a permanent error occurs due to invalid transaction data, queue entry value is set to REJECTED.
|
||||
|
||||
Any other permanent error that isn't explicitly handled will get value FUBAR.
|
||||
|
||||
:param txs: Signed raw transaction data
|
||||
:type txs: list of str, 0x-hex
|
||||
:param chain_str: Chain spec, string representation
|
||||
:type chain_str: str
|
||||
:raises TemporaryTxError: If unable to connect to node
|
||||
:raises PermanentTxError: If EVM execution fails immediately due to tx input, or if tx contents are invalid.
|
||||
:return: transaction hash of sent transaction
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
if len(txs) == 0:
|
||||
raise ValueError('no transaction to send')
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
|
||||
tx_hex = txs[0]
|
||||
logg.debug('send transaction {}'.format(tx_hex))
|
||||
|
||||
tx_hash = web3.Web3.keccak(hexstr=tx_hex)
|
||||
tx_hash_hex = tx_hash.hex()
|
||||
|
||||
queue = self.request.delivery_info.get('routing_key', None)
|
||||
if queue == None:
|
||||
logg.debug('send tx {} has no queue', tx_hash)
|
||||
|
||||
c = RpcClient(chain_spec)
|
||||
r = None
|
||||
try:
|
||||
r = c.w3.eth.send_raw_transaction(tx_hex)
|
||||
except Exception as e:
|
||||
logg.debug('e {}'.format(e))
|
||||
raiser = ParityNodeHandler(chain_spec, queue)
|
||||
(t, e, m) = raiser.handle(e, tx_hash_hex, tx_hex)
|
||||
raise e(m)
|
||||
|
||||
s_set_sent = celery.signature(
|
||||
'cic_eth.queue.tx.set_sent_status',
|
||||
[
|
||||
tx_hash_hex,
|
||||
False
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
s_set_sent.apply_async()
|
||||
|
||||
tx_tail = txs[1:]
|
||||
if len(tx_tail) > 0:
|
||||
s = celery.signature(
|
||||
'cic_eth.eth.tx.send',
|
||||
[tx_tail],
|
||||
queue=queue,
|
||||
)
|
||||
s.apply_async()
|
||||
|
||||
return r.hex()
|
||||
|
||||
|
||||
@celery_app.task(bind=True, throws=(AlreadyFillingGasError))
|
||||
def refill_gas(self, recipient_address, chain_str):
|
||||
"""Executes a native token transaction to fund the recipient's gas expenditures.
|
||||
|
||||
:param recipient_address: Recipient in need of gas
|
||||
:type recipient_address: str, 0x-hex
|
||||
:param chain_str: Chain spec, string representation
|
||||
:type chain_str: str
|
||||
:raises AlreadyFillingGasError: A gas refill transaction for this address is already executing
|
||||
:returns: Transaction hash.
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
session = SessionBase.create_session()
|
||||
q = session.query(Otx.tx_hash)
|
||||
q = q.join(TxCache)
|
||||
q = q.filter(Otx.status<=0)
|
||||
q = q.filter(TxCache.from_value!='0x00')
|
||||
q = q.filter(TxCache.recipient==recipient_address)
|
||||
c = q.count()
|
||||
session.close()
|
||||
if c > 0:
|
||||
raise AlreadyFillingGasError(recipient_address)
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
c = RpcClient(chain_spec)
|
||||
clogg = celery_app.log.get_default_logger()
|
||||
logg.debug('refill gas from provider address {}'.format(c.gas_provider()))
|
||||
default_nonce = c.w3.eth.getTransactionCount(c.gas_provider(), 'pending')
|
||||
nonce_generator = NonceOracle(c.gas_provider(), default_nonce)
|
||||
nonce = nonce_generator.next()
|
||||
gas_price = c.gas_price()
|
||||
gas_limit = c.default_gas_limit
|
||||
refill_amount = c.refill_amount()
|
||||
logg.debug('gas price {} nonce {}'.format(gas_price, nonce))
|
||||
|
||||
# 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': '',
|
||||
}
|
||||
logg.debug('txsend_gas {}'.format(tx_send_gas))
|
||||
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()
|
||||
|
||||
# TODO: route this through sign_and_register_tx instead
|
||||
logg.debug('adding queue refill gas tx {}'.format(tx_hash_hex))
|
||||
queue_create(
|
||||
nonce,
|
||||
c.gas_provider(),
|
||||
tx_hash_hex,
|
||||
tx_send_gas_signed['raw'],
|
||||
chain_str,
|
||||
)
|
||||
|
||||
s_tx_cache = celery.signature(
|
||||
'cic_eth.eth.tx.cache_gas_refill_data',
|
||||
[
|
||||
tx_hash_hex,
|
||||
tx_send_gas,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
s_status = celery.signature(
|
||||
'cic_eth.queue.tx.set_ready',
|
||||
[
|
||||
tx_hash_hex,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
celery.group(s_tx_cache, s_status)()
|
||||
return tx_send_gas_signed['raw']
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def resend_with_higher_gas(self, txold_hash_hex, chain_str, gas=None, default_factor=1.1):
|
||||
"""Create a new transaction from an existing one with same nonce and higher gas price.
|
||||
|
||||
:param txold_hash_hex: Transaction to re-create
|
||||
:type txold_hash_hex: str, 0x-hex
|
||||
:param chain_str: Chain spec, string representation
|
||||
:type chain_str: str
|
||||
:param gas: Explicitly use the specified gas amount
|
||||
:type gas: number
|
||||
:param default_factor: Default factor by which to increment the gas price by
|
||||
:type default_factor: float
|
||||
:raises NotLocalTxError: Transaction does not exist in the local queue
|
||||
:returns: Transaction hash
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
session = SessionBase.create_session()
|
||||
otx = session.query(Otx).filter(Otx.tx_hash==txold_hash_hex).first()
|
||||
if otx == None:
|
||||
session.close()
|
||||
raise NotLocalTxError(txold_hash_hex)
|
||||
session.close()
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
c = RpcClient(chain_spec)
|
||||
|
||||
tx_signed_raw_bytes = bytes.fromhex(otx.signed_tx[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||
logg.debug('otx {} {}'.format(tx, otx.signed_tx))
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
logg.debug('before {}'.format(tx))
|
||||
if gas != None:
|
||||
tx['gasPrice'] = gas
|
||||
else:
|
||||
gas_price = c.gas_price()
|
||||
if tx['gasPrice'] > gas_price:
|
||||
logg.warning('Network gas price {} is lower than overdue tx gas price {}'.format(gas_price, tx['gasPrice']))
|
||||
#tx['gasPrice'] = int(tx['gasPrice'] * default_factor)
|
||||
tx['gasPrice'] += 1
|
||||
else:
|
||||
new_gas_price = int(tx['gasPrice'] * default_factor)
|
||||
if gas_price > new_gas_price:
|
||||
tx['gasPrice'] = gas_price
|
||||
else:
|
||||
tx['gasPrice'] = new_gas_price
|
||||
|
||||
logg.debug('after {}'.format(tx))
|
||||
|
||||
#(tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx, chain_str, queue)
|
||||
(tx_hash_hex, tx_signed_raw_hex) = sign_tx(tx, chain_str)
|
||||
queue_create(
|
||||
tx['nonce'],
|
||||
tx['from'],
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
)
|
||||
TxCache.clone(txold_hash_hex, tx_hash_hex)
|
||||
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_signed_raw_hex],
|
||||
chain_str,
|
||||
tx['from'],
|
||||
tx['gasPrice'] * tx['gas'],
|
||||
[tx_hash_hex],
|
||||
queue=queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return tx_hash_hex
|
||||
|
||||
|
||||
@celery_app.task(bind=True, throws=(web3.exceptions.TransactionNotFound,))
|
||||
def sync_tx(self, tx_hash_hex, chain_str):
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
c = RpcClient(chain_spec)
|
||||
|
||||
tx = c.w3.eth.getTransaction(tx_hash_hex)
|
||||
rcpt = None
|
||||
try:
|
||||
rcpt = c.w3.eth.getTransactionReceipt(tx_hash_hex)
|
||||
except web3.exceptions.TransactionNotFound as e:
|
||||
pass
|
||||
|
||||
if rcpt != None:
|
||||
success = rcpt['status'] == 1
|
||||
logg.debug('sync tx {} mined block {} success {}'.format(tx_hash_hex, rcpt['blockNumber'], success))
|
||||
|
||||
s = celery.signature(
|
||||
'cic_eth.queue.tx.set_final_status',
|
||||
[
|
||||
tx_hash_hex,
|
||||
rcpt['blockNumber'],
|
||||
not success,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
else:
|
||||
logg.debug('sync tx {} mempool'.format(tx_hash_hex))
|
||||
|
||||
s = celery.signature(
|
||||
'cic_eth.queue.tx.set_sent_status',
|
||||
[
|
||||
tx_hash_hex,
|
||||
],
|
||||
queue=queue,
|
||||
)
|
||||
|
||||
s.apply_async()
|
||||
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def resume_tx(self, txpending_hash_hex, chain_str):
|
||||
"""Queue a suspended tranaction for (re)sending
|
||||
|
||||
:param txpending_hash_hex: Transaction hash
|
||||
:type txpending_hash_hex: str, 0x-hex
|
||||
:param chain_str: Chain spec, string representation
|
||||
:type chain_str: str
|
||||
:raises NotLocalTxError: Transaction does not exist in the local queue
|
||||
:returns: Transaction hash
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
|
||||
session = SessionBase.create_session()
|
||||
r = session.query(Otx.signed_tx).filter(Otx.tx_hash==txpending_hash_hex).first()
|
||||
session.close()
|
||||
if r == None:
|
||||
raise NotLocalTxError(txpending_hash_hex)
|
||||
|
||||
tx_signed_raw_hex = r[0]
|
||||
tx_signed_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_bytes, chain_spec.chain_id())
|
||||
|
||||
queue = self.request.delivery_info['routing_key']
|
||||
|
||||
s = create_check_gas_and_send_task(
|
||||
[tx_signed_raw_hex],
|
||||
chain_str,
|
||||
tx['from'],
|
||||
tx['gasPrice'] * tx['gas'],
|
||||
[txpending_hash_hex],
|
||||
queue=queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return txpending_hash_hex
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def otx_cache_parse_tx(
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_str,
|
||||
):
|
||||
"""Generates and commits transaction cache metadata for a gas refill transaction
|
||||
|
||||
:param tx_hash_hex: Transaction hash
|
||||
:type tx_hash_hex: str, 0x-hex
|
||||
:param tx_signed_raw_hex: Raw signed transaction
|
||||
:type tx_signed_raw_hex: str, 0x-hex
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||
:rtype: tuple
|
||||
"""
|
||||
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||
c = RpcClient(chain_spec)
|
||||
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||
(txc, cache_id) = cache_gas_refill_data(tx_hash_hex, tx)
|
||||
return txc
|
||||
|
||||
|
||||
@celery_app.task()
|
||||
def cache_gas_refill_data(
|
||||
tx_hash_hex,
|
||||
tx,
|
||||
):
|
||||
"""Helper function for otx_cache_parse_tx
|
||||
|
||||
: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
|
||||
"""
|
||||
tx_cache = TxCache(
|
||||
tx_hash_hex,
|
||||
tx['from'],
|
||||
tx['to'],
|
||||
zero_address,
|
||||
zero_address,
|
||||
tx['value'],
|
||||
tx['value'],
|
||||
)
|
||||
|
||||
session = SessionBase.create_session()
|
||||
session.add(tx_cache)
|
||||
session.commit()
|
||||
cache_id = tx_cache.id
|
||||
session.close()
|
||||
return (tx_hash_hex, cache_id)
|
||||
102
apps/cic-eth/cic_eth/eth/util.py
Normal file
102
apps/cic-eth/cic_eth/eth/util.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# standard imports
|
||||
import logging
|
||||
import sha3
|
||||
import web3
|
||||
|
||||
# third-party imports
|
||||
from rlp import decode as rlp_decode
|
||||
from rlp import encode as rlp_encode
|
||||
from eth_keys import KeyAPI
|
||||
|
||||
logg = logging.getLogger()
|
||||
|
||||
field_debugs = [
|
||||
'nonce',
|
||||
'gasPrice',
|
||||
'gas',
|
||||
'to',
|
||||
'value',
|
||||
'data',
|
||||
'v',
|
||||
'r',
|
||||
's',
|
||||
]
|
||||
|
||||
|
||||
def unpack_signed_raw_tx(tx_raw_bytes, chain_id):
|
||||
d = rlp_decode(tx_raw_bytes)
|
||||
|
||||
logg.debug('decoding using chain id {}'.format(chain_id))
|
||||
j = 0
|
||||
for i in d:
|
||||
logg.debug('decoded {}: {}'.format(field_debugs[j], i.hex()))
|
||||
j += 1
|
||||
vb = chain_id
|
||||
if chain_id != 0:
|
||||
v = int.from_bytes(d[6], 'big')
|
||||
vb = v - (chain_id * 2) - 35
|
||||
s = b''.join([d[7], d[8], bytes([vb])])
|
||||
so = KeyAPI.Signature(signature_bytes=s)
|
||||
|
||||
h = sha3.keccak_256()
|
||||
h.update(rlp_encode(d))
|
||||
signed_hash = h.digest()
|
||||
|
||||
d[6] = chain_id
|
||||
d[7] = b''
|
||||
d[8] = b''
|
||||
|
||||
h = sha3.keccak_256()
|
||||
h.update(rlp_encode(d))
|
||||
unsigned_hash = h.digest()
|
||||
|
||||
p = so.recover_public_key_from_msg_hash(unsigned_hash)
|
||||
a = p.to_checksum_address()
|
||||
logg.debug('decoded recovery byte {}'.format(vb))
|
||||
logg.debug('decoded address {}'.format(a))
|
||||
logg.debug('decoded signed hash {}'.format(signed_hash.hex()))
|
||||
logg.debug('decoded unsigned hash {}'.format(unsigned_hash.hex()))
|
||||
|
||||
to = d[3].hex() or None
|
||||
if to != None:
|
||||
to = web3.Web3.toChecksumAddress('0x' + to)
|
||||
|
||||
return {
|
||||
'from': a,
|
||||
'nonce': int.from_bytes(d[0], 'big'),
|
||||
'gasPrice': int.from_bytes(d[1], 'big'),
|
||||
'gas': int.from_bytes(d[2], 'big'),
|
||||
'to': to,
|
||||
'value': int.from_bytes(d[4], 'big'),
|
||||
'data': '0x' + d[5].hex(),
|
||||
'v': chain_id,
|
||||
'r': '0x' + s[:32].hex(),
|
||||
's': '0x' + s[32:64].hex(),
|
||||
'chainId': chain_id,
|
||||
'hash': '0x' + signed_hash.hex(),
|
||||
'hash_unsigned': '0x' + unsigned_hash.hex(),
|
||||
}
|
||||
|
||||
|
||||
def unpack_signed_raw_tx_hex(tx_raw_hex, chain_id):
|
||||
return unpack_signed_raw_tx(bytes.fromhex(tx_raw_hex[2:]), chain_id)
|
||||
|
||||
|
||||
# TODO: consider moving tx string representation generation from api_admin to here
|
||||
def tx_string(tx_raw_bytes, chain_id):
|
||||
tx_unpacked = unpack_signed_raw_tx(tx_raw_bytes, chain_id)
|
||||
return 'tx nonce {} from {} to {} hash {}'.format(
|
||||
tx_unpacked['nonce'],
|
||||
tx_unpacked['from'],
|
||||
tx_unpacked['to'],
|
||||
tx_unpacked['hash'],
|
||||
)
|
||||
|
||||
def tx_hex_string(tx_hex, chain_id):
|
||||
if len(tx_hex) < 2:
|
||||
raise ValueError('invalid data length')
|
||||
elif tx_hex[:2] == '0x':
|
||||
tx_hex = tx_hex[2:]
|
||||
|
||||
tx_raw_bytes = bytes.fromhex(tx_hex)
|
||||
return tx_string(tx_raw_bytes, chain_id)
|
||||
Reference in New Issue
Block a user