mirror of https://github.com/nucypher/nucypher.git
commit
3f94b93df3
|
@ -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
|
||||
...
|
||||
|
||||
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
|
||||
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
|
||||
`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
|
||||
|
||||
|
|
|
@ -4,30 +4,30 @@ NuCypher Staking Guide
|
|||
|
||||
**Ursula Staking Actions**
|
||||
|
||||
+-------------------+-------------------------------------------------------------------------+
|
||||
| Action | Description |
|
||||
+===================+=========================================================================+
|
||||
| `stake` | Initialize or view NuCpher 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) |
|
||||
+-------------------+-------------------------------------------------------------------------+
|
||||
+-------------------+------------------------------------------------------------------------------+
|
||||
| Action | Description |
|
||||
+===================+==============================================================================+
|
||||
| ``stake`` | Initialize or view NuCypher stakes (used with ``--value`` and ``--duration``)|
|
||||
+-------------------+------------------------------------------------------------------------------+
|
||||
| ``collect-reward``| Collect staking reward (Policy reward collection in future PR) |
|
||||
+-------------------+------------------------------------------------------------------------------+
|
||||
|
||||
|
||||
**Ursula Staking Options**
|
||||
|
||||
+----------------+----------------------------------------+
|
||||
| Option | Description |
|
||||
+================+========================================+
|
||||
| `--value` | Stake value |
|
||||
+----------------+----------------------------------------+
|
||||
| `--duration` | Stake duration of extension |
|
||||
+----------------+----------------------------------------+
|
||||
| `--index` | Stake index |
|
||||
+----------------+----------------------------------------+
|
||||
| `--list` | List stakes (used with `stake` action) |
|
||||
+----------------+----------------------------------------+
|
||||
+-----------------+--------------------------------------------+
|
||||
| Option | Description |
|
||||
+=================+============================================+
|
||||
| ``--value`` | Stake value |
|
||||
+-----------------+--------------------------------------------+
|
||||
| ``--duration`` | Stake duration of extension |
|
||||
+-----------------+--------------------------------------------+
|
||||
| ``--index`` | Stake index |
|
||||
+-----------------+--------------------------------------------+
|
||||
| ``--list`` | List stakes (used with ``stake`` action) |
|
||||
+-----------------+--------------------------------------------+
|
||||
| ``--divide`` | Divide stakes (used with ``stake`` action) |
|
||||
+-----------------+--------------------------------------------+
|
||||
|
||||
|
||||
|
||||
|
@ -39,7 +39,7 @@ Interactive Method
|
|||
(nucypher)$ nucypher ursula stake
|
||||
|
||||
|
||||
*Initial a new stake*
|
||||
*Initialize a new stake*
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
@ -92,7 +92,7 @@ Interactive Method
|
|||
|
||||
.. code:: bash
|
||||
|
||||
(nucypher)$ nucypher ursula divide-stake
|
||||
(nucypher)$ nucypher ursula stake --divide
|
||||
|
||||
|
||||
| # | Duration | Enact | Expiration | Value
|
||||
|
@ -100,7 +100,7 @@ Interactive Method
|
|||
| 0 | 32 periods . | yesterday | 14 Apr ... | 30000 NU
|
||||
|
||||
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
|
||||
|
||||
============================== ORIGINAL STAKE ============================
|
||||
|
@ -133,17 +133,17 @@ Inline Method
|
|||
+----------------+------------+--------------+
|
||||
|
||||
|
||||
*Stake 3000 NU for 90 Periods*
|
||||
*Stake 30000 NU for 90 Periods*
|
||||
|
||||
.. 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
|
||||
|
||||
(nucypher)$ nucypher ursula divide-stake --index 0 --value 1500 --duration 30
|
||||
(nucypher)$ nucypher ursula stake --divide --index 0 --value 15000 --duration 30
|
||||
...
|
|
@ -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
|
||||
|
||||
*Decentralized Ursula Configuration*
|
||||
|
||||
.. code:: bash
|
||||
|
||||
(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:
|
||||
|
||||
- *ipc:///tmp/geth.ipc* - Geth Development Node (IPC; Also use --poa flag)
|
||||
- *http://localhost:7545* - Ganache TestRPC (HTTP-JSON-RPC)
|
||||
- *ws://0.0.0.0:8080* - Websocket Provider
|
||||
- ``ipc:///tmp/geth.ipc`` - Geth Development Node (IPC; **Note**: Also use ``--poa`` flag)
|
||||
- ``http://localhost:7545`` - Ganache TestRPC (HTTP-JSON-RPC)
|
||||
- ``ws://0.0.0.0:8080`` - Websocket Provider
|
||||
|
||||
2(b). Configure a new Ursula node
|
||||
|
||||
*Federated Ursula Initialization*
|
||||
|
||||
.. code:: bash
|
||||
|
||||
(nucypher)$ nucypher ursula init --federated-only --network <NETWORK NAME>
|
||||
|
|
|
@ -18,26 +18,26 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
import json
|
||||
from collections import OrderedDict
|
||||
from json import JSONDecodeError
|
||||
from twisted.logger import Logger
|
||||
|
||||
import maya
|
||||
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 twisted.internet import task, reactor
|
||||
from twisted.logger import Logger
|
||||
from typing import Tuple, List, Dict, Union
|
||||
|
||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, PolicyAgent
|
||||
from nucypher.blockchain.eth.chains import Blockchain
|
||||
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.registry import EthereumContractRegistry, AllocationRegistry
|
||||
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
|
||||
from nucypher.blockchain.eth.registry import AllocationRegistry
|
||||
from nucypher.blockchain.eth.utils import (datetime_to_period,
|
||||
validate_stake_amount,
|
||||
validate_locktime,
|
||||
calculate_period_duration)
|
||||
from nucypher.blockchain.eth.token import NU, Stake
|
||||
|
||||
|
||||
def only_me(func):
|
||||
|
@ -95,10 +95,11 @@ class NucypherTokenActor:
|
|||
return balance
|
||||
|
||||
@property
|
||||
def token_balance(self):
|
||||
def token_balance(self) -> NU:
|
||||
"""Return this actors's current token balance"""
|
||||
balance = self.token_agent.get_balance(address=self.checksum_public_address)
|
||||
return balance
|
||||
balance = int(self.token_agent.get_balance(address=self.checksum_public_address))
|
||||
nu_balance = NU(balance, 'NuNit')
|
||||
return nu_balance
|
||||
|
||||
|
||||
class Deployer(NucypherTokenActor):
|
||||
|
@ -292,23 +293,25 @@ class Miner(NucypherTokenActor):
|
|||
|
||||
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:
|
||||
self.stake()
|
||||
|
||||
|
||||
#
|
||||
# Staking
|
||||
#
|
||||
@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"""
|
||||
|
||||
# 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)
|
||||
# Get the last stake end period of all stakes
|
||||
terminal_period = max(stake.end_period for stake in self.stakes.values())
|
||||
|
||||
if confirm_now:
|
||||
self.confirm_activity()
|
||||
|
@ -320,10 +323,6 @@ class Miner(NucypherTokenActor):
|
|||
self.__current_period = self.__uptime_period
|
||||
self.start_staking_loop()
|
||||
|
||||
#
|
||||
# Daemon
|
||||
#
|
||||
|
||||
@only_me
|
||||
def _confirm_period(self):
|
||||
|
||||
|
@ -381,16 +380,26 @@ class Miner(NucypherTokenActor):
|
|||
return self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address)
|
||||
|
||||
@property
|
||||
def total_staked(self) -> int:
|
||||
def total_staked(self) -> NU:
|
||||
if self.stakes:
|
||||
return sum(stake[-1] for stake in self.stakes)
|
||||
return 0
|
||||
return NU(sum(int(stake.value) for stake in self.stakes.values()), 'NuNit')
|
||||
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
|
||||
def stakes(self) -> Tuple[list]:
|
||||
"""Read all live stake data from the blockchain and return it as a tuple"""
|
||||
stakes_reader = self.miner_agent.get_all_stakes(miner_address=self.checksum_public_address)
|
||||
return tuple(stakes_reader)
|
||||
def stakes(self) -> Dict[str, Stake]:
|
||||
"""Return all cached stakes from the blockchain."""
|
||||
return self.__stakes
|
||||
|
||||
@only_me
|
||||
def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]:
|
||||
|
@ -409,7 +418,7 @@ class Miner(NucypherTokenActor):
|
|||
@only_me
|
||||
def divide_stake(self,
|
||||
stake_index: int,
|
||||
target_value: int,
|
||||
target_value: NU,
|
||||
additional_periods: int = None,
|
||||
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
|
||||
locking schedule of this miner, or an exception will be raised.
|
||||
|
||||
:param target_value: The quantity of tokens in the smallest denomination.
|
||||
:param expiration: The new expiration date to set.
|
||||
: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
|
||||
|
||||
"""
|
||||
|
@ -427,34 +438,35 @@ class Miner(NucypherTokenActor):
|
|||
if additional_periods and expiration:
|
||||
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:
|
||||
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:
|
||||
raise self.MinerError("Expiration {} must be at least 1 period from now.".format(expiration))
|
||||
|
||||
if target_value >= locked_value:
|
||||
raise self.MinerError("Cannot divide stake; Value must be less than the specified stake value.")
|
||||
if target_value >= stake.value:
|
||||
raise self.MinerError(f"Cannot divide stake; Value ({target_value}) must be less "
|
||||
f"than the existing stake value {stake.value}.")
|
||||
|
||||
# Ensure both halves are for valid amounts
|
||||
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,
|
||||
stake_index=stake_index,
|
||||
target_value=target_value,
|
||||
target_value=int(target_value),
|
||||
periods=additional_periods)
|
||||
|
||||
self.blockchain.wait_for_receipt(tx)
|
||||
self.__read_stakes() # update local on-chain stake cache
|
||||
return tx
|
||||
|
||||
@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..?
|
||||
assert validate_locktime(lock_periods=lock_periods)
|
||||
validate_stake_amount(amount=amount)
|
||||
validate_locktime(lock_periods=lock_periods)
|
||||
|
||||
if not self.token_balance >= amount:
|
||||
raise self.MinerError("Insufficient miner token balance ({balance})".format(balance=self.token_balance))
|
||||
|
@ -463,7 +475,7 @@ class Miner(NucypherTokenActor):
|
|||
|
||||
@only_me
|
||||
def initialize_stake(self,
|
||||
amount: int,
|
||||
amount: NU,
|
||||
lock_periods: int = None,
|
||||
expiration: maya.MayaDT = None,
|
||||
entire_balance: bool = False) -> dict:
|
||||
|
@ -484,20 +496,24 @@ class Miner(NucypherTokenActor):
|
|||
|
||||
if expiration:
|
||||
lock_periods = calculate_period_duration(future_time=expiration)
|
||||
|
||||
if entire_balance is True:
|
||||
amount = self.token_balance
|
||||
|
||||
amount = NU(int(amount), 'NuNit')
|
||||
|
||||
staking_transactions = OrderedDict() # type: OrderedDict # Time series of txhases
|
||||
|
||||
# Validate
|
||||
assert self.__validate_stake(amount=amount, lock_periods=lock_periods)
|
||||
|
||||
# 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))
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
"""Nucypher Token and Miner constants."""
|
||||
|
||||
ONE_YEAR_IN_SECONDS = 31540000
|
||||
NUCYPHER_GAS_LIMIT = 6500000 # TODO: move elsewhere?
|
||||
|
||||
#
|
||||
# Dispatcher
|
||||
|
@ -38,28 +36,19 @@ POLICY_ID_LENGTH = 16
|
|||
|
||||
NULL_ADDRESS = '0x' + '0' * 40
|
||||
|
||||
__subdigits = 18
|
||||
M = 10 ** __subdigits # Unit designation
|
||||
# Denomination
|
||||
TOKEN_DECIMALS = 18
|
||||
NUNITS_PER_TOKEN = 10 ** TOKEN_DECIMALS # Smallest unit designation
|
||||
|
||||
__initial_supply = int(1e9) * M # Initial token supply
|
||||
__saturation = int(3.89e9) * M # Token supply cap
|
||||
TOKEN_SUPPLY = __saturation - __initial_supply # Remaining supply
|
||||
# Supply
|
||||
__initial_supply = int(1e9) * NUNITS_PER_TOKEN # Initial token supply
|
||||
__saturation = int(3.89e9) * NUNITS_PER_TOKEN # Token supply cap
|
||||
TOKEN_SUPPLY = __saturation - __initial_supply # Remaining supply
|
||||
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:
|
||||
(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
|
||||
2 * 10 ** 7, # Mining coefficient (k2)
|
||||
K2, # Mining coefficient (k2)
|
||||
MAX_MINTING_PERIODS, # Locked periods coefficient (k1)
|
||||
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_ALLOWED_LOCKED, # Min amount of tokens that can be locked
|
||||
MAX_ALLOWED_LOCKED # Max amount of tokens that can be locked
|
||||
)
|
||||
|
||||
MINING_COEFFICIENT = __mining_coeff
|
||||
|
|
|
@ -34,7 +34,6 @@ from constant_sorrow.constants import (
|
|||
)
|
||||
from eth_tester import EthereumTester
|
||||
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.sol.compile import SolidityCompiler
|
||||
|
||||
|
@ -232,7 +231,8 @@ class BlockchainInterface:
|
|||
if uri_breakdown.scheme == 'tester':
|
||||
|
||||
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)
|
||||
eth_tester = EthereumTester(backend=pyevm_backend, auto_mine_transactions=True)
|
||||
provider = EthereumTesterProvider(ethereum_tester=eth_tester)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import maya
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
"""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)
|
||||
|
||||
|
||||
def datetime_at_period(period: int) -> maya.MayaDT:
|
||||
"""Returns the datetime object at a given period, future, or past."""
|
||||
|
||||
now = maya.now()
|
||||
current_period = datetime_to_period(datetime=now)
|
||||
|
||||
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())
|
||||
periods = future_period - current_period
|
||||
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)
|
||||
|
|
|
@ -98,7 +98,7 @@ def confirm_staged_stake(ursula, value, duration):
|
|||
* 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.
|
||||
|
||||
|
|
|
@ -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-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('--m', help="M", type=click.INT)
|
||||
@click.option('--n', help="N", type=click.INT)
|
||||
@click.option('--m', help="M-Threshold KFrags", 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('--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)
|
||||
|
|
|
@ -17,14 +17,12 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
"""
|
||||
|
||||
import click
|
||||
import maya
|
||||
from constant_sorrow.constants import TEMPORARY_DOMAIN
|
||||
from twisted.internet import stdio
|
||||
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.utils import datetime_at_period, calculate_period_duration
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.characters.banners import URSULA_BANNER
|
||||
from nucypher.cli import actions, painting
|
||||
from nucypher.cli.actions import destroy_system_configuration
|
||||
|
@ -34,7 +32,10 @@ from nucypher.cli.types import (
|
|||
EIP55_CHECKSUM_ADDRESS,
|
||||
NETWORK_PORT,
|
||||
EXISTING_READABLE_FILE,
|
||||
STAKE_DURATION, STAKE_EXTENSION)
|
||||
STAKE_DURATION,
|
||||
STAKE_EXTENSION,
|
||||
STAKE_VALUE
|
||||
)
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.utilities.sandbox.constants import (
|
||||
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('--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('--divide', '-d', help="Divide an existing stake into sub-stakes.", is_flag=True)
|
||||
@nucypher_click_config
|
||||
def ursula(click_config,
|
||||
action,
|
||||
|
@ -95,7 +97,8 @@ def ursula(click_config,
|
|||
value,
|
||||
duration,
|
||||
index,
|
||||
list_
|
||||
list_,
|
||||
divide
|
||||
) -> None:
|
||||
"""
|
||||
Manage and run an "Ursula" PRE node.
|
||||
|
@ -112,7 +115,6 @@ def ursula(click_config,
|
|||
destroy Delete Ursula node configuration.
|
||||
stake Manage stakes for this node.
|
||||
confirm-activity Manually confirm-activity for the current period.
|
||||
divide-stake Divide an existing stake.
|
||||
collect-reward Withdraw staking reward.
|
||||
|
||||
"""
|
||||
|
@ -139,7 +141,7 @@ def ursula(click_config,
|
|||
click.secho("WARNING: Force is enabled", fg='yellow')
|
||||
|
||||
#
|
||||
# Unauthenticated Configurations & Unconfigured Ursula Control
|
||||
# Unauthenticated Configurations & Un-configured Ursula Control
|
||||
#
|
||||
if action == "init":
|
||||
"""Create a brand-new persistent Ursula"""
|
||||
|
@ -169,7 +171,10 @@ def ursula(click_config,
|
|||
provider_uri=provider_uri,
|
||||
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
|
||||
|
||||
#
|
||||
|
@ -255,9 +260,8 @@ def ursula(click_config,
|
|||
bold=True)
|
||||
|
||||
if not URSULA.federated_only and URSULA.stakes:
|
||||
total = URSULA.blockchain.interface.w3.fromWei(URSULA.total_staked, 'ether')
|
||||
click_config.emitter(
|
||||
message=f"Staking {total} NU ~ Keep Ursula Online!",
|
||||
message=f"Staking {str(URSULA.total_staked)} ~ Keep Ursula Online!",
|
||||
color='blue',
|
||||
bold=True)
|
||||
|
||||
|
@ -320,12 +324,61 @@ def ursula(click_config,
|
|||
|
||||
elif action == 'stake':
|
||||
|
||||
# List only
|
||||
# List Only
|
||||
if list_:
|
||||
live_stakes = list(URSULA.miner_agent.get_all_stakes(miner_address=URSULA.checksum_public_address))
|
||||
if not live_stakes:
|
||||
if not URSULA.stakes:
|
||||
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
|
||||
|
||||
# Confirm new stake init
|
||||
|
@ -333,7 +386,7 @@ def ursula(click_config,
|
|||
click.confirm("Stage a new stake?", abort=True)
|
||||
|
||||
# Validate balance
|
||||
balance = URSULA.token_agent.get_balance(address=URSULA.checksum_public_address)
|
||||
balance = URSULA.token_balance
|
||||
if balance == 0:
|
||||
click.secho(f"{ursula.checksum_public_address} has 0 NU.")
|
||||
raise click.Abort
|
||||
|
@ -342,10 +395,9 @@ def ursula(click_config,
|
|||
|
||||
# Gather stake value
|
||||
if not value:
|
||||
min_stake_nu = int(URSULA.blockchain.interface.w3.fromWei(MIN_ALLOWED_LOCKED, 'ether'))
|
||||
value = click.prompt(f"Enter stake value in NU", type=click.INT, default=min_stake_nu)
|
||||
stake_wei = URSULA.blockchain.interface.w3.toWei(value, 'ether')
|
||||
stake_nu = value
|
||||
value = click.prompt(f"Enter stake value", type=STAKE_VALUE, default=NU(MIN_ALLOWED_LOCKED, 'NuNit'))
|
||||
else:
|
||||
value = NU(int(value), 'NU')
|
||||
|
||||
# Duration
|
||||
if not quiet:
|
||||
|
@ -359,8 +411,7 @@ def ursula(click_config,
|
|||
# Review
|
||||
if not force:
|
||||
painting.paint_staged_stake(ursula=URSULA,
|
||||
stake_nu=stake_nu,
|
||||
stake_wei=stake_wei,
|
||||
stake_value=value,
|
||||
duration=duration,
|
||||
start_period=start_period,
|
||||
end_period=end_period)
|
||||
|
@ -372,80 +423,17 @@ def ursula(click_config,
|
|||
if not force:
|
||||
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)
|
||||
return
|
||||
|
||||
elif action == 'confirm-activity':
|
||||
stakes = URSULA.miner_agent.get_all_stakes(miner_address=URSULA.checksum_public_address)
|
||||
if len(stakes) == 0:
|
||||
if not URSULA.stakes:
|
||||
click.secho("There are no active stakes for {}".format(URSULA.checksum_public_address))
|
||||
return
|
||||
URSULA.miner_agent.confirm_activity(node_address=URSULA.checksum_public_address)
|
||||
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':
|
||||
"""Withdraw staking reward to the specified wallet address"""
|
||||
if not force:
|
||||
|
|
|
@ -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/>.
|
||||
"""
|
||||
import os
|
||||
from typing import Tuple
|
||||
from decimal import Decimal
|
||||
|
||||
import click
|
||||
import maya
|
||||
from constant_sorrow.constants import NO_KNOWN_NODES
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.blockchain.eth.utils import datetime_at_period
|
||||
from nucypher.characters.banners import NUCYPHER_BANNER
|
||||
|
@ -37,14 +36,18 @@ def echo_version(ctx, param, value):
|
|||
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_name = character_config_class._NAME.lower()
|
||||
|
||||
emitter(message="Generated keyring {}".format(new_configuration.keyring_dir), 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...
|
||||
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"
|
||||
|
@ -194,8 +197,7 @@ def paint_contract_status(ursula_config, click_config):
|
|||
|
||||
|
||||
def paint_staged_stake(ursula,
|
||||
stake_nu,
|
||||
stake_wei,
|
||||
stake_value,
|
||||
duration,
|
||||
start_period,
|
||||
end_period,
|
||||
|
@ -209,7 +211,7 @@ def paint_staged_stake(ursula,
|
|||
|
||||
click.echo(f"""
|
||||
{ursula}
|
||||
~ Value -> {stake_nu} NU ({stake_wei} NU-wei)
|
||||
~ Value -> {stake_value} ({Decimal(int(stake_value)):.2E} NuNit)
|
||||
~ Duration -> {duration} Days ({duration} Periods)
|
||||
~ Enactment -> {datetime_at_period(period=start_period)} (period #{start_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')
|
||||
|
||||
|
||||
def prettify_stake(stake_index: int, stake_info: Tuple[int, int, str]) -> str:
|
||||
start, expiration, stake_wei = stake_info
|
||||
def prettify_stake(stake_index: int, stake) -> str:
|
||||
|
||||
stake_nu = int(Web3.fromWei(stake_wei, 'ether'))
|
||||
|
||||
start_datetime = str(datetime_at_period(period=start).slang_date())
|
||||
expiration_datetime = str(datetime_at_period(period=expiration).slang_date())
|
||||
duration = expiration - start
|
||||
start_datetime = str(stake.start_datetime.slang_date())
|
||||
expiration_datetime = str(stake.end_datetime.slang_date())
|
||||
duration = stake.duration
|
||||
|
||||
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
|
||||
|
||||
|
||||
def paint_stakes(stakes):
|
||||
header = f'| # | Duration | Enact | Expiration | Value '
|
||||
breaky = f'| - | ------------ | --------- | -----------| ----- '
|
||||
header = f'| # | Duration | Enact | Expiration | Value '
|
||||
breaky = f'| - | ------------ | ----------- | -----------| ----- '
|
||||
click.secho(header, bold=True)
|
||||
click.secho(breaky, bold=True)
|
||||
for index, stake_info in enumerate(stakes):
|
||||
row = prettify_stake(stake_index=index, stake_info=stake_info)
|
||||
for index, stake in stakes.items():
|
||||
row = prettify_stake(stake_index=index, stake=stake)
|
||||
click.echo(row)
|
||||
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)
|
||||
|
|
|
@ -20,8 +20,13 @@ from ipaddress import ip_address
|
|||
import click
|
||||
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
|
||||
)
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
|
||||
|
||||
class ChecksumAddress(click.ParamType):
|
||||
|
@ -45,11 +50,17 @@ class IPv4Address(click.ParamType):
|
|||
return value
|
||||
|
||||
|
||||
# Staking
|
||||
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_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_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)
|
||||
IPV4_ADDRESS = IPv4Address()
|
||||
EIP55_CHECKSUM_ADDRESS = ChecksumAddress()
|
||||
|
|
|
@ -382,7 +382,6 @@ class NodeConfiguration(ABC):
|
|||
storage_type = storage_payload[NodeStorage._TYPE_LABEL]
|
||||
storage_class = node_storage_subclasses[storage_type]
|
||||
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'],
|
||||
serializer=cls.NODE_SERIALIZER,
|
||||
deserializer=cls.NODE_DESERIALIZER)
|
||||
|
@ -552,7 +551,7 @@ class NodeConfiguration(ABC):
|
|||
|
||||
# Keyring
|
||||
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)
|
||||
|
||||
# Registry
|
||||
|
|
|
@ -24,7 +24,9 @@ from datetime import datetime
|
|||
from random import SystemRandom
|
||||
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.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')
|
||||
|
||||
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
|
||||
|
||||
|
@ -76,6 +78,8 @@ INSECURE_DEVELOPMENT_PASSWORD = ''.join(SystemRandom().choice(ascii_uppercase +
|
|||
|
||||
MAX_TEST_SEEDER_ENTRIES = 20
|
||||
|
||||
TESTING_ETH_AIRDROP_AMOUNT = int(Web3().fromWei(100, 'ether'))
|
||||
|
||||
|
||||
#
|
||||
# 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())
|
||||
|
||||
TEMPORARY_DOMAIN = 'TEMPORARY_DOMAIN'
|
||||
|
||||
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
|
||||
|
@ -116,3 +118,4 @@ MOCK_IP_ADDRESS_2 = '10.10.10.10'
|
|||
|
||||
MOCK_URSULA_DB_FILEPATH = ':memory:'
|
||||
|
||||
PYEVM_GAS_LIMIT = 6500000 # TODO: move elsewhere (used to set pyevm gas limit in tests)?
|
|
@ -28,6 +28,7 @@ from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
|
|||
from nucypher.blockchain.eth.registry import InMemoryEthereumContractRegistry, InMemoryAllocationRegistry
|
||||
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
|
||||
from nucypher.utilities.sandbox.blockchain import TesterBlockchain
|
||||
from nucypher.utilities.sandbox.constants import TESTING_ETH_AIRDROP_AMOUNT
|
||||
|
||||
|
||||
@pytest.mark.slow()
|
||||
|
@ -39,7 +40,7 @@ def test_rapid_deployment():
|
|||
registry=registry,
|
||||
provider_uri='tester://pyevm')
|
||||
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
|
||||
|
||||
deployer = Deployer(blockchain=blockchain,
|
||||
|
|
|
@ -19,8 +19,9 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
import maya
|
||||
import pytest
|
||||
|
||||
from nucypher.blockchain.eth import constants
|
||||
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.constants import DEVELOPMENT_TOKEN_AIRDROP_AMOUNT
|
||||
|
||||
|
@ -37,12 +38,11 @@ def miner(testerchain, three_agents):
|
|||
@pytest.mark.slow()
|
||||
def test_miner_locking_tokens(testerchain, three_agents, miner):
|
||||
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)
|
||||
miner.initialize_stake(amount=int(constants.MIN_ALLOWED_LOCKED), # Lock the minimum amount of tokens
|
||||
expiration = maya.now().add(days=MIN_LOCKED_PERIODS)
|
||||
miner.initialize_stake(amount=NU(MIN_ALLOWED_LOCKED, 'NuNit'), # Lock the minimum amount of tokens
|
||||
expiration=expiration)
|
||||
|
||||
# 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()
|
||||
assert 0 == locked_tokens
|
||||
|
||||
# testerchain.time_travel(periods=1)
|
||||
|
||||
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.usefixtures("three_agents")
|
||||
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()
|
||||
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_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 expected_old_stake == stakes[stake_index], 'Old stake values are invalid'
|
||||
assert expected_new_stake == stakes[stake_index + 1], 'New stake values are invalid'
|
||||
assert 3 == len(miner.stakes), 'A new stake was not added to this miners stakes'
|
||||
assert expected_old_stake == miner.stakes[stake_index+1].to_stake_info(), 'Old 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)
|
||||
miner.divide_stake(target_value=yet_another_stake_value, stake_index=stake_index + 1, additional_periods=2)
|
||||
yet_another_stake_value = NU(MIN_ALLOWED_LOCKED, 'NuNit')
|
||||
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_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 expected_old_stake == stakes[stake_index], '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_yet_another_stake == stakes[stake_index + 2], 'Third stake values are invalid'
|
||||
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'
|
||||
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 == miner.stakes[stake_index + 3], 'Third stake values are invalid'
|
||||
|
||||
|
||||
@pytest.mark.slow()
|
||||
|
@ -102,17 +102,16 @@ def test_miner_collects_staking_reward(testerchain, miner, three_agents):
|
|||
initial_balance = miner.token_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
|
||||
lock_periods=int(constants.MIN_LOCKED_PERIODS)) # ... for the fewest number of periods
|
||||
miner.initialize_stake(amount=NU(MIN_ALLOWED_LOCKED, 'NuNit'), # Lock the minimum amount of tokens
|
||||
lock_periods=int(MIN_LOCKED_PERIODS)) # ... for the fewest number of periods
|
||||
|
||||
# ...wait out the lock period...
|
||||
for _ in range(28):
|
||||
for _ in range(MIN_LOCKED_PERIODS):
|
||||
testerchain.time_travel(periods=1)
|
||||
miner.confirm_activity()
|
||||
|
||||
# ...wait more...
|
||||
testerchain.time_travel(periods=2)
|
||||
miner.mint()
|
||||
miner.collect_staking_reward()
|
||||
|
||||
final_balance = token_agent.get_balance(miner.checksum_public_address)
|
||||
|
|
|
@ -18,13 +18,14 @@ import pytest
|
|||
|
||||
from nucypher.blockchain.eth import constants
|
||||
from nucypher.blockchain.eth.actors import PolicyAuthor
|
||||
from nucypher.utilities.sandbox.constants import TESTING_ETH_AIRDROP_AMOUNT
|
||||
|
||||
|
||||
@pytest.mark.slow()
|
||||
@pytest.fixture(scope='module')
|
||||
def author(testerchain, 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
|
||||
author = PolicyAuthor(checksum_address=alice)
|
||||
return author
|
||||
|
@ -32,7 +33,6 @@ def author(testerchain, three_agents):
|
|||
|
||||
@pytest.mark.slow()
|
||||
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
|
||||
policy_author = PolicyAuthor(checksum_address=alice)
|
||||
assert policy_author.checksum_public_address == alice
|
||||
|
|
|
@ -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()
|
|
@ -33,7 +33,7 @@ from nucypher.blockchain.eth.sol.compile import SolidityCompiler
|
|||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.utilities.sandbox.blockchain import TesterBlockchain
|
||||
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
|
||||
|
||||
|
||||
|
@ -111,7 +111,7 @@ def deployed_blockchain():
|
|||
# Blockchain
|
||||
#
|
||||
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
|
||||
|
||||
#
|
||||
|
|
|
@ -12,6 +12,7 @@ from umbral.keys import UmbralPublicKey
|
|||
from nucypher.blockchain.eth.actors import Miner
|
||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent
|
||||
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.cli.main import nucypher_cli
|
||||
from nucypher.config.characters import UrsulaConfiguration, BobConfiguration
|
||||
|
@ -22,15 +23,14 @@ from nucypher.utilities.sandbox.constants import (
|
|||
TEST_PROVIDER_URI,
|
||||
MOCK_URSULA_STARTING_PORT,
|
||||
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.ursula import start_pytest_ursula_services
|
||||
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_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
|
||||
|
||||
# Free ETH!!!
|
||||
blockchain.ether_airdrop(amount=1000000)
|
||||
blockchain.ether_airdrop(amount=TESTING_ETH_AIRDROP_AMOUNT)
|
||||
|
||||
# Free Tokens!!!
|
||||
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):
|
||||
stake_args = ('ursula', 'stake',
|
||||
'--config-file', configuration_file_location,
|
||||
'--value', STAKE_VALUE,
|
||||
'--value', STAKE_VALUE.to_tokens(),
|
||||
'--duration', MIN_LOCKED_PERIODS,
|
||||
'--poa',
|
||||
'--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']))
|
||||
assert len(stakes) == 1
|
||||
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):
|
||||
|
@ -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):
|
||||
|
||||
divide_args = ('ursula', 'divide-stake',
|
||||
divide_args = ('ursula', 'stake',
|
||||
'--divide',
|
||||
'--config-file', configuration_file_location,
|
||||
'--poa',
|
||||
'--force',
|
||||
'--index', 0,
|
||||
'--value', MIN_ALLOWED_LOCKED,
|
||||
'--value', NU(MIN_ALLOWED_LOCKED, 'NuNit').to_tokens(),
|
||||
'--duration', 10)
|
||||
|
||||
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}'
|
||||
result = click_runner.invoke(nucypher_cli, stake_args, input=user_input, catch_exceptions=False)
|
||||
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,
|
||||
|
|
|
@ -37,7 +37,7 @@ from nucypher.keystore import keystore
|
|||
from nucypher.keystore.db import Base
|
||||
from nucypher.utilities.sandbox.blockchain import token_airdrop, TesterBlockchain
|
||||
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,
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT)
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
|
@ -347,7 +347,7 @@ def testerchain(solidity_compiler):
|
|||
|
||||
origin, *everyone = testerchain.interface.w3.eth.accounts
|
||||
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
|
||||
testerchain.sever_connection()
|
||||
|
|
Loading…
Reference in New Issue