def send_eth(self, uuid: UUID, amount_wei: int, recipient_address: str, signing_address: Optional[str] = None, encrypted_private_key: Optional[str] = None, prior_tasks: Optional[UUIDList] = None, posterior_tasks: Optional[UUIDList] = None): """ The main entrypoint sending eth. :param uuid: the celery generated uuid for the task :param amount_wei: the amount in WEI to send :param recipient_address: the recipient address :param signing_address: address of the wallet signing the txn :param encrypted_private_key: private key of the wallet making the transaction, encrypted using key from settings :param prior_tasks: a list of task uuids that must succeed before this task will be attempted :param posterior_tasks: a uuid list of tasks for which this task must succeed before they will be attempted :return: task_id """ signing_wallet_obj = self.get_signing_wallet_object(signing_address, encrypted_private_key) task = self.persistence_interface.create_send_eth_task(uuid, signing_wallet_obj, recipient_address, amount_wei, prior_tasks, posterior_tasks) # Attempt Create Async Transaction signature(utils.eth_endpoint('_attempt_transaction'), args=(task.uuid,)).delay()
def deploy_contract( self, uuid: UUID, contract_name: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None, signing_address: Optional[str] = None, encrypted_private_key: Optional[str]=None, gas_limit: Optional[int] = None, prior_tasks: Optional[UUIDList] = None ): """ The main deploy contract entrypoint for the processor. :param uuid: the celery generated uuid for the task :param contract_name: System will attempt to fetched abi and bytecode from this :param args: arguments for the constructor :param kwargs: keyword arguments for the constructor :param signing_address: address of the wallet signing the txn :param encrypted_private_key: private key of the wallet making the transaction, encrypted using key from settings :param gas_limit: limit on the amount of gas txn can use. Overrides system default :param prior_tasks: a list of task uuid that must succeed before this task will be attempted """ signing_wallet_obj = self.get_signing_wallet_object(signing_address, encrypted_private_key) task = self.persistence_interface.create_deploy_contract_task(uuid, signing_wallet_obj, contract_name, args, kwargs, gas_limit, prior_tasks) # Attempt Create Async Transaction signature(utils.eth_endpoint('_attempt_transaction'), args=(task.uuid,)).delay()
def transaction_task(signing_address, contract_address, contract_type, func, args=None, gas_limit=None, prior_tasks=None, reverses_task=None): kwargs = { 'signing_address': signing_address, 'contract_address': contract_address, 'abi_type': contract_type, 'function': func, 'args': args, 'prior_tasks': prior_tasks, 'reverses_task': reverses_task } if gas_limit: kwargs['gas_limit'] = gas_limit sig = signature(utils.eth_endpoint('transact_with_contract_function'), kwargs=kwargs) return utils.execute_task(sig)
def topup_if_required(self, wallet, posterior_task_uuid): balance = self.w3.eth.getBalance(wallet.address) wei_topup_threshold = wallet.wei_topup_threshold wei_target_balance = wallet.wei_target_balance or 0 if balance <= wei_topup_threshold and wei_target_balance > balance: sig = signature(utils.eth_endpoint('send_eth'), kwargs={ 'signing_address': config.MASTER_WALLET_ADDRESS, 'amount_wei': wei_target_balance - balance, 'recipient_address': wallet.address, 'prior_tasks': [], 'posterior_tasks': [posterior_task_uuid] }) task_uuid = utils.execute_task(sig) self.persistence_interface.set_wallet_last_topup_task_uuid( wallet.address, task_uuid) return task_uuid return None
def check_transaction_response(self, celery_task, transaction_id): def transaction_response_countdown(): t = lambda retries: ETH_CHECK_TRANSACTION_BASE_TIME * 2**retries # If the system has been longer than the max retry period # if previous_result: # submitted_at = datetime.strptime(previous_result['submitted_date'], "%Y-%m-%d %H:%M:%S.%f") # if (datetime.utcnow() - submitted_at).total_seconds() > ETH_CHECK_TRANSACTION_RETRIES_TIME_LIMIT: # if self.request.retries != self.max_retries: # self.request.retries = self.max_retries - 1 # # return 0 return t(celery_task.request.retries) try: transaction_object = self.persistence_interface.get_transaction( transaction_id) task = transaction_object.task transaction_hash = transaction_object.hash result = self.check_transaction_hash(transaction_hash) self.persistence_interface.update_transaction_data( transaction_id, result) status = result.get('status') print( f'Status for transaction {transaction_object.id} of task UUID {task.uuid} is:' f'\n {status}') if status == 'SUCCESS': unstarted_posteriors = self.get_unstarted_posteriors(task) for dep_task in unstarted_posteriors: print('Starting posterior task: {}'.format(dep_task.uuid)) signature(utils.eth_endpoint('_attempt_transaction'), args=(dep_task.uuid, )).delay() self.persistence_interface.set_task_status_text( task, 'SUCCESS') if status == 'PENDING': celery_task.request.retries = 0 raise Exception("Need Retry") if status == 'FAILED': self.new_transaction_attempt(task) except TaskRetriesExceededError as e: pass except Exception as e: print(e) celery_task.retry(countdown=transaction_response_countdown())
def send_eth_task(signing_address, amount_wei, recipient_address): sig = signature(utils.eth_endpoint('send_eth'), kwargs={ 'signing_address': signing_address, 'amount_wei': amount_wei, 'recipient_address': recipient_address }) return utils.execute_task(sig)
def synchronous_call(contract_address, contract_type, func, args=None): call_sig = signature(utils.eth_endpoint('call_contract_function'), kwargs={ 'contract_address': contract_address, 'abi_type': contract_type, 'function': func, 'args': args, }) return utils.execute_synchronous_celery(call_sig)
def deploy_contract_task(signing_address, contract_name, args=None, prior_tasks=None): deploy_sig = signature( utils.eth_endpoint('deploy_contract'), kwargs={ 'signing_address': signing_address, 'contract_name': contract_name, 'args': args, 'prior_tasks': prior_tasks }) return utils.execute_task(deploy_sig)
def await_task_success(task_uuid, timeout=None, poll_frequency=0.5): elapsed = 0 print(f'Awaiting success for task uuid: {task_uuid}') while timeout is None or elapsed <= timeout: sig = signature(utils.eth_endpoint('get_task'), kwargs={'task_uuid': task_uuid}) task = utils.execute_synchronous_celery(sig) if task and task['status'] == 'SUCCESS': return task else: sleep(poll_frequency) elapsed += poll_frequency raise TimeoutError
def new_transaction_attempt(self, task): number_of_attempts_this_round = abs( len(task.transactions) - self.task_max_retries * (task.previous_invocations or 0) ) if number_of_attempts_this_round >= self.task_max_retries: print(f"Maximum retries exceeded for task {task.uuid}") if task.status_text != 'SUCCESS': self.persistence_interface.set_task_status_text(task, 'FAILED') raise TaskRetriesExceededError else: signature(utils.eth_endpoint('_attempt_transaction'), args=(task.uuid,)).apply_async( countdown=RETRY_TRANSACTION_BASE_TIME * 4 ** number_of_attempts_this_round )
def topup_wallets(): wallets = persistence_interface.get_all_wallets() for wallet in wallets: if (wallet.wei_topup_threshold or 0) > 0: last_topup_task_uuid = wallet.last_topup_task_uuid if last_topup_task_uuid: task = persistence_interface.get_task_from_uuid( last_topup_task_uuid) if task and task.status in ['PENDING', 'UNSTARTED']: return signature(utils.eth_endpoint('topup_wallet_if_required'), kwargs={ 'address': wallet.address }).delay()
def transact_with_contract_function( self, uuid: UUID, contract_address: str, abi_type: str, function_name: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None, signing_address: Optional[str] = None, encrypted_private_key: Optional[str] = None, gas_limit: Optional[int] = None, prior_tasks: Optional[UUIDList] = None, reserves_task: Optional[UUID] = None): """ The main transaction entrypoint for the processor. :param uuid: the celery generated uuid for the task :param contract_address: the address of the contract for the function :param abi_type: the type of ABI for the contract being called :param function_name: name of the function :param args: arguments for the function :param kwargs: keyword arguments for the function :param signing_address: address of the wallet signing the txn :param encrypted_private_key: private key of the wallet making the transaction, encrypted using key from settings :param gas_limit: limit on the amount of gas txn can use. Overrides system default :param prior_tasks: a list of task uuids that must succeed before this task will be attempted, :param reserves_task: the uuid of a task that this task reverses. can only be a transferFrom :return: task_id """ signing_wallet_obj = self.get_signing_wallet_object( signing_address, encrypted_private_key) task = self.persistence_interface.create_function_task( uuid, signing_wallet_obj, contract_address, abi_type, function_name, args, kwargs, gas_limit, prior_tasks, reserves_task) # Attempt Create Async Transaction signature(utils.eth_endpoint('_attempt_transaction'), args=(task.uuid, )).delay()
def topup_if_required(address): balance = w3.eth.getBalance(address) wallet = persistence_module.get_wallet_by_address(address) wei_topup_threshold = wallet.wei_topup_threshold wei_target_balance = wallet.wei_target_balance or 0 if balance <= wei_topup_threshold and wei_target_balance > balance: sig = signature(utils.eth_endpoint('send_eth'), kwargs={ 'signing_address': config.MASTER_WALLET_ADDRESS, 'amount_wei': wei_target_balance - balance, 'recipient_address': address, 'prior_tasks': [] }) task_uuid = utils.execute_task(sig) persistence_module.set_wallet_last_topup_task_uuid(address, task_uuid) return task_uuid return None
def _retry_task(self, task): self.persistence_interface.increment_task_invokations(task) signature(utils.eth_endpoint('_attempt_transaction'), args=(task.uuid,)).delay()
def attempt_transaction(self, task_uuid): task = self.persistence_interface.get_task_from_uuid(task_uuid) unsatisfied_prior_tasks = self.get_unsatisfied_prior_tasks(task) if len(unsatisfied_prior_tasks) > 0: print('Skipping {}: prior tasks {} unsatisfied'.format( task.id, [f'{u.id} ({u.uuid})' for u in unsatisfied_prior_tasks])) return topup_uuid = self.topup_if_required(task.signing_wallet, task_uuid) if topup_uuid: print(f'Skipping {task.id}: Topup required') return # This next section is designed to ensure that we don't have two transactions running for the same task # at the same time. Under normal conditions this doesn't happen, but the 'retry failed transactions' can # get us there if it's called twice in quick succession. We use a mutex over the next lines # to prevent two processes both passing the 'current_status' test and then creating a transaction have_lock = False lock = self.red.lock(f'TaskID-{task.id}', timeout=10) try: have_lock = lock.acquire(blocking_timeout=1) if have_lock: current_status = task.status if current_status in ['SUCCESS', 'PENDING']: print(f'Skipping {task.id}: task status is currently {current_status}') return transaction_obj = self.persistence_interface.create_blockchain_transaction(task_uuid) else: print(f'Skipping {task.id}: Failed to aquire lock') return finally: if have_lock: lock.release() task_object = self.persistence_interface.get_task_from_uuid(task_uuid) number_of_attempts = len(task_object.transactions) attempt_info = f'\nAttempt number: {number_of_attempts} ' \ f' for invocation round: {task_object.previous_invocations + 1}' if task_object.type == 'SEND_ETH': transfer_amount = int(task_object.amount) print(f'Starting Send Eth Transaction for {task_uuid}.' + attempt_info) chain1 = signature(utils.eth_endpoint('_process_send_eth_transaction'), args=(transaction_obj.id, task_object.recipient_address, transfer_amount, task_object.id)) elif task_object.type == 'FUNCTION': print(f'Starting {task_object.function} Transaction for {task_uuid}.' + attempt_info) chain1 = signature(utils.eth_endpoint('_process_function_transaction'), args=(transaction_obj.id, task_object.contract_address, task_object.abi_type, task_object.function, task_object.args, task_object.kwargs, task_object.gas_limit, task_object.id)) elif task_object.type == 'DEPLOY_CONTRACT': print(f'Starting Deploy {task_object.contract_name} Contract Transaction for {task_uuid}.' + attempt_info) chain1 = signature(utils.eth_endpoint('_process_deploy_contract_transaction'), args=(transaction_obj.id, task_object.contract_name, task_object.args, task_object.kwargs, task_object.gas_limit, task_object.id)) else: raise Exception(f"Task type {task_object.type} not recognised") chain2 = signature(utils.eth_endpoint('_check_transaction_response')) error_callback = signature(utils.eth_endpoint('_log_error'), args=(transaction_obj.id,)) return chain([chain1, chain2]).on_error(error_callback).delay()
eth_config['gas_price_gwei'] = config.ETH_GAS_PRICE eth_config['gas_limit'] = config.ETH_GAS_LIMIT ETH_CHECK_TRANSACTION_RETRIES = config.ETH_CHECK_TRANSACTION_RETRIES ETH_CHECK_TRANSACTION_RETRIES_TIME_LIMIT = config.ETH_CHECK_TRANSACTION_RETRIES_TIME_LIMIT ETH_CHECK_TRANSACTION_BASE_TIME = config.ETH_CHECK_TRANSACTION_BASE_TIME celery_app = Celery('tasks', broker=config.REDIS_URL, backend=config.REDIS_URL, task_serializer='json') celery_app.conf.beat_schedule = { "maintain_eth_balances": { "task": utils.eth_endpoint('topup_wallets'), "schedule": 600.0 }, } w3 = Web3(HTTPProvider(config.ETH_HTTP_PROVIDER)) red = redis.Redis.from_url(config.REDIS_URL) persistence_interface = SQLPersistenceInterface(w3=w3, red=red) blockchain_processor = TransactionProcessor( **eth_config, w3=w3, red=red, persistence_interface=persistence_interface