mirror of https://github.com/nucypher/nucypher.git
Relocate staking methods to the Stake class, away from Actors; Staking API bug fixes and consistency check.
parent
07aa603d30
commit
13d4b5c448
|
@ -16,28 +16,38 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from json import JSONDecodeError
|
||||
from typing import Tuple, List, Dict, Union
|
||||
|
||||
import maya
|
||||
from constant_sorrow import constants
|
||||
from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_ADDRESS
|
||||
from datetime import datetime
|
||||
from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_ADDRESS, EMPTY_STAKING_SLOT
|
||||
from twisted.internet import task, reactor
|
||||
from twisted.logger import Logger
|
||||
from typing import Tuple, List, Dict, Union
|
||||
|
||||
from nucypher.blockchain.economics import TokenEconomics
|
||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, PolicyAgent, MiningAdjudicatorAgent, \
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
NucypherTokenAgent,
|
||||
MinerAgent,
|
||||
PolicyAgent,
|
||||
MiningAdjudicatorAgent,
|
||||
EthereumContractAgent
|
||||
)
|
||||
from nucypher.blockchain.eth.chains import Blockchain
|
||||
from nucypher.blockchain.eth.deployers import NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer, \
|
||||
UserEscrowProxyDeployer, UserEscrowDeployer, MiningAdjudicatorDeployer
|
||||
from nucypher.blockchain.eth.deployers import (
|
||||
NucypherTokenDeployer,
|
||||
MinerEscrowDeployer,
|
||||
PolicyManagerDeployer,
|
||||
UserEscrowProxyDeployer,
|
||||
UserEscrowDeployer,
|
||||
MiningAdjudicatorDeployer
|
||||
)
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
|
||||
from nucypher.blockchain.eth.registry import AllocationRegistry
|
||||
from nucypher.blockchain.eth.token import NU, Stake
|
||||
from nucypher.blockchain.eth.utils import (datetime_to_period,
|
||||
calculate_period_duration)
|
||||
from nucypher.blockchain.eth.token import NU, Stake
|
||||
|
||||
|
||||
def only_me(func):
|
||||
|
@ -56,10 +66,7 @@ class NucypherTokenActor:
|
|||
class ActorError(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self,
|
||||
checksum_address: str = None,
|
||||
blockchain: Blockchain = None
|
||||
) -> None:
|
||||
def __init__(self, checksum_address: str = None, blockchain: Blockchain = None):
|
||||
"""
|
||||
:param checksum_address: If not passed, we assume this is an unknown actor
|
||||
"""
|
||||
|
@ -322,6 +329,7 @@ class Miner(NucypherTokenActor):
|
|||
self.__uptime_period = constants.NO_STAKES
|
||||
self.__terminal_period = constants.NO_STAKES
|
||||
|
||||
# Read on-chain stakes
|
||||
self.__read_stakes()
|
||||
if self.stakes and start_staking_loop:
|
||||
self.stake()
|
||||
|
@ -329,11 +337,14 @@ class Miner(NucypherTokenActor):
|
|||
#
|
||||
# Staking
|
||||
#
|
||||
|
||||
@only_me
|
||||
def stake(self, confirm_now: bool = True) -> None:
|
||||
"""High-level staking looping call initialization"""
|
||||
# TODO #841: Check if there is an active stake in the current period: Resume staking daemon
|
||||
|
||||
"""
|
||||
High-level staking looping call initialization, this function aims
|
||||
to be safely called at any time - For example, it is okay to call
|
||||
this function multiple time within the same period.
|
||||
"""
|
||||
# Get the last stake end period of all stakes
|
||||
terminal_period = max(stake.end_period for stake in self.stakes.values())
|
||||
|
||||
|
@ -343,10 +354,15 @@ class Miner(NucypherTokenActor):
|
|||
# record start time and periods
|
||||
self.__start_time = maya.now()
|
||||
self.__uptime_period = self.miner_agent.get_current_period()
|
||||
self.__terminal_period = self.__uptime_period + terminal_period
|
||||
self.__terminal_period = terminal_period
|
||||
self.__current_period = self.__uptime_period
|
||||
self.start_staking_loop()
|
||||
|
||||
@property
|
||||
def last_active_period(self) -> int:
|
||||
period = self.miner_agent.get_last_active_period(address=self.checksum_public_address)
|
||||
return period
|
||||
|
||||
@only_me
|
||||
def _confirm_period(self):
|
||||
|
||||
|
@ -355,10 +371,16 @@ class Miner(NucypherTokenActor):
|
|||
|
||||
if self.__current_period != period:
|
||||
|
||||
# check for stake expiration
|
||||
# Let's see how much time has passed
|
||||
delta = self.__current_period - self.last_active_period
|
||||
if delta > 1:
|
||||
self.log.warn(f"{delta} Missed staking confirmation(s) detected")
|
||||
self.__read_stakes() # Invalidate the stake cache
|
||||
|
||||
# Check for stake expiration
|
||||
stake_expired = self.__current_period >= self.__terminal_period
|
||||
if stake_expired:
|
||||
self.log.info('Stake duration expired')
|
||||
self.log.info('STOPPED STAKING - Final stake ended.')
|
||||
return True
|
||||
|
||||
self.confirm_activity()
|
||||
|
@ -394,14 +416,14 @@ class Miner(NucypherTokenActor):
|
|||
return d
|
||||
|
||||
@property
|
||||
def is_staking(self):
|
||||
def is_staking(self) -> bool:
|
||||
"""Checks if this Miner currently has locked tokens."""
|
||||
return bool(self.locked_tokens > 0)
|
||||
|
||||
@property
|
||||
def locked_tokens(self):
|
||||
def locked_tokens(self) -> NU:
|
||||
"""Returns the amount of tokens this miner has locked."""
|
||||
return self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address)
|
||||
return NU(self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address), 'NU')
|
||||
|
||||
@property
|
||||
def total_staked(self) -> NU:
|
||||
|
@ -410,15 +432,93 @@ class Miner(NucypherTokenActor):
|
|||
else:
|
||||
return NU.ZERO()
|
||||
|
||||
@only_me
|
||||
def divide_stake(self,
|
||||
stake_index: int,
|
||||
target_value: NU,
|
||||
additional_periods: int = None,
|
||||
expiration: maya.MayaDT = None) -> tuple:
|
||||
|
||||
# Validate function input
|
||||
if additional_periods and expiration:
|
||||
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
||||
|
||||
# Select stake to divide
|
||||
try:
|
||||
current_stake = self.stakes[stake_index]
|
||||
except KeyError:
|
||||
if len(self.stakes):
|
||||
message = f"Cannot divide stake - No stake exists with index {stake_index}."
|
||||
else:
|
||||
message = "Cannot divide stake - There are no active stakes."
|
||||
raise Stake.StakingError(message)
|
||||
|
||||
# Calculate stake duration in periods
|
||||
if expiration:
|
||||
additional_periods = datetime_to_period(datetime=expiration) - current_stake.end_period
|
||||
if additional_periods <= 0:
|
||||
raise Stake.StakingError("Expiration {} must be at least 1 period from now.".format(expiration))
|
||||
|
||||
# Check the new stake will not exceed the staking limit
|
||||
if (self.total_staked + target_value) > self.economics.maximum_allowed_locked:
|
||||
raise Stake.StakingError(f"Cannot divide stake - Maximum stake value exceeded with a target value of {target_value}.")
|
||||
|
||||
# Do it already!
|
||||
modified_stake, new_stake = current_stake.divide(target_value=target_value,
|
||||
additional_periods=additional_periods)
|
||||
|
||||
# Update stake cache
|
||||
self.__read_stakes()
|
||||
|
||||
return modified_stake, new_stake
|
||||
|
||||
@only_me
|
||||
def initialize_stake(self,
|
||||
amount: NU,
|
||||
lock_periods: int = None,
|
||||
expiration: maya.MayaDT = None,
|
||||
entire_balance: bool = False) -> Stake:
|
||||
|
||||
if lock_periods and expiration:
|
||||
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
||||
if expiration:
|
||||
lock_periods = calculate_period_duration(future_time=expiration)
|
||||
|
||||
if entire_balance and amount:
|
||||
raise ValueError("Specify an amount or entire balance, not both")
|
||||
if entire_balance:
|
||||
amount = self.token_balance
|
||||
if not self.token_balance >= amount:
|
||||
raise self.MinerError(f"Insufficient token balance ({self.token_agent}) for new stake initialization of {amount}")
|
||||
|
||||
# Write to blockchain
|
||||
stake = Stake.initialize_stake(miner=self, amount=amount, lock_periods=lock_periods)
|
||||
|
||||
# Update local on-chain stake cache
|
||||
self.__read_stakes()
|
||||
return stake
|
||||
|
||||
#
|
||||
# Staking Cache
|
||||
#
|
||||
|
||||
def __read_stakes(self) -> None:
|
||||
|
||||
stakes_reader = self.miner_agent.get_all_stakes(miner_address=self.checksum_public_address)
|
||||
|
||||
stakes = dict()
|
||||
terminal_period = 0
|
||||
for index, stake_info in enumerate(stakes_reader):
|
||||
stake = Stake.from_stake_info(owner_address=self.checksum_public_address,
|
||||
stake_info=stake_info,
|
||||
index=index,
|
||||
economics=self.economics)
|
||||
if not stake_info:
|
||||
stake = EMPTY_STAKING_SLOT
|
||||
else:
|
||||
stake = Stake.from_stake_info(miner=self, stake_info=stake_info, index=index)
|
||||
if stake.end_period > terminal_period:
|
||||
terminal_period = stake.end_period
|
||||
|
||||
stakes[index] = stake
|
||||
|
||||
self.__terminal_period = terminal_period
|
||||
self.__stakes = stakes
|
||||
|
||||
@property
|
||||
|
@ -426,160 +526,8 @@ class Miner(NucypherTokenActor):
|
|||
"""Return all cached stakes from the blockchain."""
|
||||
return self.__stakes
|
||||
|
||||
@only_me
|
||||
def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]:
|
||||
"""Public facing method for token locking."""
|
||||
|
||||
approve_txhash = self.token_agent.approve_transfer(amount=amount,
|
||||
target_address=self.miner_agent.contract_address,
|
||||
sender_address=self.checksum_public_address)
|
||||
|
||||
deposit_txhash = self.miner_agent.deposit_tokens(amount=amount,
|
||||
lock_periods=lock_periods,
|
||||
sender_address=self.checksum_public_address)
|
||||
|
||||
return approve_txhash, deposit_txhash
|
||||
|
||||
@only_me
|
||||
def divide_stake(self,
|
||||
stake_index: int,
|
||||
target_value: NU,
|
||||
additional_periods: int = None,
|
||||
expiration: maya.MayaDT = None) -> dict:
|
||||
"""
|
||||
Modifies the unlocking schedule and value of already locked tokens.
|
||||
|
||||
This actor requires that is_me is True, and that the expiration datetime is after the existing
|
||||
locking schedule of this miner, or an exception will be raised.
|
||||
|
||||
:param stake_index: The miner's stake index of the stake to divide
|
||||
:param additional_periods: The number of periods to extend the stake by
|
||||
:param target_value: The quantity of tokens in the smallest denomination to divide.
|
||||
:param expiration: The new expiration date to set as an end period for stake division.
|
||||
:return: Returns the blockchain transaction hash
|
||||
|
||||
"""
|
||||
|
||||
# Validate function input
|
||||
if additional_periods and expiration:
|
||||
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
||||
|
||||
# Re-read stakes from blockchain and select stake to divide
|
||||
self.__read_stakes()
|
||||
current_stake = self.stakes[stake_index]
|
||||
|
||||
# Ensure selected stake is active
|
||||
if current_stake.is_expired:
|
||||
raise self.MinerError(f'Cannot dive an expired stake')
|
||||
|
||||
# Validate stake division parameters
|
||||
if expiration:
|
||||
additional_periods = datetime_to_period(datetime=expiration) - current_stake.end_period
|
||||
if additional_periods <= 0:
|
||||
raise self.MinerError("Expiration {} must be at least 1 period from now.".format(expiration))
|
||||
|
||||
if target_value >= current_stake.value:
|
||||
raise self.MinerError(f"Cannot divide stake; Value ({target_value}) must be less "
|
||||
f"than the existing stake value {current_stake.value}.")
|
||||
|
||||
#
|
||||
# Generate Stakes
|
||||
#
|
||||
|
||||
# Modified Original Stake
|
||||
new_stake_1 = Stake(owner_address=self.checksum_public_address,
|
||||
index=len(self.stakes)+1,
|
||||
start_period=current_stake.start_period,
|
||||
end_period=current_stake.end_period,
|
||||
value=target_value,
|
||||
economics=self.economics)
|
||||
|
||||
# New Derived Stake
|
||||
new_stake_2 = Stake(owner_address=self.checksum_public_address,
|
||||
index=len(self.stakes)+1,
|
||||
start_period=current_stake.start_period,
|
||||
end_period=current_stake.end_period + additional_periods,
|
||||
value=current_stake.value - target_value,
|
||||
economics=self.economics)
|
||||
|
||||
# Ensure both halves are for valid amounts
|
||||
new_stake_1.validate_value()
|
||||
new_stake_2.validate_value()
|
||||
|
||||
# Transmit the stake division transaction
|
||||
tx = self.miner_agent.divide_stake(miner_address=self.checksum_public_address,
|
||||
stake_index=stake_index,
|
||||
target_value=int(target_value),
|
||||
periods=additional_periods)
|
||||
self.blockchain.wait_for_receipt(tx)
|
||||
|
||||
# Update stake cache
|
||||
self.__read_stakes()
|
||||
|
||||
return tx
|
||||
|
||||
@only_me
|
||||
def __validate_stake(self, stake: Stake) -> bool:
|
||||
|
||||
stake.validate_value()
|
||||
stake.validate_duration()
|
||||
|
||||
if not self.token_balance >= stake.value:
|
||||
raise self.MinerError(f"Insufficient token balance ({self.token_agent}) for new stake of {stake.value}")
|
||||
else:
|
||||
return True
|
||||
|
||||
@only_me
|
||||
def initialize_stake(self,
|
||||
amount: NU,
|
||||
lock_periods: int = None,
|
||||
expiration: maya.MayaDT = None,
|
||||
entire_balance: bool = False) -> dict:
|
||||
"""
|
||||
High level staking method for Miners.
|
||||
|
||||
:param amount: Amount of tokens to stake denominated in the smallest unit.
|
||||
:param lock_periods: Duration of stake in periods.
|
||||
:param expiration: A MayaDT object representing the time the stake expires; used to calculate lock_periods.
|
||||
:param entire_balance: If True, stake the entire balance of this node, or the maximum possible.
|
||||
|
||||
"""
|
||||
|
||||
if lock_periods and expiration:
|
||||
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
|
||||
if entire_balance and amount:
|
||||
raise self.MinerError("Specify an amount or entire balance, not both")
|
||||
|
||||
if expiration:
|
||||
lock_periods = calculate_period_duration(future_time=expiration)
|
||||
|
||||
if entire_balance is True:
|
||||
amount = self.token_balance
|
||||
|
||||
amount = NU(int(amount), 'NuNit')
|
||||
|
||||
current_period = self.miner_agent.get_current_period()
|
||||
stake = Stake(owner_address=self.checksum_public_address,
|
||||
start_period=current_period+1,
|
||||
end_period=current_period + lock_periods,
|
||||
value=amount,
|
||||
economics=self.economics)
|
||||
|
||||
staking_transactions = OrderedDict() # type: OrderedDict # Time series of txhases
|
||||
|
||||
# Validate
|
||||
assert self.__validate_stake(stake=stake)
|
||||
|
||||
# Transact
|
||||
approve_txhash, initial_deposit_txhash = self.deposit(amount=int(amount), lock_periods=lock_periods)
|
||||
self._transaction_cache.append((datetime.utcnow(), initial_deposit_txhash))
|
||||
|
||||
staking_transactions['approve'] = approve_txhash
|
||||
staking_transactions['deposit'] = initial_deposit_txhash
|
||||
self.__read_stakes() # update local on-chain stake cache
|
||||
|
||||
self.log.info("{} Initialized new stake: {} tokens for {} periods".format(self.checksum_public_address, amount, lock_periods))
|
||||
return staking_transactions
|
||||
def refresh_staking_cache(self):
|
||||
return self.__read_stakes()
|
||||
|
||||
#
|
||||
# Reward and Collection
|
||||
|
|
|
@ -174,6 +174,10 @@ class MinerAgent(EthereumContractAgent):
|
|||
self.blockchain.wait_for_receipt(tx)
|
||||
return tx
|
||||
|
||||
def get_last_active_period(self, address: str) -> int:
|
||||
period = self.contract.functions.getLastActivePeriod(address).call()
|
||||
return int(period)
|
||||
|
||||
def confirm_activity(self, node_address: str) -> str:
|
||||
"""Miner rewarded for every confirmed period"""
|
||||
|
||||
|
@ -496,4 +500,4 @@ class MiningAdjudicatorAgent(EthereumContractAgent):
|
|||
:param precomputed_data:
|
||||
:return:
|
||||
"""
|
||||
# TODO: #931 - Challenge Agent and Actor
|
||||
# TODO: #931 - Challenge Agent and Actor - "Investigator"
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
from _pydecimal import Decimal
|
||||
|
||||
import eth_utils
|
||||
import maya
|
||||
from eth_utils import currency
|
||||
from nacl.hash import sha256
|
||||
from typing import Union, Tuple
|
||||
|
||||
from nucypher.blockchain.economics import TokenEconomics
|
||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent
|
||||
import maya
|
||||
from constant_sorrow.constants import NEW_STAKE, NO_STAKING_RECEIPT
|
||||
from eth_utils import currency
|
||||
from twisted.logger import Logger
|
||||
|
||||
from nucypher.blockchain.eth.agents import MinerAgent
|
||||
from nucypher.blockchain.eth.utils import datetime_at_period, datetime_to_period
|
||||
|
||||
|
||||
|
@ -132,17 +131,23 @@ class Stake:
|
|||
A quantity of tokens and staking duration for one stake for one miner.
|
||||
"""
|
||||
|
||||
class StakingError(Exception):
|
||||
"""Raised when a staking operation cannot be executed due to failure."""
|
||||
|
||||
__ID_LENGTH = 16
|
||||
|
||||
def __init__(self,
|
||||
owner_address: str,
|
||||
miner,
|
||||
value: NU,
|
||||
start_period: int,
|
||||
end_period: int,
|
||||
index: int = None,
|
||||
economics: TokenEconomics = None,
|
||||
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
|
||||
|
@ -156,16 +161,23 @@ class Stake:
|
|||
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
|
||||
if not economics:
|
||||
economics = TokenEconomics()
|
||||
self.economics = economics
|
||||
self.minimum_nu = NU(int(economics.minimum_allowed_locked), 'NuNit')
|
||||
self.maximum_nu = NU(int(economics.maximum_allowed_locked), 'NuNit')
|
||||
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
|
||||
|
||||
def __repr__(self) -> str:
|
||||
r = f'Stake(index={self.index}, value={self.value}, end_period={self.end_period})'
|
||||
return r
|
||||
|
@ -173,55 +185,46 @@ class Stake:
|
|||
def __eq__(self, other) -> bool:
|
||||
return bool(self.value == other.value)
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
now = maya.now()
|
||||
if now >= self.end_datetime:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
#
|
||||
# Metadata
|
||||
#
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
now = maya.now()
|
||||
if now >= self.end_datetime:
|
||||
def is_expired(self) -> bool:
|
||||
current_period = self.miner_agent.get_current_period()
|
||||
if current_period >= self.end_period:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return not self.is_expired
|
||||
|
||||
@classmethod
|
||||
def from_stake_info(cls,
|
||||
owner_address: str,
|
||||
miner,
|
||||
index: int,
|
||||
stake_info: Tuple[int, int, int],
|
||||
economics: TokenEconomics) -> 'Stake':
|
||||
stake_info: Tuple[int, int, int]
|
||||
) -> 'Stake':
|
||||
|
||||
"""Reads staking values as they exist on the blockchain"""
|
||||
start_period, end_period, value = stake_info
|
||||
instance = cls(owner_address=owner_address,
|
||||
|
||||
instance = cls(miner=miner,
|
||||
index=index,
|
||||
start_period=start_period,
|
||||
end_period=end_period,
|
||||
value=NU(value, 'NuNit'),
|
||||
economics=economics)
|
||||
value=NU(value, 'NuNit'))
|
||||
return instance
|
||||
|
||||
def to_stake_info(self) -> Tuple[int, int, int]:
|
||||
"""Returns a tuple representing the blockchain record of a stake"""
|
||||
return self.start_period, self.end_period, int(self.value)
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""TODO: Unique staking ID, currently unused"""
|
||||
digest_elements = list()
|
||||
digest_elements.append(eth_utils.to_canonical_address(address=self.owner_address))
|
||||
digest_elements.append(str(self.index).encode())
|
||||
digest_elements.append(str(self.start_period).encode())
|
||||
digest_elements.append(str(self.end_period).encode())
|
||||
digest_elements.append(str(self.value).encode())
|
||||
digest = b'|'.join(digest_elements)
|
||||
stake_id = sha256(digest).hex()[:16] # type: str
|
||||
return stake_id[:self.__ID_LENGTH]
|
||||
#
|
||||
# Duration
|
||||
#
|
||||
|
||||
@property
|
||||
def periods_remaining(self) -> int:
|
||||
|
@ -240,6 +243,10 @@ class Stake:
|
|||
result = delta.seconds
|
||||
return result
|
||||
|
||||
#
|
||||
# Validation
|
||||
#
|
||||
|
||||
@staticmethod
|
||||
def __handle_validation_failure(rulebook: Tuple[Tuple[bool, str], ...]) -> bool:
|
||||
"""Validate a staking rulebook"""
|
||||
|
@ -275,14 +282,134 @@ class Stake:
|
|||
rulebook = (
|
||||
|
||||
(self.economics.minimum_locked_periods <= self.duration,
|
||||
'Locktime ({duration}) too short; must be at least {minimum}'
|
||||
'Stake duration of ({duration}) is too short; must be at least {minimum} periods.'
|
||||
.format(minimum=self.economics.minimum_locked_periods, duration=self.duration)),
|
||||
|
||||
(self.economics.maximum_locked_periods >= self.duration,
|
||||
'Locktime ({duration}) too long; must be no more than {maximum}'
|
||||
'Stake duration of ({duration}) is too long; must be no more than {maximum} periods.'
|
||||
.format(maximum=self.economics.maximum_locked_periods, duration=self.duration)),
|
||||
)
|
||||
|
||||
if raise_on_fail is True:
|
||||
self.__handle_validation_failure(rulebook=rulebook)
|
||||
return all(rulebook)
|
||||
|
||||
#
|
||||
# Blockchain
|
||||
#
|
||||
|
||||
@classmethod
|
||||
def __deposit(cls, miner, amount: int, lock_periods: int) -> Tuple[str, str]:
|
||||
"""Public facing method for token locking."""
|
||||
|
||||
approve_txhash = miner.token_agent.approve_transfer(amount=amount,
|
||||
target_address=miner.miner_agent.contract_address,
|
||||
sender_address=miner.checksum_public_address)
|
||||
|
||||
deposit_txhash = miner.miner_agent.deposit_tokens(amount=amount,
|
||||
lock_periods=lock_periods,
|
||||
sender_address=miner.checksum_public_address)
|
||||
|
||||
return approve_txhash, deposit_txhash
|
||||
|
||||
def divide(self, target_value: NU, additional_periods: int = None) -> tuple:
|
||||
"""
|
||||
Modifies the unlocking schedule and value of already locked tokens.
|
||||
|
||||
This actor requires that is_me is True, and that the expiration datetime is after the existing
|
||||
locking schedule of this miner, or an exception will be raised.
|
||||
"""
|
||||
|
||||
# Re-read stakes from blockchain and select stake to divide
|
||||
current_stake = self
|
||||
|
||||
# Ensure selected stake is active
|
||||
if current_stake.is_expired:
|
||||
raise self.StakingError(f'Cannot divide an expired stake')
|
||||
|
||||
if target_value >= current_stake.value:
|
||||
raise self.StakingError(f"Cannot divide stake; Target value ({target_value}) must be less "
|
||||
f"than the existing stake value {current_stake.value}.")
|
||||
|
||||
#
|
||||
# Generate SubStakes
|
||||
#
|
||||
|
||||
# Modified Original Stake
|
||||
modified_stake = Stake(miner=self.miner,
|
||||
index=self.index,
|
||||
start_period=current_stake.start_period,
|
||||
end_period=current_stake.end_period,
|
||||
value=target_value)
|
||||
|
||||
# New Derived Stake
|
||||
end_period = current_stake.end_period + additional_periods
|
||||
new_stake = Stake(miner=self.miner,
|
||||
start_period=current_stake.start_period,
|
||||
end_period=end_period,
|
||||
value=current_stake.value - target_value,
|
||||
index=NEW_STAKE)
|
||||
|
||||
#
|
||||
# Validate
|
||||
#
|
||||
|
||||
# Ensure both halves are for valid amounts
|
||||
modified_stake.validate_value()
|
||||
new_stake.validate_value()
|
||||
|
||||
# Detect recycled or inconsistent slot then check start period or fail
|
||||
stake_info = self.miner_agent.get_stake_info(miner_address=self.owner_address, stake_index=self.index)
|
||||
first_period, last_period, locked_value = stake_info
|
||||
if not modified_stake.start_period == first_period:
|
||||
raise self.StakingError("Inconsistent staking cache, aborting stake division. ")
|
||||
|
||||
#
|
||||
# Transact
|
||||
#
|
||||
|
||||
# Transmit the stake division transaction
|
||||
tx = self.miner_agent.divide_stake(miner_address=self.owner_address,
|
||||
stake_index=self.index,
|
||||
target_value=int(target_value),
|
||||
periods=additional_periods)
|
||||
receipt = self.blockchain.wait_for_receipt(tx)
|
||||
new_stake.receipt = receipt
|
||||
|
||||
return modified_stake, new_stake
|
||||
|
||||
@classmethod
|
||||
def initialize_stake(cls, miner, amount: NU, lock_periods: int = None) -> 'Stake':
|
||||
|
||||
# Value
|
||||
amount = NU(int(amount), 'NuNit')
|
||||
|
||||
# Duration
|
||||
current_period = miner.miner_agent.get_current_period()
|
||||
end_period = current_period + lock_periods
|
||||
|
||||
stake = Stake(miner=miner,
|
||||
start_period=current_period+1,
|
||||
end_period=end_period,
|
||||
value=amount,
|
||||
index=NEW_STAKE)
|
||||
|
||||
# Validate
|
||||
stake.validate_value()
|
||||
stake.validate_duration()
|
||||
|
||||
# Transact
|
||||
approve_txhash, initial_deposit_txhash = stake.__deposit(amount=int(amount),
|
||||
lock_periods=lock_periods,
|
||||
miner=miner)
|
||||
|
||||
# Store the staking transactions on the instance
|
||||
staking_transactions = dict(approve=approve_txhash, deposit=initial_deposit_txhash)
|
||||
stake.transactions = staking_transactions
|
||||
|
||||
# Log and return Stake instance
|
||||
log = Logger(f'stake-{miner.checksum_public_address}-creation')
|
||||
log.info("{} Initialized new stake: {} tokens for {} periods".format(miner.checksum_public_address,
|
||||
amount,
|
||||
lock_periods))
|
||||
return stake
|
||||
|
|
|
@ -364,13 +364,13 @@ def ursula(click_config,
|
|||
|
||||
click.confirm("Is this correct?", abort=True)
|
||||
|
||||
txhash_bytes = URSULA.divide_stake(stake_index=index,
|
||||
modified_stake, new_stake = URSULA.divide_stake(stake_index=index,
|
||||
target_value=value,
|
||||
additional_periods=extension)
|
||||
|
||||
if not quiet:
|
||||
click.secho('Successfully divided stake', fg='green')
|
||||
click.secho(f'Transaction Hash ........... {txhash_bytes.hex()}')
|
||||
click.secho(f'Transaction Hash ........... {new_stake.receipt}')
|
||||
|
||||
# Show the resulting stake list
|
||||
painting.paint_stakes(stakes=URSULA.stakes)
|
||||
|
@ -421,8 +421,8 @@ def ursula(click_config,
|
|||
if not force:
|
||||
click.confirm("Publish staged stake to the blockchain?", abort=True)
|
||||
|
||||
staking_transactions = URSULA.initialize_stake(amount=int(value), lock_periods=duration)
|
||||
painting.paint_staking_confirmation(ursula=URSULA, transactions=staking_transactions)
|
||||
stake = URSULA.initialize_stake(amount=int(value), lock_periods=duration)
|
||||
painting.paint_staking_confirmation(ursula=URSULA, transactions=stake.transactions)
|
||||
return
|
||||
|
||||
elif action == 'confirm-activity':
|
||||
|
|
|
@ -82,9 +82,8 @@ def test_miner_divides_stake(miner, token_economics):
|
|||
expected_yet_another_stake = Stake(start_period=current_period + 1,
|
||||
end_period=current_period + 34,
|
||||
value=yet_another_stake_value,
|
||||
owner_address=miner.checksum_public_address,
|
||||
index=3,
|
||||
economics=token_economics)
|
||||
miner=miner,
|
||||
index=3)
|
||||
|
||||
assert 4 == len(miner.stakes), 'A new stake was not added after two stake divisions'
|
||||
assert expected_old_stake == miner.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions'
|
||||
|
|
|
@ -3,6 +3,7 @@ from decimal import InvalidOperation, Decimal
|
|||
import pytest
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.blockchain.economics import TokenEconomics
|
||||
from nucypher.blockchain.eth.token import NU, Stake
|
||||
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
|
||||
|
||||
|
@ -98,21 +99,25 @@ def test_NU(token_economics):
|
|||
_nan = NU(float('NaN'), 'NU')
|
||||
|
||||
|
||||
def test_stake():
|
||||
def test_stake(testerchain, three_agents):
|
||||
|
||||
class FakeUrsula:
|
||||
token_agent, miner_agent, _policy_agent = three_agents
|
||||
|
||||
burner_wallet = Web3().eth.account.create(INSECURE_DEVELOPMENT_PASSWORD)
|
||||
checksum_public_address = burner_wallet.address
|
||||
miner_agent = None
|
||||
miner_agent = miner_agent
|
||||
token_agent = token_agent
|
||||
blockchain = testerchain
|
||||
economics = TokenEconomics()
|
||||
|
||||
ursula = FakeUrsula()
|
||||
stake = Stake(owner_address=ursula.checksum_public_address,
|
||||
stake = Stake(miner=ursula,
|
||||
start_period=1,
|
||||
end_period=100,
|
||||
value=NU(100, 'NU'),
|
||||
index=0)
|
||||
|
||||
assert len(stake.id) == 16
|
||||
assert stake.value, 'NU' == NU(100, 'NU')
|
||||
|
||||
assert isinstance(stake.time_remaining(), int) # seconds
|
||||
|
|
Loading…
Reference in New Issue