Example #1
0
def paint_staged_stake(emitter,
                       blockchain,
                       stakeholder,
                       staking_address,
                       stake_value,
                       lock_periods,
                       start_period,
                       unlock_period,
                       division_message: str = None):
    economics = stakeholder.staker.economics
    start_datetime = datetime_at_period(
        period=start_period,
        seconds_per_period=economics.seconds_per_period,
        start_of_period=True)

    unlock_datetime = datetime_at_period(
        period=unlock_period,
        seconds_per_period=economics.seconds_per_period,
        start_of_period=True)
    locked_days = (lock_periods * economics.hours_per_period) // 24

    start_datetime_pretty = start_datetime.local_datetime().strftime(
        "%b %d %Y %H:%M %Z")
    unlock_datetime_pretty = unlock_datetime.local_datetime().strftime(
        "%b %d %Y %H:%M %Z")

    if division_message:
        emitter.echo(f"\n{'═' * 30} ORIGINAL STAKE {'═' * 28}", bold=True)
        emitter.echo(division_message)

    emitter.echo(f"\n{'═' * 30} STAGED STAKE {'═' * 30}", bold=True)

    emitter.echo(f"""
Staking address: {staking_address}
~ Chain      -> ID # {blockchain.client.chain_id} | {blockchain.client.chain_name}
~ Value      -> {stake_value} ({int(stake_value)} NuNits)
~ Duration   -> {locked_days} Days ({lock_periods} Periods)
~ Enactment  -> {start_datetime_pretty} (period #{start_period})
~ Expiration -> {unlock_datetime_pretty} (period #{unlock_period})
    """)

    # TODO: periods != Days - Do we inform the user here?

    emitter.echo('═' * 73, bold=True)
Example #2
0
    def __init__(self,
                 staking_agent: StakingEscrowAgent,
                 checksum_address: str,
                 value: NU,
                 first_locked_period: int,
                 final_locked_period: int,
                 index: int,
                 economics):

        self.log = Logger(f'stake-{checksum_address}-{index}')

        # Ownership
        self.staker_address = checksum_address

        # Stake Metadata
        self.index = index
        self.value = value

        # Periods
        self.first_locked_period = first_locked_period

        # TODO: #1502 - Move Me Brightly - Docs
        # After this period has passes, workers can go offline, if this is the only stake.
        # This is the last period that can be committed for this stake.
        # Meaning, It must be committed in the previous period,
        # and no commitment can be made in this period for this stake.
        self.final_locked_period = final_locked_period

        # Blockchain
        self.staking_agent = staking_agent

        # Economics
        self.economics = economics
        self.minimum_nu = NU(int(self.economics.minimum_allowed_locked), 'NuNit')
        self.maximum_nu = NU(int(self.economics.maximum_allowed_locked), 'NuNit')

        # Time
        self.start_datetime = datetime_at_period(period=first_locked_period,
                                                 seconds_per_period=self.economics.seconds_per_period,
                                                 start_of_period=True)
        self.unlock_datetime = datetime_at_period(period=final_locked_period + 1,
                                                  seconds_per_period=self.economics.seconds_per_period,
                                                  start_of_period=True)
        self._status = None
Example #3
0
def events(general_config, registry_options, contract_name, from_block,
           to_block, event_name):
    """
    Show events associated to NuCypher contracts
    """
    emitter = _setup_emitter(general_config)
    registry = registry_options.get_registry(emitter, general_config.debug)
    blockchain = BlockchainInterfaceFactory.get_interface(
        provider_uri=registry_options.provider_uri)

    if not contract_name:
        if event_name:
            raise click.BadOptionUsage(
                option_name='--event-name',
                message='--event-name requires --contract-name')
        contract_names = [
            STAKING_ESCROW_CONTRACT_NAME, POLICY_MANAGER_CONTRACT_NAME
        ]
    else:
        contract_names = [contract_name]

    if from_block is None:
        # Sketch of logic for getting the approximate block height of current period start,
        # so by default, this command only shows events of the current period
        last_block = blockchain.client.w3.eth.blockNumber
        staking_agent = ContractAgency.get_agent(StakingEscrowAgent,
                                                 registry=registry)
        current_period = staking_agent.get_current_period()
        current_period_start = datetime_at_period(
            period=current_period,
            seconds_per_period=staking_agent.staking_parameters()[0],
            start_of_period=True)
        seconds_from_midnight = int(
            (maya.now() - current_period_start).total_seconds())
        blocks_from_midnight = seconds_from_midnight // AVERAGE_BLOCK_TIME_IN_SECONDS

        from_block = last_block - blocks_from_midnight

    if to_block is None:
        to_block = 'latest'

    # TODO: additional input validation for block numbers

    emitter.echo(f"Showing events from block {from_block} to {to_block}")
    for contract_name in contract_names:
        title = f" {contract_name} Events ".center(40, "-")
        emitter.echo(f"\n{title}\n", bold=True, color='green')
        agent = ContractAgency.get_agent_by_contract_name(
            contract_name, registry)
        names = agent.events.names if not event_name else [event_name]
        for name in names:
            emitter.echo(f"{name}:", bold=True, color='yellow')
            event_method = agent.events[name]
            for event_record in event_method(from_block=from_block,
                                             to_block=to_block):
                emitter.echo(f"  - {event_record}")
Example #4
0
    def __init__(self,
                 owner_address: str,
                 index: int,
                 value: NU,
                 start_period: int,
                 end_period: int):

        # Stake Info
        self.owner_address = owner_address
        self.index = index
        self.value = value
        self.start_period = start_period
        self.end_period = end_period
        self.duration = (self.end_period-self.start_period) + 1

        # Internals
        self.start_datetime = datetime_at_period(period=start_period)
        self.end_datetime = datetime_at_period(period=end_period)
        self.duration_delta = self.end_datetime - self.start_datetime
Example #5
0
    def __init__(self,
                 checksum_address: str,
                 value: NU,
                 start_period: int,
                 end_period: int,
                 index: int,
                 economics=None,
                 validate_now: bool = True):

        self.log = Logger(f'stake-{checksum_address}-{index}')

        # Stake Metadata
        self.owner_address = checksum_address
        self.worker_address = UNKNOWN_WORKER_STATUS
        self.index = index
        self.value = value
        self.start_period = start_period
        self.end_period = end_period

        # Time
        self.start_datetime = datetime_at_period(period=start_period)
        self.end_datetime = datetime_at_period(period=end_period)
        self.duration_delta = self.end_datetime - self.start_datetime

        # Agency
        self.staking_agent = None
        self.token_agent = NucypherTokenAgent()  # TODO: Use Agency
        self.blockchain = self.token_agent.blockchain

        # Economics
        from nucypher.blockchain.economics import TokenEconomics
        self.economics = economics or TokenEconomics()
        self.minimum_nu = NU(int(self.economics.minimum_allowed_locked),
                             'NuNit')
        self.maximum_nu = NU(int(self.economics.maximum_allowed_locked),
                             'NuNit')

        if validate_now:
            self.validate_duration()

        self.transactions = NO_STAKING_RECEIPT
        self.receipt = NO_STAKING_RECEIPT
Example #6
0
    def __init__(self,
                 miner,
                 value: NU,
                 start_period: int,
                 end_period: int,
                 index: int,
                 validate_now: bool = True):

        self.miner = miner
        owner_address = miner.checksum_public_address
        self.log = Logger(f'stake-{owner_address}-{index}')

        # Stake Metadata
        self.owner_address = owner_address
        self.index = index
        self.value = value
        self.start_period = start_period
        self.end_period = end_period

        # Time
        self.start_datetime = datetime_at_period(period=start_period)
        self.end_datetime = datetime_at_period(period=end_period)
        self.duration_delta = self.end_datetime - self.start_datetime

        self.blockchain = miner.blockchain

        # Agency
        self.miner_agent = miner.miner_agent
        self.token_agent = miner.token_agent

        # Economics
        self.economics = miner.economics
        self.minimum_nu = NU(int(self.economics.minimum_allowed_locked),
                             'NuNit')
        self.maximum_nu = NU(int(self.economics.maximum_allowed_locked),
                             'NuNit')

        if validate_now:
            self.validate_duration()

        self.transactions = NO_STAKING_RECEIPT
        self.receipt = NO_STAKING_RECEIPT
Example #7
0
    def _measure_start_of_next_period(self) -> str:
        """Returns iso8601 datetime of next period"""
        current_period = datetime_to_period(
            datetime=maya.now(),
            seconds_per_period=self.economics.seconds_per_period)
        next_period = datetime_at_period(
            period=current_period + 1,
            seconds_per_period=self.economics.seconds_per_period,
            start_of_period=True)

        return next_period.iso8601()
Example #8
0
def test_staker_prolongs_stake(staker, token_economics):
    stake_index = 0
    origin_stake = staker.stakes[stake_index]

    # Can't use additional periods and expiration together
    new_expiration = datetime_at_period(period=origin_stake.final_locked_period + 3,
                                        seconds_per_period=token_economics.seconds_per_period,
                                        start_of_period=True)
    with pytest.raises(ValueError):
        staker.prolong_stake(stake=origin_stake, additional_periods=3, expiration=new_expiration)

    staker.prolong_stake(stake=origin_stake, additional_periods=3)

    stake = staker.stakes[stake_index]
    assert stake.first_locked_period == origin_stake.first_locked_period
    assert stake.final_locked_period == origin_stake.final_locked_period + 3
    assert stake.value == origin_stake.value

    # Provided stake must be part of current stakes
    with pytest.raises(ValueError):
        staker.prolong_stake(stake=origin_stake, additional_periods=2)
    stake.index = len(staker.stakes)
    with pytest.raises(ValueError):
        staker.prolong_stake(stake=stake, additional_periods=2)
    stake.index = stake_index

    # New expiration date must extend stake duration
    origin_stake = stake
    new_expiration = datetime_at_period(period=origin_stake.final_locked_period,
                                        seconds_per_period=token_economics.seconds_per_period,
                                        start_of_period=True)
    with pytest.raises(ValueError):
        staker.prolong_stake(stake=origin_stake, expiration=new_expiration)

    new_expiration = origin_stake.unlock_datetime
    staker.prolong_stake(stake=origin_stake, expiration=new_expiration)

    stake = staker.stakes[stake_index]
    assert stake.first_locked_period == origin_stake.first_locked_period
    assert stake.final_locked_period == origin_stake.final_locked_period + 1
    assert stake.value == origin_stake.value
Example #9
0
    def generate_policy_parameters(
        self,
        number_of_ursulas: int = None,
        duration_periods: int = None,
        expiration: maya.MayaDT = None,
        value: int = None,
        rate: int = None,
        first_period_reward: int = None,
    ) -> dict:
        """
        Construct policy creation from parameters or overrides.
        """

        if not duration_periods and not expiration:
            raise ValueError(
                "Policy end time must be specified as 'expiration' or 'duration_periods', got neither."
            )

        # Merge injected and default params.
        first_period_reward = first_period_reward or self.first_period_reward
        rate = rate or self.rate
        duration_periods = duration_periods or self.duration_periods

        # Calculate duration in periods and expiration datetime
        if expiration:
            duration_periods = calculate_period_duration(
                future_time=expiration,
                seconds_per_period=self.economics.seconds_per_period)
        else:
            duration_periods = duration_periods or self.duration_periods
            expiration = datetime_at_period(
                self.staking_agent.get_current_period() + duration_periods,
                seconds_per_period=self.economics.seconds_per_period)

        from nucypher.policy.policies import BlockchainPolicy
        blockchain_payload = BlockchainPolicy.generate_policy_parameters(
            n=number_of_ursulas,
            duration_periods=duration_periods,
            first_period_reward=first_period_reward,
            value=value,
            rate=rate)

        # These values may have been recalculated in this block.
        policy_end_time = dict(duration_periods=duration_periods,
                               expiration=expiration)
        payload = {**blockchain_payload, **policy_end_time}
        return payload
Example #10
0
def unbond_worker(general_config, transacting_staker_options, config_file,
                  force):
    """
    Unbond worker currently bonded to a staker.
    """
    emitter = setup_emitter(general_config)

    STAKEHOLDER = transacting_staker_options.create_character(
        emitter, config_file)
    blockchain = transacting_staker_options.get_blockchain()

    economics = STAKEHOLDER.economics

    client_account, staking_address = select_client_account_for_staking(
        emitter=emitter,
        stakeholder=STAKEHOLDER,
        staking_address=transacting_staker_options.staker_options.
        staking_address,
        individual_allocation=STAKEHOLDER.individual_allocation,
        force=force)

    # TODO: Check preconditions (e.g., minWorkerPeriods)
    worker_address = STAKEHOLDER.staking_agent.get_worker_from_staker(
        staking_address)

    password = transacting_staker_options.get_password(blockchain,
                                                       client_account)
    STAKEHOLDER.assimilate(password=password)
    receipt = STAKEHOLDER.unbond_worker()

    # TODO: Double-check dates
    current_period = STAKEHOLDER.staking_agent.get_current_period()
    bonded_date = datetime_at_period(
        period=current_period, seconds_per_period=economics.seconds_per_period)

    message = SUCCESSFUL_DETACH_WORKER.format(worker_address=worker_address,
                                              staking_address=staking_address)
    emitter.echo(message, color='green')
    paint_receipt_summary(emitter=emitter,
                          receipt=receipt,
                          chain_name=blockchain.client.chain_name,
                          transaction_type='unbond_worker')
    emitter.echo(DETACH_DETAILS.format(current_period=current_period,
                                       bonded_date=bonded_date),
                 color='green')
Example #11
0
def commit_to_next_period(general_config, character_options, config_file):
    """Manually make a commitment to the next period."""

    # Setup
    emitter = setup_emitter(general_config, character_options.config_options.worker_address)
    _pre_launch_warnings(emitter, dev=character_options.config_options.dev, force=None)
    _, URSULA = character_options.create_character(emitter, config_file, general_config.json_ipc, load_seednodes=False)

    committed_period = URSULA.staking_agent.get_current_period() + 1
    click.echo(CONFIRMING_ACTIVITY_NOW.format(committed_period=committed_period), color='blue')
    receipt = URSULA.commit_to_next_period(fire_and_forget=False)

    economics = EconomicsFactory.get_economics(registry=URSULA.registry)
    date = datetime_at_period(period=committed_period, seconds_per_period=economics.seconds_per_period)

    # TODO: Double-check dates here
    message = SUCCESSFUL_CONFIRM_ACTIVITY.format(committed_period=committed_period, date=date)
    emitter.echo(message, bold=True, color='blue')
    paint_receipt_summary(emitter=emitter,
                          receipt=receipt,
                          chain_name=URSULA.staking_agent.blockchain.client.chain_name)
Example #12
0
def confirm_activity(general_config, character_options, config_file):
    """
    Manually confirm-activity for the current period.
    """
    emitter = _setup_emitter(general_config, character_options.config_options.worker_address)
    _pre_launch_warnings(emitter, dev=character_options.config_options.dev, force=None)
    _, URSULA = character_options.create_character(emitter, config_file, general_config.json_ipc, load_seednodes=False)

    confirmed_period = URSULA.staking_agent.get_current_period() + 1
    click.echo(f"Confirming activity for period {confirmed_period}", color='blue')
    receipt = URSULA.confirm_activity()

    economics = TokenEconomicsFactory.get_economics(registry=URSULA.registry)
    date = datetime_at_period(period=confirmed_period,
                              seconds_per_period=economics.seconds_per_period)

    # TODO: Double-check dates here
    emitter.echo(f'\nActivity confirmed for period #{confirmed_period} '
                 f'(starting at {date})', bold=True, color='blue')
    painting.paint_receipt_summary(emitter=emitter,
                                   receipt=receipt,
                                   chain_name=URSULA.staking_agent.blockchain.client.chain_name)
Example #13
0
def bond_worker(general_config, transacting_staker_options, config_file, force,
                worker_address):
    """Bond a worker to a staker."""

    emitter = setup_emitter(general_config)
    STAKEHOLDER = transacting_staker_options.create_character(
        emitter, config_file)
    blockchain = transacting_staker_options.get_blockchain()
    economics = STAKEHOLDER.economics

    client_account, staking_address = select_client_account_for_staking(
        emitter=emitter,
        stakeholder=STAKEHOLDER,
        staking_address=transacting_staker_options.staker_options.
        staking_address,
        individual_allocation=STAKEHOLDER.individual_allocation,
        force=force)

    if not worker_address:
        worker_address = click.prompt(PROMPT_WORKER_ADDRESS,
                                      type=EIP55_CHECKSUM_ADDRESS)

    if (worker_address == staking_address) and not force:
        click.confirm(CONFIRM_WORKER_AND_STAKER_ADDRESSES_ARE_EQUAL.format(
            address=worker_address),
                      abort=True)

    # TODO: Check preconditions (e.g., minWorkerPeriods, already in use, etc)

    password = transacting_staker_options.get_password(blockchain,
                                                       client_account)

    # TODO: Double-check dates
    # Calculate release datetime
    current_period = STAKEHOLDER.staking_agent.get_current_period()
    bonded_date = datetime_at_period(
        period=current_period, seconds_per_period=economics.seconds_per_period)
    min_worker_periods = STAKEHOLDER.economics.minimum_worker_periods

    release_period = current_period + min_worker_periods
    release_date = datetime_at_period(
        period=release_period,
        seconds_per_period=economics.seconds_per_period,
        start_of_period=True)

    if not force:
        click.confirm(
            f"Commit to bonding "
            f"worker {worker_address} to staker {staking_address} "
            f"for a minimum of {STAKEHOLDER.economics.minimum_worker_periods} periods?",
            abort=True)

    STAKEHOLDER.assimilate(checksum_address=client_account, password=password)
    receipt = STAKEHOLDER.bond_worker(worker_address=worker_address)

    # Report Success
    message = SUCCESSFUL_WORKER_BONDING.format(worker_address=worker_address,
                                               staking_address=staking_address)
    emitter.echo(message, color='green')
    paint_receipt_summary(emitter=emitter,
                          receipt=receipt,
                          chain_name=blockchain.client.chain_name,
                          transaction_type='bond_worker')
    emitter.echo(BONDING_DETAILS.format(current_period=current_period,
                                        bonded_date=bonded_date),
                 color='green')
    emitter.echo(BONDING_RELEASE_INFO.format(release_period=release_period,
                                             release_date=release_date),
                 color='green')
Example #14
0
    def _learn_about_nodes(self, threaded: bool = True):
        if threaded:
            if self.__collecting_nodes:
                self.log.debug(
                    "Skipping Round - Nodes collection thread is already running"
                )
                return
            return reactor.callInThread(self._learn_about_nodes,
                                        threaded=False)
        self.__collecting_nodes = True

        agent = self.staking_agent
        known_nodes = list(self.known_nodes)

        block_time = agent.blockchain.client.w3.eth.getBlock(
            'latest').timestamp  # precision in seconds
        current_period = agent.get_current_period()

        log = f'Processing {len(known_nodes)} nodes at {MayaDT(epoch=block_time)} | Period {current_period}'
        self.log.info(log)

        data = list()
        for node in known_nodes:

            staker_address = node.checksum_address
            worker = agent.get_worker_from_staker(staker_address)

            stake = agent.owned_tokens(staker_address)
            staked_nu_tokens = float(NU.from_nunits(stake).to_tokens())
            locked_nu_tokens = float(
                NU.from_nunits(
                    agent.get_locked_tokens(
                        staker_address=staker_address)).to_tokens())

            economics = EconomicsFactory.get_economics(registry=self.registry)
            stakes = StakeList(checksum_address=staker_address,
                               registry=self.registry)
            stakes.refresh()

            if stakes.initial_period is NOT_STAKING:
                continue  # TODO: Skip this measurement for now

            start_date = datetime_at_period(
                stakes.initial_period,
                seconds_per_period=economics.seconds_per_period)
            start_date = start_date.datetime().timestamp()
            end_date = datetime_at_period(
                stakes.terminal_period,
                seconds_per_period=economics.seconds_per_period)
            end_date = end_date.datetime().timestamp()

            last_confirmed_period = agent.get_last_active_period(
                staker_address)

            num_work_orders = 0  # len(node.work_orders())  # TODO: Only works for is_me with datastore attached

            # TODO: do we need to worry about how much information is in memory if number of nodes is
            #  large i.e. should I check for size of data and write within loop if too big
            data.append(
                self.NODE_LINE_PROTOCOL.format(
                    measurement=self.NODE_MEASUREMENT,
                    staker_address=staker_address,
                    worker_address=worker,
                    start_date=start_date,
                    end_date=end_date,
                    stake=staked_nu_tokens,
                    locked_stake=locked_nu_tokens,
                    current_period=current_period,
                    last_confirmed_period=last_confirmed_period,
                    timestamp=block_time,
                    work_orders=num_work_orders))

        success = self._influx_client.write_points(
            data,
            database=self.INFLUX_DB_NAME,
            time_precision='s',
            batch_size=10000,
            protocol='line')
        self.__collecting_nodes = False
        if not success:
            # TODO: What do we do here - Event hook for alerting?
            self.log.warn(
                f'Unable to write node information to database {self.INFLUX_DB_NAME} at '
                f'{MayaDT(epoch=block_time)} | Period {current_period}')
Example #15
0
def set_worker(
        click_config,

        # Worker Options
        poa,
        light,
        registry_filepath,
        config_file,
        provider_uri,
        staking_address,
        hw_wallet,
        beneficiary_address,
        allocation_filepath,
        worker_address,

        # Other options
        force):
    """
    Bond a worker to a staker.
    """

    ### Setup ###
    emitter = _setup_emitter(click_config)

    STAKEHOLDER, blockchain = _create_stakeholder(
        config_file,
        provider_uri,
        poa,
        light,
        registry_filepath,
        staking_address,
        beneficiary_address=beneficiary_address,
        allocation_filepath=allocation_filepath)
    #############

    economics = STAKEHOLDER.economics

    client_account, staking_address = handle_client_account_for_staking(
        emitter=emitter,
        stakeholder=STAKEHOLDER,
        staking_address=staking_address,
        individual_allocation=STAKEHOLDER.individual_allocation,
        force=force)

    if not worker_address:
        worker_address = click.prompt("Enter worker address",
                                      type=EIP55_CHECKSUM_ADDRESS)

    # TODO: Check preconditions (e.g., minWorkerPeriods, already in use, etc)

    password = None
    if not hw_wallet and not blockchain.client.is_local:
        password = get_client_password(checksum_address=client_account)

    STAKEHOLDER.assimilate(checksum_address=client_account, password=password)
    receipt = STAKEHOLDER.set_worker(worker_address=worker_address)

    # TODO: Double-check dates
    current_period = STAKEHOLDER.staking_agent.get_current_period()
    bonded_date = datetime_at_period(
        period=current_period, seconds_per_period=economics.seconds_per_period)
    min_worker_periods = STAKEHOLDER.staking_agent.staking_parameters()[7]
    release_period = current_period + min_worker_periods
    release_date = datetime_at_period(
        period=release_period,
        seconds_per_period=economics.seconds_per_period,
        start_of_period=True)

    emitter.echo(
        f"\nWorker {worker_address} successfully bonded to staker {staking_address}",
        color='green')
    paint_receipt_summary(emitter=emitter,
                          receipt=receipt,
                          chain_name=blockchain.client.chain_name,
                          transaction_type='set_worker')
    emitter.echo(f"Bonded at period #{current_period} ({bonded_date})",
                 color='green')
    emitter.echo(
        f"This worker can be replaced or detached after period "
        f"#{release_period} ({release_date})",
        color='green')
Example #16
0
def detach_worker(
        click_config,

        # Worker Options
        poa,
        light,
        registry_filepath,
        config_file,
        provider_uri,
        staking_address,
        hw_wallet,
        beneficiary_address,
        allocation_filepath,
        worker_address,

        # Other options
        force):
    """
    Detach worker currently bonded to a staker.
    """

    ### Setup ###
    emitter = _setup_emitter(click_config)

    STAKEHOLDER, blockchain = _create_stakeholder(
        config_file,
        provider_uri,
        poa,
        light,
        registry_filepath,
        staking_address,
        beneficiary_address=beneficiary_address,
        allocation_filepath=allocation_filepath)
    #############

    economics = STAKEHOLDER.economics

    client_account, staking_address = handle_client_account_for_staking(
        emitter=emitter,
        stakeholder=STAKEHOLDER,
        staking_address=staking_address,
        individual_allocation=STAKEHOLDER.individual_allocation,
        force=force)

    if worker_address:
        raise click.BadOptionUsage(
            message=
            "detach-worker cannot be used together with --worker-address",
            option_name='--worker-address')

    # TODO: Check preconditions (e.g., minWorkerPeriods)

    worker_address = STAKEHOLDER.staking_agent.get_worker_from_staker(
        staking_address)

    password = None
    if not hw_wallet and not blockchain.client.is_local:
        password = get_client_password(checksum_address=client_account)

    STAKEHOLDER.assimilate(checksum_address=client_account, password=password)
    receipt = STAKEHOLDER.detach_worker()

    # TODO: Double-check dates
    current_period = STAKEHOLDER.staking_agent.get_current_period()
    bonded_date = datetime_at_period(
        period=current_period, seconds_per_period=economics.seconds_per_period)

    emitter.echo(
        f"Successfully detached worker {worker_address} from staker {staking_address}",
        color='green')
    paint_receipt_summary(emitter=emitter,
                          receipt=receipt,
                          chain_name=blockchain.client.chain_name,
                          transaction_type='detach_worker')
    emitter.echo(f"Detached at period #{current_period} ({bonded_date})",
                 color='green')
Example #17
0
def ursula(
    click_config,
    action,
    dev,
    dry_run,
    force,
    lonely,
    network,
    teacher_uri,
    min_stake,
    rest_host,
    rest_port,
    db_filepath,
    staker_address,
    worker_address,
    federated_only,
    poa,
    config_root,
    config_file,
    provider_uri,
    geth,
    registry_filepath,
    interactive,
    sync,
) -> None:
    """
    "Ursula the Untrusted" PRE Re-encryption node management commands.

    \b
    Actions
    -------------------------------------------------
    \b
    init              Create a new Ursula node configuration.
    view              View the Ursula node's configuration.
    run               Run an "Ursula" node.
    save-metadata     Manually write node metadata to disk without running
    forget            Forget all known nodes.
    destroy           Delete Ursula node configuration.
    confirm-activity  Manually confirm-activity for the current period.

    """

    emitter = click_config.emitter

    #
    # Validate
    #

    if federated_only:
        if geth:
            raise click.BadOptionUsage(
                option_name="--geth",
                message="Federated only cannot be used with the --geth flag")

        if staker_address:
            raise click.BadOptionUsage(
                option_name='--federated-only',
                message="Staking address cannot be used in federated mode.")

    # Banner
    emitter.banner(URSULA_BANNER.format(worker_address or ''))

    #
    # Pre-Launch Warnings
    #

    if dev:
        emitter.echo("WARNING: Running in Development mode",
                     color='yellow',
                     verbosity=1)
    if force:
        emitter.echo("WARNING: Force is enabled", color='yellow', verbosity=1)

    #
    # Internal Ethereum Client
    #

    ETH_NODE = NO_BLOCKCHAIN_CONNECTION
    if geth:
        ETH_NODE = actions.get_provider_process()
        provider_uri = ETH_NODE.provider_uri(scheme='file')

    #
    # Eager Actions
    #

    if action == "init":
        """Create a brand-new persistent Ursula"""

        if dev:
            raise click.BadArgumentUsage(
                "Cannot create a persistent development character")

        if (not staker_address or not worker_address) and not federated_only:

            # Connect to Blockchain
            fetch_registry = registry_filepath is None and not click_config.no_registry
            registry = None
            if registry_filepath:
                registry = EthereumContractRegistry(
                    registry_filepath=registry_filepath)
            blockchain = BlockchainInterface(provider_uri=provider_uri,
                                             registry=registry,
                                             poa=poa)
            blockchain.connect(fetch_registry=fetch_registry,
                               sync_now=sync,
                               emitter=emitter)

            if not staker_address:
                prompt = "Select staker account"
                staker_address = select_client_account(emitter=emitter,
                                                       blockchain=blockchain,
                                                       prompt=prompt)

            if not worker_address:
                prompt = "Select worker account"
                worker_address = select_client_account(emitter=emitter,
                                                       blockchain=blockchain,
                                                       prompt=prompt)

        if not config_root:  # Flag
            config_root = click_config.config_file  # Envvar

        if not rest_host:
            rest_host = actions.determine_external_ip_address(emitter,
                                                              force=force)

        download_registry = not federated_only and not click_config.no_registry
        ursula_config = UrsulaConfiguration.generate(
            password=get_nucypher_password(confirm=True),
            config_root=config_root,
            rest_host=rest_host,
            rest_port=rest_port,
            db_filepath=db_filepath,
            domains={network} if network else None,
            federated_only=federated_only,
            checksum_address=staker_address,
            worker_address=worker_address,
            download_registry=download_registry,
            registry_filepath=registry_filepath,
            provider_process=ETH_NODE,
            provider_uri=provider_uri,
            poa=poa)

        painting.paint_new_installation_help(emitter,
                                             new_configuration=ursula_config)
        return

    #
    # Make Ursula
    #

    if dev:
        ursula_config = UrsulaConfiguration(
            dev_mode=True,
            domains={TEMPORARY_DOMAIN},
            poa=poa,
            download_registry=False,
            registry_filepath=registry_filepath,
            provider_process=ETH_NODE,
            provider_uri=provider_uri,
            checksum_address=staker_address,
            worker_address=worker_address,
            federated_only=federated_only,
            rest_host=rest_host,
            rest_port=rest_port,
            db_filepath=db_filepath)
    else:
        try:
            ursula_config = UrsulaConfiguration.from_configuration_file(
                filepath=config_file,
                domains={network} if network else None,
                registry_filepath=registry_filepath,
                provider_process=ETH_NODE,
                provider_uri=provider_uri,
                rest_host=rest_host,
                rest_port=rest_port,
                db_filepath=db_filepath,
                poa=poa,
                federated_only=federated_only)
        except FileNotFoundError:
            return actions.handle_missing_configuration_file(
                character_config_class=UrsulaConfiguration,
                config_file=config_file)
        except NucypherKeyring.AuthenticationFailed as e:
            emitter.echo(str(e), color='red', bold=True)
            click.get_current_context().exit(1)
            # TODO: Exit codes (not only for this, but for other exceptions)

    #
    # Configured Pre-Authentication Actions
    #

    # Handle destruction and forget *before* network bootstrap and character initialization below
    if action == "destroy":
        """Delete all configuration files from the disk"""
        if dev:
            message = "'nucypher ursula destroy' cannot be used in --dev mode - There is nothing to destroy."
            raise click.BadOptionUsage(option_name='--dev', message=message)
        actions.destroy_configuration(emitter,
                                      character_config=ursula_config,
                                      force=force)
        return

    elif action == "forget":
        actions.forget(emitter, configuration=ursula_config)
        return

    #
    # Make Ursula
    #
    client_password = None
    if not ursula_config.federated_only:
        if not dev and not click_config.json_ipc:
            client_password = get_client_password(
                checksum_address=ursula_config.worker_address,
                envvar="NUCYPHER_WORKER_ETH_PASSWORD")

    try:
        URSULA = actions.make_cli_character(character_config=ursula_config,
                                            click_config=click_config,
                                            min_stake=min_stake,
                                            teacher_uri=teacher_uri,
                                            dev=dev,
                                            lonely=lonely,
                                            client_password=client_password)
    except NucypherKeyring.AuthenticationFailed as e:
        emitter.echo(str(e), color='red', bold=True)
        click.get_current_context().exit(1)
        # TODO: Exit codes (not only for this, but for other exceptions)

    #
    # Authenticated Action Switch
    #

    if action == 'run':
        """Seed, Produce, Run!"""

        # GO!
        try:

            # Ursula Deploy Warnings
            emitter.message(f"Starting Ursula on {URSULA.rest_interface}",
                            color='green',
                            bold=True)

            emitter.message(f"Connecting to {','.join(ursula_config.domains)}",
                            color='green',
                            bold=True)

            emitter.message("Working ~ Keep Ursula Online!",
                            color='blue',
                            bold=True)

            if interactive:
                stdio.StandardIO(
                    UrsulaCommandProtocol(ursula=URSULA, emitter=emitter))

            if dry_run:
                return  # <-- ABORT - (Last Chance)

            # Run - Step 3
            node_deployer = URSULA.get_deployer()
            node_deployer.addServices()
            node_deployer.catalogServers(node_deployer.hendrix)
            node_deployer.run()  # <--- Blocking Call (Reactor)

        # Handle Crash
        except Exception as e:
            ursula_config.log.critical(str(e))
            emitter.message(f"{e.__class__.__name__} {e}",
                            color='red',
                            bold=True)
            raise  # Crash :-(

        # Graceful Exit
        finally:
            emitter.message("Stopping Ursula", color='green')
            ursula_config.cleanup()
            emitter.message("Ursula Stopped", color='red')
        return

    elif action == "save-metadata":
        """Manually save a node self-metadata file"""
        metadata_path = ursula.write_node_metadata(node=URSULA)
        emitter.message(
            f"Successfully saved node metadata to {metadata_path}.",
            color='green')
        return

    elif action == "view":
        """Paint an existing configuration to the console"""

        if not URSULA.federated_only:
            emitter.echo("BLOCKCHAIN ----------\n")
            painting.paint_contract_status(emitter=emitter,
                                           blockchain=URSULA.blockchain)
            current_block = URSULA.blockchain.w3.eth.blockNumber
            emitter.echo(f'Block # {current_block}')
            # TODO: 1231
            emitter.echo(f'NU Balance (staker): {URSULA.token_balance}')
            emitter.echo(
                f'ETH Balance (worker): {URSULA.blockchain.client.get_balance(URSULA.worker_address)}'
            )
            emitter.echo(
                f'Current Gas Price {URSULA.blockchain.client.gas_price}')

        emitter.echo("CONFIGURATION --------")
        response = UrsulaConfiguration._read_configuration_file(
            filepath=config_file or ursula_config.config_file_location)
        return emitter.ipc(
            response=response, request_id=0, duration=0
        )  # FIXME: #1216 - what are request_id and duration here?

    elif action == 'confirm-activity':
        receipt = URSULA.confirm_activity()

        confirmed_period = URSULA.staking_agent.get_current_period() + 1
        date = datetime_at_period(period=confirmed_period)

        # TODO: Double-check dates here
        emitter.echo(
            f'\nActivity confirmed for period #{confirmed_period} '
            f'(starting at {date})',
            bold=True,
            color='blue')
        painting.paint_receipt_summary(
            emitter=emitter,
            receipt=receipt,
            chain_name=URSULA.blockchain.client.chain_name)

        # TODO: Check ActivityConfirmation event (see #1193)
        return

    else:
        raise click.BadArgumentUsage("No such argument {}".format(action))
Example #18
0
def test_staker_divides_stake(staker, token_economics):
    stake_value = NU(token_economics.minimum_allowed_locked * 5, 'NuNit')
    new_stake_value = NU(token_economics.minimum_allowed_locked * 2, 'NuNit')

    stake_index = 0
    staker.initialize_stake(amount=stake_value, lock_periods=int(token_economics.minimum_locked_periods))
    stake = staker.stakes[stake_index + 1]

    # Can't use additional periods and expiration together
    with pytest.raises(ValueError):
        staker.divide_stake(target_value=new_stake_value, stake=stake, additional_periods=2, expiration=maya.now())

    staker.divide_stake(target_value=new_stake_value, stake=stake, additional_periods=2)

    current_period = staker.staking_agent.get_current_period()
    expected_old_stake = (current_period + 1, current_period + 30, stake_value - new_stake_value)
    expected_new_stake = (current_period + 1, current_period + 32, new_stake_value)

    assert 3 == len(staker.stakes), 'A new stake was not added to this stakers stakes'
    assert expected_old_stake == staker.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid'
    assert expected_new_stake == staker.stakes[stake_index + 2].to_stake_info(), 'New stake values are invalid'

    # Provided stake must be part of current stakes
    new_stake_value = NU.from_nunits(token_economics.minimum_allowed_locked)
    with pytest.raises(ValueError):
        staker.divide_stake(target_value=new_stake_value, stake=stake, additional_periods=2)
    stake = staker.stakes[stake_index + 1]
    stake.index = len(staker.stakes)
    with pytest.raises(ValueError):
        staker.divide_stake(target_value=new_stake_value, stake=stake, additional_periods=2)

    yet_another_stake_value = NU(token_economics.minimum_allowed_locked, 'NuNit')
    stake = staker.stakes[stake_index + 2]

    # New expiration date must extend stake duration
    origin_stake = stake
    new_expiration = datetime_at_period(period=origin_stake.final_locked_period,
                                        seconds_per_period=token_economics.seconds_per_period,
                                        start_of_period=True)
    with pytest.raises(ValueError):
        staker.divide_stake(target_value=yet_another_stake_value, stake=stake, expiration=new_expiration)

    new_expiration = datetime_at_period(period=origin_stake.final_locked_period + 2,
                                        seconds_per_period=token_economics.seconds_per_period,
                                        start_of_period=True)
    staker.divide_stake(target_value=yet_another_stake_value, stake=stake, expiration=new_expiration)

    expected_new_stake = (current_period + 1, current_period + 32, new_stake_value)
    expected_yet_another_stake = Stake(first_locked_period=current_period + 1,
                                       final_locked_period=current_period + 34,
                                       value=yet_another_stake_value,
                                       checksum_address=staker.checksum_address,
                                       index=3,
                                       staking_agent=staker.staking_agent,
                                       economics=token_economics)

    assert 4 == len(staker.stakes), 'A new stake was not added after two stake divisions'
    assert expected_old_stake == staker.stakes[
        stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions'
    assert expected_new_stake == staker.stakes[
        stake_index + 2].to_stake_info(), 'New stake values are invalid after two stake divisions'
    assert expected_yet_another_stake.value == staker.stakes[stake_index + 3].value, 'Third stake values are invalid'
Example #19
0
def stake(click_config,
          action,

          config_root,
          config_file,

          # Mode
          force,
          offline,
          hw_wallet,

          # Blockchain
          poa,
          registry_filepath,
          provider_uri,
          sync,

          # Stake
          staking_address,
          worker_address,
          withdraw_address,
          value,
          lock_periods,
          index,
          policy_reward,
          staking_reward,
          enable,
          lock_until,

          ) -> None:
    """
    Manage stakes and other staker-related operations.

    \b
    Actions
    -------------------------------------------------
    init-stakeholder  Create a new stakeholder configuration
    list              List active stakes for current stakeholder
    accounts          Show ETH and NU balances for stakeholder's accounts
    sync              Synchronize stake data with on-chain information
    set-worker        Bond a worker to a staker
    detach-worker     Detach worker currently bonded to a staker
    init              Create a new stake
    restake           Manage re-staking with --enable or --disable
    divide            Create a new stake from part of an existing one
    collect-reward    Withdraw staking reward

    """

    # Banner
    emitter = click_config.emitter
    emitter.clear()
    emitter.banner(StakeHolder.banner)

    if action == 'init-stakeholder':

        if not provider_uri:
            raise click.BadOptionUsage(option_name='--provider',
                                       message="--provider is required to create a new stakeholder.")

        new_stakeholder = StakeHolderConfiguration.generate(config_root=config_root,
                                                            provider_uri=provider_uri,
                                                            poa=poa,
                                                            sync=False,
                                                            registry_filepath=registry_filepath)

        filepath = new_stakeholder.to_configuration_file(override=force)
        emitter.echo(f"Wrote new stakeholder configuration to {filepath}", color='green')
        return  # Exit

    try:
        stakeholder_config = StakeHolderConfiguration.from_configuration_file(filepath=config_file,
                                                                              provider_uri=provider_uri,
                                                                              poa=poa,
                                                                              sync=False,
                                                                              registry_filepath=registry_filepath)
    except FileNotFoundError:
        return actions.handle_missing_configuration_file(character_config_class=StakeHolderConfiguration,
                                                         config_file=config_file)

    #
    # Make Stakeholder
    #

    STAKEHOLDER = stakeholder_config.produce(initial_address=staking_address)
    blockchain = BlockchainInterfaceFactory.get_interface(provider_uri=provider_uri)  # Eager connection
    economics = STAKEHOLDER.economics

    # Dynamic click types (Economics)
    min_locked = economics.minimum_allowed_locked
    stake_value_range = click.FloatRange(min=NU.from_nunits(min_locked).to_tokens(), clamp=False)
    stake_duration_range = click.IntRange(min=economics.minimum_locked_periods, clamp=False)
    stake_extension_range = click.IntRange(min=1, max=economics.maximum_allowed_locked, clamp=False)

    #
    # Eager Actions
    #

    if action == 'list':
        stakes = STAKEHOLDER.all_stakes
        if not stakes:
            emitter.echo(f"There are no active stakes")
        else:
            painting.paint_stakes(emitter=emitter, stakes=stakes)
        return  # Exit

    elif action == 'accounts':
        for address, balances in STAKEHOLDER.wallet.balances.items():
            emitter.echo(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}")
        return  # Exit

    elif action == 'set-worker':

        if not staking_address:
            staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).staker_address

        if not worker_address:
            worker_address = click.prompt("Enter worker address", type=EIP55_CHECKSUM_ADDRESS)

        # TODO: Check preconditions (e.g., minWorkerPeriods, already in use, etc)

        password = None
        if not hw_wallet and not blockchain.client.is_local:
            password = get_client_password(checksum_address=staking_address)

        STAKEHOLDER.assimilate(checksum_address=staking_address, password=password)
        receipt = STAKEHOLDER.set_worker(worker_address=worker_address)

        # TODO: Double-check dates
        current_period = STAKEHOLDER.staking_agent.get_current_period()
        bonded_date = datetime_at_period(period=current_period, seconds_per_period=economics.seconds_per_period)
        min_worker_periods = STAKEHOLDER.staking_agent.staking_parameters()[7]
        release_period = current_period + min_worker_periods
        release_date = datetime_at_period(period=release_period, seconds_per_period=economics.seconds_per_period)

        emitter.echo(f"\nWorker {worker_address} successfully bonded to staker {staking_address}", color='green')
        paint_receipt_summary(emitter=emitter,
                              receipt=receipt,
                              chain_name=blockchain.client.chain_name,
                              transaction_type='set_worker')
        emitter.echo(f"Bonded at period #{current_period} ({bonded_date})", color='green')
        emitter.echo(f"This worker can be replaced or detached after period "
                     f"#{release_period} ({release_date})", color='green')
        return  # Exit

    elif action == 'detach-worker':

        if not staking_address:
            staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).staker_address

        if worker_address:
            raise click.BadOptionUsage(message="detach-worker cannot be used together with --worker-address",
                                       option_name='--worker-address')

        # TODO: Check preconditions (e.g., minWorkerPeriods)

        worker_address = STAKEHOLDER.staking_agent.get_worker_from_staker(staking_address)

        password = None
        if not hw_wallet and not blockchain.client.is_local:
            password = get_client_password(checksum_address=staking_address)

        # TODO: Create Stakeholder.detach_worker() and use it here
        STAKEHOLDER.assimilate(checksum_address=staking_address, password=password)
        receipt = STAKEHOLDER.set_worker(worker_address=BlockchainInterface.NULL_ADDRESS)

        # TODO: Double-check dates
        current_period = STAKEHOLDER.staking_agent.get_current_period()
        bonded_date = datetime_at_period(period=current_period, seconds_per_period=economics.seconds_per_period)

        emitter.echo(f"Successfully detached worker {worker_address} from staker {staking_address}", color='green')
        paint_receipt_summary(emitter=emitter,
                              receipt=receipt,
                              chain_name=blockchain.client.chain_name,
                              transaction_type='detach_worker')
        emitter.echo(f"Detached at period #{current_period} ({bonded_date})", color='green')
        return  # Exit

    elif action == 'create':
        """Initialize a new stake"""

        #
        # Get Staking Account
        #

        password = None
        if not staking_address:
            staking_address = select_client_account(prompt="Select staking account",
                                                    emitter=emitter,
                                                    provider_uri=STAKEHOLDER.wallet.blockchain.provider_uri)

        if not hw_wallet and not blockchain.client.is_local:
            password = click.prompt(f"Enter password to unlock {staking_address}",
                                    hide_input=True,
                                    confirmation_prompt=False)
        #
        # Stage Stake
        #

        if not value:
            value = click.prompt(f"Enter stake value in NU",
                                 type=stake_value_range,
                                 default=NU.from_nunits(min_locked).to_tokens())
        value = NU.from_tokens(value)

        if not lock_periods:
            prompt = f"Enter stake duration ({STAKEHOLDER.economics.minimum_locked_periods} periods minimum)"
            lock_periods = click.prompt(prompt, type=stake_duration_range)

        start_period = STAKEHOLDER.staking_agent.get_current_period()
        end_period = start_period + lock_periods

        #
        # Review
        #

        if not force:
            painting.paint_staged_stake(emitter=emitter,
                                        stakeholder=STAKEHOLDER,
                                        staking_address=staking_address,
                                        stake_value=value,
                                        lock_periods=lock_periods,
                                        start_period=start_period,
                                        end_period=end_period)

            confirm_staged_stake(staker_address=staking_address, value=value, lock_periods=lock_periods)

        # Last chance to bail
        click.confirm("Publish staged stake to the blockchain?", abort=True)

        # Execute
        STAKEHOLDER.assimilate(checksum_address=staking_address, password=password)
        new_stake = STAKEHOLDER.initialize_stake(amount=value, lock_periods=lock_periods)

        painting.paint_staking_confirmation(emitter=emitter,
                                            ursula=STAKEHOLDER,
                                            transactions=new_stake.transactions)
        return  # Exit

    elif action == "restake":

        # Authenticate
        if not staking_address:
            staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).staker_address
        password = None
        if not hw_wallet and not blockchain.client.is_local:
            password = get_client_password(checksum_address=staking_address)
        STAKEHOLDER.assimilate(checksum_address=staking_address, password=password)

        # Inner Exclusive Switch
        if lock_until:
            if not force:
                confirm_enable_restaking_lock(emitter, staking_address=staking_address, release_period=lock_until)
            receipt = STAKEHOLDER.enable_restaking_lock(release_period=lock_until)
            emitter.echo(f'Successfully enabled re-staking lock for {staking_address} until {lock_until}',
                         color='green', verbosity=1)
        elif enable:
            if not force:
                confirm_enable_restaking(emitter, staking_address=staking_address)
            receipt = STAKEHOLDER.enable_restaking()
            emitter.echo(f'Successfully enabled re-staking for {staking_address}', color='green', verbosity=1)
        else:
            if not force:
                click.confirm(f"Confirm disable re-staking for staker {staking_address}?", abort=True)
            receipt = STAKEHOLDER.disable_restaking()
            emitter.echo(f'Successfully disabled re-staking for {staking_address}', color='green', verbosity=1)

        paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=blockchain.client.chain_name)
        return  # Exit

    elif action == 'divide':
        """Divide an existing stake by specifying the new target value and end period"""

        if staking_address and index is not None:  # 0 is valid.
            current_stake = STAKEHOLDER.stakes[index]
        else:
            current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter)

        #
        # Stage Stake
        #

        # Value
        if not value:
            value = click.prompt(f"Enter target value (must be less than or equal to {str(current_stake.value)})",
                                 type=stake_value_range)
        value = NU(value, 'NU')

        # Duration
        if not lock_periods:
            extension = click.prompt("Enter number of periods to extend", type=stake_extension_range)
        else:
            extension = lock_periods

        if not force:
            painting.paint_staged_stake_division(emitter=emitter,
                                                 stakeholder=STAKEHOLDER,
                                                 original_stake=current_stake,
                                                 target_value=value,
                                                 extension=extension)
            click.confirm("Is this correct?", abort=True)

        # Execute
        password = None
        if not hw_wallet and not blockchain.client.is_local:
            password = get_client_password(checksum_address=current_stake.staker_address)

        STAKEHOLDER.assimilate(checksum_address=current_stake.staker_address, password=password)
        modified_stake, new_stake = STAKEHOLDER.divide_stake(stake_index=current_stake.index,
                                                             target_value=value,
                                                             additional_periods=extension)
        emitter.echo('Successfully divided stake', color='green', verbosity=1)
        paint_receipt_summary(emitter=emitter,
                              receipt=new_stake.receipt,
                              chain_name=blockchain.client.chain_name)

        # Show the resulting stake list
        painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes)
        return  # Exit

    elif action == 'collect-reward':
        """Withdraw staking reward to the specified wallet address"""

        password = None
        if not hw_wallet and not blockchain.client.is_local:
            password = get_client_password(checksum_address=staking_address)

        if not staking_reward and not policy_reward:
            raise click.BadArgumentUsage(f"Either --staking-reward or --policy-reward must be True to collect rewards.")

        STAKEHOLDER.assimilate(checksum_address=staking_address, password=password)
        if staking_reward:
            # Note: Sending staking / inflation rewards to another account is not allowed.
            staking_receipt = STAKEHOLDER.collect_staking_reward()
            paint_receipt_summary(receipt=staking_receipt,
                                  chain_name=STAKEHOLDER.wallet.blockchain.client.chain_name,
                                  emitter=emitter)

        if policy_reward:
            policy_receipt = STAKEHOLDER.collect_policy_reward(collector_address=withdraw_address)
            paint_receipt_summary(receipt=policy_receipt,
                                  chain_name=STAKEHOLDER.wallet.blockchain.client.chain_name,
                                  emitter=emitter)

        return  # Exit

    # Catch-All for unknown actions
    else:
        ctx = click.get_current_context()
        raise click.UsageError(message=f"Unknown action '{action}'.", ctx=ctx)
Example #20
0
def set_worker(general_config, transacting_staker_options, config_file, force,
               worker_address):
    """
    Bond a worker to a staker.
    """
    emitter = _setup_emitter(general_config)

    STAKEHOLDER = transacting_staker_options.create_character(
        emitter, config_file)
    blockchain = transacting_staker_options.get_blockchain()

    economics = STAKEHOLDER.economics

    client_account, staking_address = handle_client_account_for_staking(
        emitter=emitter,
        stakeholder=STAKEHOLDER,
        staking_address=transacting_staker_options.staker_options.
        staking_address,
        individual_allocation=STAKEHOLDER.individual_allocation,
        force=force)

    if not worker_address:
        worker_address = click.prompt("Enter worker address",
                                      type=EIP55_CHECKSUM_ADDRESS)

    if (worker_address == staking_address) and not force:
        click.confirm(
            "The worker address provided is the same as the staking account.  "
            "It is highly recommended to use a different accounts for staker and worker roles.",
            abort=True)

    # TODO: Check preconditions (e.g., minWorkerPeriods, already in use, etc)

    password = transacting_staker_options.get_password(blockchain,
                                                       client_account)

    # TODO: Double-check dates
    # Calculate release datetime
    current_period = STAKEHOLDER.staking_agent.get_current_period()
    bonded_date = datetime_at_period(
        period=current_period, seconds_per_period=economics.seconds_per_period)
    min_worker_periods = STAKEHOLDER.economics.minimum_worker_periods

    release_period = current_period + min_worker_periods
    release_date = datetime_at_period(
        period=release_period,
        seconds_per_period=economics.seconds_per_period,
        start_of_period=True)

    if not force:
        click.confirm(
            f"Commit to bonding "
            f"worker {worker_address} to staker {staking_address} "
            f"for a minimum of {STAKEHOLDER.economics.minimum_worker_periods} periods?",
            abort=True)

    STAKEHOLDER.assimilate(checksum_address=client_account, password=password)
    receipt = STAKEHOLDER.set_worker(worker_address=worker_address)

    # Report Success
    emitter.echo(
        f"\nWorker {worker_address} successfully bonded to staker {staking_address}",
        color='green')
    paint_receipt_summary(emitter=emitter,
                          receipt=receipt,
                          chain_name=blockchain.client.chain_name,
                          transaction_type='set_worker')
    emitter.echo(f"Bonded at period #{current_period} ({bonded_date})",
                 color='green')
    emitter.echo(
        f"This worker can be replaced or detached after period "
        f"#{release_period} ({release_date})",
        color='green')
Example #21
0
def stake(click_config,
          action,

          config_root,
          config_file,

          # Mode
          force,
          offline,
          hw_wallet,

          # Blockchain
          poa,
          registry_filepath,
          provider_uri,
          sync,

          # Stake
          staking_address,
          worker_address,
          withdraw_address,
          value,
          duration,
          index,
          policy_reward,
          staking_reward,

          ) -> None:
    """
    Manage stakes and other staker-related operations.

    \b
    Actions
    -------------------------------------------------
    new-stakeholder  Create a new stakeholder configuration
    list             List active stakes for current stakeholder
    accounts         Show ETH and NU balances for stakeholder's accounts
    sync             Synchronize stake data with on-chain information
    set-worker       Bond a worker to a staker
    detach-worker    Detach worker currently bonded to a staker
    init             Create a new stake
    divide           Create a new stake from part of an existing one
    collect-reward   Withdraw staking reward

    """

    # Banner
    emitter = click_config.emitter
    emitter.clear()
    emitter.banner(NU_BANNER)

    if action == 'new-stakeholder':

        if not provider_uri:
            raise click.BadOptionUsage(option_name='--provider',
                                       message="--provider is required to create a new stakeholder.")

        fetch_registry = registry_filepath is None and not click_config.no_registry
        registry = None
        if registry_filepath:
            registry = EthereumContractRegistry(registry_filepath=registry_filepath)
        blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa)

        blockchain.connect(fetch_registry=fetch_registry, sync_now=sync, emitter=emitter)

        new_stakeholder = StakeHolder(config_root=config_root,
                                      offline_mode=offline,
                                      blockchain=blockchain)

        filepath = new_stakeholder.to_configuration_file(override=force)
        emitter.echo(f"Wrote new stakeholder configuration to {filepath}", color='green')
        return  # Exit

    #
    # Make Stakeholder
    #

    STAKEHOLDER = StakeHolder.from_configuration_file(filepath=config_file,
                                                      provider_uri=provider_uri,
                                                      registry_filepath=registry_filepath,
                                                      offline=offline,
                                                      sync_now=sync)
    #
    # Eager Actions
    #

    if action == 'list':
        if not STAKEHOLDER.stakes:
            emitter.echo(f"There are no active stakes")
        else:
            painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes)
        return

    elif action == 'accounts':
        for address, balances in STAKEHOLDER.account_balances.items():
            emitter.echo(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}")
        return  # Exit

    elif action == 'sync':
        emitter.echo("Reading on-chain stake data...")
        STAKEHOLDER.read_onchain_stakes()
        STAKEHOLDER.to_configuration_file(override=True)
        emitter.echo("OK!", color='green')
        return  # Exit

    elif action == 'set-worker':

        if not staking_address:
            staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).owner_address

        if not worker_address:
            worker_address = click.prompt("Enter worker address", type=EIP55_CHECKSUM_ADDRESS)

        # TODO: Check preconditions (e.g., minWorkerPeriods, already in use, etc)

        password = None
        if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local:
            password = get_client_password(checksum_address=staking_address)
        receipt = STAKEHOLDER.set_worker(staker_address=staking_address,
                                         password=password,
                                         worker_address=worker_address)

        # TODO: Double-check dates
        current_period = STAKEHOLDER.staking_agent.get_current_period()
        bonded_date = datetime_at_period(period=current_period)
        min_worker_periods = STAKEHOLDER.staking_agent.staking_parameters()[7]
        release_period = current_period + min_worker_periods
        release_date = datetime_at_period(period=release_period)

        emitter.echo(f"\nWorker {worker_address} successfully bonded to staker {staking_address}", color='green')
        paint_receipt_summary(emitter=emitter,
                              receipt=receipt,
                              chain_name=STAKEHOLDER.blockchain.client.chain_name,
                              transaction_type='set_worker')
        emitter.echo(f"Bonded at period #{current_period} ({bonded_date})", color='green')
        emitter.echo(f"This worker can be replaced or detached after period "
                     f"#{release_period} ({release_date})", color='green')
        return  # Exit

    elif action == 'detach-worker':

        if not staking_address:
            staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).owner_address

        if worker_address:
            raise click.BadOptionUsage(message="detach-worker cannot be used together with --worker-address")

        # TODO: Check preconditions (e.g., minWorkerPeriods)

        worker_address = STAKEHOLDER.staking_agent.get_worker_from_staker(staking_address)

        password = None
        if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local:
            password = get_client_password(checksum_address=staking_address)

        # TODO: Create Stakeholder.detach_worker() and use it here
        receipt = STAKEHOLDER.set_worker(staker_address=staking_address,
                                         password=password,
                                         worker_address=BlockchainInterface.NULL_ADDRESS)

        # TODO: Double-check dates
        current_period = STAKEHOLDER.staking_agent.get_current_period()
        bonded_date = datetime_at_period(period=current_period)

        emitter.echo(f"Successfully detached worker {worker_address} from staker {staking_address}", color='green')
        paint_receipt_summary(emitter=emitter,
                              receipt=receipt,
                              chain_name=STAKEHOLDER.blockchain.client.chain_name,
                              transaction_type='detach_worker')
        emitter.echo(f"Detached at period #{current_period} ({bonded_date})", color='green')
        return  # Exit

    elif action == 'init':
        """Initialize a new stake"""

        #
        # Get Staking Account
        #

        password = None
        if not staking_address:
            staking_address = select_client_account(blockchain=STAKEHOLDER.blockchain,
                                                    prompt="Select staking account",
                                                    emitter=emitter)

        if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local:
            password = click.prompt(f"Enter password to unlock {staking_address}",
                                    hide_input=True,
                                    confirmation_prompt=False)
        #
        # Stage Stake
        #

        if not value:
            min_locked = STAKEHOLDER.economics.minimum_allowed_locked
            value = click.prompt(f"Enter stake value in NU", type=STAKE_VALUE, default=NU.from_nunits(min_locked).to_tokens())
        value = NU.from_tokens(value)

        if not duration:
            prompt = f"Enter stake duration ({STAKEHOLDER.economics.minimum_locked_periods} periods minimum)"
            duration = click.prompt(prompt, type=STAKE_DURATION)

        start_period = STAKEHOLDER.staking_agent.get_current_period()
        end_period = start_period + duration

        #
        # Review
        #

        if not force:
            painting.paint_staged_stake(emitter=emitter,
                                        stakeholder=STAKEHOLDER,
                                        staking_address=staking_address,
                                        stake_value=value,
                                        duration=duration,
                                        start_period=start_period,
                                        end_period=end_period)

            confirm_staged_stake(staker_address=staking_address, value=value, duration=duration)

        # Last chance to bail
        click.confirm("Publish staged stake to the blockchain?", abort=True)

        # Execute
        new_stake = STAKEHOLDER.initialize_stake(amount=value,
                                                 duration=duration,
                                                 checksum_address=staking_address,
                                                 password=password)

        painting.paint_staking_confirmation(emitter=emitter,
                                            ursula=STAKEHOLDER,
                                            transactions=new_stake.transactions)
        return  # Exit

    elif action == 'divide':
        """Divide an existing stake by specifying the new target value and end period"""

        if staking_address and index is not None:
            staker = STAKEHOLDER.get_active_staker(address=staking_address)
            current_stake = staker.stakes[index]
        else:
            current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter)

        #
        # Stage Stake
        #

        # Value
        if not value:
            value = click.prompt(f"Enter target value (must be less than or equal to {str(current_stake.value)})",
                                 type=STAKE_VALUE)
        value = NU(value, 'NU')

        # Duration
        if not duration:
            extension = click.prompt("Enter number of periods to extend", type=STAKE_EXTENSION)
        else:
            extension = duration

        if not force:
            painting.paint_staged_stake_division(emitter=emitter,
                                                 stakeholder=STAKEHOLDER,
                                                 original_stake=current_stake,
                                                 target_value=value,
                                                 extension=extension)
            click.confirm("Is this correct?", abort=True)

        # Execute
        password = None
        if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local:
            password = get_client_password(checksum_address=current_stake.owner_address)
        modified_stake, new_stake = STAKEHOLDER.divide_stake(address=current_stake.owner_address,
                                                             index=current_stake.index,
                                                             value=value,
                                                             duration=extension,
                                                             password=password)
        emitter.echo('Successfully divided stake', color='green', verbosity=1)
        paint_receipt_summary(emitter=emitter,
                              receipt=new_stake.receipt,
                              chain_name=STAKEHOLDER.blockchain.client.chain_name)

        # Show the resulting stake list
        painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes)
        return  # Exit

    elif action == 'collect-reward':
        """Withdraw staking reward to the specified wallet address"""
        password = None
        if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local:
            password = get_client_password(checksum_address=staking_address)
        STAKEHOLDER.collect_rewards(staker_address=staking_address,
                                    withdraw_address=withdraw_address,
                                    password=password,
                                    staking=staking_reward,
                                    policy=policy_reward)

    else:
        ctx = click.get_current_context()
        click.UsageError(message=f"Unknown action '{action}'.", ctx=ctx).show()
    return  # Exit