def test_transacting_power_sign_transaction(testerchain): eth_address = testerchain.unassigned_accounts[2] power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, signer=Web3Signer(testerchain.client), account=eth_address) transaction_dict = { 'nonce': testerchain.client.w3.eth.getTransactionCount(eth_address), 'gasPrice': testerchain.client.w3.eth.gasPrice, 'gas': 100000, 'from': eth_address, 'to': testerchain.unassigned_accounts[1], 'value': 1, 'data': b'' } # Sign power.activate() signed_transaction = power.sign_transaction( transaction_dict=transaction_dict) # Demonstrate that the transaction is valid RLP encoded. from eth_account._utils.transactions import Transaction restored_transaction = Transaction.from_bytes( serialized_bytes=signed_transaction) restored_dict = restored_transaction.as_dict() assert to_checksum_address(restored_dict['to']) == transaction_dict['to'] # Try signing with missing transaction fields del transaction_dict['gas'] del transaction_dict['nonce'] with pytest.raises(TypeError): power.sign_transaction(transaction_dict=transaction_dict)
def bootstrap_network( cls, economics: BaseEconomics = None ) -> Tuple['TesterBlockchain', 'InMemoryContractRegistry']: """For use with metric testing scripts""" registry = InMemoryContractRegistry() testerchain = cls(compiler=SolidityCompiler()) BlockchainInterfaceFactory.register_interface(testerchain) power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, account=testerchain.etherbase_account) power.activate() testerchain.transacting_power = power origin = testerchain.client.etherbase deployer = ContractAdministrator(deployer_address=origin, registry=registry, economics=economics or cls._default_token_economics, staking_escrow_test_mode=True) secrets = dict() for deployer_class in deployer.upgradeable_deployer_classes: secrets[ deployer_class.contract_name] = INSECURE_DEVELOPMENT_PASSWORD _receipts = deployer.deploy_network_contracts(secrets=secrets, interactive=False) return testerchain, registry
def test_transacting_power_sign_agent_transaction(testerchain, agency): token_agent = NucypherTokenAgent(blockchain=testerchain) contract_function = token_agent.contract.functions.approve( testerchain.etherbase_account, 100) payload = { 'chainId': int(testerchain.client.chain_id), 'nonce': testerchain.client.w3.eth.getTransactionCount( testerchain.etherbase_account), 'from': testerchain.etherbase_account, 'gasPrice': testerchain.client.gas_price } unsigned_transaction = contract_function.buildTransaction(payload) # Sign with Transacting Power transacting_power = TransactingPower( blockchain=testerchain, password=INSECURE_DEVELOPMENT_PASSWORD, account=testerchain.etherbase_account) transacting_power.activate() signed_raw_transaction = transacting_power.sign_transaction( unsigned_transaction) # Demonstrate that the transaction is valid RLP encoded. restored_transaction = Transaction.from_bytes( serialized_bytes=signed_raw_transaction) restored_dict = restored_transaction.as_dict() assert to_checksum_address( restored_dict['to']) == unsigned_transaction['to']
def test_transacting_power_sign_transaction(testerchain): eth_address = testerchain.unassigned_accounts[2] power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, signer=Web3Signer(testerchain.client), account=eth_address) assert power.is_active is False assert power.is_unlocked is False transaction_dict = { 'nonce': testerchain.client.w3.eth.getTransactionCount(eth_address), 'gasPrice': testerchain.client.w3.eth.gasPrice, 'gas': 100000, 'from': eth_address, 'to': testerchain.unassigned_accounts[1], 'value': 1, 'data': b'' } # The default state of the account is locked. assert not power.is_unlocked # Test a signature without unlocking the account with pytest.raises(power.AccountLocked): power.sign_transaction(transaction_dict=transaction_dict) # Sign power.activate() assert power.is_unlocked is True signed_transaction = power.sign_transaction( transaction_dict=transaction_dict) # Demonstrate that the transaction is valid RLP encoded. from eth_account._utils.transactions import Transaction restored_transaction = Transaction.from_bytes( serialized_bytes=signed_transaction) restored_dict = restored_transaction.as_dict() assert to_checksum_address(restored_dict['to']) == transaction_dict['to'] # Try signing with missing transaction fields del transaction_dict['gas'] del transaction_dict['nonce'] with pytest.raises(TypeError): power.sign_transaction(transaction_dict=transaction_dict) # Try signing with a re-locked account. power.lock_account() with pytest.raises(power.AccountLocked): power.sign_transaction(transaction_dict=transaction_dict) power.unlock_account(password=INSECURE_DEVELOPMENT_PASSWORD) assert power.is_unlocked is True # Tear-Down Test power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, signer=Web3Signer(testerchain.client), account=testerchain.etherbase_account) power.activate(password=INSECURE_DEVELOPMENT_PASSWORD)
def attach_transacting_power(self, checksum_address: str, password: str = None) -> None: try: transacting_power = self.__transacting_powers[checksum_address] except KeyError: transacting_power = TransactingPower(blockchain=self.blockchain, password=password, account=checksum_address) self.__transacting_powers[checksum_address] = transacting_power transacting_power.activate(password=password)
def software_stakeholder(testerchain, agency, stakeholder_config_file_location): # Setup path = stakeholder_config_file_location if os.path.exists(path): os.remove(path) # 0xaAa482c790b4301bE18D75A0D1B11B2ACBEF798B stakeholder_private_key = '255f64a948eeb1595b8a2d1e76740f4683eca1c8f1433d13293db9b6e27676cc' address = testerchain.provider.ethereum_tester.add_account( stakeholder_private_key, password=INSECURE_DEVELOPMENT_PASSWORD) testerchain.provider.ethereum_tester.unlock_account( address, password=INSECURE_DEVELOPMENT_PASSWORD) tx = { 'to': address, 'from': testerchain.etherbase_account, 'value': Web3.toWei('1', 'ether') } txhash = testerchain.client.w3.eth.sendTransaction(tx) _receipt = testerchain.wait_for_receipt(txhash) # Mock TransactingPower consumption (Etherbase) transacting_power = TransactingPower( account=testerchain.etherbase_account, password=INSECURE_DEVELOPMENT_PASSWORD, blockchain=testerchain) transacting_power.activate() token_agent = Agency.get_agent(NucypherTokenAgent) token_agent.transfer(amount=NU(200_000, 'NU').to_nunits(), sender_address=testerchain.etherbase_account, target_address=address) # Create stakeholder from on-chain values given accounts over a web3 provider stakeholder = StakeHolder(blockchain=testerchain, funding_account=address, funding_password=INSECURE_DEVELOPMENT_PASSWORD, trezor=False) # Teardown yield stakeholder if os.path.exists(path): os.remove(path)
def bootstrap_network(cls) -> Tuple['TesterBlockchain', Dict[str, EthereumContractAgent]]: """For use with metric testing scripts""" testerchain = cls(compiler=SolidityCompiler()) power = TransactingPower(blockchain=testerchain, password=INSECURE_DEVELOPMENT_PASSWORD, account=testerchain.etherbase_account) power.activate() testerchain.transacting_power = power origin = testerchain.client.etherbase deployer = Deployer(blockchain=testerchain, deployer_address=origin, bare=True) _txhashes, agents = deployer.deploy_network_contracts(staker_secret=STAKING_ESCROW_DEPLOYMENT_SECRET, policy_secret=POLICY_MANAGER_DEPLOYMENT_SECRET, adjudicator_secret=ADJUDICATOR_DEPLOYMENT_SECRET, user_escrow_proxy_secret=USER_ESCROW_PROXY_DEPLOYMENT_SECRET) return testerchain, agents
def bootstrap_network(cls, economics: BaseEconomics = None) -> Tuple['TesterBlockchain', 'InMemoryContractRegistry']: """For use with metric testing scripts""" registry = InMemoryContractRegistry() testerchain = cls() BlockchainInterfaceFactory.register_interface(testerchain) power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, account=testerchain.etherbase_account) power.activate() testerchain.transacting_power = power origin = testerchain.client.etherbase deployer = ContractAdministrator(deployer_address=origin, registry=registry, economics=economics or cls.DEFAULT_ECONOMICS, staking_escrow_test_mode=True) _receipts = deployer.deploy_network_contracts(interactive=False) return testerchain, registry
def bootstrap_network(cls) -> 'TesterBlockchain': """For use with metric testing scripts""" testerchain = cls(compiler=SolidityCompiler()) power = TransactingPower(blockchain=testerchain, password=INSECURE_DEVELOPMENT_PASSWORD, account=testerchain.etherbase_account) power.activate() testerchain.transacting_power = power origin = testerchain.client.etherbase deployer = DeployerActor(blockchain=testerchain, deployer_address=origin, bare=True) secrets = dict() for deployer_class in deployer.upgradeable_deployer_classes: secrets[ deployer_class.contract_name] = INSECURE_DEVELOPMENT_PASSWORD _receipts = deployer.deploy_network_contracts(secrets=secrets, interactive=False) return testerchain
def bootstrap_network( cls, registry: Optional[BaseContractRegistry] = None, economics: BaseEconomics = None ) -> Tuple['TesterBlockchain', 'InMemoryContractRegistry']: """For use with metric testing scripts""" if registry is None: registry = InMemoryContractRegistry() testerchain = cls() if not BlockchainInterfaceFactory.is_interface_initialized( provider_uri=testerchain.provider_uri): BlockchainInterfaceFactory.register_interface( interface=testerchain) power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, account=testerchain.etherbase_account) power.activate() testerchain.transacting_power = power origin = testerchain.client.etherbase admin = ContractAdministrator(deployer_address=origin, registry=registry, economics=economics or cls.DEFAULT_ECONOMICS) gas_limit = None # TODO: Gas management - #842 for deployer_class in admin.primary_deployer_classes: if deployer_class is StakingEscrowDeployer: admin.deploy_contract( contract_name=deployer_class.contract_name, gas_limit=gas_limit, deployment_mode=INIT) else: admin.deploy_contract( contract_name=deployer_class.contract_name, gas_limit=gas_limit) admin.deploy_contract( contract_name=StakingEscrowDeployer.contract_name, gas_limit=gas_limit) return testerchain, registry
class ContractAdministrator(NucypherTokenActor): """ The administrator of network contracts. """ __interface_class = BlockchainDeployerInterface # # Deployer classes sorted by deployment dependency order. # standard_deployer_classes = (NucypherTokenDeployer, ) dispatched_upgradeable_deployer_classes = ( StakingEscrowDeployer, PolicyManagerDeployer, AdjudicatorDeployer, ) upgradeable_deployer_classes = ( *dispatched_upgradeable_deployer_classes, StakingInterfaceDeployer, ) ownable_deployer_classes = (*dispatched_upgradeable_deployer_classes, ) deployer_classes = (*standard_deployer_classes, *upgradeable_deployer_classes) class UnknownContract(ValueError): pass def __init__(self, registry: BaseContractRegistry, deployer_address: str = None, client_password: str = None, economics: TokenEconomics = None): """ Note: super() is not called here to avoid setting the token agent. TODO: Review this logic ^^ "bare mode". """ self.log = Logger("Deployment-Actor") self.deployer_address = deployer_address self.checksum_address = self.deployer_address self.economics = economics or StandardTokenEconomics() self.registry = registry self.preallocation_escrow_deployers = dict() self.deployers = {d.contract_name: d for d in self.deployer_classes} self.transacting_power = TransactingPower(password=client_password, account=deployer_address) self.transacting_power.activate() def __repr__(self): r = '{name} - {deployer_address})'.format( name=self.__class__.__name__, deployer_address=self.deployer_address) return r def __get_deployer(self, contract_name: str): try: Deployer = self.deployers[contract_name] except KeyError: raise self.UnknownContract(contract_name) return Deployer @staticmethod def collect_deployment_secret(deployer) -> str: secret = click.prompt( f'Enter {deployer.contract_name} Deployment Secret', hide_input=True, confirmation_prompt=True) return secret def collect_deployment_secrets(self) -> dict: secrets = dict() for deployer in self.upgradeable_deployer_classes: secrets[deployer.contract_name] = self.collect_deployment_secret( deployer) return secrets def deploy_contract( self, contract_name: str, gas_limit: int = None, plaintext_secret: str = None, bare: bool = False, progress=None, *args, **kwargs, ) -> Tuple[dict, BaseContractDeployer]: Deployer = self.__get_deployer(contract_name=contract_name) deployer = Deployer(registry=self.registry, deployer_address=self.deployer_address, economics=self.economics, *args, **kwargs) if Deployer._upgradeable: is_initial_deployment = not bare if is_initial_deployment and not plaintext_secret: raise ValueError( "An upgrade secret must be passed to perform initial deployment of a Dispatcher." ) secret_hash = None if plaintext_secret: secret_hash = keccak(bytes(plaintext_secret, encoding='utf-8')) receipts = deployer.deploy( secret_hash=secret_hash, gas_limit=gas_limit, initial_deployment=is_initial_deployment, progress=progress) else: receipts = deployer.deploy(gas_limit=gas_limit, progress=progress) return receipts, deployer def upgrade_contract(self, contract_name: str, existing_plaintext_secret: str, new_plaintext_secret: str) -> dict: Deployer = self.__get_deployer(contract_name=contract_name) deployer = Deployer(registry=self.registry, deployer_address=self.deployer_address) new_secret_hash = keccak(bytes(new_plaintext_secret, encoding='utf-8')) receipts = deployer.upgrade(existing_secret_plaintext=bytes( existing_plaintext_secret, encoding='utf-8'), new_secret_hash=new_secret_hash) return receipts def retarget_proxy(self, contract_name: str, target_address: str, existing_plaintext_secret: str, new_plaintext_secret: str): Deployer = self.__get_deployer(contract_name=contract_name) deployer = Deployer(registry=self.registry, deployer_address=self.deployer_address) new_secret_hash = keccak(bytes(new_plaintext_secret, encoding='utf-8')) receipts = deployer.retarget(target_address=target_address, existing_secret_plaintext=bytes( existing_plaintext_secret, encoding='utf-8'), new_secret_hash=new_secret_hash) return receipts def rollback_contract(self, contract_name: str, existing_plaintext_secret: str, new_plaintext_secret: str): Deployer = self.__get_deployer(contract_name=contract_name) deployer = Deployer(registry=self.registry, deployer_address=self.deployer_address) new_secret_hash = keccak(bytes(new_plaintext_secret, encoding='utf-8')) receipts = deployer.rollback(existing_secret_plaintext=bytes( existing_plaintext_secret, encoding='utf-8'), new_secret_hash=new_secret_hash) return receipts def deploy_preallocation_escrow( self, allocation_registry: AllocationRegistry, progress=None) -> PreallocationEscrowDeployer: preallocation_escrow_deployer = PreallocationEscrowDeployer( registry=self.registry, deployer_address=self.deployer_address, allocation_registry=allocation_registry) preallocation_escrow_deployer.deploy(progress=progress) principal_address = preallocation_escrow_deployer.contract.address self.preallocation_escrow_deployers[ principal_address] = preallocation_escrow_deployer return preallocation_escrow_deployer def deploy_network_contracts(self, secrets: dict, interactive: bool = True, emitter: StdoutEmitter = None, etherscan: bool = False) -> dict: """ :param secrets: Contract upgrade secrets dictionary :param interactive: If True, wait for keypress after each contract deployment :param emitter: A console output emitter instance. If emitter is None, no output will be echoed to the console. :param etherscan: Open deployed contracts in Etherscan :return: Returns a dictionary of deployment receipts keyed by contract name """ if interactive and not emitter: raise ValueError( "'emitter' is a required keyword argument when interactive is True." ) deployment_receipts = dict() gas_limit = None # TODO: Gas management # deploy contracts total_deployment_transactions = 0 for deployer_class in self.deployer_classes: total_deployment_transactions += len( deployer_class.deployment_steps) first_iteration = True with click.progressbar(length=total_deployment_transactions, label="Deployment progress", show_eta=False) as bar: bar.short_limit = 0 for deployer_class in self.deployer_classes: if interactive and not first_iteration: click.pause( info= f"\nPress any key to continue with deployment of {deployer_class.contract_name}" ) if emitter: emitter.echo( f"\nDeploying {deployer_class.contract_name} ...") bar._last_line = None bar.render_progress() if deployer_class in self.standard_deployer_classes: receipts, deployer = self.deploy_contract( contract_name=deployer_class.contract_name, gas_limit=gas_limit, progress=bar) else: receipts, deployer = self.deploy_contract( contract_name=deployer_class.contract_name, plaintext_secret=secrets[deployer_class.contract_name], gas_limit=gas_limit, progress=bar) if emitter: blockchain = BlockchainInterfaceFactory.get_interface() paint_contract_deployment( contract_name=deployer_class.contract_name, receipts=receipts, contract_address=deployer.contract_address, emitter=emitter, chain_name=blockchain.client.chain_name, open_in_browser=etherscan) deployment_receipts[deployer_class.contract_name] = receipts first_iteration = False return deployment_receipts def relinquish_ownership(self, new_owner: str, emitter: StdoutEmitter = None, interactive: bool = True, transaction_gas_limit: int = None) -> dict: if not is_checksum_address(new_owner): raise ValueError( f"{new_owner} is an invalid EIP-55 checksum address.") receipts = dict() for contract_deployer in self.ownable_deployer_classes: deployer = contract_deployer( registry=self.registry, deployer_address=self.deployer_address) deployer.transfer_ownership( new_owner=new_owner, transaction_gas_limit=transaction_gas_limit) if emitter: emitter.echo( f"Transferred ownership of {deployer.contract_name} to {new_owner}" ) if interactive: click.pause(info="Press any key to continue") receipts[contract_deployer.contract_name] = receipts return receipts def deploy_beneficiary_contracts( self, allocations: List[Dict[str, Union[str, int]]], allocation_outfile: str = None, allocation_registry: AllocationRegistry = None, crash_on_failure: bool = True, interactive: bool = True, emitter: StdoutEmitter = None, ) -> Dict[str, dict]: """ The allocation file is a JSON file containing a list of allocations. Each allocation has a: * 'beneficiary_address': Checksum address of the beneficiary * 'name': User-friendly name of the beneficiary (Optional) * 'amount': Amount of tokens locked, in NuNits * 'duration_seconds': Lock duration expressed in seconds Example allocation file: [ {'beneficiary_address': '0xdeadbeef', 'name': 'H. E. Pennypacker', 'amount': 100, 'duration_seconds': 31536000}, {'beneficiary_address': '0xabced120', 'amount': 133432, 'duration_seconds': 31536000}, {'beneficiary_address': '0xf7aefec2', 'amount': 999, 'duration_seconds': 31536000}] """ if interactive and not emitter: raise ValueError( "'emitter' is a required keyword argument when interactive is True." ) if allocation_registry and allocation_outfile: raise self.ActorError( "Pass either allocation registry or allocation_outfile, not both." ) if allocation_registry is None: allocation_registry = AllocationRegistry( filepath=allocation_outfile) if emitter: paint_input_allocation_file(emitter, allocations) if interactive: click.confirm("Continue with the allocation process?", abort=True) total_to_allocate = NU.from_nunits( sum(allocation['amount'] for allocation in allocations)) balance = ContractAgency.get_agent(NucypherTokenAgent, self.registry).get_balance( self.deployer_address) if balance < total_to_allocate: raise ValueError( f"Not enough tokens to allocate. We need at least {total_to_allocate}." ) allocation_receipts, failed, allocated = dict(), list(), list() total_deployment_transactions = len(allocations) * 4 # Create an allocation template file, containing the allocation contract ABI and placeholder values # for the beneficiary and contract addresses. This file will be shared with all allocation users. empty_allocation_escrow_deployer = PreallocationEscrowDeployer( registry=self.registry) allocation_contract_abi = empty_allocation_escrow_deployer.get_contract_abi( ) allocation_template = { "BENEFICIARY_ADDRESS": ["ALLOCATION_CONTRACT_ADDRESS", allocation_contract_abi] } parent_path = Path(allocation_registry.filepath ).parent # Use same folder as allocation registry template_filename = IndividualAllocationRegistry.REGISTRY_NAME template_filepath = os.path.join(parent_path, template_filename) AllocationRegistry(filepath=template_filepath).write( registry_data=allocation_template) if emitter: emitter.echo( f"Saved allocation template file to {template_filepath}", color='blue', bold=True) # Deploy each allocation contract with click.progressbar(length=total_deployment_transactions, label="Allocation progress", show_eta=False) as bar: bar.short_limit = 0 for allocation in allocations: # TODO: Check if allocation already exists in allocation registry beneficiary = allocation['beneficiary_address'] name = allocation.get('name', 'No name provided') if interactive: click.pause( info=f"\nPress any key to continue with allocation for " f"beneficiary {beneficiary} ({name})") if emitter: emitter.echo( f"\nDeploying PreallocationEscrow contract for beneficiary {beneficiary} ({name})..." ) bar._last_line = None bar.render_progress() deployer = self.deploy_preallocation_escrow( allocation_registry=allocation_registry, progress=bar) amount = allocation['amount'] duration = allocation['duration_seconds'] try: receipts = deployer.deliver( value=amount, duration=duration, beneficiary_address=beneficiary, progress=bar) except TransactionFailed as e: if crash_on_failure: raise self.log.debug( f"Failed allocation transaction for {NU.from_nunits(amount)} to {beneficiary}: {e}" ) failed.append(allocation) continue else: allocation_receipts[beneficiary] = receipts allocation_contract_address = deployer.contract_address self.log.info( f"Created {deployer.contract_name} contract at {allocation_contract_address} " f"for beneficiary {beneficiary}.") allocated.append((allocation, allocation_contract_address)) # Create individual allocation file individual_allocation_filename = f'allocation-{beneficiary}.json' individual_allocation_filepath = os.path.join( parent_path, individual_allocation_filename) individual_allocation_file_data = { 'beneficiary_address': beneficiary, 'contract_address': allocation_contract_address } with open(individual_allocation_filepath, 'w') as outfile: json.dump(individual_allocation_file_data, outfile) if emitter: blockchain = BlockchainInterfaceFactory.get_interface() paint_contract_deployment( contract_name=deployer.contract_name, receipts=receipts, contract_address=deployer.contract_address, emitter=emitter, chain_name=blockchain.client.chain_name, open_in_browser=False) emitter.echo( f"Saved individual allocation file to {individual_allocation_filepath}", color='blue', bold=True) if emitter: paint_deployed_allocations(emitter, allocated, failed) csv_filename = f'allocations-{self.deployer_address[:6]}-{maya.now().epoch}.csv' csv_filepath = os.path.join(parent_path, csv_filename) write_deployed_allocations_to_csv(csv_filepath, allocated, failed) if emitter: emitter.echo(f"Saved allocation summary CSV to {csv_filepath}", color='blue', bold=True) if failed: # TODO: More with these failures: send to isolated logfile, and reattempt self.log.critical( f"FAILED TOKEN ALLOCATION - {len(failed)} allocations failed." ) return allocation_receipts @staticmethod def __read_allocation_data(filepath: str) -> list: with open(filepath, 'r') as allocation_file: data = allocation_file.read() try: allocation_data = json.loads(data) except JSONDecodeError: raise return allocation_data def deploy_beneficiaries_from_file(self, allocation_data_filepath: str, allocation_outfile: str = None, emitter=None, interactive=None) -> dict: allocations = self.__read_allocation_data( filepath=allocation_data_filepath) receipts = self.deploy_beneficiary_contracts( allocations=allocations, allocation_outfile=allocation_outfile, emitter=emitter, interactive=interactive) # Save transaction metadata receipts_filepath = self.save_deployment_receipts( receipts=receipts, filename_prefix='allocation') if emitter: emitter.echo(f"Saved allocation receipts to {receipts_filepath}", color='blue', bold=True) return receipts def save_deployment_receipts(self, receipts: dict, filename_prefix: str = 'deployment') -> str: filename = f'{filename_prefix}-receipts-{self.deployer_address[:6]}-{maya.now().epoch}.json' filepath = os.path.join(DEFAULT_CONFIG_ROOT, filename) # TODO: Do not assume default config root os.makedirs(DEFAULT_CONFIG_ROOT, exist_ok=True) with open(filepath, 'w') as file: data = dict() for contract_name, receipts in receipts.items(): contract_records = dict() for tx_name, receipt in receipts.items(): # Formatting receipt = { item: str(result) for item, result in receipt.items() } contract_records.update( {tx_name: receipt for tx_name in receipts}) data[contract_name] = contract_records data = json.dumps(data, indent=4) file.write(data) return filepath
class ContractAdministrator(NucypherTokenActor): """ The administrator of network contracts. """ __interface_class = BlockchainDeployerInterface # # Deployer classes sorted by deployment dependency order. # standard_deployer_classes = (NucypherTokenDeployer, ) dispatched_upgradeable_deployer_classes = ( StakingEscrowDeployer, PolicyManagerDeployer, AdjudicatorDeployer, ) upgradeable_deployer_classes = ( *dispatched_upgradeable_deployer_classes, UserEscrowProxyDeployer, ) deployer_classes = (*standard_deployer_classes, *upgradeable_deployer_classes) class UnknownContract(ValueError): pass def __init__(self, registry: BaseContractRegistry, deployer_address: str = None, client_password: str = None, economics: TokenEconomics = None): """ Note: super() is not called here to avoid setting the token agent. TODO: Review this logic ^^ "bare mode". """ self.log = Logger("Deployment-Actor") self.deployer_address = deployer_address self.checksum_address = self.deployer_address self.economics = economics or StandardTokenEconomics() self.registry = registry self.user_escrow_deployers = dict() self.deployers = {d.contract_name: d for d in self.deployer_classes} self.transacting_power = TransactingPower(password=client_password, account=deployer_address) self.transacting_power.activate() def __repr__(self): r = '{name} - {deployer_address})'.format( name=self.__class__.__name__, deployer_address=self.deployer_address) return r def __get_deployer(self, contract_name: str): try: Deployer = self.deployers[contract_name] except KeyError: raise self.UnknownContract(contract_name) return Deployer @staticmethod def collect_deployment_secret(deployer) -> str: secret = click.prompt( f'Enter {deployer.contract_name} Deployment Secret', hide_input=True, confirmation_prompt=True) return secret def collect_deployment_secrets(self) -> dict: secrets = dict() for deployer in self.upgradeable_deployer_classes: secrets[deployer.contract_name] = self.collect_deployment_secret( deployer) return secrets def deploy_contract( self, contract_name: str, gas_limit: int = None, plaintext_secret: str = None, progress=None, *args, **kwargs, ) -> Tuple[dict, ContractDeployer]: Deployer = self.__get_deployer(contract_name=contract_name) deployer = Deployer(registry=self.registry, deployer_address=self.deployer_address, economics=self.economics, *args, **kwargs) if Deployer._upgradeable: if not plaintext_secret: raise ValueError( "Upgrade plaintext_secret must be passed to deploy an upgradeable contract." ) secret_hash = keccak(bytes(plaintext_secret, encoding='utf-8')) txhashes = deployer.deploy(secret_hash=secret_hash, gas_limit=gas_limit, progress=progress) else: txhashes = deployer.deploy(gas_limit=gas_limit, progress=progress) return txhashes, deployer def upgrade_contract(self, contract_name: str, existing_plaintext_secret: str, new_plaintext_secret: str) -> dict: Deployer = self.__get_deployer(contract_name=contract_name) deployer = Deployer(registry=self.registry, deployer_address=self.deployer_address) new_secret_hash = keccak(bytes(new_plaintext_secret, encoding='utf-8')) txhashes = deployer.upgrade(existing_secret_plaintext=bytes( existing_plaintext_secret, encoding='utf-8'), new_secret_hash=new_secret_hash) return txhashes def rollback_contract(self, contract_name: str, existing_plaintext_secret: str, new_plaintext_secret: str): Deployer = self.__get_deployer(contract_name=contract_name) deployer = Deployer(registry=self.registry, deployer_address=self.deployer_address) new_secret_hash = keccak(bytes(new_plaintext_secret, encoding='utf-8')) txhash = deployer.rollback(existing_secret_plaintext=bytes( existing_plaintext_secret, encoding='utf-8'), new_secret_hash=new_secret_hash) return txhash def deploy_user_escrow(self, allocation_registry: AllocationRegistry): user_escrow_deployer = UserEscrowDeployer( registry=self.registry, deployer_address=self.deployer_address, allocation_registry=allocation_registry) user_escrow_deployer.deploy() principal_address = user_escrow_deployer.contract.address self.user_escrow_deployers[principal_address] = user_escrow_deployer return user_escrow_deployer def deploy_network_contracts(self, secrets: dict, interactive: bool = True, emitter: StdoutEmitter = None, etherscan: bool = False) -> dict: """ :param secrets: Contract upgrade secrets dictionary :param interactive: If True, wait for keypress after each contract deployment :param emitter: A console output emitter instance. If emitter is None, no output will be echoed to the console. :param etherscan: Open deployed contracts in Etherscan :return: Returns a dictionary of deployment receipts keyed by contract name """ if interactive and not emitter: raise ValueError( "'emitter' is a required keyword argument when interactive is True." ) deployment_receipts = dict() gas_limit = None # TODO: Gas management # deploy contracts total_deployment_transactions = 0 for deployer_class in self.deployer_classes: total_deployment_transactions += len( deployer_class.deployment_steps) first_iteration = True with click.progressbar(length=total_deployment_transactions, label="Deployment progress", show_eta=False) as bar: bar.short_limit = 0 for deployer_class in self.deployer_classes: if interactive and not first_iteration: click.pause( info= f"\nPress any key to continue with deployment of {deployer_class.contract_name}" ) if emitter: emitter.echo( f"\nDeploying {deployer_class.contract_name} ...") bar._last_line = None bar.render_progress() if deployer_class in self.standard_deployer_classes: receipts, deployer = self.deploy_contract( contract_name=deployer_class.contract_name, gas_limit=gas_limit, progress=bar) else: receipts, deployer = self.deploy_contract( contract_name=deployer_class.contract_name, plaintext_secret=secrets[deployer_class.contract_name], gas_limit=gas_limit, progress=bar) if emitter: blockchain = BlockchainInterfaceFactory.get_interface() paint_contract_deployment( contract_name=deployer_class.contract_name, receipts=receipts, contract_address=deployer.contract_address, emitter=emitter, chain_name=blockchain.client.chain_name, open_in_browser=etherscan) deployment_receipts[deployer_class.contract_name] = receipts first_iteration = False return deployment_receipts def relinquish_ownership(self, new_owner: str, emitter: StdoutEmitter = None, interactive: bool = True, transaction_gas_limit: int = None) -> dict: if not is_checksum_address(new_owner): raise ValueError( f"{new_owner} is an invalid EIP-55 checksum address.") receipts = dict() for contract_deployer in self.upgradeable_deployer_classes: deployer = contract_deployer( registry=self.registry, deployer_address=self.deployer_address) deployer.transfer_ownership( new_owner=new_owner, transaction_gas_limit=transaction_gas_limit) if emitter: emitter.echo( f"Transferred ownership of {deployer.contract_name} to {new_owner}" ) if interactive: click.pause(info="Press any key to continue") receipts[contract_deployer.contract_name] = receipts return receipts def deploy_beneficiary_contracts( self, allocations: List[Dict[str, Union[str, int]]], allocation_outfile: str = None, allocation_registry: AllocationRegistry = None, crash_on_failure: bool = True, ) -> Dict[str, dict]: """ Example allocation dataset (one year is 31536000 seconds): data = [{'beneficiary_address': '0xdeadbeef', 'amount': 100, 'duration_seconds': 31536000}, {'beneficiary_address': '0xabced120', 'amount': 133432, 'duration_seconds': 31536000*2}, {'beneficiary_address': '0xf7aefec2', 'amount': 999, 'duration_seconds': 31536000*3}] """ if allocation_registry and allocation_outfile: raise self.ActorError( "Pass either allocation registry or allocation_outfile, not both." ) if allocation_registry is None: allocation_registry = AllocationRegistry( filepath=allocation_outfile) allocation_txhashes, failed = dict(), list() for allocation in allocations: deployer = self.deploy_user_escrow( allocation_registry=allocation_registry) try: txhashes = deployer.deliver( value=allocation['amount'], duration=allocation['duration_seconds'], beneficiary_address=allocation['beneficiary_address']) except TransactionFailed: if crash_on_failure: raise self.log.debug( f"Failed allocation transaction for {allocation['amount']} to {allocation['beneficiary_address']}" ) failed.append(allocation) continue else: allocation_txhashes[ allocation['beneficiary_address']] = txhashes if failed: # TODO: More with these failures: send to isolated logfile, and reattempt self.log.critical( f"FAILED TOKEN ALLOCATION - {len(failed)} Allocations failed.") return allocation_txhashes @staticmethod def __read_allocation_data(filepath: str) -> list: with open(filepath, 'r') as allocation_file: data = allocation_file.read() try: allocation_data = json.loads(data) except JSONDecodeError: raise return allocation_data def deploy_beneficiaries_from_file(self, allocation_data_filepath: str, allocation_outfile: str = None) -> dict: allocations = self.__read_allocation_data( filepath=allocation_data_filepath) txhashes = self.deploy_beneficiary_contracts( allocations=allocations, allocation_outfile=allocation_outfile) return txhashes def save_deployment_receipts(self, receipts: dict) -> str: filename = f'deployment-receipts-{self.deployer_address[:6]}-{maya.now().epoch}.json' filepath = os.path.join(DEFAULT_CONFIG_ROOT, filename) # TODO: Do not assume default config root os.makedirs(DEFAULT_CONFIG_ROOT, exist_ok=True) with open(filepath, 'w') as file: data = dict() for contract_name, receipts in receipts.items(): contract_records = dict() for tx_name, receipt in receipts.items(): # Formatting receipt = { item: str(result) for item, result in receipt.items() } contract_records.update( {tx_name: receipt for tx_name in receipts}) data[contract_name] = contract_records data = json.dumps(data, indent=4) file.write(data) return filepath