Validator class (#8)
* [account] Add basic check for valid ONE address * [rpc] Add ability to access error message from RPCError * [validator] Inital commit of Validator class * [validator] Add pretty print option to JSON export [validator] Fix TypeError when setting string values * [validator] Update error message * [validator] Address PR comments [exceptions] Address PR commentsfix
parent
ea6926ab4d
commit
5bb67df3e7
@ -0,0 +1,636 @@ |
||||
import json |
||||
|
||||
from decimal import ( |
||||
Decimal, |
||||
InvalidOperation |
||||
) |
||||
|
||||
from .account import ( |
||||
get_balance, |
||||
is_valid_address |
||||
) |
||||
|
||||
from .exceptions import ( |
||||
InvalidValidatorError, |
||||
RPCError, |
||||
RequestsError, |
||||
RequestsTimeoutError |
||||
) |
||||
|
||||
from .numbers import ( |
||||
convert_atto_to_one, |
||||
convert_one_to_atto |
||||
) |
||||
|
||||
from .staking import ( |
||||
get_all_validator_addresses, |
||||
get_validator_information |
||||
) |
||||
|
||||
|
||||
_default_endpoint = 'http://localhost:9500' |
||||
_default_timeout = 30 |
||||
|
||||
|
||||
# TODO: Add validator transcation functions |
||||
# TODO: Add unit testing |
||||
class Validator: |
||||
|
||||
name_char_limit = 140 |
||||
identity_char_limit = 140 |
||||
website_char_limit = 140 |
||||
security_contact_char_limit = 140 |
||||
details_char_limit = 280 |
||||
min_required_delegation = 10000 |
||||
|
||||
def __init__(self, address): |
||||
if not is_valid_address(address): |
||||
raise InvalidValidatorError(1, f'{address} is not valid ONE address') |
||||
self._address = address |
||||
self._bls_keys = [] |
||||
|
||||
self._name = None |
||||
self._identity = None |
||||
self._website = None |
||||
self._details = None |
||||
self._security_contact = None |
||||
|
||||
self._min_self_delegation = None |
||||
self._max_total_delegation = None |
||||
self._inital_delegation = None |
||||
|
||||
self._rate = None |
||||
self._max_change_rate = None |
||||
self._max_rate = None |
||||
|
||||
def __str__(self) -> str: |
||||
""" |
||||
Returns JSON string representation of Validator fields |
||||
""" |
||||
info = self.export() |
||||
for key, value in info.items(): |
||||
if isinstance(value, Decimal): |
||||
info[key] = str(value) |
||||
return json.dumps(info) |
||||
|
||||
def __repr__(self) -> str: |
||||
return f'<Validator: {hex(id(self))}>' |
||||
|
||||
def get_address(self) -> str: |
||||
""" |
||||
Get validator address |
||||
|
||||
Returns |
||||
------- |
||||
str |
||||
Validator address |
||||
""" |
||||
return self._address |
||||
|
||||
def add_bls_key(self, key) -> bool: |
||||
""" |
||||
Add BLS public key to validator BLS keys if not already in list |
||||
|
||||
Returns |
||||
------- |
||||
bool |
||||
If adding BLS key succeeded |
||||
""" |
||||
if key not in self.bls_keys: |
||||
self.bls_keys.append(key) |
||||
return True |
||||
return False |
||||
|
||||
def remove_bls_key(self, key) -> bool: |
||||
""" |
||||
Remove BLS public key from validator BLS keys if exists |
||||
|
||||
Returns |
||||
------- |
||||
bool |
||||
If removing BLS key succeeded |
||||
""" |
||||
if key in self.bls_keys: |
||||
self.bls_keys.remove(key) |
||||
return True |
||||
return False |
||||
|
||||
def get_bls_keys(self) -> list: |
||||
""" |
||||
Get list of validator BLS keys |
||||
|
||||
Returns |
||||
------- |
||||
list |
||||
List of validator BLS keys |
||||
""" |
||||
return self._bls_keys |
||||
|
||||
def set_name(self, name): |
||||
""" |
||||
Set validator name |
||||
|
||||
Parameters |
||||
---------- |
||||
name: str |
||||
Name of validator |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
name = str(name) |
||||
if len(name) > self.name_char_limit: |
||||
raise InvalidValidatorError(3, f'Name must be less than {self.name_char_limit} characters') |
||||
self._name = name |
||||
|
||||
def get_name(self) -> str: |
||||
""" |
||||
Get validator name |
||||
|
||||
Returns |
||||
------- |
||||
str |
||||
Validator name |
||||
""" |
||||
return self._name |
||||
|
||||
def set_identity(self, identity): |
||||
""" |
||||
Set validator identity |
||||
|
||||
Parameters |
||||
---------- |
||||
identity: str |
||||
Identity of validator |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
identity = str(identity) |
||||
if len(identity) > self.identity_char_limit: |
||||
raise InvalidValidatorError(3, f'Identity must be less than {self.identity_char_limit} characters') |
||||
self._identity = identity |
||||
|
||||
def get_identity(self) -> str: |
||||
""" |
||||
Get validator identity |
||||
|
||||
Returns |
||||
------- |
||||
str |
||||
Validator identity |
||||
""" |
||||
return self._identity |
||||
|
||||
def set_website(self, website): |
||||
""" |
||||
Set validator website |
||||
|
||||
Parameters |
||||
---------- |
||||
website: str |
||||
Website of validator |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
website = str(website) |
||||
if len(website) > self.website_char_limit: |
||||
raise InvalidValidatorError(3, f'Website must be less than {self.website_char_limit} characters') |
||||
self._website = website |
||||
|
||||
def get_website(self) -> str: |
||||
""" |
||||
Get validator website |
||||
|
||||
Returns |
||||
------- |
||||
str |
||||
Validator website |
||||
""" |
||||
return self._website |
||||
|
||||
def set_security_contact(self, contact): |
||||
""" |
||||
Set validator security contact |
||||
|
||||
Parameters |
||||
---------- |
||||
contact: str |
||||
Security contact of validator |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
contact = str(contact) |
||||
if len(contact) > self.security_contact_char_limit: |
||||
raise InvalidValidatorError(3, f'Security contact must be less than {self.security_contact_char_limit} characters') |
||||
self._security_contact = contact |
||||
|
||||
def get_security_contact(self) -> str: |
||||
""" |
||||
Get validator security contact |
||||
|
||||
Returns |
||||
------- |
||||
str |
||||
Validator security contact |
||||
""" |
||||
return self._security_contact |
||||
|
||||
def set_details(self, details): |
||||
""" |
||||
Set validator details |
||||
|
||||
Parameters |
||||
---------- |
||||
details: str |
||||
Details of validator |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
details = str(details) |
||||
if len(details) > self.details_char_limit: |
||||
raise InvalidValidatorError(3, f'Details must be less than {self.details_char_limit} characters') |
||||
self._details = details |
||||
|
||||
def get_details(self) -> str: |
||||
""" |
||||
Get validator details |
||||
|
||||
Returns |
||||
------- |
||||
str |
||||
Validator details |
||||
""" |
||||
return self._details |
||||
|
||||
def set_min_self_delegation(self, min): |
||||
""" |
||||
Set validator min self delegation |
||||
|
||||
Parameters |
||||
---------- |
||||
min: str |
||||
Minimum self delegation of validator in ONE |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
try: |
||||
min = Decimal(min) |
||||
except InvalidOperation as e: |
||||
raise InvalidValidatorError(3, f'Min self delegation must be a number') from e |
||||
if min < self.min_required_delegation: |
||||
raise InvalidValidatorError(3, f'Min self delegation must be greater than {self.min_required_delegation} ONE') |
||||
self._min_self_delegation = min.normalize() |
||||
|
||||
def get_min_self_delegation(self) -> Decimal: |
||||
""" |
||||
Get validator min self delegation |
||||
|
||||
Returns |
||||
------- |
||||
Decimal |
||||
Validator min self delegation in ONE |
||||
""" |
||||
return self._min_self_delegation |
||||
|
||||
def set_max_total_delegation(self, max): |
||||
""" |
||||
Set validator max total delegation |
||||
|
||||
Parameters |
||||
---------- |
||||
max: str |
||||
Maximum total delegation of validator in ONE |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
try: |
||||
max = Decimal(max) |
||||
except InvalidOperation as e: |
||||
raise InvalidValidatorError(3, 'Max total delegation must be a number') from e |
||||
if self._min_self_delegation: |
||||
if max < self._min_self_delegation: |
||||
raise InvalidValidatorError(3, f'Max total delegation must be greater than min self delegation: {self._min_self_delegation}') |
||||
else: |
||||
raise InvalidValidatorError(4, 'Min self delegation must be set before max total delegation') |
||||
self._max_total_delegation = max.normalize() |
||||
|
||||
def get_max_total_delegation(self) -> Decimal: |
||||
""" |
||||
Get validator max total delegation |
||||
|
||||
Returns |
||||
------- |
||||
Decimal |
||||
Validator max total delegation in ONE |
||||
""" |
||||
return self._max_total_delegation |
||||
|
||||
def set_amount(self, amount): |
||||
""" |
||||
Set validator initial delegation amount |
||||
|
||||
Parameters |
||||
---------- |
||||
amount: str |
||||
Initial delegation amount of validator in ONE |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
try: |
||||
amount = Decimal(amount) |
||||
except InvalidOperation as e: |
||||
raise InvalidValidatorError(3, f'Amount must be a number') from e |
||||
if self._min_self_delegation: |
||||
if amount < self._min_self_delegation: |
||||
raise InvalidValidatorError(3, f'Amount must be greater than min self delegation: {self._min_self_delegation}') |
||||
else: |
||||
raise InvalidValidatorError(4, f'Min self delegation must be set before amount') |
||||
if self._max_total_delegation: |
||||
if amount > self._max_total_delegation: |
||||
raise InvalidValidatorError(3, f'Amount must be less than max total delegation: {self._max_self_delegation}') |
||||
else: |
||||
raise InvalidValidatorError(4, f'Max total delegation must be set before amount') |
||||
self._inital_delegation = amount.normalize() |
||||
|
||||
def get_amount(self) -> Decimal: |
||||
""" |
||||
Get validator initial delegation amount |
||||
|
||||
Returns |
||||
------- |
||||
Decimal |
||||
Intended initial delegation amount in ONE |
||||
""" |
||||
return self._inital_delegation |
||||
|
||||
def set_max_rate(self, rate): |
||||
""" |
||||
Set validator max commission rate |
||||
|
||||
Parameters |
||||
---------- |
||||
rate: str |
||||
Max commission rate of validator |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
try: |
||||
rate = Decimal(rate) |
||||
except InvalidOperation as e: |
||||
raise InvalidValidatorError(3, f'Max rate must be a number') from e |
||||
if rate < 0 or rate > 1: |
||||
raise InvalidValidatorError(3, f'Max rate must be between 0 and 1') |
||||
self._max_rate = rate.normalize() |
||||
|
||||
def get_max_rate(self) -> Decimal: |
||||
""" |
||||
Get validator max commission rate |
||||
|
||||
Returns |
||||
------- |
||||
Decimal |
||||
Validator max rate |
||||
""" |
||||
return self._max_rate |
||||
|
||||
def set_max_change_rate(self, rate): |
||||
""" |
||||
Set validator max commission change rate |
||||
|
||||
Parameters |
||||
---------- |
||||
rate: str |
||||
Max commission change rate of validator |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
try: |
||||
rate = Decimal(rate) |
||||
except InvalidOperation as e: |
||||
raise InvalidValidatorError(3, f'Max change rate must be a number') from e |
||||
if rate < 0: |
||||
raise InvalidValidatorError(3, f'Max change rate must be greater than or equal to 0') |
||||
if self._max_rate: |
||||
if rate > self._max_rate: |
||||
raise InvalidValidatorError(3, f'Max change rate must be less than or equal to max rate: {self._max_rate}') |
||||
else: |
||||
raise InvalidValidatorError(4, f'Max rate must be set before max change rate') |
||||
self._max_change_rate = rate.normalize() |
||||
|
||||
def get_max_change_rate(self) -> Decimal: |
||||
""" |
||||
Get validator max commission change rate |
||||
|
||||
Returns |
||||
------- |
||||
Decimal |
||||
Validator max change rate |
||||
""" |
||||
return self._max_change_rate |
||||
|
||||
def set_rate(self, rate): |
||||
""" |
||||
Set validator commission rate |
||||
|
||||
Parameters |
||||
---------- |
||||
rate: str |
||||
Commission rate of validator |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input is invalid |
||||
""" |
||||
try: |
||||
rate = Decimal(rate) |
||||
except InvalidOperation as e: |
||||
raise InvalidValidatorError(3, f'Rate must be a number') from e |
||||
if rate < 0: |
||||
raise InvalidValidatorError(3, f'Rate must be greater than or equal to 0') |
||||
if self._max_rate: |
||||
if rate > self._max_rate: |
||||
raise InvalidValidatorError(3, f'Rate must be less than or equal to max rate: {self._max_rate}') |
||||
else: |
||||
raise InvalidValidatorError(4, f'Max rate must be set before rate') |
||||
self._rate = rate.normalize() |
||||
|
||||
def get_rate(self) -> Decimal: |
||||
""" |
||||
Get validator commission rate |
||||
|
||||
Returns |
||||
------- |
||||
Decimal |
||||
Validator rate |
||||
""" |
||||
return self._rate |
||||
|
||||
def does_validator_exist(self, endpoint=_default_endpoint, timeout=_default_timeout) -> bool: |
||||
""" |
||||
Check if validator exists on blockchain |
||||
|
||||
Parameters |
||||
---------- |
||||
endpoint: :obj:`str`, optional |
||||
Endpoint to send request to |
||||
timeout: :obj:`int`, optional |
||||
Timeout in seconds |
||||
|
||||
Returns |
||||
------- |
||||
bool |
||||
Does validator exist on chain |
||||
|
||||
Raises |
||||
------ |
||||
RPCError, RequestsError, RequestsTimeoutError |
||||
If unable to get list of validators on chain |
||||
""" |
||||
all_validators = get_all_validator_addresses(endpoint, timeout) |
||||
if self._address in all_validators: |
||||
return True |
||||
return False |
||||
|
||||
def load(self, info): |
||||
""" |
||||
Import validator information |
||||
|
||||
Parameters |
||||
---------- |
||||
info: dict |
||||
Validator information with dictionary |
||||
Will ignore any extra fields in the input dictionary |
||||
Example input: |
||||
{ |
||||
"name": "", |
||||
"website": "", |
||||
"security-contact": "", |
||||
"identity": "", |
||||
"details": "", |
||||
"amount": 0, |
||||
"min-self-delegation": 0, |
||||
"max-total-delegation": 0, |
||||
"rate": 0, |
||||
"max-rate": 0, |
||||
"max-change-rate": 0 |
||||
} |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If input value is invalid |
||||
""" |
||||
self.set_name(info['name']) |
||||
self.set_identity(info['identity']) |
||||
self.set_website(info['website']) |
||||
self.set_details(info['details']) |
||||
self.set_security_contact(info['security-contact']) |
||||
|
||||
self.set_min_self_delegation(info['min-self-delegation']) |
||||
self.set_max_total_delegation(info['max-total-delegation']) |
||||
self.set_amount(info['amount']) |
||||
|
||||
self.set_max_rate(info['max-rate']) |
||||
self.set_max_change_rate(info['max-change-rate']) |
||||
self.set_rate(info['rate']) |
||||
|
||||
def load_from_blockchain(self, endpoint=_default_endpoint, timeout=_default_timeout): |
||||
""" |
||||
Import validator information from blockchain with given address |
||||
|
||||
Parameters |
||||
---------- |
||||
endpoint: :obj:`str`, optional |
||||
Endpoint to send request to |
||||
timeout: :obj:`int`, optional |
||||
Timeout in seconds |
||||
|
||||
Raises |
||||
------ |
||||
InvalidValidatorError |
||||
If any error occur getting & importing validator information from the blockchain |
||||
""" |
||||
try: |
||||
if not self.does_validator_exist(endpoint, timeout): |
||||
raise InvalidValidatorError(5, f'Validator does not exist on chain according to {endpoint}') |
||||
except (RPCError, RequestsError, RequestsTimeoutError) as e: |
||||
raise InvalidValidatorError(5, f'Error requesting validator information') from e |
||||
try: |
||||
validator_info = get_validator_information(self._address, endpoint, timeout) |
||||
except (RPCError, RequestsError, RequestsTimeoutError) as e: |
||||
raise InvalidValidatorError(5, f'Error requesting validator information') from e |
||||
|
||||
# Skip additional sanity checks when importing from chain |
||||
try: |
||||
info = validator_info['validator'] |
||||
self._name = info['name'] |
||||
self._identity = info['identity'] |
||||
self._website = info['website'] |
||||
self._details = info['details'] |
||||
self._security_contact = info['security-contact'] |
||||
|
||||
self._min_self_delegation = convert_atto_to_one(info['min-self-delegation']).normalize() |
||||
self._max_total_delegation = convert_atto_to_one(info['max-total-delegation']).normalize() |
||||
self._amount = Decimal(0) # Since validator exists, set initial delegation to 0 |
||||
|
||||
self._max_rate = Decimal(info['max-rate']).normalize() |
||||
self._max_change_rate = Decimal(info['max-change-rate']).normalize() |
||||
self._rate = Decimal(info['rate']).normalize() |
||||
except KeyError as e: |
||||
raise InvalidValidatorError(5, f'Error importing validator information from RPC result') from e |
||||
|
||||
|
||||
def export(self) -> dict: |
||||
""" |
||||
Export validator information as dict |
||||
|
||||
Returns |
||||
------- |
||||
dict |
||||
Dictionary representation of validator |
||||
""" |
||||
info = { |
||||
"validator-addr": self._address, |
||||
"name": self._name, |
||||
"website": self._website, |
||||
"security-contact": self._security_contact, |
||||
"identity": self._identity, |
||||
"details": self._details, |
||||
"amount": self._inital_delegation, |
||||
"min-self-delegation": self._min_self_delegation, |
||||
"max-total-delegation": self._max_total_delegation, |
||||
"rate": self._rate, |
||||
"max-rate": self._max_rate, |
||||
"max-change-rate": self._max_change_rate |
||||
} |
||||
return info |
Loading…
Reference in new issue