diff --git a/nucypher/blockchain/eth/agents.py b/nucypher/blockchain/eth/agents.py index a767e335a..9df30f6cf 100644 --- a/nucypher/blockchain/eth/agents.py +++ b/nucypher/blockchain/eth/agents.py @@ -19,16 +19,20 @@ import random import math import sys -from constant_sorrow.constants import ( +from constant_sorrow.constants import ( # type: ignore CONTRACT_CALL, NO_CONTRACT_AVAILABLE, TRANSACTION, CONTRACT_ATTRIBUTE ) +from eth_typing.encoding import HexStr +from eth_typing.evm import ChecksumAddress from eth_utils.address import to_checksum_address -from twisted.logger import Logger -from typing import Dict, Generator, Iterable, List, Tuple, Type, TypeVar, Union -from web3.contract import Contract +from hexbytes.main import HexBytes +from twisted.logger import Logger # type: ignore +from typing import Dict, Iterable, List, Tuple, Type, Union, Any, Optional +from web3.contract import Contract, ContractFunction +from web3.types import Wei, Timestamp, TxReceipt, TxParams, Nonce from nucypher.blockchain.eth.constants import ( ADJUDICATOR_CONTRACT_NAME, @@ -41,31 +45,40 @@ from nucypher.blockchain.eth.constants import ( PREALLOCATION_ESCROW_CONTRACT_NAME, STAKING_ESCROW_CONTRACT_NAME, STAKING_INTERFACE_CONTRACT_NAME, - STAKING_INTERFACE_ROUTER_CONTRACT_NAME + STAKING_INTERFACE_ROUTER_CONTRACT_NAME, + WORKLOCK_CONTRACT_NAME ) from nucypher.blockchain.eth.decorators import contract_api, validate_checksum_address from nucypher.blockchain.eth.events import ContractEvents from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory from nucypher.blockchain.eth.registry import AllocationRegistry, BaseContractRegistry +from nucypher.types import ( + Agent, + NuNits, + SubStakeInfo, + RawSubStakeInfo, + Period, + WorklockParameters, + StakerFlags, + StakerInfo, + PeriodDelta, + StakingEscrowParameters +) from nucypher.blockchain.eth.utils import epoch_to_period from nucypher.crypto.api import sha256_digest -# TODO: Use everywhere -# TODO: Uses string to avoid circular import -Agent = TypeVar('Agent', bound='EthereumContractAgent') - class ContractAgency: + """Where agents live and die.""" + # TODO: Enforce singleton - #1506 - Okay, actually, make this into a module - - __agents = dict() - TRANSACTION_REGISTRY = dict() + __agents: Dict[Type[Agent], Agent] = dict() @classmethod def get_agent(cls, agent_class: Type[Agent], - registry: BaseContractRegistry = None, - provider_uri: str = None) -> Agent: + registry: Optional[BaseContractRegistry] = None, + provider_uri: Optional[str] = None) -> Agent: if not issubclass(agent_class, EthereumContractAgent): raise TypeError(f"Only agent subclasses can be used from the agency.") @@ -75,15 +88,13 @@ class ContractAgency: registry = list(cls.__agents.keys()).pop() else: raise ValueError("Need to specify a registry in order to get an agent from the ContractAgency") - else: - registry_id = registry.id try: - return cls.__agents[registry_id][agent_class] + return cls.__agents[registry.id][agent_class] except KeyError: agent = agent_class(registry=registry, provider_uri=provider_uri) - cls.__agents[registry_id] = cls.__agents.get(registry_id, dict()) - cls.__agents[registry_id][agent_class] = agent + cls.__agents[registry.id] = cls.__agents.get(registry.id, dict()) + cls.__agents[registry.id][agent_class] = agent return agent @staticmethod @@ -98,12 +109,12 @@ class ContractAgency: def get_agent_by_contract_name(cls, contract_name: str, registry: BaseContractRegistry, - provider_uri: str = None - ) -> 'EthereumContractAgent': - agent_name = cls._contract_name_to_agent_name(name=contract_name) + provider_uri: Optional[str] = None + ) -> Agent: + agent_name: str = cls._contract_name_to_agent_name(name=contract_name) agents_module = sys.modules[__name__] - agent_class = getattr(agents_module, agent_name) - agent = cls.get_agent(agent_class=agent_class, registry=registry, provider_uri=provider_uri) + agent_class: Type[Agent] = getattr(agents_module, agent_name) + agent: Agent = cls.get_agent(agent_class=agent_class, registry=registry, provider_uri=provider_uri) return agent @@ -112,15 +123,16 @@ class EthereumContractAgent: Base class for ethereum contract wrapper types that interact with blockchain contract instances """ - registry_contract_name = NotImplemented - _forward_address = True - _proxy_name = None + registry_contract_name: str = NotImplemented + _forward_address: bool = True + _proxy_name: Optional[str] = None # TODO - #842: Gas Management - DEFAULT_TRANSACTION_GAS_LIMITS = {} + DEFAULT_TRANSACTION_GAS_LIMITS: Dict[str, Union[Wei, None]] + DEFAULT_TRANSACTION_GAS_LIMITS = {'default': None} class ContractNotDeployed(Exception): - pass + """Raised when attempting to access a contract that is not deployed on the current network.""" class RequirementError(Exception): """ @@ -130,19 +142,16 @@ class EthereumContractAgent: def __init__(self, registry: BaseContractRegistry, - provider_uri: str = None, - contract: Contract = None, - transaction_gas: int = None - ) -> None: + provider_uri: Optional[str] = None, + contract: Optional[Contract] = None, + transaction_gas: Optional[Wei] = None): self.log = Logger(self.__class__.__name__) - self.registry = registry - # NOTE: Entry-point for multi-provider support self.blockchain = BlockchainInterfaceFactory.get_or_create_interface(provider_uri=provider_uri) - if contract is None: # Fetch the contract + if not contract: # Fetch the contract contract = self.blockchain.get_contract_by_name(registry=self.registry, contract_name=self.registry_contract_name, proxy_name=self._proxy_name, @@ -150,7 +159,7 @@ class EthereumContractAgent: self.__contract = contract self.events = ContractEvents(contract) if not transaction_gas: - transaction_gas = EthereumContractAgent.DEFAULT_TRANSACTION_GAS_LIMITS + transaction_gas = EthereumContractAgent.DEFAULT_TRANSACTION_GAS_LIMITS['default'] self.transaction_gas = transaction_gas super().__init__() @@ -159,20 +168,20 @@ class EthereumContractAgent: self.blockchain.provider_uri, self.registry)) - def __repr__(self): + def __repr__(self) -> str: class_name = self.__class__.__name__ r = "{}(registry={}, contract={})" return r.format(class_name, self.registry, self.registry_contract_name) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return bool(self.contract.address == other.contract.address) @property - def contract(self): + def contract(self) -> Contract: return self.__contract @property - def contract_address(self): + def contract_address(self) -> ChecksumAddress: return self.__contract.address @property @@ -181,7 +190,7 @@ class EthereumContractAgent: @property @contract_api(CONTRACT_ATTRIBUTE) - def owner(self): + def owner(self) -> Optional[ChecksumAddress]: if not self._proxy_name: # Only upgradeable + ownable contracts can implement ownership transference. return None @@ -190,68 +199,77 @@ class EthereumContractAgent: class NucypherTokenAgent(EthereumContractAgent): - registry_contract_name = NUCYPHER_TOKEN_CONTRACT_NAME + registry_contract_name: str = NUCYPHER_TOKEN_CONTRACT_NAME @contract_api(CONTRACT_CALL) - def get_balance(self, address: str = None) -> int: + def get_balance(self, address: Optional[ChecksumAddress] = None) -> NuNits: """Get the NU balance (in NuNits) of a token holder address, or of this contract address""" - address = address if address is not None else self.contract_address + address: ChecksumAddress = address if address is not None else self.contract_address return self.contract.functions.balanceOf(address).call() @contract_api(CONTRACT_CALL) - def get_allowance(self, owner: str, spender: str) -> int: + def get_allowance(self, owner: ChecksumAddress, spender: ChecksumAddress) -> NuNits: """Check the amount of tokens that an owner allowed to a spender""" - return self.contract.functions.allowance(owner, spender).call() + allowance: int = self.contract.functions.allowance(owner, spender).call() + return NuNits(allowance) @contract_api(TRANSACTION) - def increase_allowance(self, sender_address: str, target_address: str, increase: int) -> dict: - contract_function = self.contract.functions.increaseAllowance(target_address, increase) - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address) + def increase_allowance(self, + sender_address: ChecksumAddress, + target_address: ChecksumAddress, + increase: NuNits + ) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.increaseAllowance(target_address, increase) + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address) return receipt @contract_api(TRANSACTION) - def approve_transfer(self, amount: int, target_address: str, sender_address: str) -> dict: + def approve_transfer(self, amount: NuNits, + target_address: ChecksumAddress, + sender_address: ChecksumAddress + ) -> TxReceipt: """Approve the transfer of tokens from the sender address to the target address.""" payload = {'gas': 500_000} # TODO #842: gas needed for use with geth! <<<< Is this still open? - contract_function = self.contract.functions.approve(target_address, amount) - receipt = self.blockchain.send_transaction(contract_function=contract_function, - payload=payload, - sender_address=sender_address) + contract_function: ContractFunction = self.contract.functions.approve(target_address, amount) + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, + payload=payload, + sender_address=sender_address) return receipt @contract_api(TRANSACTION) - def transfer(self, amount: int, target_address: str, sender_address: str) -> dict: - contract_function = self.contract.functions.transfer(target_address, amount) - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address) + def transfer(self, amount: NuNits, target_address: ChecksumAddress, sender_address: ChecksumAddress) -> TxReceipt: + """Transfer an amount of tokens from the sender address to the target address.""" + contract_function: ContractFunction = self.contract.functions.transfer(target_address, amount) + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address) return receipt @contract_api(TRANSACTION) def approve_and_call(self, - amount: int, - target_address: str, - sender_address: str, + amount: NuNits, + target_address: ChecksumAddress, + sender_address: ChecksumAddress, call_data: bytes = b'', - gas_limit: int = None) -> dict: + gas_limit: Optional[Wei] = None + ) -> TxReceipt: payload = None if gas_limit: # TODO: Gas management - #842 payload = {'gas': gas_limit} - - approve_and_call = self.contract.functions.approveAndCall(target_address, amount, call_data) - approve_and_call_receipt = self.blockchain.send_transaction(contract_function=approve_and_call, - sender_address=sender_address, - payload=payload) + approve_and_call: ContractFunction = self.contract.functions.approveAndCall(target_address, amount, call_data) + approve_and_call_receipt: TxReceipt = self.blockchain.send_transaction(contract_function=approve_and_call, + sender_address=sender_address, + payload=payload) return approve_and_call_receipt class StakingEscrowAgent(EthereumContractAgent): - registry_contract_name = STAKING_ESCROW_CONTRACT_NAME - _proxy_name = DISPATCHER_CONTRACT_NAME + registry_contract_name: str = STAKING_ESCROW_CONTRACT_NAME + _proxy_name: str = DISPATCHER_CONTRACT_NAME - DEFAULT_PAGINATION_SIZE = 30 # TODO: Use dynamic pagination size (see #1424) + DEFAULT_PAGINATION_SIZE: int = 30 # TODO: Use dynamic pagination size (see #1424) class NotEnoughStakers(Exception): - pass + """Raised when the are not enough stakers available to complete an operation""" # # Staker Network Status @@ -263,28 +281,32 @@ class StakingEscrowAgent(EthereumContractAgent): return self.contract.functions.getStakersLength().call() @contract_api(CONTRACT_CALL) - def get_current_period(self) -> int: + def get_current_period(self) -> Period: """Returns the current period""" return self.contract.functions.getCurrentPeriod().call() @contract_api(CONTRACT_CALL) - def get_stakers(self) -> List[str]: + def get_stakers(self) -> List[ChecksumAddress]: """Returns a list of stakers""" - num_stakers = self.get_staker_population() - stakers = [self.contract.functions.stakers(i).call() for i in range(num_stakers)] + num_stakers: int = self.get_staker_population() + stakers: List[ChecksumAddress] = [self.contract.functions.stakers(i).call() for i in range(num_stakers)] return stakers @contract_api(CONTRACT_CALL) - def partition_stakers_by_activity(self) -> Tuple[List[str], List[str], List[str]]: - """Returns three lists of stakers depending on their commitments: + def partition_stakers_by_activity(self) -> Tuple[List[ChecksumAddress], List[ChecksumAddress], List[ChecksumAddress]]: + """ + Returns three lists of stakers depending on their commitments: The first list contains stakers that already committed to next period. The second, stakers that committed to current period but haven't committed to next yet. - The third contains stakers that have missed commitments before current period""" + The third contains stakers that have missed commitments before current period + """ - num_stakers = self.get_staker_population() - current_period = self.get_current_period() + num_stakers: int = self.get_staker_population() + current_period: Period = self.get_current_period() + active_stakers: List[ChecksumAddress] = list() + pending_stakers: List[ChecksumAddress] = list() + missing_stakers: List[ChecksumAddress] = list() - active_stakers, pending_stakers, missing_stakers = [], [], [] for i in range(num_stakers): staker = self.contract.functions.stakers(i).call() last_committed_period = self.get_last_committed_period(staker) @@ -298,7 +320,7 @@ class StakingEscrowAgent(EthereumContractAgent): return active_stakers, pending_stakers, missing_stakers @contract_api(CONTRACT_CALL) - def get_all_active_stakers(self, periods: int, pagination_size: int = None) -> Tuple[int, List[str]]: + def get_all_active_stakers(self, periods: int, pagination_size: Optional[int] = None) -> Tuple[NuNits, List[ChecksumAddress]]: """Only stakers which committed to the current period (in the previous period) are used.""" if not periods > 0: raise ValueError("Period must be > 0") @@ -309,13 +331,13 @@ class StakingEscrowAgent(EthereumContractAgent): raise ValueError("Pagination size must be >= 0") if pagination_size > 0: - num_stakers = self.get_staker_population() + num_stakers: int = self.get_staker_population() start_index = 0 n_tokens = 0 stakers = list() while start_index < num_stakers: - temp_locked_tokens, temp_stakers = \ - self.contract.functions.getActiveStakers(periods, start_index, pagination_size).call() + active_stakers: Tuple[tuple, tuple] = self.contract.functions.getActiveStakers(periods, start_index, pagination_size).call() + temp_locked_tokens, temp_stakers = active_stakers n_tokens += temp_locked_tokens stakers += temp_stakers start_index += pagination_size @@ -329,7 +351,7 @@ class StakingEscrowAgent(EthereumContractAgent): return n_tokens, stakers @contract_api(CONTRACT_CALL) - def get_all_locked_tokens(self, periods: int, pagination_size: int = None) -> int: + def get_all_locked_tokens(self, periods: int, pagination_size: Optional[int] = None) -> NuNits: all_locked_tokens, _stakers = self.get_all_active_stakers(periods=periods, pagination_size=pagination_size) return all_locked_tokens @@ -338,7 +360,7 @@ class StakingEscrowAgent(EthereumContractAgent): # @contract_api(CONTRACT_CALL) - def get_global_locked_tokens(self, at_period: int = None) -> int: + def get_global_locked_tokens(self, at_period: Optional[Period] = None) -> NuNits: """ Gets the number of locked tokens for *all* stakers that have made a commitment to the specified period. @@ -356,54 +378,59 @@ class StakingEscrowAgent(EthereumContractAgent): Returns an amount of NuNits. """ - if at_period is None: + if not at_period: # Get the current period on-chain by default. - at_period = self.contract.functions.getCurrentPeriod().call() - return self.contract.functions.lockedPerPeriod(at_period).call() + at_period: int = self.contract.functions.getCurrentPeriod().call() + return NuNits(self.contract.functions.lockedPerPeriod(at_period).call()) @contract_api(CONTRACT_CALL) - def get_staker_info(self, staker_address: str): - return self.contract.functions.stakerInfo(staker_address).call() + def get_staker_info(self, staker_address: ChecksumAddress) -> StakerInfo: + return StakerInfo(self.contract.functions.stakerInfo(staker_address).call()) @contract_api(CONTRACT_CALL) - def get_locked_tokens(self, staker_address: str, periods: int = 0) -> int: + def get_locked_tokens(self, staker_address: ChecksumAddress, periods: int = 0) -> NuNits: """ Returns the amount of tokens this staker has locked for a given duration in periods measured from the current period forwards. """ if periods < 0: raise ValueError(f"Periods value must not be negative, Got '{periods}'.") - return self.contract.functions.getLockedTokens(staker_address, periods).call() + return NuNits(self.contract.functions.getLockedTokens(staker_address, periods).call()) @contract_api(CONTRACT_CALL) - def owned_tokens(self, staker_address: str) -> int: + def owned_tokens(self, staker_address: ChecksumAddress) -> NuNits: """ Returns all tokens that belong to staker_address, including locked, unlocked and rewards. """ - return self.contract.functions.getAllTokens(staker_address).call() + return NuNits(self.contract.functions.getAllTokens(staker_address).call()) @contract_api(CONTRACT_CALL) - def get_substake_info(self, staker_address: str, stake_index: int) -> Tuple[int, int, int]: + def get_substake_info(self, staker_address: ChecksumAddress, stake_index: int) -> SubStakeInfo: first_period, *others, locked_value = self.contract.functions.getSubStakeInfo(staker_address, stake_index).call() - last_period = self.contract.functions.getLastPeriodOfSubStake(staker_address, stake_index).call() - return first_period, last_period, locked_value + last_period: Period = self.contract.functions.getLastPeriodOfSubStake(staker_address, stake_index).call() + return SubStakeInfo((first_period, last_period, locked_value)) @contract_api(CONTRACT_CALL) - def get_raw_substake_info(self, staker_address: str, stake_index: int) -> Tuple[int, int, int, int]: - result = self.contract.functions.getSubStakeInfo(staker_address, stake_index).call() + def get_raw_substake_info(self, staker_address: ChecksumAddress, stake_index: int) -> RawSubStakeInfo: + result: RawSubStakeInfo = self.contract.functions.getSubStakeInfo(staker_address, stake_index).call() first_period, last_period, periods, locked = result - return first_period, last_period, periods, locked + return RawSubStakeInfo((first_period, last_period, periods, locked)) @contract_api(CONTRACT_CALL) - def get_all_stakes(self, staker_address: str) -> Iterable[Tuple[int, int, int, int]]: - stakes_length = self.contract.functions.getSubStakesLength(staker_address).call() + def get_all_stakes(self, staker_address: ChecksumAddress) -> Iterable[RawSubStakeInfo]: + stakes_length: int = self.contract.functions.getSubStakesLength(staker_address).call() if stakes_length == 0: return iter(()) # Empty iterable, There are no stakes for stake_index in range(stakes_length): yield self.get_substake_info(staker_address=staker_address, stake_index=stake_index) @contract_api(TRANSACTION) - def deposit_tokens(self, staker_address: str, amount: int, lock_periods: int, sender_address: str = None) -> dict: + def deposit_tokens(self, + staker_address: ChecksumAddress, + amount: NuNits, + lock_periods: PeriodDelta, + sender_address: Optional[ChecksumAddress] = None + ) -> TxReceipt: """ Send tokens to the escrow from the sender's address to be locked on behalf of the staker address. If the sender address is not provided, the stakers address is used. @@ -411,14 +438,17 @@ class StakingEscrowAgent(EthereumContractAgent): """ if not sender_address: sender_address = staker_address - contract_function = self.contract.functions.deposit(staker_address, amount, lock_periods) - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address) + contract_function: ContractFunction = self.contract.functions.deposit(staker_address, amount, lock_periods) + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address) return receipt @contract_api(CONTRACT_CALL) - def construct_batch_deposit_parameters(self, deposits: Dict[str, List[Tuple[int, int]]]) -> Tuple[list, list, list, list]: - max_substakes = self.contract.functions.MAX_SUB_STAKES().call() - stakers, number_of_substakes, amounts, lock_periods = list(), list(), list(), list() + def construct_batch_deposit_parameters(self, deposits: Dict[ChecksumAddress, List[Tuple[int, int]]]) -> Tuple[list, list, list, list]: + max_substakes: int = self.contract.functions.MAX_SUB_STAKES().call() + stakers: List[ChecksumAddress] = list() + number_of_substakes: List[int] = list() + amounts: List[NuNits] = list() + lock_periods: List[int] = list() for staker, substakes in deposits.items(): if not 0 < len(substakes) <= max_substakes: raise self.RequirementError(f"Number of substakes for staker {staker} must be >0 and ≤{max_substakes}") @@ -435,23 +465,25 @@ class StakingEscrowAgent(EthereumContractAgent): @contract_api(TRANSACTION) def batch_deposit(self, - stakers: list, - number_of_substakes: list, - amounts: list, - lock_periods: list, - sender_address: str, + stakers: List[ChecksumAddress], + number_of_substakes: List, + amounts: List[NuNits], + lock_periods: PeriodDelta, + sender_address: ChecksumAddress, dry_run: bool = False, - gas_limit: int = None) -> Union[dict, int]: - min_gas_batch_deposit = 250_000 + gas_limit: Optional[Wei] = None + ) -> Union[TxReceipt, Wei]: + + min_gas_batch_deposit: Wei = Wei(250_000) # TODO: move elsewhere? if gas_limit and gas_limit < min_gas_batch_deposit: raise ValueError(f"{gas_limit} is not enough gas for any batch deposit") - contract_function = self.contract.functions.batchDeposit(stakers, number_of_substakes, amounts, lock_periods) + contract_function: ContractFunction = self.contract.functions.batchDeposit(stakers, number_of_substakes, amounts, lock_periods) if dry_run: payload = {'from': sender_address} if gas_limit: payload['gas'] = gas_limit - estimated_gas = contract_function.estimateGas(payload) # If TX is not correct, or there's not enough gas, this will fail. + estimated_gas: Wei = Wei(contract_function.estimateGas(payload)) # If TX is not correct, or there's not enough gas, this will fail. if gas_limit and estimated_gas > gas_limit: raise ValueError(f"Estimated gas for transaction exceeds gas limit {gas_limit}") return estimated_gas @@ -462,163 +494,170 @@ class StakingEscrowAgent(EthereumContractAgent): return receipt @contract_api(TRANSACTION) - def divide_stake(self, staker_address: str, stake_index: int, target_value: int, periods: int) -> dict: - contract_function = self.contract.functions.divideStake(stake_index, target_value, periods) + def divide_stake(self, + staker_address: ChecksumAddress, + stake_index: int, + target_value: NuNits, + periods: PeriodDelta + ) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.divideStake(stake_index, target_value, periods) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) return receipt @contract_api(TRANSACTION) - def prolong_stake(self, staker_address: str, stake_index: int, periods: int) -> dict: - contract_function = self.contract.functions.prolongStake(stake_index, periods) + def prolong_stake(self, staker_address: ChecksumAddress, stake_index: int, periods: PeriodDelta) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.prolongStake(stake_index, periods) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) return receipt @contract_api(CONTRACT_CALL) - def get_last_committed_period(self, staker_address: str) -> int: - period = self.contract.functions.getLastCommittedPeriod(staker_address).call() - return int(period) + def get_last_committed_period(self, staker_address: ChecksumAddress) -> Period: + period: int = self.contract.functions.getLastCommittedPeriod(staker_address).call() + return Period(period) @contract_api(CONTRACT_CALL) - def get_worker_from_staker(self, staker_address: str) -> str: - worker = self.contract.functions.getWorkerFromStaker(staker_address).call() + def get_worker_from_staker(self, staker_address: ChecksumAddress) -> ChecksumAddress: + worker: str = self.contract.functions.getWorkerFromStaker(staker_address).call() return to_checksum_address(worker) @contract_api(CONTRACT_CALL) - def get_staker_from_worker(self, worker_address: str) -> str: + def get_staker_from_worker(self, worker_address: ChecksumAddress) -> ChecksumAddress: staker = self.contract.functions.stakerFromWorker(worker_address).call() return to_checksum_address(staker) @contract_api(TRANSACTION) - def bond_worker(self, staker_address: str, worker_address: str) -> dict: - contract_function = self.contract.functions.bondWorker(worker_address) - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) + def bond_worker(self, staker_address: ChecksumAddress, worker_address: ChecksumAddress) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.bondWorker(worker_address) + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) return receipt # TODO: Decorate methods like this one (indirect contract call/tx) @contract_api() ? - @validate_checksum_address - def release_worker(self, staker_address: str) -> dict: + @contract_api(TRANSACTION) + def release_worker(self, staker_address: ChecksumAddress) -> TxReceipt: return self.bond_worker(staker_address=staker_address, worker_address=NULL_ADDRESS) @contract_api(TRANSACTION) - def commit_to_next_period(self, worker_address: str) -> dict: + def commit_to_next_period(self, worker_address: ChecksumAddress) -> TxReceipt: """ For each period that the worker makes a commitment, the staker is rewarded. """ - contract_function = self.contract.functions.commitToNextPeriod() - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=worker_address) + contract_function: ContractFunction = self.contract.functions.commitToNextPeriod() + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=worker_address) return receipt @contract_api(TRANSACTION) - def mint(self, staker_address: str) -> dict: + def mint(self, staker_address: ChecksumAddress) -> TxReceipt: """ Computes reward tokens for the staker's account; This is only used to calculate the reward for the final period of a stake, when you intend to withdraw 100% of tokens. """ - contract_function = self.contract.functions.mint() - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) + contract_function: ContractFunction = self.contract.functions.mint() + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) return receipt @contract_api(CONTRACT_CALL) - def calculate_staking_reward(self, staker_address: str) -> int: - token_amount = self.owned_tokens(staker_address) - staked_amount = max(self.contract.functions.getLockedTokens(staker_address, 0).call(), - self.contract.functions.getLockedTokens(staker_address, 1).call()) - reward_amount = token_amount - staked_amount - return reward_amount + def calculate_staking_reward(self, staker_address: ChecksumAddress) -> NuNits: + token_amount: NuNits = self.owned_tokens(staker_address) + staked_amount: int = max(self.contract.functions.getLockedTokens(staker_address, 0).call(), + self.contract.functions.getLockedTokens(staker_address, 1).call()) + reward_amount: int = token_amount - staked_amount + return NuNits(reward_amount) @contract_api(TRANSACTION) - def collect_staking_reward(self, staker_address: str) -> dict: + def collect_staking_reward(self, staker_address: ChecksumAddress) -> TxReceipt: """Withdraw tokens rewarded for staking.""" - reward_amount = self.calculate_staking_reward(staker_address=staker_address) + reward_amount: NuNits = self.calculate_staking_reward(staker_address=staker_address) from nucypher.blockchain.eth.token import NU self.log.debug(f"Withdrawing staking reward ({NU.from_nunits(reward_amount)}) to {staker_address}") - return self.withdraw(staker_address=staker_address, amount=reward_amount) + receipt: TxReceipt = self.withdraw(staker_address=staker_address, amount=reward_amount) + return receipt @contract_api(TRANSACTION) - def withdraw(self, staker_address: str, amount: int) -> dict: + def withdraw(self, staker_address: ChecksumAddress, amount: NuNits) -> TxReceipt: """Withdraw tokens""" payload = {'gas': 500_000} # TODO: #842 Gas Management - contract_function = self.contract.functions.withdraw(amount) + contract_function: ContractFunction = self.contract.functions.withdraw(amount) receipt = self.blockchain.send_transaction(contract_function=contract_function, payload=payload, sender_address=staker_address) return receipt @contract_api(CONTRACT_CALL) - def get_flags(self, staker_address: str) -> Tuple[bool, bool, bool, bool]: - flags = self.contract.functions.getFlags(staker_address).call() + def get_flags(self, staker_address: ChecksumAddress) -> StakerFlags: + flags: tuple = self.contract.functions.getFlags(staker_address).call() wind_down_flag, restake_flag, measure_work_flag, snapshot_flag = flags - return wind_down_flag, restake_flag, measure_work_flag, snapshot_flag + return StakerFlags((wind_down_flag, restake_flag, measure_work_flag, snapshot_flag)) @contract_api(CONTRACT_CALL) - def is_restaking(self, staker_address: str) -> bool: - _winddown_flag, restake_flag, _measure_work_flag, _snapshots_flag = self.get_flags(staker_address) - return restake_flag + def is_restaking(self, staker_address: ChecksumAddress) -> bool: + flags = StakerFlags(self.get_flags(staker_address)) + return flags.restake_flag @contract_api(CONTRACT_CALL) - def is_restaking_locked(self, staker_address: str) -> bool: + def is_restaking_locked(self, staker_address: ChecksumAddress) -> bool: return self.contract.functions.isReStakeLocked(staker_address).call() @contract_api(TRANSACTION) - def set_restaking(self, staker_address: str, value: bool) -> dict: + def set_restaking(self, staker_address: ChecksumAddress, value: bool) -> TxReceipt: """ Enable automatic restaking for a fixed duration of lock periods. If set to True, then all staking rewards will be automatically added to locked stake. """ - contract_function = self.contract.functions.setReStake(value) - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) + contract_function: ContractFunction = self.contract.functions.setReStake(value) + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) # TODO: Handle ReStakeSet event (see #1193) return receipt @contract_api(TRANSACTION) - def lock_restaking(self, staker_address: str, release_period: int) -> dict: - contract_function = self.contract.functions.lockReStake(release_period) - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) + def lock_restaking(self, staker_address: ChecksumAddress, release_period: Period) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.lockReStake(release_period) + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) # TODO: Handle ReStakeLocked event (see #1193) return receipt @contract_api(CONTRACT_CALL) - def get_restake_unlock_period(self, staker_address: str) -> int: - staker_info = self.get_staker_info(staker_address) - restake_unlock_period = int(staker_info[4]) # TODO: #1348 Use constant or enum - return restake_unlock_period + def get_restake_unlock_period(self, staker_address: ChecksumAddress) -> Period: + staker_info: StakerInfo = self.get_staker_info(staker_address) + restake_unlock_period: int = int(staker_info[4]) # TODO: #1348 Use constant or enum + return Period(restake_unlock_period) @contract_api(CONTRACT_CALL) - def is_winding_down(self, staker_address: str) -> bool: - winddown_flag, _restake_flag, _measure_work_flag, _snapshots_flag = self.get_flags(staker_address) - return winddown_flag + def is_winding_down(self, staker_address: ChecksumAddress) -> bool: + flags = StakerFlags(self.get_flags(staker_address)) + return flags.wind_down_flag @contract_api(TRANSACTION) - def set_winding_down(self, staker_address: str, value: bool) -> dict: + def set_winding_down(self, staker_address: ChecksumAddress, value: bool) -> TxReceipt: """ Enable wind down for stake. If set to True, then stakes duration will decrease in each period with `commitToNextPeriod()`. """ - contract_function = self.contract.functions.setWindDown(value) + contract_function: ContractFunction = self.contract.functions.setWindDown(value) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) # TODO: Handle WindDownSet event (see #1193) return receipt @contract_api(CONTRACT_CALL) - def is_taking_snapshots(self, staker_address: str) -> bool: + def is_taking_snapshots(self, staker_address: ChecksumAddress) -> bool: _winddown_flag, _restake_flag, _measure_work_flag, snapshots_flag = self.get_flags(staker_address) return snapshots_flag @contract_api(TRANSACTION) - def set_snapshots(self, staker_address: str, activate: bool) -> dict: + def set_snapshots(self, staker_address: ChecksumAddress, activate: bool) -> TxReceipt: """ Activate/deactivate taking balance snapshots. If set to True, then each time the balance changes, a snapshot associated to current block number is stored. """ - contract_function = self.contract.functions.setSnapshots(activate) + contract_function: ContractFunction = self.contract.functions.setSnapshots(activate) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) # TODO: Handle SnapshotSet event (see #1193) return receipt @contract_api(CONTRACT_CALL) - def staking_parameters(self) -> Tuple: + def staking_parameters(self) -> StakingEscrowParameters: parameter_signatures = ( + # Period 'secondsPerPeriod', # Seconds in single period @@ -637,10 +676,10 @@ class StakingEscrowAgent(EthereumContractAgent): 'minWorkerPeriods' # Min amount of periods while a worker can't be changed ) - def _call_function_by_name(name: str): + def _call_function_by_name(name: str) -> int: return getattr(self.contract.functions, name)().call() - staking_parameters = tuple(map(_call_function_by_name, parameter_signatures)) + staking_parameters = StakingEscrowParameters(tuple(map(_call_function_by_name, parameter_signatures))) return staking_parameters # @@ -648,23 +687,23 @@ class StakingEscrowAgent(EthereumContractAgent): # @contract_api(CONTRACT_CALL) - def swarm(self) -> Union[Generator[str, None, None], Generator[str, None, None]]: + def swarm(self) -> Iterable[ChecksumAddress]: """ Returns an iterator of all staker addresses via cumulative sum, on-network. Staker addresses are returned in the order in which they registered with the StakingEscrow contract's ledger """ for index in range(self.get_staker_population()): - staker_address = self.contract.functions.stakers(index).call() + staker_address: ChecksumAddress = self.contract.functions.stakers(index).call() yield staker_address @contract_api(CONTRACT_CALL) def sample(self, quantity: int, duration: int, - additional_ursulas:float = 1.5, + additional_ursulas: float = 1.5, attempts: int = 5, - pagination_size: int = None - ) -> List[str]: + pagination_size: Optional[int] = None + ) -> List[ChecksumAddress]: """ Select n random Stakers, according to their stake distribution. @@ -725,12 +764,12 @@ class StakingEscrowAgent(EthereumContractAgent): raise self.NotEnoughStakers('Selection failed after {} attempts'.format(attempts)) @contract_api(CONTRACT_CALL) - def get_completed_work(self, bidder_address: str): + def get_completed_work(self, bidder_address: ChecksumAddress) -> NuNits: total_completed_work = self.contract.functions.getCompletedWork(bidder_address).call() return total_completed_work @contract_api(CONTRACT_CALL) - def get_missing_commitments(self, checksum_address: str) -> int: + def get_missing_commitments(self, checksum_address: ChecksumAddress) -> int: # TODO: Move this up one layer, since it utilizes a combination of contract API methods. last_committed_period = self.get_last_committed_period(checksum_address) current_period = self.get_current_period() @@ -752,37 +791,37 @@ class StakingEscrowAgent(EthereumContractAgent): @property @contract_api(CONTRACT_ATTRIBUTE) - def worklock(self) -> str: + def worklock(self) -> ChecksumAddress: return self.contract.functions.workLock().call() @property @contract_api(CONTRACT_ATTRIBUTE) - def adjudicator(self) -> str: + def adjudicator(self) -> ChecksumAddress: return self.contract.functions.adjudicator().call() @property @contract_api(CONTRACT_ATTRIBUTE) - def policy_manager(self) -> str: + def policy_manager(self) -> ChecksumAddress: return self.contract.functions.policyManager().call() class PolicyManagerAgent(EthereumContractAgent): - registry_contract_name = POLICY_MANAGER_CONTRACT_NAME - _proxy_name = DISPATCHER_CONTRACT_NAME + registry_contract_name: str = POLICY_MANAGER_CONTRACT_NAME + _proxy_name: str = DISPATCHER_CONTRACT_NAME @contract_api(TRANSACTION) def create_policy(self, policy_id: str, - author_address: str, - value: int, - end_timestamp: int, - node_addresses: List[str], - owner_address: str = None): + author_address: ChecksumAddress, + value: Wei, + end_timestamp: Timestamp, + node_addresses: List[ChecksumAddress], + owner_address: Optional[ChecksumAddress] = None) -> TxReceipt: owner_address = owner_address or author_address payload = {'value': value} - contract_function = self.contract.functions.createPolicy(policy_id, + contract_function: ContractFunction = self.contract.functions.createPolicy(policy_id, owner_address, end_timestamp, node_addresses) @@ -797,109 +836,116 @@ class PolicyManagerAgent(EthereumContractAgent): blockchain_record = self.contract.functions.policies(policy_id).call() return blockchain_record - def fetch_arrangement_addresses_from_policy_txid(self, txhash: bytes, timeout: int = 600): + def fetch_arrangement_addresses_from_policy_txid(self, txhash: str, timeout: int = 600) -> Iterable: # TODO: Won't it be great when this is impossible? #1274 - _receipt = self.blockchain.wait_for_receipt(txhash, timeout=timeout) + _receipt = self.blockchain.client.wait_for_receipt(txhash, timeout=timeout) transaction = self.blockchain.client.w3.eth.getTransaction(txhash) _signature, parameters = self.contract.decode_function_input(transaction.data) return parameters['_nodes'] @contract_api(TRANSACTION) - def revoke_policy(self, policy_id: bytes, author_address: str) -> str: + def revoke_policy(self, policy_id: bytes, author_address: ChecksumAddress) -> TxReceipt: """Revoke by arrangement ID; Only the policy's author_address can revoke the policy.""" - contract_function = self.contract.functions.revokePolicy(policy_id) + contract_function: ContractFunction = self.contract.functions.revokePolicy(policy_id) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=author_address) return receipt @contract_api(TRANSACTION) - def collect_policy_fee(self, collector_address: str, staker_address: str) -> str: + def collect_policy_fee(self, collector_address: ChecksumAddress, staker_address: ChecksumAddress) -> TxReceipt: """Collect fees (ETH) earned since last withdrawal""" - contract_function = self.contract.functions.withdraw(collector_address) + contract_function: ContractFunction = self.contract.functions.withdraw(collector_address) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) return receipt @contract_api(CONTRACT_CALL) - def fetch_policy_arrangements(self, policy_id) -> Iterable[tuple]: + def fetch_policy_arrangements(self, policy_id: str) -> Iterable[Tuple[ChecksumAddress, int, int]]: + """ + struct ArrangementInfo { + address node; + uint256 indexOfDowntimePeriods; + uint16 lastRefundedPeriod; + } + """ record_count = self.contract.functions.getArrangementsLength(policy_id).call() for index in range(record_count): arrangement = self.contract.functions.getArrangementInfo(policy_id, index).call() yield arrangement @contract_api(TRANSACTION) - def revoke_arrangement(self, policy_id: str, node_address: str, author_address: str) -> dict: - contract_function = self.contract.functions.revokeArrangement(policy_id, node_address) + def revoke_arrangement(self, policy_id: str, node_address: ChecksumAddress, author_address: ChecksumAddress) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.revokeArrangement(policy_id, node_address) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=author_address) return receipt @contract_api(TRANSACTION) - def calculate_refund(self, policy_id: str, author_address: str) -> dict: - contract_function = self.contract.functions.calculateRefundValue(policy_id) + def calculate_refund(self, policy_id: str, author_address: ChecksumAddress) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.calculateRefundValue(policy_id) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=author_address) return receipt @contract_api(TRANSACTION) - def collect_refund(self, policy_id: str, author_address: str) -> dict: - contract_function = self.contract.functions.refund(policy_id) + def collect_refund(self, policy_id: str, author_address: ChecksumAddress) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.refund(policy_id) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=author_address) return receipt @contract_api(CONTRACT_CALL) - def get_fee_amount(self, staker_address: str) -> int: + def get_fee_amount(self, staker_address: ChecksumAddress) -> Wei: fee_amount = self.contract.functions.nodes(staker_address).call()[0] return fee_amount @contract_api(CONTRACT_CALL) - def get_fee_rate_range(self) -> Tuple[int, int, int]: + def get_fee_rate_range(self) -> Tuple[Wei, Wei, Wei]: """Check minimum, default & maximum fee rate for all policies ('global fee range')""" minimum, default, maximum = self.contract.functions.feeRateRange().call() return minimum, default, maximum @contract_api(CONTRACT_CALL) - def get_min_fee_rate(self, staker_address: str) -> int: + def get_min_fee_rate(self, staker_address: ChecksumAddress) -> Wei: """Check minimum fee rate that staker accepts""" min_rate = self.contract.functions.getMinFeeRate(staker_address).call() return min_rate @contract_api(CONTRACT_CALL) - def get_raw_min_fee_rate(self, staker_address: str) -> int: + def get_raw_min_fee_rate(self, staker_address: ChecksumAddress) -> Wei: """Check minimum acceptable fee rate set by staker for their associated worker""" min_rate = self.contract.functions.nodes(staker_address).call()[3] return min_rate @contract_api(TRANSACTION) - def set_min_fee_rate(self, staker_address: str, min_rate: int) -> dict: - contract_function = self.contract.functions.setMinFeeRate(min_rate) + def set_min_fee_rate(self, staker_address: ChecksumAddress, min_rate: Wei) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.setMinFeeRate(min_rate) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=staker_address) return receipt class PreallocationEscrowAgent(EthereumContractAgent): - registry_contract_name = PREALLOCATION_ESCROW_CONTRACT_NAME - _proxy_name = NotImplemented - _forward_address = False + registry_contract_name: str = PREALLOCATION_ESCROW_CONTRACT_NAME + _proxy_name: str = NotImplemented + _forward_address: bool = False __allocation_registry = AllocationRegistry class StakingInterfaceAgent(EthereumContractAgent): - registry_contract_name = STAKING_INTERFACE_CONTRACT_NAME - _proxy_name = STAKING_INTERFACE_ROUTER_CONTRACT_NAME - _forward_address = False + registry_contract_name: str = STAKING_INTERFACE_CONTRACT_NAME + _proxy_name: bool = STAKING_INTERFACE_ROUTER_CONTRACT_NAME + _forward_address: bool = False @validate_checksum_address - def _generate_beneficiary_agency(self, principal_address: str): + def _generate_beneficiary_agency(self, principal_address: ChecksumAddress) -> Contract: contract = self.blockchain.client.get_contract(address=principal_address, abi=self.contract.abi) return contract def __init__(self, - beneficiary: str, + beneficiary: ChecksumAddress, registry: BaseContractRegistry, - allocation_registry: AllocationRegistry = None, + allocation_registry: Optional[AllocationRegistry] = None, *args, **kwargs): - self.__allocation_registry = allocation_registry or self.__allocation_registry() + self.__allocation_registry = allocation_registry or PreallocationEscrowAgent.__allocation_registry() self.__beneficiary = beneficiary - self.__principal_contract = NO_CONTRACT_AVAILABLE - self.__interface_agent = NO_CONTRACT_AVAILABLE + self.__principal_contract: Contract = NO_CONTRACT_AVAILABLE + self.__interface_agent: Agent = NO_CONTRACT_AVAILABLE # Sets the above self.__read_principal() @@ -907,13 +953,13 @@ class PreallocationEscrowAgent(EthereumContractAgent): super().__init__(contract=self.principal_contract, registry=registry, *args, **kwargs) - def __read_interface(self, registry: BaseContractRegistry): + def __read_interface(self, registry: BaseContractRegistry) -> None: self.__interface_agent = self.StakingInterfaceAgent(registry=registry) contract = self.__interface_agent._generate_beneficiary_agency(principal_address=self.principal_contract.address) self.__interface_agent = contract @validate_checksum_address - def __fetch_principal_contract(self, contract_address: str = None) -> None: + def __fetch_principal_contract(self, contract_address: Optional[ChecksumAddress] = None) -> None: """Fetch the PreallocationEscrow deployment directly from the AllocationRegistry.""" if contract_address is not None: contract_data = self.__allocation_registry.search(contract_address=contract_address) @@ -928,18 +974,18 @@ class PreallocationEscrowAgent(EthereumContractAgent): self.__beneficiary = self.owner @validate_checksum_address - def __read_principal(self, contract_address: str = None) -> None: + def __read_principal(self, contract_address: Optional[ChecksumAddress] = None) -> None: self.__fetch_principal_contract(contract_address=contract_address) self.__set_owner() @property @contract_api(CONTRACT_ATTRIBUTE) - def owner(self) -> str: + def owner(self) -> ChecksumAddress: owner = self.principal_contract.functions.owner().call() return owner @property - def beneficiary(self) -> str: + def beneficiary(self) -> ChecksumAddress: return self.__beneficiary @property @@ -988,93 +1034,93 @@ class PreallocationEscrowAgent(EthereumContractAgent): @property @contract_api(CONTRACT_ATTRIBUTE) - def end_timestamp(self) -> int: + def end_timestamp(self) -> Timestamp: return self.principal_contract.functions.endLockTimestamp().call() @contract_api(TRANSACTION) - def lock(self, amount: int, periods: int) -> dict: - contract_function = self.__interface_agent.functions.lock(amount, periods) + def lock(self, amount: NuNits, periods: PeriodDelta) -> TxReceipt: + contract_function: ContractFunction = self.__interface_agent.functions.lock(amount, periods) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def withdraw_tokens(self, value: int) -> dict: - contract_function = self.principal_contract.functions.withdrawTokens(value) + def withdraw_tokens(self, value: NuNits) -> TxReceipt: + contract_function: ContractFunction = self.principal_contract.functions.withdrawTokens(value) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def withdraw_eth(self) -> dict: - contract_function = self.principal_contract.functions.withdrawETH() + def withdraw_eth(self) -> TxReceipt: + contract_function: ContractFunction = self.principal_contract.functions.withdrawETH() receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def deposit_as_staker(self, amount: int, lock_periods: int) -> dict: - contract_function = self.__interface_agent.functions.depositAsStaker(amount, lock_periods) + def deposit_as_staker(self, amount: NuNits, lock_periods: PeriodDelta) -> TxReceipt: + contract_function: ContractFunction = self.__interface_agent.functions.depositAsStaker(amount, lock_periods) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def withdraw_as_staker(self, value: int) -> dict: - contract_function = self.__interface_agent.functions.withdrawAsStaker(value) + def withdraw_as_staker(self, value: NuNits) -> TxReceipt: + contract_function: ContractFunction = self.__interface_agent.functions.withdrawAsStaker(value) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def bond_worker(self, worker_address: str) -> dict: - contract_function = self.__interface_agent.functions.bondWorker(worker_address) + def bond_worker(self, worker_address: ChecksumAddress) -> TxReceipt: + contract_function: ContractFunction = self.__interface_agent.functions.bondWorker(worker_address) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def release_worker(self) -> dict: + def release_worker(self) -> TxReceipt: receipt = self.bond_worker(worker_address=NULL_ADDRESS) return receipt @contract_api(TRANSACTION) - def mint(self) -> dict: - contract_function = self.__interface_agent.functions.mint() + def mint(self) -> TxReceipt: + contract_function: ContractFunction = self.__interface_agent.functions.mint() receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def collect_policy_fee(self) -> dict: - contract_function = self.__interface_agent.functions.withdrawPolicyFee() + def collect_policy_fee(self) -> TxReceipt: + contract_function: ContractFunction = self.__interface_agent.functions.withdrawPolicyFee() receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def set_min_fee_rate(self, min_rate: int) -> dict: - contract_function = self.__interface_agent.functions.setMinFeeRate(min_rate) + def set_min_fee_rate(self, min_rate: Wei) -> TxReceipt: + contract_function: ContractFunction = self.__interface_agent.functions.setMinFeeRate(min_rate) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) return receipt @contract_api(TRANSACTION) - def set_restaking(self, value: bool) -> dict: + def set_restaking(self, value: bool) -> TxReceipt: """ Enable automatic restaking for a fixed duration of lock periods. If set to True, then all staking rewards will be automatically added to locked stake. """ - contract_function = self.__interface_agent.functions.setReStake(value) + contract_function: ContractFunction = self.__interface_agent.functions.setReStake(value) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) # TODO: Handle ReStakeSet event (see #1193) return receipt @contract_api(TRANSACTION) - def lock_restaking(self, release_period: int) -> dict: - contract_function = self.__interface_agent.functions.lockReStake(release_period) + def lock_restaking(self, release_period: Period) -> TxReceipt: + contract_function: ContractFunction = self.__interface_agent.functions.lockReStake(release_period) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) # TODO: Handle ReStakeLocked event (see #1193) return receipt @contract_api(TRANSACTION) - def set_winding_down(self, value: bool) -> dict: + def set_winding_down(self, value: bool) -> TxReceipt: """ Enable wind down for stake. If set to True, then the stake's duration will decrease each period with `commitToNextPeriod()`. """ - contract_function = self.__interface_agent.functions.setWindDown(value) + contract_function: ContractFunction = self.__interface_agent.functions.setWindDown(value) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=self.__beneficiary) # TODO: Handle WindDownSet event (see #1193) return receipt @@ -1082,27 +1128,28 @@ class PreallocationEscrowAgent(EthereumContractAgent): class AdjudicatorAgent(EthereumContractAgent): - registry_contract_name = ADJUDICATOR_CONTRACT_NAME - _proxy_name = DISPATCHER_CONTRACT_NAME + registry_contract_name: str = ADJUDICATOR_CONTRACT_NAME + _proxy_name: str = DISPATCHER_CONTRACT_NAME @contract_api(TRANSACTION) - def evaluate_cfrag(self, evidence, sender_address: str) -> dict: + def evaluate_cfrag(self, evidence: 'IndisputableEvidence', sender_address: ChecksumAddress) -> TxReceipt: """Submits proof that a worker created wrong CFrag""" payload = {'gas': 500_000} # TODO #842: gas needed for use with geth. - contract_function = self.contract.functions.evaluateCFrag(*evidence.evaluation_arguments()) + contract_function: ContractFunction = self.contract.functions.evaluateCFrag(*evidence.evaluation_arguments()) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address, payload=payload) return receipt @contract_api(CONTRACT_CALL) - def was_this_evidence_evaluated(self, evidence) -> bool: - data_hash = sha256_digest(evidence.task.capsule, evidence.task.cfrag) - return self.contract.functions.evaluatedCFrags(data_hash).call() + def was_this_evidence_evaluated(self, evidence: 'IndisputableEvidence') -> bool: + data_hash: bytes = sha256_digest(evidence.task.capsule, evidence.task.cfrag) + result: bool = self.contract.functions.evaluatedCFrags(data_hash).call() + return result @property @contract_api(CONTRACT_ATTRIBUTE) - def staking_escrow_contract(self) -> str: + def staking_escrow_contract(self) -> ChecksumAddress: return self.contract.functions.escrow().call() @property @@ -1135,7 +1182,7 @@ class AdjudicatorAgent(EthereumContractAgent): return self.contract.functions.penaltyHistory(staker_address).call() @contract_api(CONTRACT_CALL) - def slashing_parameters(self) -> Tuple: + def slashing_parameters(self) -> Tuple[int, ...]: parameter_signatures = ( 'hashAlgorithm', # Hashing algorithm 'basePenalty', # Base for the penalty calculation @@ -1144,7 +1191,7 @@ class AdjudicatorAgent(EthereumContractAgent): 'rewardCoefficient', # Coefficient for calculating the reward ) - def _call_function_by_name(name: str): + def _call_function_by_name(name: str) -> int: return getattr(self.contract.functions, name)().call() staking_parameters = tuple(map(_call_function_by_name, parameter_signatures)) @@ -1153,74 +1200,74 @@ class AdjudicatorAgent(EthereumContractAgent): class WorkLockAgent(EthereumContractAgent): - registry_contract_name = "WorkLock" + registry_contract_name: str = WORKLOCK_CONTRACT_NAME # # Transactions # @contract_api(TRANSACTION) - def bid(self, value: int, checksum_address: str) -> dict: + def bid(self, value: Wei, checksum_address: ChecksumAddress) -> TxReceipt: """Bid for NU tokens with ETH.""" - contract_function = self.contract.functions.bid() + contract_function: ContractFunction = self.contract.functions.bid() receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address, payload={'value': value}) return receipt @contract_api(TRANSACTION) - def cancel_bid(self, checksum_address: str) -> dict: + def cancel_bid(self, checksum_address: ChecksumAddress) -> TxReceipt: """Cancel bid and refund deposited ETH.""" - contract_function = self.contract.functions.cancelBid() + contract_function: ContractFunction = self.contract.functions.cancelBid() receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address) return receipt @contract_api(TRANSACTION) - def force_refund(self, checksum_address: str, addresses: List[str]) -> dict: + def force_refund(self, checksum_address: ChecksumAddress, addresses: List[ChecksumAddress]) -> TxReceipt: """Force refund to bidders who can get tokens more than maximum allowed.""" addresses = sorted(addresses, key=str.casefold) - contract_function = self.contract.functions.forceRefund(addresses) + contract_function: ContractFunction = self.contract.functions.forceRefund(addresses) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address) return receipt @contract_api(TRANSACTION) def verify_bidding_correctness(self, - checksum_address: str, - gas_limit: int, # TODO - #842: Gas Management - gas_to_save_state: int = 30000) -> dict: + checksum_address: ChecksumAddress, + gas_limit: Wei, # TODO - #842: Gas Management + gas_to_save_state: Wei = Wei(30000)) -> TxReceipt: """Verify all bids are less than max allowed bid""" - contract_function = self.contract.functions.verifyBiddingCorrectness(gas_to_save_state) + contract_function: ContractFunction = self.contract.functions.verifyBiddingCorrectness(gas_to_save_state) receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address, transaction_gas_limit=gas_limit) return receipt @contract_api(TRANSACTION) - def claim(self, checksum_address: str) -> dict: + def claim(self, checksum_address: ChecksumAddress) -> TxReceipt: """ Claim tokens - will be deposited and locked as stake in the StakingEscrow contract. """ - contract_function = self.contract.functions.claim() + contract_function: ContractFunction = self.contract.functions.claim() receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address) return receipt @contract_api(TRANSACTION) - def refund(self, checksum_address: str) -> dict: + def refund(self, checksum_address: ChecksumAddress) -> TxReceipt: """Refund ETH for completed work.""" - contract_function = self.contract.functions.refund() - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address) + contract_function: ContractFunction = self.contract.functions.refund() + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address) return receipt @contract_api(TRANSACTION) - def withdraw_compensation(self, checksum_address: str) -> dict: + def withdraw_compensation(self, checksum_address: ChecksumAddress) -> TxReceipt: """Withdraw compensation after force refund.""" - contract_function = self.contract.functions.withdrawCompensation() - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address) + contract_function: ContractFunction = self.contract.functions.withdrawCompensation() + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=checksum_address) return receipt @contract_api(CONTRACT_CALL) - def check_claim(self, checksum_address: str) -> bool: - has_claimed = bool(self.contract.functions.workInfo(checksum_address).call()[2]) + def check_claim(self, checksum_address: ChecksumAddress) -> bool: + has_claimed: bool = bool(self.contract.functions.workInfo(checksum_address).call()[2]) return has_claimed # @@ -1228,65 +1275,65 @@ class WorkLockAgent(EthereumContractAgent): # @contract_api(CONTRACT_CALL) - def get_refunded_work(self, checksum_address: str) -> int: + def get_refunded_work(self, checksum_address: ChecksumAddress) -> NuNits: work = self.contract.functions.workInfo(checksum_address).call()[1] - return work + return NuNits(work) # # Calls # @contract_api(CONTRACT_CALL) - def get_available_refund(self, checksum_address: str) -> int: - refund_eth = self.contract.functions.getAvailableRefund(checksum_address).call() - return refund_eth + def get_available_refund(self, checksum_address: ChecksumAddress) -> Wei: + refund_eth: int = self.contract.functions.getAvailableRefund(checksum_address).call() + return Wei(refund_eth) @contract_api(CONTRACT_CALL) - def get_available_compensation(self, checksum_address: str) -> int: - compensation_eth = self.contract.functions.compensation(checksum_address).call() - return compensation_eth + def get_available_compensation(self, checksum_address: ChecksumAddress) -> Wei: + compensation_eth: int = self.contract.functions.compensation(checksum_address).call() + return Wei(compensation_eth) @contract_api(CONTRACT_CALL) - def get_deposited_eth(self, checksum_address: str) -> int: - current_bid = self.contract.functions.workInfo(checksum_address).call()[0] - return current_bid + def get_deposited_eth(self, checksum_address: ChecksumAddress) -> Wei: + current_bid: int = self.contract.functions.workInfo(checksum_address).call()[0] + return Wei(current_bid) @property @contract_api(CONTRACT_ATTRIBUTE) - def lot_value(self) -> int: + def lot_value(self) -> NuNits: """ Total number of tokens than can be bid for and awarded in or the number of NU tokens deposited before the bidding windows begins via tokenDeposit(). """ - supply = self.contract.functions.tokenSupply().call() - return supply + supply: int = self.contract.functions.tokenSupply().call() + return NuNits(supply) @contract_api(CONTRACT_CALL) - def get_bonus_lot_value(self) -> int: + def get_bonus_lot_value(self) -> NuNits: """ Total number of tokens than can be awarded for bonus part of bid. """ - num_bidders = self.get_bidders_population() - supply = self.lot_value - num_bidders * self.contract.functions.minAllowableLockedTokens().call() - return supply + num_bidders: int = self.get_bidders_population() + supply: int = self.lot_value - num_bidders * self.contract.functions.minAllowableLockedTokens().call() + return NuNits(supply) @contract_api(CONTRACT_CALL) - def get_remaining_work(self, checksum_address: str) -> int: + def get_remaining_work(self, checksum_address: str) -> PeriodDelta: """Get remaining work periods until full refund for the target address.""" result = self.contract.functions.getRemainingWork(checksum_address).call() - return result + return PeriodDelta(result) # TODO: Double check this type @contract_api(CONTRACT_CALL) - def get_bonus_eth_supply(self) -> int: + def get_bonus_eth_supply(self) -> Wei: supply = self.contract.functions.bonusETHSupply().call() - return supply + return Wei(supply) @contract_api(CONTRACT_CALL) - def get_eth_supply(self) -> int: - num_bidders = self.get_bidders_population() - min_bid = self.minimum_allowed_bid - supply = num_bidders * min_bid + self.get_bonus_eth_supply() - return supply + def get_eth_supply(self) -> Wei: + num_bidders: int = self.get_bidders_population() + min_bid: int = self.minimum_allowed_bid + supply: int = num_bidders * min_bid + self.get_bonus_eth_supply() + return Wei(supply) @property @contract_api(CONTRACT_ATTRIBUTE) @@ -1297,15 +1344,15 @@ class WorkLockAgent(EthereumContractAgent): @property @contract_api(CONTRACT_ATTRIBUTE) def slowing_refund(self) -> int: - refund = self.contract.functions.SLOWING_REFUND().call() + refund: int = self.contract.functions.SLOWING_REFUND().call() return refund @contract_api(CONTRACT_CALL) - def get_bonus_refund_rate(self) -> int: + def get_bonus_refund_rate(self) -> float: f = self.contract.functions - slowing_refund = f.SLOWING_REFUND().call() - boosting_refund = f.boostingRefund().call() - refund_rate = self.get_bonus_deposit_rate() * slowing_refund / boosting_refund + slowing_refund: int = f.SLOWING_REFUND().call() + boosting_refund: int = f.boostingRefund().call() + refund_rate: float = self.get_bonus_deposit_rate() * slowing_refund / boosting_refund return refund_rate @contract_api(CONTRACT_CALL) @@ -1318,33 +1365,32 @@ class WorkLockAgent(EthereumContractAgent): @contract_api(CONTRACT_CALL) def get_base_deposit_rate(self) -> int: - f = self.contract.functions - min_allowed_locked_tokens = f.minAllowableLockedTokens().call() - deposit_rate = min_allowed_locked_tokens // self.minimum_allowed_bid # should never divide by 0 + min_allowed_locked_tokens: NuNits = self.contract.functions.minAllowableLockedTokens().call() + deposit_rate: int = min_allowed_locked_tokens // self.minimum_allowed_bid # should never divide by 0 return deposit_rate @contract_api(CONTRACT_CALL) def get_bonus_deposit_rate(self) -> int: try: - deposit_rate = self.get_bonus_lot_value() // self.get_bonus_eth_supply() + deposit_rate: int = self.get_bonus_lot_value() // self.get_bonus_eth_supply() except ZeroDivisionError: return 0 return deposit_rate @contract_api(CONTRACT_CALL) - def eth_to_tokens(self, value: int) -> int: - tokens = self.contract.functions.ethToTokens(value).call() - return tokens + def eth_to_tokens(self, value: Wei) -> NuNits: + tokens: int = self.contract.functions.ethToTokens(value).call() + return NuNits(tokens) @contract_api(CONTRACT_CALL) - def eth_to_work(self, value: int) -> int: - tokens = self.contract.functions.ethToWork(value).call() - return tokens + def eth_to_work(self, value: Wei) -> NuNits: + tokens: int = self.contract.functions.ethToWork(value).call() + return NuNits(tokens) @contract_api(CONTRACT_CALL) - def work_to_eth(self, value: int) -> int: - tokens = self.contract.functions.workToETH(value).call() - return tokens + def work_to_eth(self, value: NuNits) -> Wei: + wei: Wei = self.contract.functions.workToETH(value).call() + return Wei(wei) @contract_api(CONTRACT_CALL) def get_bidders_population(self) -> int: @@ -1352,20 +1398,20 @@ class WorkLockAgent(EthereumContractAgent): return self.contract.functions.getBiddersLength().call() @contract_api(CONTRACT_CALL) - def get_bidders(self) -> List[str]: + def get_bidders(self) -> List[ChecksumAddress]: """Returns a list of bidders""" - num_bidders = self.get_bidders_population() - bidders = [self.contract.functions.bidders(i).call() for i in range(num_bidders)] + num_bidders: int = self.get_bidders_population() + bidders: List[ChecksumAddress] = [self.contract.functions.bidders(i).call() for i in range(num_bidders)] return bidders @contract_api(CONTRACT_CALL) def is_claiming_available(self) -> bool: """Returns True if claiming is available""" - result = self.contract.functions.isClaimingAvailable().call() + result: bool = self.contract.functions.isClaimingAvailable().call() return result @contract_api(CONTRACT_CALL) - def estimate_verifying_correctness(self, gas_limit: int, gas_to_save_state: int = 30000) -> int: # TODO - #842: Gas Management + def estimate_verifying_correctness(self, gas_limit: Wei, gas_to_save_state: Wei = Wei(30000)) -> int: # TODO - #842: Gas Management """Returns how many bidders will be verified using specified gas limit""" return self.contract.functions.verifyBiddingCorrectness(gas_to_save_state).call({'gas': gas_limit}) @@ -1377,35 +1423,35 @@ class WorkLockAgent(EthereumContractAgent): @contract_api(CONTRACT_CALL) def bidders_checked(self) -> bool: """Returns True if bidders have been checked""" - bidders_population = self.get_bidders_population() + bidders_population: int = self.get_bidders_population() return self.next_bidder_to_check() == bidders_population @property @contract_api(CONTRACT_ATTRIBUTE) - def minimum_allowed_bid(self) -> int: - min_bid = self.contract.functions.minAllowedBid().call() + def minimum_allowed_bid(self) -> Wei: + min_bid: Wei = self.contract.functions.minAllowedBid().call() return min_bid @property @contract_api(CONTRACT_ATTRIBUTE) - def start_bidding_date(self) -> int: - date = self.contract.functions.startBidDate().call() - return date + def start_bidding_date(self) -> Timestamp: + date: int = self.contract.functions.startBidDate().call() + return Timestamp(date) @property @contract_api(CONTRACT_ATTRIBUTE) - def end_bidding_date(self) -> int: - date = self.contract.functions.endBidDate().call() - return date + def end_bidding_date(self) -> Timestamp: + date: int = self.contract.functions.endBidDate().call() + return Timestamp(date) @property @contract_api(CONTRACT_ATTRIBUTE) - def end_cancellation_date(self) -> int: - date = self.contract.functions.endCancellationDate().call() - return date + def end_cancellation_date(self) -> Timestamp: + date: int = self.contract.functions.endCancellationDate().call() + return Timestamp(date) @contract_api(CONTRACT_CALL) - def worklock_parameters(self) -> Tuple: + def worklock_parameters(self) -> WorklockParameters: parameter_signatures = ( 'tokenSupply', 'startBidDate', @@ -1416,112 +1462,107 @@ class WorkLockAgent(EthereumContractAgent): 'minAllowedBid', ) - def _call_function_by_name(name: str): + def _call_function_by_name(name: str) -> int: return getattr(self.contract.functions, name)().call() - parameters = tuple(map(_call_function_by_name, parameter_signatures)) + parameters = WorklockParameters(map(_call_function_by_name, parameter_signatures)) return parameters class MultiSigAgent(EthereumContractAgent): - registry_contract_name = MULTISIG_CONTRACT_NAME + registry_contract_name: str = MULTISIG_CONTRACT_NAME @property @contract_api(CONTRACT_ATTRIBUTE) - def nonce(self) -> int: - nonce = self.contract.functions.nonce().call() - return nonce + def nonce(self) -> Nonce: + nonce: int = self.contract.functions.nonce().call() + return Nonce(nonce) @contract_api(CONTRACT_CALL) - def get_owner(self, index: int) -> str: - owner = self.contract.functions.owners(index).call() + def get_owner(self, index: int) -> ChecksumAddress: + owner: ChecksumAddress = self.contract.functions.owners(index).call() return owner @property @contract_api(CONTRACT_ATTRIBUTE) def number_of_owners(self) -> int: - number = self.contract.functions.getNumberOfOwners().call() + number: int = self.contract.functions.getNumberOfOwners().call() return number @property - def owners(self) -> Tuple[str]: + def owners(self) -> Tuple[ChecksumAddress]: return tuple(self.get_owner(i) for i in range(self.number_of_owners)) @property @contract_api(CONTRACT_ATTRIBUTE) def threshold(self) -> int: - threshold = self.contract.functions.required().call() + threshold: int = self.contract.functions.required().call() return threshold @contract_api(CONTRACT_CALL) - def is_owner(self, checksum_address: str) -> bool: - result = self.contract.functions.isOwner(checksum_address).call() + def is_owner(self, checksum_address: ChecksumAddress) -> bool: + result: bool = self.contract.functions.isOwner(checksum_address).call() return result @validate_checksum_address - def build_add_owner_tx(self, new_owner_address: str) -> dict: - max_owner_count = self.contract.functions.MAX_OWNER_COUNT().call() + def build_add_owner_tx(self, new_owner_address: ChecksumAddress) -> TxParams: + max_owner_count: int = self.contract.functions.MAX_OWNER_COUNT().call() if not self.number_of_owners < max_owner_count: raise self.RequirementError(f"MultiSig already has the maximum number of owners") if new_owner_address == NULL_ADDRESS: raise self.RequirementError(f"Invalid MultiSig owner address (NULL ADDRESS)") if self.is_owner(new_owner_address): raise self.RequirementError(f"{new_owner_address} is already an owner of the MultiSig.") - transaction_function = self.contract.functions.addOwner(new_owner_address) - transaction = self.blockchain.build_transaction(contract_function=transaction_function, - sender_address=self.contract_address) + transaction_function: ContractFunction = self.contract.functions.addOwner(new_owner_address) + transaction: TxParams = self.blockchain.build_transaction(contract_function=transaction_function, + sender_address=self.contract_address) return transaction @validate_checksum_address - def build_remove_owner_tx(self, owner_address: str) -> dict: + def build_remove_owner_tx(self, owner_address: ChecksumAddress) -> TxParams: if not self.number_of_owners > self.threshold: raise self.RequirementError(f"Need at least one owner above the threshold to remove an owner.") if not self.is_owner(owner_address): raise self.RequirementError(f"{owner_address} is not owner of the MultiSig.") - transaction_function = self.contract.functions.removeOwner(owner_address) - transaction = self.blockchain.build_transaction(contract_function=transaction_function, - sender_address=self.contract_address) + transaction_function: ContractFunction = self.contract.functions.removeOwner(owner_address) + transaction: TxReceipt = self.blockchain.build_transaction(contract_function=transaction_function, + sender_address=self.contract_address) return transaction @validate_checksum_address - def build_change_threshold_tx(self, threshold: int) -> dict: + def build_change_threshold_tx(self, threshold: int) -> TxParams: if not 0 < threshold <= self.number_of_owners: - raise self.RequirementError(f"New threshold {threshold} doesn't satisfy " + raise self.RequirementError(f"New threshold {threshold} does not satisfy " f"0 < threshold ≤ number of owners = {self.number_of_owners}") - transaction_function = self.contract.functions.changeRequirement(threshold) - transaction = self.blockchain.build_transaction(contract_function=transaction_function, - sender_address=self.contract_address) + transaction_function: ContractFunction = self.contract.functions.changeRequirement(threshold) + transaction: TxParams = self.blockchain.build_transaction(contract_function=transaction_function, + sender_address=self.contract_address) return transaction @contract_api(CONTRACT_CALL) def get_unsigned_transaction_hash(self, - trustee_address: str, - target_address: str, - value: int, + trustee_address: ChecksumAddress, + target_address: ChecksumAddress, + value: Wei, data: bytes, - nonce: int - ) -> bytes: - transaction_args = (trustee_address, - target_address, - value, - data, - nonce) - - transaction_hash = self.contract.functions.getUnsignedTransactionHash(*transaction_args).call() - return transaction_hash + nonce: Nonce + ) -> HexBytes: + transaction_args = trustee_address, target_address, value, data, nonce + transaction_hash: bytes = self.contract.functions.getUnsignedTransactionHash(*transaction_args).call() + return HexBytes(transaction_hash) @contract_api(TRANSACTION) def execute(self, - v: List[str], + v: List[str], # TODO: Use bytes? r: List[str], s: List[str], - destination: str, - value: int, - data: bytes, - sender_address: str - ) -> dict: - contract_function = self.contract.functions.execute(v, r, s, destination, value, data) - receipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address) + destination: ChecksumAddress, + value: Wei, + data: Union[bytes, HexStr], + sender_address: ChecksumAddress, + ) -> TxReceipt: + contract_function: ContractFunction = self.contract.functions.execute(v, r, s, destination, value, data) + receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function, sender_address=sender_address) return receipt