cic-internal-integration/apps/cic-eth/cic_eth/eth/account.py

434 lines
14 KiB
Python
Raw Normal View History

2021-02-01 18:12:51 +01:00
# standard imports
import logging
# external imports
2021-02-01 18:12:51 +01:00
import celery
from erc20_single_shot_faucet import Faucet
from chainlib.eth.constant import ZERO_ADDRESS
2021-03-01 21:15:17 +01:00
from hexathon import strip_0x
from chainlib.connection import RPCConnection
2021-03-11 11:40:02 +01:00
from chainlib.eth.sign import (
new_account,
sign_message,
)
2021-03-12 18:44:29 +01:00
from chainlib.eth.address import to_checksum_address
from chainlib.eth.tx import TxFormat
from chainlib.chain import ChainSpec
from eth_accounts_index import AccountRegistry
2021-02-01 18:12:51 +01:00
# local import
#from cic_eth.registry import safe_registry
#from cic_eth.eth import RpcClient
from cic_eth_registry import CICRegistry
2021-02-01 18:12:51 +01:00
from cic_eth.eth import registry_extra_identifiers
from cic_eth.eth.task import (
register_tx,
create_check_gas_task,
)
2021-02-01 18:12:51 +01:00
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
2021-03-07 14:51:59 +01:00
from cic_eth.error import (
RoleMissingError,
SignerError,
)
from cic_eth.task import (
CriticalSQLAlchemyTask,
CriticalSQLAlchemyAndSignerTask,
2021-03-11 11:40:02 +01:00
BaseTask,
2021-03-07 14:51:59 +01:00
)
2021-02-01 18:12:51 +01:00
#logg = logging.getLogger(__name__)
logg = logging.getLogger()
celery_app = celery.current_app
#celery_app.log.setup_task_loggers(loglevel=logging.DEBUG)
#celery_app.log.redirect_stdouts_to_logger(logg, loglevel=logging.DEBUG)
2021-02-01 18:12:51 +01:00
class AccountTxFactory(TxFactory):
"""Factory for creating account index contract transactions
"""
def add(
self,
address,
chain_spec,
2021-03-06 18:55:51 +01:00
uuid,
2021-03-01 21:15:17 +01:00
session=None,
2021-02-01 18:12:51 +01:00
):
"""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
"""
2021-03-08 10:11:04 +01:00
c = self.registry.get_contract(chain_spec, 'AccountRegistry')
2021-02-01 18:12:51 +01:00
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(),
2021-03-06 18:55:51 +01:00
'nonce': self.next_nonce(uuid, session=session),
2021-02-01 18:12:51 +01:00
'value': 0,
})
return tx_add
def gift(
self,
address,
chain_spec,
2021-03-06 18:55:51 +01:00
uuid,
2021-03-01 21:15:17 +01:00
session=None,
2021-02-01 18:12:51 +01:00
):
"""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
"""
2021-03-08 10:11:04 +01:00
c = self.registry.get_contract(chain_spec, 'Faucet')
2021-02-01 18:12:51 +01:00
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(),
2021-03-06 18:55:51 +01:00
'nonce': self.next_nonce(uuid, session=session),
2021-02-01 18:12:51 +01:00
'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
"""
2021-03-01 21:15:17 +01:00
data = strip_0x(data)
f = data[:8]
2021-02-01 18:12:51 +01:00
if f != '0a3b0a4f':
raise ValueError('Invalid account index register data ({})'.format(f))
2021-03-01 21:15:17 +01:00
d = data[8:]
2021-02-01 18:12:51 +01:00
return {
2021-03-12 18:44:29 +01:00
'to': to_checksum_address(d[64-40:64]),
2021-02-01 18:12:51 +01:00
}
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
"""
2021-03-01 21:15:17 +01:00
data = strip_0x(data)
f = data[:8]
2021-02-01 18:12:51 +01:00
if f != '63e4bff4':
2021-03-01 21:15:17 +01:00
raise ValueError('Invalid gift data ({})'.format(f))
2021-02-01 18:12:51 +01:00
2021-03-01 21:15:17 +01:00
d = data[8:]
2021-02-01 18:12:51 +01:00
return {
2021-03-12 18:44:29 +01:00
'to': to_checksum_address(d[64-40:64]),
2021-02-01 18:12:51 +01:00
}
2021-03-01 21:15:17 +01:00
# TODO: Separate out nonce initialization task
2021-03-11 11:40:02 +01:00
@celery_app.task(bind=True, base=CriticalSQLAlchemyAndSignerTask)
def create(self, password, chain_str):
2021-02-01 18:12:51 +01:00
"""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)
2021-03-11 11:40:02 +01:00
#c = RpcClient(chain_spec)
2021-03-07 14:51:59 +01:00
a = None
conn = RPCConnection.connect(chain_spec, 'signer')
2021-03-11 11:40:02 +01:00
o = new_account()
a = conn.do(o)
#try:
# a = c.w3.eth.personal.new_account(password)
#except FileNotFoundError:
# pass
2021-03-07 14:51:59 +01:00
if a == None:
raise SignerError('create account')
2021-02-01 18:12:51 +01:00
logg.debug('created account {}'.format(a))
# Initialize nonce provider record for account
2021-03-11 11:40:02 +01:00
#session = SessionBase.create_session()
session = self.create_session()
2021-03-06 18:55:51 +01:00
Nonce.init(a, session=session)
session.commit()
2021-02-01 18:12:51 +01:00
session.close()
return a
2021-03-07 14:51:59 +01:00
@celery_app.task(bind=True, throws=(RoleMissingError,), base=CriticalSQLAlchemyAndSignerTask)
def register(self, account_address, chain_spec_dict, writer_address=None):
2021-02-01 18:12:51 +01:00
"""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_dict(chain_spec_dict)
2021-02-01 18:12:51 +01:00
2021-03-11 11:40:02 +01:00
session = self.create_session()
#session = SessionBase.create_session()
2021-02-01 18:12:51 +01:00
if writer_address == None:
writer_address = AccountRole.get_address('ACCOUNT_REGISTRY_WRITER', session=session)
2021-02-01 18:12:51 +01:00
if writer_address == ZERO_ADDRESS:
2021-03-01 21:15:17 +01:00
session.close()
2021-02-01 18:12:51 +01:00
raise RoleMissingError(account_address)
logg.debug('adding account address {} to index; writer {}'.format(account_address, writer_address))
queue = self.request.delivery_info.get('routing_key')
2021-02-01 18:12:51 +01:00
# Retrieve account index address
rpc = RPCConnection.connect(chain_spec, 'default')
reg = CICRegistry(chain_spec, rpc)
call_address = AccountRole.get_address('DEFAULT', session=session)
account_registry_address = reg.by_name('AccountRegistry', sender_address=call_address)
# Generate and sign transaction
rpc_signer = RPCConnection.connect(chain_spec, 'signer')
nonce_oracle = self.create_nonce_oracle(writer_address, rpc)
gas_oracle = self.create_gas_oracle(rpc)
account_registry = AccountRegistry(signer=rpc_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_id=chain_spec.chain_id())
(tx_hash_hex, tx_signed_raw_hex) = account_registry.add(account_registry_address, writer_address, account_address, tx_format=TxFormat.RLP_SIGNED)
# TODO: if cache task fails, task chain will not return
cache_task = 'cic_eth.eth.account.cache_account_data'
cache_task = None
# add transaction to queue
register_tx(tx_hash_hex, tx_signed_raw_hex, chain_spec, queue, cache_task=cache_task, session=session)
2021-03-11 11:40:02 +01:00
session.commit()
2021-03-01 21:15:17 +01:00
session.close()
2021-02-01 18:12:51 +01:00
#gas_budget = tx_add['gas'] * tx_add['gasPrice']
2021-02-01 18:12:51 +01:00
gas_budget = account_registry.gas(tx_signed_raw_hex)
logg.debug('register user tx {} {} {}'.format(tx_hash_hex, queue, gas_budget))
s = create_check_gas_task(
2021-02-01 18:12:51 +01:00
[tx_signed_raw_hex],
chain_spec,
2021-02-01 18:12:51 +01:00
writer_address,
gas=gas_budget,
2021-02-01 18:12:51 +01:00
tx_hashes_hex=[tx_hash_hex],
queue=queue,
)
s.apply_async()
return account_address
2021-03-07 14:51:59 +01:00
@celery_app.task(bind=True, base=CriticalSQLAlchemyAndSignerTask)
2021-02-01 18:12:51 +01:00
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)
2021-03-08 10:11:04 +01:00
registry = safe_registry(c.w3)
txf = AccountTxFactory(account_address, c, registry=registry)
2021-02-01 18:12:51 +01:00
2021-03-11 11:40:02 +01:00
#session = SessionBase.create_session()
session = self.create_session()
2021-03-06 18:55:51 +01:00
tx_add = txf.gift(account_address, chain_spec, self.request.root_id, session=session)
2021-03-03 08:37:26 +01:00
(tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_add, chain_str, queue, 'cic_eth.eth.account.cache_gift_data', session=session)
2021-03-11 11:40:02 +01:00
session.commit()
2021-03-03 08:37:26 +01:00
session.close()
2021-02-01 18:12:51 +01:00
gas_budget = tx_add['gas'] * tx_add['gasPrice']
2021-03-03 08:37:26 +01:00
logg.debug('gift user tx {}'.format(tx_hash_hex))
2021-02-01 18:12:51 +01:00
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()
2021-02-01 18:12:51 +01:00
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
"""
chain_spec = ChainSpec.from_chain_str(chain_str)
2021-03-11 11:40:02 +01:00
o = sign_message(account, '0x2a')
2021-02-01 18:12:51 +01:00
try:
conn = RPCConnection.connect(chain_spec, 'signer')
2021-03-11 11:40:02 +01:00
conn.do(o)
2021-02-01 18:12:51 +01:00
return account
except Exception as e:
logg.debug('cannot sign with {}: {}'.format(account, e))
return None
2021-03-11 11:40:02 +01:00
2021-02-01 18:12:51 +01:00
2021-03-11 11:40:02 +01:00
@celery_app.task(bind=True, base=BaseTask)
2021-02-01 18:12:51 +01:00
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
"""
2021-03-11 11:40:02 +01:00
session = self.create_session()
role_tag = AccountRole.role_for(account, session=session)
session.close()
return role_tag
2021-02-01 18:12:51 +01:00
2021-03-11 11:40:02 +01:00
@celery_app.task(bind=True, base=CriticalSQLAlchemyTask)
2021-02-01 18:12:51 +01:00
def cache_gift_data(
2021-03-11 11:40:02 +01:00
self,
2021-02-01 18:12:51 +01:00
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'])
2021-03-11 11:40:02 +01:00
#session = SessionBase.create_session()
session = self.create_session()
2021-02-01 18:12:51 +01:00
tx_cache = TxCache(
tx_hash_hex,
tx['from'],
tx['to'],
ZERO_ADDRESS,
ZERO_ADDRESS,
2021-02-01 18:12:51 +01:00
0,
0,
session=session,
2021-02-01 18:12:51 +01:00
)
session.add(tx_cache)
session.commit()
cache_id = tx_cache.id
session.close()
return (tx_hash_hex, cache_id)
2021-03-11 11:40:02 +01:00
@celery_app.task(bind=True, base=CriticalSQLAlchemyTask)
2021-02-01 18:12:51 +01:00
def cache_account_data(
2021-03-11 11:40:02 +01:00
self,
2021-02-01 18:12:51 +01:00
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'])
session = SessionBase.create_session()
2021-02-01 18:12:51 +01:00
tx_cache = TxCache(
tx_hash_hex,
tx['from'],
tx['to'],
ZERO_ADDRESS,
ZERO_ADDRESS,
2021-02-01 18:12:51 +01:00
0,
0,
session=session,
2021-02-01 18:12:51 +01:00
)
session.add(tx_cache)
session.commit()
cache_id = tx_cache.id
session.close()
return (tx_hash_hex, cache_id)