Merge pull request #830 from KPrasch/denominations

Base Denominations Management
pull/890/head
David Núñez 2019-03-19 20:42:19 +01:00 committed by GitHub
commit 3f94b93df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 693 additions and 310 deletions

View File

@ -20,12 +20,12 @@ to deploy a single-host network with the geth CLI.
(nucypher)$ nucypher-deploy contracts --provider-uri ipc:///tmp/geth.ipc --poa (nucypher)$ nucypher-deploy contracts --provider-uri ipc:///tmp/geth.ipc --poa
... ...
This will deploy the main NuCypher contracts: NuCypherToken, MinerEscrow, PolicyManager, This will deploy the main NuCypher contracts, namely ``NuCypherToken``, ``MinerEscrow``, and ``PolicyManager``,
along with their proxies, as well as executing initialization transactions. You will need to enter along with their proxies, as well as executing initialization transactions. You will need to enter
the contract's upgrade secrets, which can be any alphanumeric string. the contract's upgrade secrets, which can be any alphanumeric string.
A summary of deployed contract addresses, transactions, and gas usage will be displayed on success, and a A summary of deployed contract addresses, transactions, and gas usage will be displayed on success, and a
`contract_registry.json` file will be generated in your nucypher application directory. ``contract_registry.json`` file will be generated in your nucypher application directory.
.. code:: bash .. code:: bash

View File

@ -4,30 +4,30 @@ NuCypher Staking Guide
**Ursula Staking Actions** **Ursula Staking Actions**
+-------------------+-------------------------------------------------------------------------+ +-------------------+------------------------------------------------------------------------------+
| Action | Description | | Action | Description |
+===================+=========================================================================+ +===================+==============================================================================+
| `stake` | Initialize or view NuCpher stakes (used with `--value` and `--duration`)| | ``stake`` | Initialize or view NuCypher stakes (used with ``--value`` and ``--duration``)|
+-------------------+-------------------------------------------------------------------------+ +-------------------+------------------------------------------------------------------------------+
| `divide-stake` | Divide stake, used with `--value` and `--duration` | | ``collect-reward``| Collect staking reward (Policy reward collection in future PR) |
+-------------------+-------------------------------------------------------------------------+ +-------------------+------------------------------------------------------------------------------+
| `collect-reward` | Collect staking reward (Policy reward collection in future PR) |
+-------------------+-------------------------------------------------------------------------+
**Ursula Staking Options** **Ursula Staking Options**
+----------------+----------------------------------------+ +-----------------+--------------------------------------------+
| Option | Description | | Option | Description |
+================+========================================+ +=================+============================================+
| `--value` | Stake value | | ``--value`` | Stake value |
+----------------+----------------------------------------+ +-----------------+--------------------------------------------+
| `--duration` | Stake duration of extension | | ``--duration`` | Stake duration of extension |
+----------------+----------------------------------------+ +-----------------+--------------------------------------------+
| `--index` | Stake index | | ``--index`` | Stake index |
+----------------+----------------------------------------+ +-----------------+--------------------------------------------+
| `--list` | List stakes (used with `stake` action) | | ``--list`` | List stakes (used with ``stake`` action) |
+----------------+----------------------------------------+ +-----------------+--------------------------------------------+
| ``--divide`` | Divide stakes (used with ``stake`` action) |
+-----------------+--------------------------------------------+
@ -39,7 +39,7 @@ Interactive Method
(nucypher)$ nucypher ursula stake (nucypher)$ nucypher ursula stake
*Initial a new stake* *Initialize a new stake*
.. code:: bash .. code:: bash
@ -92,7 +92,7 @@ Interactive Method
.. code:: bash .. code:: bash
(nucypher)$ nucypher ursula divide-stake (nucypher)$ nucypher ursula stake --divide
| # | Duration | Enact | Expiration | Value | # | Duration | Enact | Expiration | Value
@ -100,7 +100,7 @@ Interactive Method
| 0 | 32 periods . | yesterday | 14 Apr ... | 30000 NU | 0 | 32 periods . | yesterday | 14 Apr ... | 30000 NU
Select a stake to divide: 0 Select a stake to divide: 0
Enter target value (must be less than 3000 NU): 1500 Enter target value (must be less than 30000 NU): 15000
Enter number of periods to extend: 30 Enter number of periods to extend: 30
============================== ORIGINAL STAKE ============================ ============================== ORIGINAL STAKE ============================
@ -133,17 +133,17 @@ Inline Method
+----------------+------------+--------------+ +----------------+------------+--------------+
*Stake 3000 NU for 90 Periods* *Stake 30000 NU for 90 Periods*
.. code:: bash .. code:: bash
(nucypher)$ nucypher ursula stake --value 3000 --duration 90 (nucypher)$ nucypher ursula stake --value 30000 --duration 90
... ...
*Divide stake at index 0, at 1500 NU for 30 additional Periods* *Divide stake at index 0, at 15000 NU for 30 additional Periods*
.. code:: bash .. code:: bash
(nucypher)$ nucypher ursula divide-stake --index 0 --value 1500 --duration 30 (nucypher)$ nucypher ursula stake --divide --index 0 --value 15000 --duration 30
... ...

View File

@ -24,19 +24,21 @@ If your installation in non-functional, be sure you have the latest version inst
2(a). Configure a new Ursula node 2(a). Configure a new Ursula node
*Decentralized Ursula Configuration* *Decentralized Ursula Configuration*
.. code:: bash .. code:: bash
(nucypher)$ nucypher ursula init --provider-uri <YOUR PROVIDER URI> --network <NETWORK NAME> (nucypher)$ nucypher ursula init --provider-uri <YOUR PROVIDER URI> --network <NETWORK NAME>
Replace ``<YOUR PROVIDER URI>`` with a valid node web3 node provider string, for example: Replace ``<YOUR PROVIDER URI>`` with a valid node web3 node provider string, for example:
- *ipc:///tmp/geth.ipc* - Geth Development Node (IPC; Also use --poa flag) - ``ipc:///tmp/geth.ipc`` - Geth Development Node (IPC; **Note**: Also use ``--poa`` flag)
- *http://localhost:7545* - Ganache TestRPC (HTTP-JSON-RPC) - ``http://localhost:7545`` - Ganache TestRPC (HTTP-JSON-RPC)
- *ws://0.0.0.0:8080* - Websocket Provider - ``ws://0.0.0.0:8080`` - Websocket Provider
2(b). Configure a new Ursula node 2(b). Configure a new Ursula node
*Federated Ursula Initialization* *Federated Ursula Initialization*
.. code:: bash .. code:: bash
(nucypher)$ nucypher ursula init --federated-only --network <NETWORK NAME> (nucypher)$ nucypher ursula init --federated-only --network <NETWORK NAME>

View File

@ -18,26 +18,26 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import json import json
from collections import OrderedDict from collections import OrderedDict
from json import JSONDecodeError from json import JSONDecodeError
from twisted.logger import Logger
import maya import maya
from constant_sorrow import constants from constant_sorrow import constants
from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_CONTRACT_AVAILABLE, NO_DEPLOYER_ADDRESS from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_ADDRESS
from datetime import datetime from datetime import datetime
from twisted.internet import task, reactor from twisted.internet import task, reactor
from twisted.logger import Logger
from typing import Tuple, List, Dict, Union from typing import Tuple, List, Dict, Union
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, PolicyAgent from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, PolicyAgent
from nucypher.blockchain.eth.chains import Blockchain from nucypher.blockchain.eth.chains import Blockchain
from nucypher.blockchain.eth.deployers import NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer, \ from nucypher.blockchain.eth.deployers import NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer, \
UserEscrowProxyDeployer, UserEscrowDeployer, DispatcherDeployer UserEscrowProxyDeployer, UserEscrowDeployer
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
from nucypher.blockchain.eth.registry import EthereumContractRegistry, AllocationRegistry from nucypher.blockchain.eth.registry import AllocationRegistry
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
from nucypher.blockchain.eth.utils import (datetime_to_period, from nucypher.blockchain.eth.utils import (datetime_to_period,
validate_stake_amount, validate_stake_amount,
validate_locktime, validate_locktime,
calculate_period_duration) calculate_period_duration)
from nucypher.blockchain.eth.token import NU, Stake
def only_me(func): def only_me(func):
@ -95,10 +95,11 @@ class NucypherTokenActor:
return balance return balance
@property @property
def token_balance(self): def token_balance(self) -> NU:
"""Return this actors's current token balance""" """Return this actors's current token balance"""
balance = self.token_agent.get_balance(address=self.checksum_public_address) balance = int(self.token_agent.get_balance(address=self.checksum_public_address))
return balance nu_balance = NU(balance, 'NuNit')
return nu_balance
class Deployer(NucypherTokenActor): class Deployer(NucypherTokenActor):
@ -292,23 +293,25 @@ class Miner(NucypherTokenActor):
self.miner_agent = MinerAgent(blockchain=self.blockchain) self.miner_agent = MinerAgent(blockchain=self.blockchain)
self.__stakes = constants.NO_STAKES
self.__start_time = constants.NO_STAKES
self.__uptime_period = constants.NO_STAKES
self.__terminal_period = constants.NO_STAKES
self.__read_stakes()
if self.stakes and start_staking_loop: if self.stakes and start_staking_loop:
self.stake() self.stake()
# #
# Staking # Staking
# #
@only_me @only_me
def stake(self, confirm_now=False) -> None: 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 daemon loop""" # Get the last stake end period of all stakes
terminal_period = max(stake.end_period for stake in self.stakes.values())
# TODO: Check if this period has already been confirmed
# TODO: Check if there is an active stake in the current period: Resume staking daemon
# TODO: Validation and Sanity checks
terminal_period = max(stake[1] for stake in self.stakes)
if confirm_now: if confirm_now:
self.confirm_activity() self.confirm_activity()
@ -320,10 +323,6 @@ class Miner(NucypherTokenActor):
self.__current_period = self.__uptime_period self.__current_period = self.__uptime_period
self.start_staking_loop() self.start_staking_loop()
#
# Daemon
#
@only_me @only_me
def _confirm_period(self): def _confirm_period(self):
@ -381,16 +380,26 @@ class Miner(NucypherTokenActor):
return self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address) return self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address)
@property @property
def total_staked(self) -> int: def total_staked(self) -> NU:
if self.stakes: if self.stakes:
return sum(stake[-1] for stake in self.stakes) return NU(sum(int(stake.value) for stake in self.stakes.values()), 'NuNit')
return 0 else:
return NU(0, 'NuNit')
def __read_stakes(self) -> None:
stakes_reader = self.miner_agent.get_all_stakes(miner_address=self.checksum_public_address)
stakes = dict()
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)
stakes[index] = stake
self.__stakes = stakes
@property @property
def stakes(self) -> Tuple[list]: def stakes(self) -> Dict[str, Stake]:
"""Read all live stake data from the blockchain and return it as a tuple""" """Return all cached stakes from the blockchain."""
stakes_reader = self.miner_agent.get_all_stakes(miner_address=self.checksum_public_address) return self.__stakes
return tuple(stakes_reader)
@only_me @only_me
def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]: def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]:
@ -409,7 +418,7 @@ class Miner(NucypherTokenActor):
@only_me @only_me
def divide_stake(self, def divide_stake(self,
stake_index: int, stake_index: int,
target_value: int, target_value: NU,
additional_periods: int = None, additional_periods: int = None,
expiration: maya.MayaDT = None) -> dict: expiration: maya.MayaDT = None) -> dict:
""" """
@ -418,8 +427,10 @@ class Miner(NucypherTokenActor):
This actor requires that is_me is True, and that the expiration datetime is after the existing 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. locking schedule of this miner, or an exception will be raised.
:param target_value: The quantity of tokens in the smallest denomination. :param stake_index: The miner's stake index of the stake to divide
:param expiration: The new expiration date to set. :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 :return: Returns the blockchain transaction hash
""" """
@ -427,34 +438,35 @@ class Miner(NucypherTokenActor):
if additional_periods and expiration: if additional_periods and expiration:
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.") raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
_first_period, last_period, locked_value = self.miner_agent.get_stake_info(miner_address=self.checksum_public_address, stake_index=stake_index) stake = self.__stakes[stake_index]
if expiration: if expiration:
additional_periods = datetime_to_period(datetime=expiration) - last_period additional_periods = datetime_to_period(datetime=expiration) - stake.end_period
if additional_periods <= 0:
raise self.MinerError("Expiration {} must be at least 1 period from now.".format(expiration))
if additional_periods <= 0: if target_value >= stake.value:
raise self.MinerError("Expiration {} must be at least 1 period from now.".format(expiration)) raise self.MinerError(f"Cannot divide stake; Value ({target_value}) must be less "
f"than the existing stake value {stake.value}.")
if target_value >= locked_value:
raise self.MinerError("Cannot divide stake; Value must be less than the specified stake value.")
# Ensure both halves are for valid amounts # Ensure both halves are for valid amounts
validate_stake_amount(amount=target_value) validate_stake_amount(amount=target_value)
validate_stake_amount(amount=locked_value - target_value) validate_stake_amount(amount=stake.value - target_value)
tx = self.miner_agent.divide_stake(miner_address=self.checksum_public_address, tx = self.miner_agent.divide_stake(miner_address=self.checksum_public_address,
stake_index=stake_index, stake_index=stake_index,
target_value=target_value, target_value=int(target_value),
periods=additional_periods) periods=additional_periods)
self.blockchain.wait_for_receipt(tx) self.blockchain.wait_for_receipt(tx)
self.__read_stakes() # update local on-chain stake cache
return tx return tx
@only_me @only_me
def __validate_stake(self, amount: int, lock_periods: int) -> bool: def __validate_stake(self, amount: NU, lock_periods: int) -> bool:
assert validate_stake_amount(amount=amount) # TODO: remove assertions..? validate_stake_amount(amount=amount)
assert validate_locktime(lock_periods=lock_periods) validate_locktime(lock_periods=lock_periods)
if not self.token_balance >= amount: if not self.token_balance >= amount:
raise self.MinerError("Insufficient miner token balance ({balance})".format(balance=self.token_balance)) raise self.MinerError("Insufficient miner token balance ({balance})".format(balance=self.token_balance))
@ -463,7 +475,7 @@ class Miner(NucypherTokenActor):
@only_me @only_me
def initialize_stake(self, def initialize_stake(self,
amount: int, amount: NU,
lock_periods: int = None, lock_periods: int = None,
expiration: maya.MayaDT = None, expiration: maya.MayaDT = None,
entire_balance: bool = False) -> dict: entire_balance: bool = False) -> dict:
@ -484,20 +496,24 @@ class Miner(NucypherTokenActor):
if expiration: if expiration:
lock_periods = calculate_period_duration(future_time=expiration) lock_periods = calculate_period_duration(future_time=expiration)
if entire_balance is True: if entire_balance is True:
amount = self.token_balance amount = self.token_balance
amount = NU(int(amount), 'NuNit')
staking_transactions = OrderedDict() # type: OrderedDict # Time series of txhases staking_transactions = OrderedDict() # type: OrderedDict # Time series of txhases
# Validate # Validate
assert self.__validate_stake(amount=amount, lock_periods=lock_periods) assert self.__validate_stake(amount=amount, lock_periods=lock_periods)
# Transact # Transact
approve_txhash, initial_deposit_txhash = self.deposit(amount=amount, lock_periods=lock_periods) approve_txhash, initial_deposit_txhash = self.deposit(amount=int(amount), lock_periods=lock_periods)
self._transaction_cache.append((datetime.utcnow(), initial_deposit_txhash)) self._transaction_cache.append((datetime.utcnow(), initial_deposit_txhash))
staking_transactions['approve'] = approve_txhash staking_transactions['approve'] = approve_txhash
staking_transactions['deposit'] = initial_deposit_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)) self.log.info("{} Initialized new stake: {} tokens for {} periods".format(self.checksum_public_address, amount, lock_periods))
return staking_transactions return staking_transactions

View File

@ -14,10 +14,8 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>. along with nucypher. If not, see <https://www.gnu.org/licenses/>.
""" """
"""Nucypher Token and Miner constants."""
ONE_YEAR_IN_SECONDS = 31540000 ONE_YEAR_IN_SECONDS = 31540000
NUCYPHER_GAS_LIMIT = 6500000 # TODO: move elsewhere?
# #
# Dispatcher # Dispatcher
@ -38,28 +36,19 @@ POLICY_ID_LENGTH = 16
NULL_ADDRESS = '0x' + '0' * 40 NULL_ADDRESS = '0x' + '0' * 40
__subdigits = 18 # Denomination
M = 10 ** __subdigits # Unit designation TOKEN_DECIMALS = 18
NUNITS_PER_TOKEN = 10 ** TOKEN_DECIMALS # Smallest unit designation
__initial_supply = int(1e9) * M # Initial token supply # Supply
__saturation = int(3.89e9) * M # Token supply cap __initial_supply = int(1e9) * NUNITS_PER_TOKEN # Initial token supply
TOKEN_SUPPLY = __saturation - __initial_supply # Remaining supply __saturation = int(3.89e9) * NUNITS_PER_TOKEN # Token supply cap
TOKEN_SUPPLY = __saturation - __initial_supply # Remaining supply
TOKEN_SATURATION = __saturation TOKEN_SATURATION = __saturation
#
# Miner
#
HOURS_PER_PERIOD = 24 # Hours in single period
SECONDS_PER_PERIOD = HOURS_PER_PERIOD * 60 * 60 # Seconds in single period
MIN_LOCKED_PERIODS = 30 # 720 Hours minimum
MAX_MINTING_PERIODS = 365 # Maximum number of periods
MIN_ALLOWED_LOCKED = 15000 * M
MAX_ALLOWED_LOCKED = int(4e6) * M
""" """
~ Staking ~
Mining formula for one stake in one period: Mining formula for one stake in one period:
(totalSupply - currentSupply) * (lockedValue / totalLockedValue) * (k1 + allLockedPeriods) / k2 (totalSupply - currentSupply) * (lockedValue / totalLockedValue) * (k1 + allLockedPeriods) / k2
@ -73,14 +62,28 @@ Mining formula for one stake in one period:
""" """
__mining_coeff = (
# Periods
HOURS_PER_PERIOD = 24 # Hours in single period
SECONDS_PER_PERIOD = HOURS_PER_PERIOD * 60 * 60 # Seconds in single period
# Lock Time
MIN_LOCKED_PERIODS = 30 # 720 Hours minimum
MAX_MINTING_PERIODS = 365 # Maximum number of periods
# Lock Value
MIN_ALLOWED_LOCKED = 15000 * NUNITS_PER_TOKEN
MAX_ALLOWED_LOCKED = int(4e6) * NUNITS_PER_TOKEN
# Rewards
K2 = 2 * 10 ** 7 # Mining Coefficient
MINING_COEFFICIENT = (
HOURS_PER_PERIOD, # Hours in single period HOURS_PER_PERIOD, # Hours in single period
2 * 10 ** 7, # Mining coefficient (k2) K2, # Mining coefficient (k2)
MAX_MINTING_PERIODS, # Locked periods coefficient (k1) MAX_MINTING_PERIODS, # Locked periods coefficient (k1)
MAX_MINTING_PERIODS, # Max periods that will be additionally rewarded (rewardedPeriods) MAX_MINTING_PERIODS, # Max periods that will be additionally rewarded (rewardedPeriods)
MIN_LOCKED_PERIODS, # Min amount of periods during which tokens can be locked MIN_LOCKED_PERIODS, # Min amount of periods during which tokens can be locked
MIN_ALLOWED_LOCKED, # Min amount of tokens that can be locked MIN_ALLOWED_LOCKED, # Min amount of tokens that can be locked
MAX_ALLOWED_LOCKED # Max amount of tokens that can be locked MAX_ALLOWED_LOCKED # Max amount of tokens that can be locked
) )
MINING_COEFFICIENT = __mining_coeff

View File

@ -34,7 +34,6 @@ from constant_sorrow.constants import (
) )
from eth_tester import EthereumTester from eth_tester import EthereumTester
from eth_tester import PyEVMBackend from eth_tester import PyEVMBackend
from nucypher.blockchain.eth.constants import NUCYPHER_GAS_LIMIT
from nucypher.blockchain.eth.registry import EthereumContractRegistry from nucypher.blockchain.eth.registry import EthereumContractRegistry
from nucypher.blockchain.eth.sol.compile import SolidityCompiler from nucypher.blockchain.eth.sol.compile import SolidityCompiler
@ -232,7 +231,8 @@ class BlockchainInterface:
if uri_breakdown.scheme == 'tester': if uri_breakdown.scheme == 'tester':
if uri_breakdown.netloc == 'pyevm': if uri_breakdown.netloc == 'pyevm':
genesis_params = PyEVMBackend._generate_genesis_params(overrides={'gas_limit': NUCYPHER_GAS_LIMIT}) from nucypher.utilities.sandbox.constants import PYEVM_GAS_LIMIT
genesis_params = PyEVMBackend._generate_genesis_params(overrides={'gas_limit': PYEVM_GAS_LIMIT})
pyevm_backend = PyEVMBackend(genesis_parameters=genesis_params) pyevm_backend = PyEVMBackend(genesis_parameters=genesis_params)
eth_tester = EthereumTester(backend=pyevm_backend, auto_mine_transactions=True) eth_tester = EthereumTester(backend=pyevm_backend, auto_mine_transactions=True)
provider = EthereumTesterProvider(ethereum_tester=eth_tester) provider = EthereumTesterProvider(ethereum_tester=eth_tester)

View File

@ -0,0 +1,196 @@
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.eth.agents import NucypherTokenAgent
from nucypher.blockchain.eth.constants import TOKEN_DECIMALS
from nucypher.blockchain.eth.utils import datetime_at_period, datetime_to_period
class NU:
"""
An amount of NuCypher tokens that doesn't hurt your eyes.
Wraps the eth_utils currency conversion methods.
The easiest way to use NU, is to pass an int, float, or str, and denomination string:
Int: nu = NU(100, 'NU')
Int: nu_wei = NU(15000000000000000000000, 'NuNit')
Float: nu = NU(15042.445, 'NU')
String: nu = NU('10002.302', 'NU')
...or alternately...
Float: nu = NU.from_tokens(100.50)
Int: nu_wei = NU.from_nu_wei(15000000000000000000000)
Token quantity is stored internally as an int in the smallest denomination,
and all arithmetic operations use this value.
"""
__symbol = 'NU'
__decimals = TOKEN_DECIMALS
__agent_class = NucypherTokenAgent
# conversions
__denominations = {'NuNit': 'wei',
'NU': 'ether'}
class InvalidAmount(ValueError):
"""Raised when an invalid input amount is provided"""
def __init__(self, value: Union[int, float, str], denomination: str):
# Calculate smallest denomination and store it
wrapped_denom = self.__denominations[denomination]
# Convert or Raise
try:
self.__value = currency.to_wei(number=value, unit=wrapped_denom)
except ValueError as e:
raise NU.InvalidAmount(f"{value} is an invalid amount of tokens: {str(e)}")
@classmethod
def from_nunits(cls, value: int):
return cls(value, denomination='NuNit')
@classmethod
def from_tokens(cls, value: Union[int, float, str]):
return cls(value, denomination='NU')
def to_tokens(self) -> Decimal:
"""Returns an decimal value of NU"""
return currency.from_wei(self.__value, unit='ether')
def to_nunits(self) -> int:
"""Returns an int value in NuNit"""
return int(self.__value)
def __eq__(self, other) -> bool:
return int(self) == int(other)
def __radd__(self, other) -> 'NU':
return NU(int(self) + int(other), 'NuNit')
def __add__(self, other) -> 'NU':
return NU(int(self) + int(other), 'NuNit')
def __sub__(self, other) -> 'NU':
return NU(int(self) - int(other), 'NuNit')
def __rmul__(self, other) -> 'NU':
return NU(int(self) * int(other), 'NuNit')
def __mul__(self, other) -> 'NU':
return NU(int(self) * int(other), 'NuNit')
def __floordiv__(self, other) -> 'NU':
return NU(int(self) // int(other), 'NuNit')
def __gt__(self, other) -> bool:
return int(self) > int(other)
def __ge__(self, other) -> bool:
return int(self) >= int(other)
def __lt__(self, other) -> bool:
return int(self) < int(other)
def __le__(self, other) -> bool:
return int(self) <= int(other)
def __int__(self) -> int:
"""Cast to smallest denomination"""
return int(self.to_nunits())
def __repr__(self) -> str:
r = f'{self.__symbol}(value={str(self.__value)})'
return r
def __str__(self) -> str:
return f'{str(self.to_tokens())} {self.__symbol}'
class Stake:
"""
A quantity of tokens, and staking time-frame for one stake for one miner.
"""
__ID_LENGTH = 16
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
def __repr__(self):
r = f'Stake(index={self.index}, value={self.value}, end_period={self.end_period})'
return r
def __eq__(self, other):
return bool(self.value == other.value)
@classmethod
def from_stake_info(cls, owner_address, index: int, stake_info: Tuple[int, int, int]):
"""Reads staking values as they exist on the blockchain"""
start_period, end_period, value = stake_info
instance = cls(owner_address=owner_address,
index=index,
start_period=start_period,
end_period=end_period,
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]
return stake_id[:self.__ID_LENGTH]
@property
def periods_remaining(self) -> int:
"""Returns the number of periods remaining in the stake from now."""
current_period = datetime_to_period(datetime=maya.now())
return self.end_period - current_period
def time_remaining(self, slang: bool = False) -> Union[int, str]:
"""Returns the time delta remaining in the stake from now."""
now = maya.now()
delta = self.end_datetime - now
if slang:
result = self.end_datetime.slang_date()
else:
result = delta.seconds
return result

View File

@ -14,6 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>. along with nucypher. If not, see <https://www.gnu.org/licenses/>.
""" """
import maya import maya
from nucypher.blockchain.eth.constants import (MIN_ALLOWED_LOCKED, from nucypher.blockchain.eth.constants import (MIN_ALLOWED_LOCKED,
@ -23,59 +24,17 @@ from nucypher.blockchain.eth.constants import (MIN_ALLOWED_LOCKED,
SECONDS_PER_PERIOD) SECONDS_PER_PERIOD)
def __validate(rulebook) -> bool:
for rule, failure_message in rulebook:
if not rule:
raise ValueError(failure_message)
return True
def validate_stake_amount(amount: int, raise_on_fail=True) -> bool:
rulebook = (
(MIN_ALLOWED_LOCKED <= amount,
'Stake amount too low; ({amount}) must be at least {minimum}'
.format(minimum=MIN_ALLOWED_LOCKED, amount=amount)),
(MAX_ALLOWED_LOCKED >= amount,
'Stake amount too high; ({amount}) must be no more than {maximum}.'
.format(maximum=MAX_ALLOWED_LOCKED, amount=amount)),
)
if raise_on_fail is True:
__validate(rulebook=rulebook)
return all(rulebook)
def validate_locktime(lock_periods: int, raise_on_fail=True) -> bool:
rulebook = (
(MIN_LOCKED_PERIODS <= lock_periods,
'Locktime ({locktime}) too short; must be at least {minimum}'
.format(minimum=MIN_LOCKED_PERIODS, locktime=lock_periods)),
(MAX_MINTING_PERIODS >= lock_periods,
'Locktime ({locktime}) too long; must be no more than {maximum}'
.format(maximum=MAX_MINTING_PERIODS, locktime=lock_periods)),
)
if raise_on_fail is True:
__validate(rulebook=rulebook)
return all(rulebook)
def datetime_to_period(datetime: maya.MayaDT) -> int: def datetime_to_period(datetime: maya.MayaDT) -> int:
"""Converts a MayaDT instance to a period number.""" """Converts a MayaDT instance to a period number."""
future_period = datetime._epoch // int(SECONDS_PER_PERIOD) future_period = datetime.epoch // int(SECONDS_PER_PERIOD)
return int(future_period) return int(future_period)
def datetime_at_period(period: int) -> maya.MayaDT: def datetime_at_period(period: int) -> maya.MayaDT:
"""Returns the datetime object at a given period, future, or past."""
now = maya.now() now = maya.now()
current_period = datetime_to_period(datetime=now) current_period = datetime_to_period(datetime=now)
delta_periods = period - current_period delta_periods = period - current_period
# + # +
@ -95,3 +54,53 @@ def calculate_period_duration(future_time: maya.MayaDT) -> int:
current_period = datetime_to_period(datetime=maya.now()) current_period = datetime_to_period(datetime=maya.now())
periods = future_period - current_period periods = future_period - current_period
return periods return periods
def __validate(rulebook) -> bool:
"""Validate a rulebook"""
for rule, failure_message in rulebook:
if not rule:
raise ValueError(failure_message)
return True
def validate_stake_amount(amount, raise_on_fail=True) -> bool:
"""Validate a single staking value against pre-defined requirements"""
from nucypher.blockchain.eth.token import NU
min_locked = NU(MIN_ALLOWED_LOCKED, 'NuNit')
max_locked = NU(MAX_ALLOWED_LOCKED, 'NuNit')
rulebook = (
(min_locked <= amount,
'Stake amount too low; ({amount}) must be at least {minimum}'
.format(minimum=min_locked, amount=amount)),
(max_locked >= amount,
'Stake amount too high; ({amount}) must be no more than {maximum}.'
.format(maximum=max_locked, amount=amount)),
)
if raise_on_fail is True:
__validate(rulebook=rulebook)
return all(rulebook)
def validate_locktime(lock_periods: int, raise_on_fail=True) -> bool:
"""Validate a single staking lock-time against pre-defined requirements"""
rulebook = (
(MIN_LOCKED_PERIODS <= lock_periods,
'Locktime ({locktime}) too short; must be at least {minimum}'
.format(minimum=MIN_LOCKED_PERIODS, locktime=lock_periods)),
(MAX_MINTING_PERIODS >= lock_periods,
'Locktime ({locktime}) too long; must be no more than {maximum}'
.format(maximum=MAX_MINTING_PERIODS, locktime=lock_periods)),
)
if raise_on_fail is True:
__validate(rulebook=rulebook)
return all(rulebook)

View File

@ -98,7 +98,7 @@ def confirm_staged_stake(ursula, value, duration):
* Ursula Node Operator Notice * * Ursula Node Operator Notice *
------------------------------- -------------------------------
By agreeing to stake {value} NU: By agreeing to stake {str(value)}:
- Staked tokens will be locked, and unavailable for transactions for the stake duration. - Staked tokens will be locked, and unavailable for transactions for the stake duration.

View File

@ -29,8 +29,8 @@ from nucypher.config.constants import GLOBAL_DOMAIN
@click.option('--bob-encrypting-key', help="Bob's encrypting key as a hexideicmal string", type=click.STRING) @click.option('--bob-encrypting-key', help="Bob's encrypting key as a hexideicmal string", type=click.STRING)
@click.option('--bob-verifying-key', help="Bob's verifying key as a hexideicmal string", type=click.STRING) @click.option('--bob-verifying-key', help="Bob's verifying key as a hexideicmal string", type=click.STRING)
@click.option('--label', help="The label for a policy", type=click.STRING) @click.option('--label', help="The label for a policy", type=click.STRING)
@click.option('--m', help="M", type=click.INT) @click.option('--m', help="M-Threshold KFrags", type=click.INT)
@click.option('--n', help="N", type=click.INT) @click.option('--n', help="N-Total KFrags", type=click.INT)
@click.option('--dev', '-d', help="Enable development mode", is_flag=True) @click.option('--dev', '-d', help="Enable development mode", is_flag=True)
@click.option('--force', help="Don't ask for confirmation", is_flag=True) @click.option('--force', help="Don't ask for confirmation", is_flag=True)
@click.option('--dry-run', '-x', help="Execute normally without actually starting the node", is_flag=True) @click.option('--dry-run', '-x', help="Execute normally without actually starting the node", is_flag=True)

View File

@ -17,14 +17,12 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
""" """
import click import click
import maya
from constant_sorrow.constants import TEMPORARY_DOMAIN from constant_sorrow.constants import TEMPORARY_DOMAIN
from twisted.internet import stdio from twisted.internet import stdio
from twisted.logger import Logger from twisted.logger import Logger
from web3 import Web3
from nucypher.blockchain.eth.constants import MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS, MIN_ALLOWED_LOCKED from nucypher.blockchain.eth.constants import MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS, MIN_ALLOWED_LOCKED
from nucypher.blockchain.eth.utils import datetime_at_period, calculate_period_duration from nucypher.blockchain.eth.token import NU
from nucypher.characters.banners import URSULA_BANNER from nucypher.characters.banners import URSULA_BANNER
from nucypher.cli import actions, painting from nucypher.cli import actions, painting
from nucypher.cli.actions import destroy_system_configuration from nucypher.cli.actions import destroy_system_configuration
@ -34,7 +32,10 @@ from nucypher.cli.types import (
EIP55_CHECKSUM_ADDRESS, EIP55_CHECKSUM_ADDRESS,
NETWORK_PORT, NETWORK_PORT,
EXISTING_READABLE_FILE, EXISTING_READABLE_FILE,
STAKE_DURATION, STAKE_EXTENSION) STAKE_DURATION,
STAKE_EXTENSION,
STAKE_VALUE
)
from nucypher.config.characters import UrsulaConfiguration from nucypher.config.characters import UrsulaConfiguration
from nucypher.utilities.sandbox.constants import ( from nucypher.utilities.sandbox.constants import (
TEMPORARY_DOMAIN, TEMPORARY_DOMAIN,
@ -68,6 +69,7 @@ from nucypher.utilities.sandbox.constants import (
@click.option('--duration', help="Period duration of stake", type=click.INT) @click.option('--duration', help="Period duration of stake", type=click.INT)
@click.option('--index', help="A specific stake index to resume", type=click.INT) @click.option('--index', help="A specific stake index to resume", type=click.INT)
@click.option('--list', '-l', 'list_', help="List all blockchain stakes", is_flag=True) @click.option('--list', '-l', 'list_', help="List all blockchain stakes", is_flag=True)
@click.option('--divide', '-d', help="Divide an existing stake into sub-stakes.", is_flag=True)
@nucypher_click_config @nucypher_click_config
def ursula(click_config, def ursula(click_config,
action, action,
@ -95,7 +97,8 @@ def ursula(click_config,
value, value,
duration, duration,
index, index,
list_ list_,
divide
) -> None: ) -> None:
""" """
Manage and run an "Ursula" PRE node. Manage and run an "Ursula" PRE node.
@ -112,7 +115,6 @@ def ursula(click_config,
destroy Delete Ursula node configuration. destroy Delete Ursula node configuration.
stake Manage stakes for this node. stake Manage stakes for this node.
confirm-activity Manually confirm-activity for the current period. confirm-activity Manually confirm-activity for the current period.
divide-stake Divide an existing stake.
collect-reward Withdraw staking reward. collect-reward Withdraw staking reward.
""" """
@ -139,7 +141,7 @@ def ursula(click_config,
click.secho("WARNING: Force is enabled", fg='yellow') click.secho("WARNING: Force is enabled", fg='yellow')
# #
# Unauthenticated Configurations & Unconfigured Ursula Control # Unauthenticated Configurations & Un-configured Ursula Control
# #
if action == "init": if action == "init":
"""Create a brand-new persistent Ursula""" """Create a brand-new persistent Ursula"""
@ -169,7 +171,10 @@ def ursula(click_config,
provider_uri=provider_uri, provider_uri=provider_uri,
poa=poa) poa=poa)
painting.paint_new_installation_help(new_configuration=ursula_config, config_root=config_root, config_file=config_file) painting.paint_new_installation_help(new_configuration=ursula_config,
config_root=config_root,
config_file=config_file,
federated_only=federated_only)
return return
# #
@ -255,9 +260,8 @@ def ursula(click_config,
bold=True) bold=True)
if not URSULA.federated_only and URSULA.stakes: if not URSULA.federated_only and URSULA.stakes:
total = URSULA.blockchain.interface.w3.fromWei(URSULA.total_staked, 'ether')
click_config.emitter( click_config.emitter(
message=f"Staking {total} NU ~ Keep Ursula Online!", message=f"Staking {str(URSULA.total_staked)} ~ Keep Ursula Online!",
color='blue', color='blue',
bold=True) bold=True)
@ -320,12 +324,61 @@ def ursula(click_config,
elif action == 'stake': elif action == 'stake':
# List only # List Only
if list_: if list_:
live_stakes = list(URSULA.miner_agent.get_all_stakes(miner_address=URSULA.checksum_public_address)) if not URSULA.stakes:
if not live_stakes:
click.echo(f"There are no existing stakes for {URSULA.checksum_public_address}") click.echo(f"There are no existing stakes for {URSULA.checksum_public_address}")
painting.paint_stakes(stakes=live_stakes) painting.paint_stakes(stakes=URSULA.stakes)
return
# Divide Only
if divide:
"""Divide an existing stake by specifying the new target value and end period"""
# Validate
if len(URSULA.stakes) == 0:
click.secho("There are no active stakes for {}".format(URSULA.checksum_public_address))
return
# Selection
if index is None:
painting.paint_stakes(stakes=URSULA.stakes)
index = click.prompt("Select a stake to divide", type=click.IntRange(min=0, max=len(URSULA.stakes)-1))
# Lookup the stake
current_stake = URSULA.stakes[index]
# Value
if not value:
value = click.prompt(f"Enter target value (must be less than {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(ursula=URSULA,
original_index=index,
original_stake=current_stake,
target_value=value,
extension=extension)
click.confirm("Is this correct?", abort=True)
txhash_bytes = 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()}')
# Show the resulting stake list
painting.paint_stakes(stakes=URSULA.stakes)
return return
# Confirm new stake init # Confirm new stake init
@ -333,7 +386,7 @@ def ursula(click_config,
click.confirm("Stage a new stake?", abort=True) click.confirm("Stage a new stake?", abort=True)
# Validate balance # Validate balance
balance = URSULA.token_agent.get_balance(address=URSULA.checksum_public_address) balance = URSULA.token_balance
if balance == 0: if balance == 0:
click.secho(f"{ursula.checksum_public_address} has 0 NU.") click.secho(f"{ursula.checksum_public_address} has 0 NU.")
raise click.Abort raise click.Abort
@ -342,10 +395,9 @@ def ursula(click_config,
# Gather stake value # Gather stake value
if not value: if not value:
min_stake_nu = int(URSULA.blockchain.interface.w3.fromWei(MIN_ALLOWED_LOCKED, 'ether')) value = click.prompt(f"Enter stake value", type=STAKE_VALUE, default=NU(MIN_ALLOWED_LOCKED, 'NuNit'))
value = click.prompt(f"Enter stake value in NU", type=click.INT, default=min_stake_nu) else:
stake_wei = URSULA.blockchain.interface.w3.toWei(value, 'ether') value = NU(int(value), 'NU')
stake_nu = value
# Duration # Duration
if not quiet: if not quiet:
@ -359,8 +411,7 @@ def ursula(click_config,
# Review # Review
if not force: if not force:
painting.paint_staged_stake(ursula=URSULA, painting.paint_staged_stake(ursula=URSULA,
stake_nu=stake_nu, stake_value=value,
stake_wei=stake_wei,
duration=duration, duration=duration,
start_period=start_period, start_period=start_period,
end_period=end_period) end_period=end_period)
@ -372,80 +423,17 @@ def ursula(click_config,
if not force: if not force:
click.confirm("Publish staged stake to the blockchain?", abort=True) click.confirm("Publish staged stake to the blockchain?", abort=True)
staking_transactions = URSULA.initialize_stake(amount=stake_wei, lock_periods=duration) staking_transactions = URSULA.initialize_stake(amount=int(value), lock_periods=duration)
painting.paint_staking_confirmation(ursula=URSULA, transactions=staking_transactions) painting.paint_staking_confirmation(ursula=URSULA, transactions=staking_transactions)
return return
elif action == 'confirm-activity': elif action == 'confirm-activity':
stakes = URSULA.miner_agent.get_all_stakes(miner_address=URSULA.checksum_public_address) if not URSULA.stakes:
if len(stakes) == 0:
click.secho("There are no active stakes for {}".format(URSULA.checksum_public_address)) click.secho("There are no active stakes for {}".format(URSULA.checksum_public_address))
return return
URSULA.miner_agent.confirm_activity(node_address=URSULA.checksum_public_address) URSULA.miner_agent.confirm_activity(node_address=URSULA.checksum_public_address)
return return
elif action == 'divide-stake':
"""Divide an existing stake by specifying the new target value and end period"""
stakes = list(URSULA.stakes)
if len(stakes) == 0:
click.secho("There are no active stakes for {}".format(URSULA.checksum_public_address))
return
if index is None:
painting.paint_stakes(stakes=stakes)
index = click.prompt("Select a stake to divide", type=click.IntRange(min=0, max=len(stakes)-1, clamp=False))
stake_info = stakes[index]
start_period, end_period, current_stake_wei = stake_info
current_stake_nu = int(Web3.fromWei(current_stake_wei, 'ether'))
# Value
if not value:
min_value = URSULA.blockchain.interface.w3.fromWei(MIN_ALLOWED_LOCKED, 'ether')
target_value = click.prompt(f"Enter target value (must be less than {current_stake_nu} NU)",
type=click.IntRange(min=min_value, max=current_stake_nu, clamp=False))
target_value = Web3.toWei(target_value, 'ether')
else:
target_value = value
# Duration
if not duration:
extension = click.prompt("Enter number of periods to extend", type=STAKE_EXTENSION)
else:
extension = duration
if not force:
new_end_period = end_period + extension
duration = new_end_period - start_period
division_message = f"""
{URSULA}
~ Original Stake: {painting.prettify_stake(stake_index=index, stake_info=stake_info)}
"""
painting.paint_staged_stake(ursula=URSULA,
stake_nu=int(Web3.fromWei(target_value, 'ether')),
stake_wei=target_value,
duration=duration,
start_period=stakes[index][0],
end_period=new_end_period,
division_message=division_message)
click.confirm("Is this correct?", abort=True)
txhash_bytes = URSULA.divide_stake(stake_index=index,
target_value=target_value,
additional_periods=extension)
if not quiet:
click.secho('\nSuccessfully divided stake', fg='green')
click.secho(f'Transaction Hash ........... {txhash_bytes.hex()}\n', fg='green')
# Show the resulting stake list
painting.paint_stakes(stakes=URSULA.stakes)
return
elif action == 'collect-reward': elif action == 'collect-reward':
"""Withdraw staking reward to the specified wallet address""" """Withdraw staking reward to the specified wallet address"""
if not force: if not force:

View File

@ -15,12 +15,11 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>. along with nucypher. If not, see <https://www.gnu.org/licenses/>.
""" """
import os import os
from typing import Tuple from decimal import Decimal
import click import click
import maya import maya
from constant_sorrow.constants import NO_KNOWN_NODES from constant_sorrow.constants import NO_KNOWN_NODES
from web3 import Web3
from nucypher.blockchain.eth.utils import datetime_at_period from nucypher.blockchain.eth.utils import datetime_at_period
from nucypher.characters.banners import NUCYPHER_BANNER from nucypher.characters.banners import NUCYPHER_BANNER
@ -37,14 +36,18 @@ def echo_version(ctx, param, value):
ctx.exit() ctx.exit()
def paint_new_installation_help(new_configuration, config_root=None, config_file=None): def paint_new_installation_help(new_configuration, federated_only: bool = False, config_root=None, config_file=None):
character_config_class = new_configuration.__class__ character_config_class = new_configuration.__class__
character_name = character_config_class._NAME.lower() character_name = character_config_class._NAME.lower()
emitter(message="Generated keyring {}".format(new_configuration.keyring_dir), color='green') emitter(message="Generated keyring {}".format(new_configuration.keyring_dir), color='green')
emitter(message="Saved configuration file {}".format(new_configuration.config_file_location), color='green') emitter(message="Saved configuration file {}".format(new_configuration.config_file_location), color='green')
if character_name == 'ursula' and not federated_only:
suggested_staking_command = f'nucypher ursula stake'
how_to_stake_message = f"\nTo initialize a NU stake, run '{suggested_staking_command}' or"
emitter(message=how_to_stake_message, color='green')
# Give the use a suggestion as to what to do next... # Give the use a suggestion as to what to do next...
suggested_command = f'nucypher {character_name} run' suggested_command = f'nucypher {character_name} run'
how_to_run_message = f"\nTo run an {character_name.capitalize()} node from the default configuration filepath run: \n\n'{suggested_command}'\n" how_to_run_message = f"\nTo run an {character_name.capitalize()} node from the default configuration filepath run: \n\n'{suggested_command}'\n"
@ -194,8 +197,7 @@ def paint_contract_status(ursula_config, click_config):
def paint_staged_stake(ursula, def paint_staged_stake(ursula,
stake_nu, stake_value,
stake_wei,
duration, duration,
start_period, start_period,
end_period, end_period,
@ -209,7 +211,7 @@ def paint_staged_stake(ursula,
click.echo(f""" click.echo(f"""
{ursula} {ursula}
~ Value -> {stake_nu} NU ({stake_wei} NU-wei) ~ Value -> {stake_value} ({Decimal(int(stake_value)):.2E} NuNit)
~ Duration -> {duration} Days ({duration} Periods) ~ Duration -> {duration} Days ({duration} Periods)
~ Enactment -> {datetime_at_period(period=start_period)} (period #{start_period}) ~ Enactment -> {datetime_at_period(period=start_period)} (period #{start_period})
~ Expiration -> {datetime_at_period(period=end_period)} (period #{end_period}) ~ Expiration -> {datetime_at_period(period=end_period)} (period #{end_period})
@ -231,26 +233,45 @@ or start your Ursula node by running 'nucypher ursula run'.
''', fg='green') ''', fg='green')
def prettify_stake(stake_index: int, stake_info: Tuple[int, int, str]) -> str: def prettify_stake(stake_index: int, stake) -> str:
start, expiration, stake_wei = stake_info
stake_nu = int(Web3.fromWei(stake_wei, 'ether')) start_datetime = str(stake.start_datetime.slang_date())
expiration_datetime = str(stake.end_datetime.slang_date())
start_datetime = str(datetime_at_period(period=start).slang_date()) duration = stake.duration
expiration_datetime = str(datetime_at_period(period=expiration).slang_date())
duration = expiration - start
pretty_periods = f'{duration} periods {"." if len(str(duration)) == 2 else ""}' pretty_periods = f'{duration} periods {"." if len(str(duration)) == 2 else ""}'
pretty = f'| {stake_index} | {pretty_periods} | {start_datetime} .. | {expiration_datetime} ... | {stake_nu} NU ' pretty = f'| {stake_index} | {pretty_periods} | {start_datetime} .. | {expiration_datetime} ... | {str(stake.value)}'
return pretty return pretty
def paint_stakes(stakes): def paint_stakes(stakes):
header = f'| # | Duration | Enact | Expiration | Value ' header = f'| # | Duration | Enact | Expiration | Value '
breaky = f'| - | ------------ | --------- | -----------| ----- ' breaky = f'| - | ------------ | ----------- | -----------| ----- '
click.secho(header, bold=True) click.secho(header, bold=True)
click.secho(breaky, bold=True) click.secho(breaky, bold=True)
for index, stake_info in enumerate(stakes): for index, stake in stakes.items():
row = prettify_stake(stake_index=index, stake_info=stake_info) row = prettify_stake(stake_index=index, stake=stake)
click.echo(row) click.echo(row)
return return
def paint_staged_stake_division(ursula,
original_index,
original_stake,
target_value,
extension):
new_end_period = original_stake.end_period + extension
new_duration = new_end_period - original_stake.start_period
division_message = f"""
{ursula}
~ Original Stake: {prettify_stake(stake_index=original_index, stake=original_stake)}
"""
paint_staged_stake(ursula=ursula,
stake_value=target_value,
duration=new_duration,
start_period=original_stake.start_period,
end_period=new_end_period,
division_message=division_message)

View File

@ -20,8 +20,13 @@ from ipaddress import ip_address
import click import click
from eth_utils import is_checksum_address from eth_utils import is_checksum_address
from nucypher.blockchain.eth.constants import MIN_ALLOWED_LOCKED, MAX_MINTING_PERIODS, MIN_LOCKED_PERIODS, \ from nucypher.blockchain.eth.constants import (
MIN_ALLOWED_LOCKED,
MAX_MINTING_PERIODS,
MIN_LOCKED_PERIODS,
MAX_ALLOWED_LOCKED MAX_ALLOWED_LOCKED
)
from nucypher.blockchain.eth.token import NU
class ChecksumAddress(click.ParamType): class ChecksumAddress(click.ParamType):
@ -45,11 +50,17 @@ class IPv4Address(click.ParamType):
return value return value
# Staking
STAKE_DURATION = click.IntRange(min=MIN_LOCKED_PERIODS, max=MAX_MINTING_PERIODS, clamp=False) STAKE_DURATION = click.IntRange(min=MIN_LOCKED_PERIODS, max=MAX_MINTING_PERIODS, clamp=False)
STAKE_EXTENSION = click.IntRange(min=1, max=MAX_MINTING_PERIODS, clamp=False) STAKE_EXTENSION = click.IntRange(min=1, max=MAX_MINTING_PERIODS, clamp=False)
STAKE_VALUE = click.IntRange(min=MIN_ALLOWED_LOCKED, max=MAX_ALLOWED_LOCKED, clamp=False) STAKE_VALUE = click.IntRange(min=NU(MIN_ALLOWED_LOCKED, 'NuNit').to_tokens(),
max=NU(MAX_ALLOWED_LOCKED, 'NuNit').to_tokens(), clamp=False)
# Filesystem
EXISTING_WRITABLE_DIRECTORY = click.Path(exists=True, dir_okay=True, file_okay=False, writable=True) EXISTING_WRITABLE_DIRECTORY = click.Path(exists=True, dir_okay=True, file_okay=False, writable=True)
EXISTING_READABLE_FILE = click.Path(exists=True, dir_okay=False, file_okay=True, readable=True) EXISTING_READABLE_FILE = click.Path(exists=True, dir_okay=False, file_okay=True, readable=True)
# Network
NETWORK_PORT = click.IntRange(min=0, max=65535, clamp=False) NETWORK_PORT = click.IntRange(min=0, max=65535, clamp=False)
IPV4_ADDRESS = IPv4Address() IPV4_ADDRESS = IPv4Address()
EIP55_CHECKSUM_ADDRESS = ChecksumAddress() EIP55_CHECKSUM_ADDRESS = ChecksumAddress()

View File

@ -382,7 +382,6 @@ class NodeConfiguration(ABC):
storage_type = storage_payload[NodeStorage._TYPE_LABEL] storage_type = storage_payload[NodeStorage._TYPE_LABEL]
storage_class = node_storage_subclasses[storage_type] storage_class = node_storage_subclasses[storage_type]
node_storage = storage_class.from_payload(payload=storage_payload, node_storage = storage_class.from_payload(payload=storage_payload,
# character_class=cls._CHARACTER_CLASS, # TODO: Do not pass this here - Always Use Ursula
federated_only=payload['federated_only'], federated_only=payload['federated_only'],
serializer=cls.NODE_SERIALIZER, serializer=cls.NODE_SERIALIZER,
deserializer=cls.NODE_DESERIALIZER) deserializer=cls.NODE_DESERIALIZER)
@ -552,7 +551,7 @@ class NodeConfiguration(ABC):
# Keyring # Keyring
if not self.dev_mode: if not self.dev_mode:
os.mkdir(self.keyring_dir, mode=0o700) # keyring TODO: Keyring backend entry point os.mkdir(self.keyring_dir, mode=0o700) # keyring TODO: Keyring backend entry point: COS
self.write_keyring(password=password) self.write_keyring(password=password)
# Registry # Registry

View File

@ -24,7 +24,9 @@ from datetime import datetime
from random import SystemRandom from random import SystemRandom
from string import digits, ascii_uppercase from string import digits, ascii_uppercase
from nucypher.blockchain.eth.constants import DISPATCHER_SECRET_LENGTH, M from web3 import Web3
from nucypher.blockchain.eth.constants import DISPATCHER_SECRET_LENGTH, NUNITS_PER_TOKEN
from nucypher.config.characters import UrsulaConfiguration from nucypher.config.characters import UrsulaConfiguration
from nucypher.config.constants import BASE_DIR from nucypher.config.constants import BASE_DIR
@ -64,7 +66,7 @@ NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK = 10
TEST_CONTRACTS_DIR = os.path.join(BASE_DIR, 'tests', 'blockchain', 'eth', 'contracts', 'contracts') TEST_CONTRACTS_DIR = os.path.join(BASE_DIR, 'tests', 'blockchain', 'eth', 'contracts', 'contracts')
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT = 1000000 * int(M) DEVELOPMENT_TOKEN_AIRDROP_AMOUNT = 1000000 * int(NUNITS_PER_TOKEN)
DEVELOPMENT_ETH_AIRDROP_AMOUNT = 10 ** 6 * 10 ** 18 # wei -> ether DEVELOPMENT_ETH_AIRDROP_AMOUNT = 10 ** 6 * 10 ** 18 # wei -> ether
@ -76,6 +78,8 @@ INSECURE_DEVELOPMENT_PASSWORD = ''.join(SystemRandom().choice(ascii_uppercase +
MAX_TEST_SEEDER_ENTRIES = 20 MAX_TEST_SEEDER_ENTRIES = 20
TESTING_ETH_AIRDROP_AMOUNT = int(Web3().fromWei(100, 'ether'))
# #
# Temporary Directories and Files # Temporary Directories and Files
@ -93,8 +97,6 @@ MOCK_ALLOCATION_REGISTRY_FILEPATH = os.path.join(BASE_TEMP_DIR, f'{BASE_TEMP_PRE
MOCK_CUSTOM_INSTALLATION_PATH_2 = '/tmp/nucypher-tmp-test-custom-2-{}'.format(time.time()) MOCK_CUSTOM_INSTALLATION_PATH_2 = '/tmp/nucypher-tmp-test-custom-2-{}'.format(time.time())
TEMPORARY_DOMAIN = 'TEMPORARY_DOMAIN'
MOCK_REGISTRY_FILEPATH = os.path.join(BASE_TEMP_DIR, f'{BASE_TEMP_PREFIX}mock-registry-{str(datetime.now())}.json') MOCK_REGISTRY_FILEPATH = os.path.join(BASE_TEMP_DIR, f'{BASE_TEMP_PREFIX}mock-registry-{str(datetime.now())}.json')
TEMPORARY_DOMAIN = b':TEMPORARY_DOMAIN:' # for use with `--dev` node runtimes TEMPORARY_DOMAIN = b':TEMPORARY_DOMAIN:' # for use with `--dev` node runtimes
@ -116,3 +118,4 @@ MOCK_IP_ADDRESS_2 = '10.10.10.10'
MOCK_URSULA_DB_FILEPATH = ':memory:' MOCK_URSULA_DB_FILEPATH = ':memory:'
PYEVM_GAS_LIMIT = 6500000 # TODO: move elsewhere (used to set pyevm gas limit in tests)?

View File

@ -28,6 +28,7 @@ from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
from nucypher.blockchain.eth.registry import InMemoryEthereumContractRegistry, InMemoryAllocationRegistry from nucypher.blockchain.eth.registry import InMemoryEthereumContractRegistry, InMemoryAllocationRegistry
from nucypher.blockchain.eth.sol.compile import SolidityCompiler from nucypher.blockchain.eth.sol.compile import SolidityCompiler
from nucypher.utilities.sandbox.blockchain import TesterBlockchain from nucypher.utilities.sandbox.blockchain import TesterBlockchain
from nucypher.utilities.sandbox.constants import TESTING_ETH_AIRDROP_AMOUNT
@pytest.mark.slow() @pytest.mark.slow()
@ -39,7 +40,7 @@ def test_rapid_deployment():
registry=registry, registry=registry,
provider_uri='tester://pyevm') provider_uri='tester://pyevm')
blockchain = TesterBlockchain(interface=interface, airdrop=False, test_accounts=4) blockchain = TesterBlockchain(interface=interface, airdrop=False, test_accounts=4)
blockchain.ether_airdrop(amount=1000000000) blockchain.ether_airdrop(amount=TESTING_ETH_AIRDROP_AMOUNT)
origin, *everyone = blockchain.interface.w3.eth.accounts origin, *everyone = blockchain.interface.w3.eth.accounts
deployer = Deployer(blockchain=blockchain, deployer = Deployer(blockchain=blockchain,

View File

@ -19,8 +19,9 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import maya import maya
import pytest import pytest
from nucypher.blockchain.eth import constants
from nucypher.blockchain.eth.actors import Miner from nucypher.blockchain.eth.actors import Miner
from nucypher.blockchain.eth.constants import MIN_ALLOWED_LOCKED, MIN_LOCKED_PERIODS
from nucypher.blockchain.eth.token import NU, Stake
from nucypher.utilities.sandbox.blockchain import token_airdrop from nucypher.utilities.sandbox.blockchain import token_airdrop
from nucypher.utilities.sandbox.constants import DEVELOPMENT_TOKEN_AIRDROP_AMOUNT from nucypher.utilities.sandbox.constants import DEVELOPMENT_TOKEN_AIRDROP_AMOUNT
@ -37,12 +38,11 @@ def miner(testerchain, three_agents):
@pytest.mark.slow() @pytest.mark.slow()
def test_miner_locking_tokens(testerchain, three_agents, miner): def test_miner_locking_tokens(testerchain, three_agents, miner):
token_agent, miner_agent, policy_agent = three_agents token_agent, miner_agent, policy_agent = three_agents
# testerchain.ether_airdrop(amount=10000)
assert constants.MIN_ALLOWED_LOCKED < miner.token_balance, "Insufficient miner balance" assert NU(MIN_ALLOWED_LOCKED, 'NuNit') < miner.token_balance, "Insufficient miner balance"
expiration = maya.now().add(days=constants.MIN_LOCKED_PERIODS) expiration = maya.now().add(days=MIN_LOCKED_PERIODS)
miner.initialize_stake(amount=int(constants.MIN_ALLOWED_LOCKED), # Lock the minimum amount of tokens miner.initialize_stake(amount=NU(MIN_ALLOWED_LOCKED, 'NuNit'), # Lock the minimum amount of tokens
expiration=expiration) expiration=expiration)
# Verify that the escrow is "approved" to receive tokens # Verify that the escrow is "approved" to receive tokens
@ -55,42 +55,42 @@ def test_miner_locking_tokens(testerchain, three_agents, miner):
locked_tokens = miner_agent.contract.functions.getLockedTokens(miner.checksum_public_address).call() locked_tokens = miner_agent.contract.functions.getLockedTokens(miner.checksum_public_address).call()
assert 0 == locked_tokens assert 0 == locked_tokens
# testerchain.time_travel(periods=1)
locked_tokens = miner_agent.contract.functions.getLockedTokens(miner.checksum_public_address, 1).call() locked_tokens = miner_agent.contract.functions.getLockedTokens(miner.checksum_public_address, 1).call()
assert constants.MIN_ALLOWED_LOCKED == locked_tokens assert MIN_ALLOWED_LOCKED == locked_tokens
@pytest.mark.slow() @pytest.mark.slow()
@pytest.mark.usefixtures("three_agents") @pytest.mark.usefixtures("three_agents")
def test_miner_divides_stake(miner): def test_miner_divides_stake(miner):
stake_value = NU(MIN_ALLOWED_LOCKED*5, 'NuNit')
new_stake_value = NU(MIN_ALLOWED_LOCKED*2, 'NuNit')
stake_index = 0
miner.initialize_stake(amount=stake_value, lock_periods=int(MIN_LOCKED_PERIODS))
miner.divide_stake(target_value=new_stake_value, stake_index=stake_index+1, additional_periods=2)
current_period = miner.miner_agent.get_current_period() current_period = miner.miner_agent.get_current_period()
stake_value = int(constants.MIN_ALLOWED_LOCKED) * 5
new_stake_value = int(constants.MIN_ALLOWED_LOCKED) * 2
stake_index = len(list(miner.stakes))
miner.initialize_stake(amount=stake_value, lock_periods=int(constants.MIN_LOCKED_PERIODS))
miner.divide_stake(target_value=new_stake_value, stake_index=stake_index, additional_periods=2)
stakes = list(miner.stakes)
expected_old_stake = (current_period + 1, current_period + 30, stake_value - new_stake_value) 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) expected_new_stake = (current_period + 1, current_period + 32, new_stake_value)
assert stake_index + 2 == len(stakes), 'A new stake was not added to this miners stakes' assert 3 == len(miner.stakes), 'A new stake was not added to this miners stakes'
assert expected_old_stake == stakes[stake_index], 'Old stake values are invalid' assert expected_old_stake == miner.stakes[stake_index+1].to_stake_info(), 'Old stake values are invalid'
assert expected_new_stake == stakes[stake_index + 1], 'New stake values are invalid' assert expected_new_stake == miner.stakes[stake_index + 2].to_stake_info(), 'New stake values are invalid'
yet_another_stake_value = int(constants.MIN_ALLOWED_LOCKED) yet_another_stake_value = NU(MIN_ALLOWED_LOCKED, 'NuNit')
miner.divide_stake(target_value=yet_another_stake_value, stake_index=stake_index + 1, additional_periods=2) miner.divide_stake(target_value=yet_another_stake_value, stake_index=stake_index + 2, additional_periods=2)
stakes = list(miner.stakes)
expected_new_stake = (current_period + 1, current_period + 32, new_stake_value - yet_another_stake_value) expected_new_stake = (current_period + 1, current_period + 32, new_stake_value - yet_another_stake_value)
expected_yet_another_stake = (current_period + 1, current_period + 34, yet_another_stake_value) 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)
assert stake_index + 3 == len(stakes), 'A new stake was not added after two stake divisions' assert 4 == len(miner.stakes), 'A new stake was not added after two stake divisions'
assert expected_old_stake == stakes[stake_index], 'Old stake values are invalid 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'
assert expected_new_stake == stakes[stake_index + 1], 'New stake values are invalid after two stake divisions' assert expected_new_stake == miner.stakes[stake_index + 2].to_stake_info(), 'New stake values are invalid after two stake divisions'
assert expected_yet_another_stake == stakes[stake_index + 2], 'Third stake values are invalid' assert expected_yet_another_stake == miner.stakes[stake_index + 3], 'Third stake values are invalid'
@pytest.mark.slow() @pytest.mark.slow()
@ -102,17 +102,16 @@ def test_miner_collects_staking_reward(testerchain, miner, three_agents):
initial_balance = miner.token_balance initial_balance = miner.token_balance
assert token_agent.get_balance(miner.checksum_public_address) == initial_balance assert token_agent.get_balance(miner.checksum_public_address) == initial_balance
miner.initialize_stake(amount=int(constants.MIN_ALLOWED_LOCKED), # Lock the minimum amount of tokens miner.initialize_stake(amount=NU(MIN_ALLOWED_LOCKED, 'NuNit'), # Lock the minimum amount of tokens
lock_periods=int(constants.MIN_LOCKED_PERIODS)) # ... for the fewest number of periods lock_periods=int(MIN_LOCKED_PERIODS)) # ... for the fewest number of periods
# ...wait out the lock period... # ...wait out the lock period...
for _ in range(28): for _ in range(MIN_LOCKED_PERIODS):
testerchain.time_travel(periods=1) testerchain.time_travel(periods=1)
miner.confirm_activity() miner.confirm_activity()
# ...wait more... # ...wait more...
testerchain.time_travel(periods=2) testerchain.time_travel(periods=2)
miner.mint()
miner.collect_staking_reward() miner.collect_staking_reward()
final_balance = token_agent.get_balance(miner.checksum_public_address) final_balance = token_agent.get_balance(miner.checksum_public_address)

View File

@ -18,13 +18,14 @@ import pytest
from nucypher.blockchain.eth import constants from nucypher.blockchain.eth import constants
from nucypher.blockchain.eth.actors import PolicyAuthor from nucypher.blockchain.eth.actors import PolicyAuthor
from nucypher.utilities.sandbox.constants import TESTING_ETH_AIRDROP_AMOUNT
@pytest.mark.slow() @pytest.mark.slow()
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def author(testerchain, three_agents): def author(testerchain, three_agents):
token_agent, miner_agent, policy_agent = three_agents token_agent, miner_agent, policy_agent = three_agents
token_agent.ether_airdrop(amount=100000 * constants.M) token_agent.ether_airdrop(amount=TESTING_ETH_AIRDROP_AMOUNT)
_origin, ursula, alice, *everybody_else = testerchain.interface.w3.eth.accounts _origin, ursula, alice, *everybody_else = testerchain.interface.w3.eth.accounts
author = PolicyAuthor(checksum_address=alice) author = PolicyAuthor(checksum_address=alice)
return author return author
@ -32,7 +33,6 @@ def author(testerchain, three_agents):
@pytest.mark.slow() @pytest.mark.slow()
def test_create_policy_author(testerchain, three_agents): def test_create_policy_author(testerchain, three_agents):
token_agent, miner_agent, policy_agent = three_agents
_origin, ursula, alice, *everybody_else = testerchain.interface.w3.eth.accounts _origin, ursula, alice, *everybody_else = testerchain.interface.w3.eth.accounts
policy_author = PolicyAuthor(checksum_address=alice) policy_author = PolicyAuthor(checksum_address=alice)
assert policy_author.checksum_public_address == alice assert policy_author.checksum_public_address == alice

View File

@ -0,0 +1,134 @@
from decimal import InvalidOperation, Decimal
import pytest
from web3 import Web3
from nucypher.blockchain.eth.constants import MIN_ALLOWED_LOCKED
from nucypher.blockchain.eth.token import NU, Stake
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
def test_NU():
# Starting Small
min_allowed_locked = NU(MIN_ALLOWED_LOCKED, 'NuNit')
assert MIN_ALLOWED_LOCKED == int(min_allowed_locked.to_nunits())
min_NU_locked = int(str(MIN_ALLOWED_LOCKED)[0:-18])
expected = NU(min_NU_locked, 'NU')
assert min_allowed_locked == expected
# Starting Big
min_allowed_locked = NU(min_NU_locked, 'NU')
assert MIN_ALLOWED_LOCKED == int(min_allowed_locked)
assert MIN_ALLOWED_LOCKED == int(min_allowed_locked.to_nunits())
assert str(min_allowed_locked) == '15000 NU'
# Alternate construction
assert NU(1, 'NU') == NU('1.0', 'NU') == NU(1.0, 'NU')
# Arithmetic
# NUs
one_nu = NU(1, 'NU')
zero_nu = NU(0, 'NU')
one_hundred_nu = NU(100, 'NU')
two_hundred_nu = NU(200, 'NU')
three_hundred_nu = NU(300, 'NU')
# Nits
one_nu_wei = NU(1, 'NuNit')
three_nu_wei = NU(3, 'NuNit')
assert three_nu_wei.to_tokens() == Decimal('3E-18')
assert one_nu_wei.to_tokens() == Decimal('1E-18')
# Base Operations
assert one_hundred_nu < two_hundred_nu < three_hundred_nu
assert one_hundred_nu <= two_hundred_nu <= three_hundred_nu
assert three_hundred_nu > two_hundred_nu > one_hundred_nu
assert three_hundred_nu >= two_hundred_nu >= one_hundred_nu
assert (one_hundred_nu + two_hundred_nu) == three_hundred_nu
assert (three_hundred_nu - two_hundred_nu) == one_hundred_nu
difference = one_nu - one_nu_wei
assert not difference == zero_nu
actual = float(difference.to_tokens())
expected = 0.999999999999999999
assert actual == expected
# 3.14 NU is 3_140_000_000_000_000_000 NuNit
pi_nuweis = NU(3.14, 'NU')
assert NU('3.14', 'NU') == pi_nuweis.to_nunits() == NU(3_140_000_000_000_000_000, 'NuNit')
# Mixed type operations
difference = NU('3.14159265', 'NU') - NU(1.1, 'NU')
assert difference == NU('2.04159265', 'NU')
result = difference + one_nu_wei
assert result == NU(2041592650000000001, 'NuNit')
# Similar to stake read + metadata operations in Miner
collection = [one_hundred_nu, two_hundred_nu, three_hundred_nu]
assert sum(collection) == NU('600', 'NU') == NU(600, 'NU') == NU(600.0, 'NU') == NU(600e+18, 'NuNit')
#
# Fractional Inputs
#
# A decimal amount of NuNit (i.e., a fraction of a NuNit)
pi_nuweis = NU('3.14', 'NuNit')
assert pi_nuweis == three_nu_wei # Floor
# A decimal amount of NU, which amounts to NuNit with decimals
pi_nus = NU('3.14159265358979323846', 'NU')
assert pi_nus == NU(3141592653589793238, 'NuNit') # Floor
# Positive Infinity
with pytest.raises(NU.InvalidAmount):
_inf = NU(float('infinity'), 'NU')
# Negative Infinity
with pytest.raises(NU.InvalidAmount):
_neg_inf = NU(float('-infinity'), 'NU')
# Not a Number
with pytest.raises(InvalidOperation):
_nan = NU(float('NaN'), 'NU')
def test_stake():
class FakeUrsula:
burner_wallet = Web3().eth.account.create(INSECURE_DEVELOPMENT_PASSWORD)
checksum_public_address = burner_wallet.address
miner_agent = None
ursula = FakeUrsula()
stake = Stake(owner_address=ursula.checksum_public_address,
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
slang_remaining = stake.time_remaining(slang=True) # words
assert isinstance(slang_remaining, str)
def test_stake_integration(blockchain_ursulas):
staking_ursula = list(blockchain_ursulas)[1]
stakes = staking_ursula.stakes
assert stakes
stake = stakes[0]
blockchain_stakes = staking_ursula.miner_agent.get_all_stakes(miner_address=staking_ursula.checksum_public_address)
stake_info = (stake.start_period, stake.end_period, int(stake.value))
published_stake_info = list(blockchain_stakes)[0]
assert stake_info == published_stake_info == stake.to_stake_info()

View File

@ -33,7 +33,7 @@ from nucypher.blockchain.eth.sol.compile import SolidityCompiler
from nucypher.config.characters import UrsulaConfiguration from nucypher.config.characters import UrsulaConfiguration
from nucypher.utilities.sandbox.blockchain import TesterBlockchain from nucypher.utilities.sandbox.blockchain import TesterBlockchain
from nucypher.utilities.sandbox.constants import MOCK_CUSTOM_INSTALLATION_PATH, TEST_PROVIDER_URI, \ from nucypher.utilities.sandbox.constants import MOCK_CUSTOM_INSTALLATION_PATH, TEST_PROVIDER_URI, \
MOCK_ALLOCATION_INFILE, MOCK_REGISTRY_FILEPATH MOCK_ALLOCATION_INFILE, MOCK_REGISTRY_FILEPATH, TESTING_ETH_AIRDROP_AMOUNT
from nucypher.utilities.sandbox.constants import MOCK_CUSTOM_INSTALLATION_PATH_2 from nucypher.utilities.sandbox.constants import MOCK_CUSTOM_INSTALLATION_PATH_2
@ -111,7 +111,7 @@ def deployed_blockchain():
# Blockchain # Blockchain
# #
blockchain = TesterBlockchain(interface=interface, airdrop=False, test_accounts=5, poa=True) blockchain = TesterBlockchain(interface=interface, airdrop=False, test_accounts=5, poa=True)
blockchain.ether_airdrop(amount=1000000000) blockchain.ether_airdrop(amount=TESTING_ETH_AIRDROP_AMOUNT)
origin, *everyone = blockchain.interface.w3.eth.accounts origin, *everyone = blockchain.interface.w3.eth.accounts
# #

View File

@ -12,6 +12,7 @@ from umbral.keys import UmbralPublicKey
from nucypher.blockchain.eth.actors import Miner from nucypher.blockchain.eth.actors import Miner
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent
from nucypher.blockchain.eth.constants import MIN_LOCKED_PERIODS, MIN_ALLOWED_LOCKED from nucypher.blockchain.eth.constants import MIN_LOCKED_PERIODS, MIN_ALLOWED_LOCKED
from nucypher.blockchain.eth.token import NU
from nucypher.characters.lawful import Enrico from nucypher.characters.lawful import Enrico
from nucypher.cli.main import nucypher_cli from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import UrsulaConfiguration, BobConfiguration from nucypher.config.characters import UrsulaConfiguration, BobConfiguration
@ -22,15 +23,14 @@ from nucypher.utilities.sandbox.constants import (
TEST_PROVIDER_URI, TEST_PROVIDER_URI,
MOCK_URSULA_STARTING_PORT, MOCK_URSULA_STARTING_PORT,
INSECURE_DEVELOPMENT_PASSWORD, INSECURE_DEVELOPMENT_PASSWORD,
MOCK_REGISTRY_FILEPATH, TEMPORARY_DOMAIN) MOCK_REGISTRY_FILEPATH, TEMPORARY_DOMAIN, TESTING_ETH_AIRDROP_AMOUNT)
from nucypher.utilities.sandbox.middleware import MockRestMiddleware from nucypher.utilities.sandbox.middleware import MockRestMiddleware
from nucypher.utilities.sandbox.ursula import start_pytest_ursula_services from nucypher.utilities.sandbox.ursula import start_pytest_ursula_services
from web3 import Web3
from web3 import Web3 from web3 import Web3
STAKE_VALUE = Web3.fromWei(MIN_ALLOWED_LOCKED * 2, 'ether') STAKE_VALUE = NU(MIN_ALLOWED_LOCKED * 2, 'NuNit')
POLICY_RATE = Web3.toWei(21, 'gwei') POLICY_RATE = Web3.toWei(21, 'gwei')
POLICY_VALUE = POLICY_RATE * MIN_LOCKED_PERIODS # * len(ursula) POLICY_VALUE = POLICY_RATE * MIN_LOCKED_PERIODS # * len(ursula)
@ -43,7 +43,7 @@ def funded_blockchain(deployed_blockchain):
deployer_address, *everyone_else, staking_participant = blockchain.interface.w3.eth.accounts deployer_address, *everyone_else, staking_participant = blockchain.interface.w3.eth.accounts
# Free ETH!!! # Free ETH!!!
blockchain.ether_airdrop(amount=1000000) blockchain.ether_airdrop(amount=TESTING_ETH_AIRDROP_AMOUNT)
# Free Tokens!!! # Free Tokens!!!
token_airdrop(token_agent=NucypherTokenAgent(blockchain=blockchain), token_airdrop(token_agent=NucypherTokenAgent(blockchain=blockchain),
@ -148,7 +148,7 @@ def test_initialize_system_blockchain_configuration(click_runner,
def test_init_ursula_stake(click_runner, configuration_file_location, funded_blockchain): def test_init_ursula_stake(click_runner, configuration_file_location, funded_blockchain):
stake_args = ('ursula', 'stake', stake_args = ('ursula', 'stake',
'--config-file', configuration_file_location, '--config-file', configuration_file_location,
'--value', STAKE_VALUE, '--value', STAKE_VALUE.to_tokens(),
'--duration', MIN_LOCKED_PERIODS, '--duration', MIN_LOCKED_PERIODS,
'--poa', '--poa',
'--force') '--force')
@ -164,7 +164,7 @@ def test_init_ursula_stake(click_runner, configuration_file_location, funded_blo
stakes = list(miner_agent.get_all_stakes(miner_address=config_data['checksum_public_address'])) stakes = list(miner_agent.get_all_stakes(miner_address=config_data['checksum_public_address']))
assert len(stakes) == 1 assert len(stakes) == 1
start_period, end_period, value = stakes[0] start_period, end_period, value = stakes[0]
assert Web3.fromWei(value, 'ether') == int(STAKE_VALUE) assert NU(int(value), 'NuNit') == STAKE_VALUE
def test_list_ursula_stakes(click_runner, funded_blockchain, configuration_file_location): def test_list_ursula_stakes(click_runner, funded_blockchain, configuration_file_location):
@ -183,12 +183,13 @@ def test_list_ursula_stakes(click_runner, funded_blockchain, configuration_file_
def test_ursula_divide_stakes(click_runner, configuration_file_location): def test_ursula_divide_stakes(click_runner, configuration_file_location):
divide_args = ('ursula', 'divide-stake', divide_args = ('ursula', 'stake',
'--divide',
'--config-file', configuration_file_location, '--config-file', configuration_file_location,
'--poa', '--poa',
'--force', '--force',
'--index', 0, '--index', 0,
'--value', MIN_ALLOWED_LOCKED, '--value', NU(MIN_ALLOWED_LOCKED, 'NuNit').to_tokens(),
'--duration', 10) '--duration', 10)
result = click_runner.invoke(nucypher_cli, result = click_runner.invoke(nucypher_cli,
@ -205,7 +206,7 @@ def test_ursula_divide_stakes(click_runner, configuration_file_location):
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}' user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}'
result = click_runner.invoke(nucypher_cli, stake_args, input=user_input, catch_exceptions=False) result = click_runner.invoke(nucypher_cli, stake_args, input=user_input, catch_exceptions=False)
assert result.exit_code == 0 assert result.exit_code == 0
assert str(Web3.fromWei(MIN_ALLOWED_LOCKED, 'ether')) in result.output assert str(NU(MIN_ALLOWED_LOCKED, 'NuNit').to_tokens()) in result.output
def test_run_blockchain_ursula(click_runner, def test_run_blockchain_ursula(click_runner,

View File

@ -37,7 +37,7 @@ from nucypher.keystore import keystore
from nucypher.keystore.db import Base from nucypher.keystore.db import Base
from nucypher.utilities.sandbox.blockchain import token_airdrop, TesterBlockchain from nucypher.utilities.sandbox.blockchain import token_airdrop, TesterBlockchain
from nucypher.utilities.sandbox.constants import (MOCK_URSULA_STARTING_PORT, from nucypher.utilities.sandbox.constants import (MOCK_URSULA_STARTING_PORT,
MOCK_POLICY_DEFAULT_M) MOCK_POLICY_DEFAULT_M, TESTING_ETH_AIRDROP_AMOUNT)
from nucypher.utilities.sandbox.constants import (NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK, from nucypher.utilities.sandbox.constants import (NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT) DEVELOPMENT_TOKEN_AIRDROP_AMOUNT)
from nucypher.utilities.sandbox.middleware import MockRestMiddleware from nucypher.utilities.sandbox.middleware import MockRestMiddleware
@ -347,7 +347,7 @@ def testerchain(solidity_compiler):
origin, *everyone = testerchain.interface.w3.eth.accounts origin, *everyone = testerchain.interface.w3.eth.accounts
deployer_interface.deployer_address = origin # Set the deployer address from a freshly created test account deployer_interface.deployer_address = origin # Set the deployer address from a freshly created test account
testerchain.ether_airdrop(amount=1000000000) # TODO: Use test constant testerchain.ether_airdrop(amount=TESTING_ETH_AIRDROP_AMOUNT)
yield testerchain yield testerchain
testerchain.sever_connection() testerchain.sever_connection()